refactor(inbound-api): use the shared ZB.MOM.WW.Health IActiveNodeGate (ScadaBridge#80)

The InboundAPI carried its own IActiveNodeGate (implemented by Host/Health/ActiveNodeGate)
whose rule already matched the shared Health 0.3.0 AkkaActiveNodeGate — both route to
ClusterActiveNode (oldest Up member). The private interface and implementation are deleted;
InboundAPI references ZB.MOM.WW.Health (0.3.0, already pinned) and InboundApiEndpointFilter,
AuditWriteMiddleware, InboundMethodWarmupService and FlowDebugRunner consume
ZB.MOM.WW.Health.IActiveNodeGate. The Central branch of Program.cs registers
AkkaActiveNodeGate (unscoped, like the active-node health check).

Warm-up behaviour is kept: the shared gate answers false while no ActorSystem resolves, while
this member is not Up, and on any cluster-init fault. It resolves the ActorSystem through the
existing DI singleton bridge (GetOrCreateActorSystem), the same path the health probes use.

Tests: CompositionRoot asserts the shared gate type (fails with the registration removed);
the evaluator wiring test now drives AkkaActiveNodeGate; new warm-up test (no ActorSystem ->
not active). Nothing was missing from the Health 0.3.0 package.

(ScadaBridge#80)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 13:39:54 -05:00
parent 1ee2e171cf
commit 23c35df234
18 changed files with 52 additions and 91 deletions
+11 -9
View File
@@ -603,15 +603,17 @@ try
sp.GetRequiredService<IOptions<NodeOptions>>(),
sp.GetRequiredService<ILogger<ClusterBootstrapCoordinator>>()));
// Register the production IActiveNodeGate implementation so
// standby-node gating is actually enforced (the InboundApiEndpointFilter
// consults IActiveNodeGate and defaults to "allow" when none is registered,
// which leaves the design's "central cluster only (active node)" guarantee
// unenforced in deployed binaries). The gate is backed by the same oldest-member
// evaluator (ClusterActivityEvaluator, which delegates to the shared ClusterActiveNode) as
// the active-node check above, so the inbound API and the /health/active endpoint Traefik
// routes against agree on which node is active.
builder.Services.AddSingleton<ZB.MOM.WW.ScadaBridge.InboundAPI.IActiveNodeGate, ActiveNodeGate>();
// Register the production active-node gate so standby-node gating is actually enforced
// (the InboundApiEndpointFilter consults IActiveNodeGate and defaults to "allow" when none
// is registered, which leaves the design's "central cluster only (active node)" guarantee
// unenforced in deployed binaries). Since Gitea #80 this is the SHARED seam
// (ZB.MOM.WW.Health.IActiveNodeGate) with the shared AkkaActiveNodeGate, unscoped like the
// active-node check above — both route to ClusterActiveNode, so the inbound API and the
// /health/active endpoint Traefik routes against can never disagree on which node is
// active. Warm-up stays safe: the gate resolves the ActorSystem through the singleton
// bridge above and answers false until this member is Up (and on any cluster-init fault).
builder.Services.AddSingleton<ZB.MOM.WW.Health.IActiveNodeGate>(
sp => new ZB.MOM.WW.Health.Akka.AkkaActiveNodeGate(sp));
// Warm the inbound method compile cache in the BACKGROUND once the host is running and
// this node is the active one (review 2026-10-21 05/U3). Registered after