fix(store-and-forward): inline ordered replication dispatch; Warning + counter on replication failures
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
@@ -163,6 +164,47 @@ public class ReplicationServiceTests : IAsyncLifetime, IDisposable
|
||||
// No exception -- fire-and-forget, best-effort
|
||||
}
|
||||
|
||||
// ── Task 10 (arch review 02): ordered, observable replication dispatch ──
|
||||
|
||||
[Fact]
|
||||
public void ReplicationOperations_AreDispatchedInIssueOrder()
|
||||
{
|
||||
var seen = new List<(ReplicationOperationType, string)>();
|
||||
_replicationService.SetReplicationHandler(op =>
|
||||
{
|
||||
seen.Add((op.OperationType, op.MessageId));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
_replicationService.ReplicateEnqueue(CreateMessage($"m{i}"));
|
||||
_replicationService.ReplicateRemove($"m{i}");
|
||||
}
|
||||
|
||||
// Inline dispatch: by the time the calls return, every op was handed to the
|
||||
// handler, Add strictly before Remove per id. Pre-fix (Task.Run) this was
|
||||
// racy in both count and order.
|
||||
Assert.Equal(400, seen.Count);
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
Assert.Equal((ReplicationOperationType.Add, $"m{i}"), seen[2 * i]);
|
||||
Assert.Equal((ReplicationOperationType.Remove, $"m{i}"), seen[2 * i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplicationHandlerThrow_IsSwallowed_AndLoggedAtWarning()
|
||||
{
|
||||
var logger = new CapturingLogger<ReplicationService>();
|
||||
var service = new ReplicationService(new StoreAndForwardOptions(), logger);
|
||||
service.SetReplicationHandler(_ => throw new InvalidOperationException("peer gone"));
|
||||
|
||||
service.ReplicateRemove("m1"); // must not throw
|
||||
|
||||
Assert.Contains(logger.Entries, e => e.Level == LogLevel.Warning);
|
||||
}
|
||||
|
||||
private static StoreAndForwardMessage CreateMessage(string id)
|
||||
{
|
||||
return new StoreAndForwardMessage
|
||||
@@ -178,4 +220,15 @@ public class ReplicationServiceTests : IAsyncLifetime, IDisposable
|
||||
Status = StoreAndForwardMessageStatus.Pending
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Minimal in-memory logger that records level + rendered message.</summary>
|
||||
private sealed class CapturingLogger<T> : ILogger<T>
|
||||
{
|
||||
public List<(LogLevel Level, string Message)> Entries { get; } = new();
|
||||
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)
|
||||
=> Entries.Add((logLevel, formatter(state, exception)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user