using Akka.Actor;
using Akka.Cluster.Tools.PublishSubscribe;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Communication;
using ZB.MOM.WW.ScadaBridge.Host;
namespace ZB.MOM.WW.ScadaBridge.IntegrationTests.Cluster;
///
/// ScadaBridge#100 item 3 / #83 / #103 across a REAL two-node central cluster built from the
/// production HOCON: an invalidation published on one node's bus is delivered, through
/// DistributedPubSub and the production serializer, to the other node's handlers — and
/// not back to the publishing node's own handlers.
///
public class CentralCacheInvalidationTwoNodeTests
{
private sealed class RecordingHandler : ICentralCacheInvalidationHandler
{
private readonly List _applied = new();
public IReadOnlyList Applied
{
get { lock (_applied) return _applied.ToList(); }
}
public void Apply(ICentralCacheInvalidation invalidation)
{
lock (_applied) _applied.Add(invalidation);
}
}
private static (CentralCacheInvalidationBus Bus, RecordingHandler Handler) Wire(ActorSystem node)
{
var bus = new CentralCacheInvalidationBus(NullLogger.Instance);
var handler = new RecordingHandler();
node.ActorOf(Props.Create(() => new CentralCacheInvalidationSubscriber(
bus, new ICentralCacheInvalidationHandler[] { handler })), "central-cache-invalidation");
bus.SetMediator(DistributedPubSub.Get(node).Mediator);
return (bus, handler);
}
///
/// Every typed invalidation crosses to the peer intact; the publisher never applies its own.
///
/// A task that represents the asynchronous operation.
[Fact]
public async Task Invalidations_ReachThePeerNode_AndNotThePublisher()
{
await using var cluster = await TwoNodeClusterFixture.StartAsync();
var a = Wire(cluster.NodeA);
var b = Wire(cluster.NodeB);
// Subscriptions spread by gossip, so probe until the peer has registered its subscriber.
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(20);
while (b.Handler.Applied.Count == 0)
{
Assert.True(DateTime.UtcNow < deadline, "node B never received node A's invalidation");
a.Bus.Publish(new RoleMappingsInvalidated());
await Task.Delay(250);
}
a.Bus.Publish(new ExternalDefinitionsInvalidated());
a.Bus.Publish(new InboundApiMethodInvalidated("MesMoveIn"));
a.Bus.Publish(new SitePskInvalidated("site-a"));
deadline = DateTime.UtcNow + TimeSpan.FromSeconds(10);
while (!b.Handler.Applied.Any(i => i is SitePskInvalidated))
{
Assert.True(DateTime.UtcNow < deadline, "node B did not receive the later invalidations");
await Task.Delay(100);
}
var applied = b.Handler.Applied;
Assert.Contains(applied, i => i is RoleMappingsInvalidated);
Assert.Contains(new ExternalDefinitionsInvalidated(), applied);
Assert.Contains(new InboundApiMethodInvalidated("MesMoveIn"), applied);
// The PSK invalidation arrives twice during the migration window (shared + legacy topic).
Assert.Contains(new SitePskInvalidated("site-a"), applied);
// The publisher skips its own envelopes; only the bare legacy PSK message (no origin)
// comes back to it, which is the pre-existing #96 behaviour and idempotent.
await Task.Delay(500);
Assert.All(a.Handler.Applied, i => Assert.IsType(i));
}
}