Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/Cluster/CentralCacheInvalidationTwoNodeTests.cs
T
Joseph Doherty 89d2becba9 refactor(host): remove legacy site-psk-invalidation topic (#105)
Removes the #96 compatibility path that the unified central-cache-invalidation
topic (#100 item 3, 153609ef; compat 3a31dd2d) carried for one release:
- CentralCacheInvalidationBus no longer publishes a bare SitePskInvalidated
  on site-psk-invalidation; LegacySitePskTopic constant deleted.
- CentralCacheInvalidationSubscriber no longer subscribes to that topic or
  handles a bare SitePskInvalidated; it applies envelopes only.
central-cache-invalidation behaviour is unchanged (one envelope per
invalidation, origin filtering, per-handler fault isolation).

Gate met: 8e32f353 (carries 153609ef and 3a31dd2d) runs on every central
node - DEV vd03 (record 581e25ca) and QA vq03/vq04 (record 8ba5d5d7).

Mixed-version / rolling-upgrade constraint: a central node built before
153609ef listens only on site-psk-invalidation and no longer receives PSK
invalidations from a newer peer (only its TTL and the site-rejection refresh
bound the staleness). Mixed pairs across that boundary are no longer
supported; central pairs are stopped and started together anyway.
Documented in Component-Host.md and Component-Communication.md.

Tests: the legacy publish/subscribe tests are replaced by
PskRotation_OnOneNode_ReachesThePeersCaches_ThroughTheUnifiedTopic (node A
publishes, node B's PSK cache and secret-resolver cache are invalidated, and
nothing else is published) and a bare-message-ignored test with an envelope
positive control. The two-node cluster test now asserts the PSK invalidation
arrives exactly once and the publisher applies nothing.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-13 08:43:55 -05:00

89 lines
3.7 KiB
C#

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;
/// <summary>
/// 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
/// <c>DistributedPubSub</c> and the production serializer, to the other node's handlers — and
/// not back to the publishing node's own handlers.
/// </summary>
public class CentralCacheInvalidationTwoNodeTests
{
private sealed class RecordingHandler : ICentralCacheInvalidationHandler
{
private readonly List<ICentralCacheInvalidation> _applied = new();
public IReadOnlyList<ICentralCacheInvalidation> 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<CentralCacheInvalidationBus>.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);
}
/// <summary>
/// Every typed invalidation crosses to the peer intact; the publisher never applies its own.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
// One topic only since ScadaBridge#105: the PSK invalidation arrives exactly once.
await Task.Delay(500);
Assert.Single(b.Handler.Applied, i => i is SitePskInvalidated);
Assert.Contains(new SitePskInvalidated("site-a"), b.Handler.Applied);
// The publisher skips its own envelopes, and with the legacy bare PSK topic gone nothing
// comes back to it at all.
Assert.Empty(a.Handler.Applied);
}
}