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:
@@ -1,52 +0,0 @@
|
||||
using Akka.Cluster;
|
||||
using ZB.MOM.WW.ScadaBridge.Host.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
|
||||
|
||||
/// <summary>
|
||||
/// Production implementation of
|
||||
/// <see cref="IActiveNodeGate"/> backed by the running Akka.NET cluster.
|
||||
///
|
||||
/// The inbound API is "Central cluster only (active node)" — a standby central
|
||||
/// node must not execute method scripts or <c>Route.To()</c> calls. Active =
|
||||
/// the oldest Up member (the singleton host), evaluated by
|
||||
/// <see cref="ClusterActivityEvaluator.SelfIsOldest(Cluster, string?)"/> — NOT
|
||||
/// the cluster leader, whose address-ordered definition diverges from singleton
|
||||
/// placement after a restart (review 01 [High]). This is the same evaluator
|
||||
/// backing the shared <c>ActiveNodeHealthCheck</c> registered on the active tier
|
||||
/// — both route to <c>ClusterActiveNode</c> — so
|
||||
/// <see cref="InboundApiEndpointFilter"/> can return HTTP 503 on a standby.
|
||||
///
|
||||
/// Registered only in the Central-role branch of <c>Program.cs</c>. The gate
|
||||
/// is resolved per request from <c>HttpContext.RequestServices</c>; while the
|
||||
/// <c>AkkaHostedService</c> is still warming up (<c>ActorSystem == null</c>)
|
||||
/// or the node has not yet reached <see cref="MemberStatus.Up"/>, this
|
||||
/// implementation reports <c>IsActiveNode == false</c> — the safe-by-default
|
||||
/// answer matching the standby case.
|
||||
/// </summary>
|
||||
public sealed class ActiveNodeGate : IActiveNodeGate
|
||||
{
|
||||
private readonly AkkaHostedService _akkaService;
|
||||
|
||||
/// <summary>Initializes a new <see cref="ActiveNodeGate"/> bound to the given Akka hosted service.</summary>
|
||||
/// <param name="akkaService">The Akka hosted service exposing the cluster's <see cref="Akka.Actor.ActorSystem"/>.</param>
|
||||
public ActiveNodeGate(AkkaHostedService akkaService)
|
||||
{
|
||||
_akkaService = akkaService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsActiveNode
|
||||
{
|
||||
get
|
||||
{
|
||||
var system = _akkaService.ActorSystem;
|
||||
if (system == null)
|
||||
return false;
|
||||
|
||||
var cluster = Cluster.Get(system);
|
||||
return ClusterActivityEvaluator.SelfIsOldest(cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction the inbound API endpoint uses to determine whether
|
||||
/// this node is the active (cluster-leader) central node.
|
||||
///
|
||||
/// The design states the inbound API is "Central cluster only (active node)" and
|
||||
/// "fails over with it". A standby central node must not execute method scripts or
|
||||
/// <c>Route.To()</c> calls — that can race the active node or run against stale
|
||||
/// singleton state. <see cref="InboundApiEndpointFilter"/> consults this gate and
|
||||
/// returns HTTP 503 from a standby so Traefik/clients only reach the live node.
|
||||
///
|
||||
/// The implementation lives in the Host (it needs Akka cluster state); when no
|
||||
/// implementation is registered, the endpoint defaults to "allow" so non-clustered
|
||||
/// hosts and tests are unaffected.
|
||||
/// </summary>
|
||||
public interface IActiveNodeGate
|
||||
{
|
||||
/// <summary>
|
||||
/// <c>true</c> when this node is the active central node and may serve the
|
||||
/// inbound API; <c>false</c> on a standby node.
|
||||
/// </summary>
|
||||
bool IsActiveNode { get; }
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.Health;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.Health;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.Health;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware;
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
Abstractions contracts (IApiKeyVerifier / ApiKeyOptions) transitively. -->
|
||||
<PackageReference Include="ZB.MOM.WW.Auth.ApiKeys" />
|
||||
<PackageReference Include="ZB.MOM.WW.Configuration" />
|
||||
<!-- Active-node gating (Gitea #80): InboundApiEndpointFilter, AuditWriteMiddleware and the
|
||||
warm-up service consult the shared ZB.MOM.WW.Health.IActiveNodeGate; the Host registers
|
||||
ZB.MOM.WW.Health.Akka's AkkaActiveNodeGate (oldest Up member via ClusterActiveNode). -->
|
||||
<PackageReference Include="ZB.MOM.WW.Health" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -16,6 +16,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.DeploymentManager;
|
||||
using ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using ZB.MOM.WW.Health;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ManagementService.FlowDebug;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user