fix(auditlog): explicit permanent rejections on the ingest ack (IngestAck.rejected_event_ids, field 2) with a dedicated counter; reconciler ignores transient faults for abandonment; persist the served high-water mark; prune only on a non-empty enumeration (review 2026-10-21 06/PERF-1, 06/IMPL-1 adversarial follow-up)
This commit is contained in:
+32
-8
@@ -18,7 +18,6 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
|
||||
public class SqliteAuditWriterForwardStateTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"audit-fwdstate-{Guid.NewGuid():N}.db");
|
||||
private readonly CountingFailureCounter _counter = new();
|
||||
|
||||
/// <summary>Deletes the temporary database.</summary>
|
||||
public void Dispose()
|
||||
@@ -32,8 +31,7 @@ public class SqliteAuditWriterForwardStateTests : IDisposable
|
||||
private SqliteAuditWriter CreateWriter() => new(
|
||||
Options.Create(new SqliteAuditWriterOptions { DatabasePath = _dbPath }),
|
||||
NullLogger<SqliteAuditWriter>.Instance,
|
||||
new FakeNodeIdentityProvider(),
|
||||
failureCounter: _counter);
|
||||
new FakeNodeIdentityProvider());
|
||||
|
||||
private static async Task<Guid> WriteAsync(SqliteAuditWriter writer, DateTime occurredAtUtc)
|
||||
{
|
||||
@@ -132,13 +130,39 @@ public class SqliteAuditWriterForwardStateTests : IDisposable
|
||||
Assert.Equal(behind, Assert.Single(await writer.ReadPendingAsync(1)).EventId);
|
||||
Assert.Contains(await writer.ReadPendingSinceAsync(DateTime.MinValue, 100), e => e.EventId == poison);
|
||||
Assert.Equal(0, await writer.PurgeExpiredAsync(DateTime.UtcNow.AddDays(1)));
|
||||
Assert.Equal(1, _counter.Count);
|
||||
Assert.Equal(1, writer.RejectedPushRowCount);
|
||||
}
|
||||
|
||||
private sealed class CountingFailureCounter : IAuditWriteFailureCounter
|
||||
/// <summary>
|
||||
/// The served high-water mark survives a writer restart: rows served before the restart are
|
||||
/// retired by the first cursor that covers them after it, instead of being stranded owed
|
||||
/// (and counted on the backlog) until something happens to re-serve them.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ServedHighWaterMark_SurvivesRestart_SoTheNextCursorStillRetires()
|
||||
{
|
||||
private int _count;
|
||||
public int Count => Volatile.Read(ref _count);
|
||||
public void Increment() => Interlocked.Increment(ref _count);
|
||||
var t = DateTime.UtcNow.AddMinutes(-5);
|
||||
Guid abandoned;
|
||||
IReadOnlyList<AuditEvent> served;
|
||||
await using (var writer = CreateWriter())
|
||||
{
|
||||
abandoned = await WriteAsync(writer, t);
|
||||
await writer.MarkPushAbandonedAsync(new[] { abandoned });
|
||||
served = await writer.ReadPendingSinceAsync(DateTime.MinValue, 100);
|
||||
Assert.Single(served);
|
||||
}
|
||||
|
||||
await using (var restarted = CreateWriter())
|
||||
{
|
||||
Assert.True(restarted.MaxServedRowIdForTests > 0);
|
||||
|
||||
var flipped = await restarted.MarkReconciledUpToAsync(
|
||||
served[0].OccurredAtUtc.UtcDateTime, served[0].EventId.ToString());
|
||||
|
||||
Assert.Equal(1, flipped);
|
||||
Assert.Equal("Reconciled", restarted.GetForwardStateForTests(abandoned)!.Value.State);
|
||||
Assert.Equal(0, (await restarted.GetBacklogStatsAsync()).PendingCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user