fix(transport): robust failure-audit when rollback throws + doc clarifications

Address one Blocker and three Important findings from code review of
2c34f12 (BundleImporter.ApplyAsync):

- BLOCKER: wrap RollbackAsync in nested try/catch so a rollback fault
  does not swallow the BundleImportFailed audit row. Dispose the
  failed transaction before the audit-write so the new SaveChangesAsync
  uses a fresh implicit transaction instead of enlisting in the broken
  one. Surface the rollback exception's message on the failure row
  alongside the original cause, and swallow audit-write faults per the
  design's best-effort-audit invariant. Add regression integration
  test using a SQLite transaction interceptor that throws on rollback.

- Document re-entrancy assumption on IAuditCorrelationContext: scoped
  lifetime, single circuit, concurrent imports within a shared scope
  must serialize externally.

- Document repository audit responsibility on BundleImporter: repos
  are thin EF wrappers; ApplyAsync writes audit rows explicitly. If
  repos ever start emitting audit rows, the explicit calls here must
  be removed to avoid double-logging.

- Document BundleSessionStore thread-safety: ConcurrentDictionary
  primitives are safe under concurrent callers; BundleSession itself
  is not thread-safe.
This commit is contained in:
Joseph Doherty
2026-05-24 05:06:04 -04:00
parent 2c34f12a6f
commit cda80cf821
4 changed files with 397 additions and 16 deletions
@@ -11,6 +11,17 @@ namespace ScadaLink.Transport.Import;
/// at read time (<see cref="Get"/>) and on-demand via <see cref="EvictExpired"/>;
/// there is no background timer.
/// <para>
/// Thread-safety: backed by <see cref="ConcurrentDictionary{TKey,TValue}"/> of
/// <see cref="Guid"/> to <see cref="BundleSession"/>. All store operations
/// (<see cref="Get"/> / <see cref="Open"/> / <see cref="Remove"/> /
/// <see cref="EvictExpired"/>) use the concurrent dictionary's safe primitives
/// (<c>TryGetValue</c>, indexer assignment, <c>TryRemove</c>) and are safe
/// under concurrent callers. The <see cref="BundleSession"/> instance itself
/// is NOT thread-safe — callers that share a session reference (e.g. two
/// importers mutating <c>FailedUnlockAttempts</c> on the same session) MUST
/// serialize their mutations on that shared reference.
/// </para>
/// <para>
/// TTL is supplied by the importer via <see cref="BundleSession.ExpiresAt"/>;
/// this store does not impose its own. The injected <see cref="TimeProvider"/>
/// is used purely to determine <c>now</c> when checking <c>ExpiresAt</c>, which