fix(store-and-forward): resolve S&F delivery + replication wiring (3 Critical findings)
Resolves StoreAndForward-001, ExternalSystemGateway-001, NotificationService-001 — one systemic gap where buffered messages were persisted but never delivered, and the active node never replicated its buffer to the standby. Delivery handlers (ExternalSystemGateway-001 / NotificationService-001): - AkkaHostedService registers delivery handlers for the ExternalSystem, CachedDbWrite and Notification categories after StoreAndForwardService starts; each resolves its scoped consumer in a fresh DI scope. - ExternalSystemClient, DatabaseGateway and NotificationDeliveryService each gain a DeliverBufferedAsync method: re-resolve the target and re-attempt delivery, returning true/false/throwing per the transient-vs-permanent contract. - EnqueueAsync gains an attemptImmediateDelivery flag; CachedCallAsync and NotificationDeliveryService.SendAsync pass false (they already attempted delivery themselves) so registering a handler does not dispatch twice. Replication (StoreAndForward-001): - ReplicationService is injected into StoreAndForwardService; a new BufferAsync helper replicates every enqueue, and successful-retry removes and parks are replicated too. Fire-and-forget, no-op when replication is disabled. Tests: StoreAndForwardReplicationTests (Add/Remove/Park observed), attemptImmediateDelivery behaviour, and DeliverBufferedAsync paths for each consumer. Full solution builds; StoreAndForward/ExternalSystemGateway/ NotificationService suites green.
This commit is contained in:
@@ -192,4 +192,37 @@ public class NotificationDeliveryServiceTests
|
||||
Assert.True(result.Success);
|
||||
Assert.True(result.WasBuffered);
|
||||
}
|
||||
|
||||
// ── NotificationService-001: buffered-notification delivery handler ──
|
||||
|
||||
private static StoreAndForward.StoreAndForwardMessage BufferedNotification(string listName) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Category = ScadaLink.Commons.Types.Enums.StoreAndForwardCategory.Notification,
|
||||
Target = listName,
|
||||
PayloadJson = $$"""{"ListName":"{{listName}}","Subject":"Alert","Message":"Body"}""",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task DeliverBuffered_HappyPath_ReturnsTrue()
|
||||
{
|
||||
SetupHappyPath();
|
||||
var service = CreateService();
|
||||
|
||||
var delivered = await service.DeliverBufferedAsync(BufferedNotification("ops-team"));
|
||||
|
||||
Assert.True(delivered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeliverBuffered_ListNoLongerExists_ReturnsFalseSoMessageParks()
|
||||
{
|
||||
_repository.GetListByNameAsync("gone-list").Returns((NotificationList?)null);
|
||||
var service = CreateService();
|
||||
|
||||
var delivered = await service.DeliverBufferedAsync(BufferedNotification("gone-list"));
|
||||
|
||||
Assert.False(delivered); // permanent — the S&F engine parks the message
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user