Merge branch 'worktree-agent-a71052048b4ee11d4' into arch-review-remediation
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
@@ -965,4 +966,109 @@ public class StoreAndForwardServiceTests : IAsyncLifetime, IDisposable
|
||||
TestLocalDb.DeleteFiles(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures every message logged through it, keyed by <see cref="LogLevel"/>. Minimal
|
||||
/// test double — no scopes, no filtering — just enough to assert on log VOLUME.
|
||||
/// </summary>
|
||||
private sealed class CapturingLogger<T> : ILogger<T>
|
||||
{
|
||||
private readonly List<(LogLevel Level, string Message)> _entries = new();
|
||||
private readonly object _gate = new();
|
||||
|
||||
public IReadOnlyList<(LogLevel Level, string Message)> Entries
|
||||
{
|
||||
get { lock (_gate) return _entries.ToList(); }
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
var message = formatter(state, exception);
|
||||
lock (_gate) _entries.Add((logLevel, message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// arch-review adversarial finding F3: the observer-queue onDropped callback used to
|
||||
/// log a Warning PER dropped item — a stuck observer with a large
|
||||
/// <see cref="StoreAndForwardOptions.ObserverQueueCapacity"/> would flood the log at
|
||||
/// sweep rate. Extends <see cref="ObserverQueue_BoundedCapacity_DropsOldestAndCountsDrops"/>
|
||||
/// (same bounded-queue setup) to pin the fix: many drops in one episode still
|
||||
/// increment <see cref="StoreAndForwardService.ObserverQueueDroppedCount"/> per drop, but
|
||||
/// produce exactly ONE observer-queue-drop Warning log — the first-drop Warning — because
|
||||
/// the episode never runs long enough to cross the periodic rollup interval.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ObserverQueue_ManyDropsInOneEpisode_LogsExactlyOneWarning()
|
||||
{
|
||||
var gate = new TaskCompletionSource();
|
||||
var observer = new BlockingObserver(gate);
|
||||
var logger = new CapturingLogger<StoreAndForwardService>();
|
||||
|
||||
var localDb = TestLocalDb.CreateTemp("ObsQueueDropLogRollup");
|
||||
var storage = new StoreAndForwardStorage(localDb.Db, NullLogger<StoreAndForwardStorage>.Instance);
|
||||
await storage.InitializeAsync();
|
||||
var service = new StoreAndForwardService(
|
||||
storage,
|
||||
new StoreAndForwardOptions
|
||||
{
|
||||
DefaultRetryInterval = TimeSpan.Zero,
|
||||
DefaultMaxRetries = 5,
|
||||
RetryTimerInterval = TimeSpan.FromHours(1), // timer never fires in-test
|
||||
ObserverQueueCapacity = 2,
|
||||
},
|
||||
logger,
|
||||
cachedCallObserver: observer,
|
||||
siteId: "site-78");
|
||||
|
||||
await service.StartAsync();
|
||||
try
|
||||
{
|
||||
service.RegisterDeliveryHandler(StoreAndForwardCategory.ExternalSystem,
|
||||
_ => throw new HttpRequestException("transient"));
|
||||
|
||||
// Enqueue far more than the bounded capacity (2) so the queue overflows many
|
||||
// times over in one sweep — same mechanism as the sibling test above, just a
|
||||
// larger flood to make a per-item log flood obvious if the fix regresses.
|
||||
for (var i = 0; i < 50; i++)
|
||||
{
|
||||
await service.EnqueueAsync(
|
||||
StoreAndForwardCategory.ExternalSystem, $"t{i}", "{}",
|
||||
attemptImmediateDelivery: false, retryInterval: TimeSpan.Zero,
|
||||
messageId: TrackedOperationId.New().ToString());
|
||||
}
|
||||
|
||||
await service.RetryPendingMessagesAsync();
|
||||
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
|
||||
while (DateTime.UtcNow < deadline && service.ObserverQueueDroppedCount < 10)
|
||||
await Task.Delay(10);
|
||||
|
||||
Assert.True(service.ObserverQueueDroppedCount >= 10,
|
||||
"expected many notifications to be dropped once the bounded queue filled");
|
||||
|
||||
var dropWarnings = logger.Entries
|
||||
.Where(e => e.Level == LogLevel.Warning
|
||||
&& e.Message.Contains("audit-observer queue", StringComparison.Ordinal))
|
||||
.ToList();
|
||||
|
||||
Assert.True(dropWarnings.Count == 1,
|
||||
$"expected exactly one observer-queue-drop Warning per episode (rate-limited), " +
|
||||
$"but got {dropWarnings.Count} for {service.ObserverQueueDroppedCount} drops");
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.TrySetResult();
|
||||
await service.StopAsync();
|
||||
var path = localDb.Path;
|
||||
localDb.Dispose();
|
||||
TestLocalDb.DeleteFiles(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user