Commit Graph

2 Commits

Author SHA1 Message Date
Joseph Doherty
d745ef0715 fix(configdb): InsertIfNotExistsAsync swallows duplicate-key races + add keyset tiebreaker test (#23)
Two concurrent sessions can both pass the IF NOT EXISTS check and then both attempt the INSERT against UX_AuditLog_EventId; the loser surfaced as SqlException 2601 (or 2627 for PK violations) and aborted the audit write. First-write-wins idempotency is the documented contract, so the race outcome is semantically a no-op — catch the two duplicate-key error numbers and log at Debug, let any other SqlException bubble.

Tests:

- InsertIfNotExistsAsync_ConcurrentDuplicateInserts_ProduceExactlyOneRow: 50 parallel inserters with the same EventId end with exactly one row and no escaped exceptions.

- QueryAsync_Keyset_SameOccurredAtUtc_TiebreaksOnEventId: four rows sharing the same OccurredAtUtc page deterministically through the (OccurredAtUtc, EventId) keyset cursor — exercises the e.OccurredAtUtc == after && e.EventId.CompareTo(afterId) < 0 branch and verifies EF Core 10's Guid.CompareTo translation against SQL Server uniqueidentifier order (deferred Bundle D reviewer recommendation).

AuditLogRepository now takes an optional ILogger<AuditLogRepository> (NullLogger default, mirrors InboundApiRepository); DI registration unchanged.
2026-05-20 12:12:50 -04:00
Joseph Doherty
de839627ed feat(configdb): AuditLogRepository (EF) + DI registration, append-only with M6-deferred SwitchOutPartition (#23)
EF Core implementation of IAuditLogRepository:
- InsertIfNotExistsAsync: single IF NOT EXISTS ... INSERT via
  ExecuteSqlInterpolatedAsync, bypasses the change tracker. Enum
  values converted to string in C# (columns are varchar(32) via
  HasConversion<string>).
- QueryAsync: AsNoTracking, predicate-per-non-null-filter, keyset
  paging on (OccurredAtUtc DESC, EventId DESC) — EF Core 10
  translates Guid.CompareTo to a uniqueidentifier < comparison
  natively (verified against MSSQL 2022).
- SwitchOutPartitionAsync: throws NotSupportedException naming M6;
  the non-aligned UX_AuditLog_EventId unique index blocks
  ALTER TABLE SWITCH PARTITION until the drop-and-rebuild dance
  ships with the purge actor.

DI: AddScoped<IAuditLogRepository, AuditLogRepository>() added after
the NotificationOutboxRepository registration; existing DI smoke test
extended with an IAuditLogRepository assertion.

Integration tests (8 new) use the Bundle C MsSqlMigrationFixture and
scope by a per-test SourceSiteId guid so they neither collide nor
require cleanup.

Bundle D of the Audit Log #23 M1 Foundation plan.
2026-05-20 11:05:18 -04:00