fix(auditlog): reconciliation pull serves only unacked rows (PushAbandoned/Rejected push-exit states), central pages within a tick with set-based inserts; refused push rows leave the queue after N attempts (review 2026-10-21 06/PERF-1, 06/COR-3, 06/IMPL-1)
This commit is contained in:
+70
@@ -752,6 +752,76 @@ public class SiteAuditTelemetryActorTests : TestKit
|
||||
Assert.DoesNotContain(logger.Levels, l => l >= Microsoft.Extensions.Logging.LogLevel.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 06/PERF-1: against a queue that supports the push lifecycle, an orphan
|
||||
/// cached row past grace is marked PushAbandoned (still owed to the pull), NOT Forwarded.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task CachedDrain_OrphanPastGrace_IsMarkedPushAbandoned_NotForwarded()
|
||||
{
|
||||
var queue = Substitute.For<ISiteAuditQueue, ZB.MOM.WW.ScadaBridge.AuditLog.Site.ISiteAuditPushLifecycle>();
|
||||
var lifecycle = (ZB.MOM.WW.ScadaBridge.AuditLog.Site.ISiteAuditPushLifecycle)queue;
|
||||
var orphan = NewCachedEvent(AuditKind.CachedSubmit);
|
||||
queue.ReadPendingAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
|
||||
queue.ReadPendingCachedTelemetryAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
Task.FromResult<IReadOnlyList<AuditEvent>>(new[] { orphan }),
|
||||
Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
|
||||
_trackingStore.GetStatusAsync(Arg.Any<TrackedOperationId>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<TrackingStatusSnapshot?>(null));
|
||||
|
||||
Sys.ActorOf(Props.Create(() => new SiteAuditTelemetryActor(
|
||||
queue, _client, Opts(), NullLogger<SiteAuditTelemetryActor>.Instance,
|
||||
(IOperationTrackingStore?)_trackingStore)));
|
||||
|
||||
await AwaitAssertAsync(async () =>
|
||||
await lifecycle.Received(1).MarkPushAbandonedAsync(
|
||||
Arg.Is<IReadOnlyList<Guid>>(g => g.Count == 1 && g[0] == orphan.EventId),
|
||||
Arg.Any<CancellationToken>()),
|
||||
TimeSpan.FromSeconds(10));
|
||||
await queue.DidNotReceive().MarkForwardedAsync(Arg.Any<IReadOnlyList<Guid>>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 06/IMPL-1: rows central ANSWERED for but did not accept get a refused
|
||||
/// attempt recorded (so a poison head row eventually leaves the push queue); accepted rows
|
||||
/// do not, and a transport fault records nothing.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Drain_RefusedRows_RecordRejectedAttempts_TransportFaultDoesNot()
|
||||
{
|
||||
var queue = Substitute.For<ISiteAuditQueue, ZB.MOM.WW.ScadaBridge.AuditLog.Site.ISiteAuditPushLifecycle>();
|
||||
var lifecycle = (ZB.MOM.WW.ScadaBridge.AuditLog.Site.ISiteAuditPushLifecycle)queue;
|
||||
var accepted = NewEvent();
|
||||
var refused = NewEvent();
|
||||
queue.ReadPendingAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
Task.FromResult<IReadOnlyList<AuditEvent>>(new[] { accepted, refused }),
|
||||
Task.FromResult<IReadOnlyList<AuditEvent>>(new[] { accepted, refused }),
|
||||
Task.FromResult<IReadOnlyList<AuditEvent>>(Array.Empty<AuditEvent>()));
|
||||
_client.IngestAuditEventsAsync(Arg.Any<AuditEventBatch>(), Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
_ => Task.FromException<IngestAck>(new InvalidOperationException("transport down")),
|
||||
_ => Task.FromResult(AckAll(new[] { accepted })));
|
||||
|
||||
Sys.ActorOf(Props.Create(() => new SiteAuditTelemetryActor(
|
||||
queue, _client, Opts(), NullLogger<SiteAuditTelemetryActor>.Instance,
|
||||
(IOperationTrackingStore?)null)));
|
||||
|
||||
await AwaitAssertAsync(async () =>
|
||||
await lifecycle.Received(1).RecordRejectedPushAttemptsAsync(
|
||||
Arg.Is<IReadOnlyList<Guid>>(g => g.Count == 1 && g[0] == refused.EventId),
|
||||
10,
|
||||
Arg.Any<CancellationToken>()),
|
||||
TimeSpan.FromSeconds(10));
|
||||
await queue.Received(1).MarkForwardedAsync(
|
||||
Arg.Is<IReadOnlyList<Guid>>(g => g.Count == 1 && g[0] == accepted.EventId),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
private sealed class CapturingLogger<T> : Microsoft.Extensions.Logging.ILogger<T>
|
||||
{
|
||||
public System.Collections.Concurrent.ConcurrentQueue<Microsoft.Extensions.Logging.LogLevel> Levels { get; } = new();
|
||||
|
||||
Reference in New Issue
Block a user