fix(auditlog): advance the served-rowid mark under the write lock so a purge clamp is never overwritten stale-high (review 2026-10-21 follow-up L1)
This commit is contained in:
@@ -165,4 +165,38 @@ public class SqliteAuditWriterForwardStateTests : IDisposable
|
||||
Assert.Equal(0, (await restarted.GetBacklogStatsAsync()).PendingCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Follow-up L1: the served high-water mark is only ever MUTATED under the write lock — the
|
||||
/// same lock the purge's clamp runs under. Before the fix the read path wrote the mark under
|
||||
/// its own read lock from a value it sampled before the SELECT, so a purge that clamped the
|
||||
/// mark in between was overwritten with the stale-high value, re-vouching for reusable rowids.
|
||||
/// Holding the write lock across a pull must therefore leave the mark untouched until released.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ServedHighWaterMark_IsOnlyAdvancedUnderTheWriteLock()
|
||||
{
|
||||
await using var writer = CreateWriter();
|
||||
await WriteAsync(writer, DateTime.UtcNow.AddMinutes(-5));
|
||||
var before = writer.MaxServedRowIdForTests;
|
||||
|
||||
Task<IReadOnlyList<AuditEvent>> pull;
|
||||
System.Threading.Monitor.Enter(writer.WriteLockForTests);
|
||||
try
|
||||
{
|
||||
pull = Task.Run(() => writer.ReadPendingSinceAsync(DateTime.MinValue, 100));
|
||||
// The SELECT itself needs only the read lock; give it ample time to run. A
|
||||
// synchronous sleep (not an await) so Monitor.Exit runs on the owning thread.
|
||||
Thread.Sleep(500);
|
||||
Assert.Equal(before, writer.MaxServedRowIdForTests);
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Threading.Monitor.Exit(writer.WriteLockForTests);
|
||||
}
|
||||
|
||||
Assert.Single(await pull.WaitAsync(TimeSpan.FromSeconds(10)));
|
||||
Assert.True(writer.MaxServedRowIdForTests > before);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user