feat(mesh-phase4): register ConfigDb only on admin-role nodes

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-23 11:38:40 -04:00
parent d630a7e267
commit a41582ea6a
3 changed files with 133 additions and 13 deletions
@@ -17,21 +17,37 @@ public static class HealthEndpoints
{
/// <summary>
/// Registers the shared ZB.MOM.WW health probes. Tier semantics preserved: configdb + akka on
/// ready+active; admin-leader on active only.
/// ready+active; admin-leader on active only. The configdb probe is admin-only (per-cluster mesh
/// Phase 4): a driver-only node holds no ConfigDb, so it is registered iff <paramref name="hasAdmin"/>.
/// </summary>
/// <param name="services">The service collection to register the health checks on.</param>
/// <param name="hasAdmin">
/// Whether this node carries the <c>admin</c> role. When <c>false</c> (a driver-only node) the
/// <c>configdb</c> probe is skipped — the node registers no <see cref="OtOpcUaConfigDbContext"/>
/// factory, so probing it would throw when the readiness endpoint resolves the factory. Defaults to
/// <c>true</c> so admin/fused callers (including the test harnesses) keep the probe unchanged.
/// </param>
/// <returns>The same service collection, for chaining.</returns>
public static IServiceCollection AddOtOpcUaHealth(this IServiceCollection services)
public static IServiceCollection AddOtOpcUaHealth(this IServiceCollection services, bool hasAdmin = true)
{
services.AddHealthChecks()
.AddTypeActivatedCheck<DatabaseHealthCheck<OtOpcUaConfigDbContext>>(
var checks = services.AddHealthChecks();
// configdb probe is admin-only (per-cluster mesh Phase 4). A driver-only node gets its config
// from central via ConfigSource:Mode=FetchAndCache and never registers the ConfigDb factory, so
// the type-activated DatabaseHealthCheck would throw resolving it. Admin + fused nodes: unchanged.
if (hasAdmin)
{
checks.AddTypeActivatedCheck<DatabaseHealthCheck<OtOpcUaConfigDbContext>>(
"configdb",
failureStatus: null,
tags: new[] { ZbHealthTags.Ready, ZbHealthTags.Active },
args: new DatabaseHealthCheckOptions<OtOpcUaConfigDbContext>
{
ProbeQuery = static (db, ct) => db.Deployments.AsNoTracking().Take(1).ToListAsync(ct),
})
});
}
checks
.AddTypeActivatedCheck<AkkaClusterHealthCheck>(
"akka",
failureStatus: null,
@@ -42,11 +58,10 @@ public static class HealthEndpoints
failureStatus: null,
tags: new[] { ZbHealthTags.Active },
args: "admin")
// Registered unconditionally, not driver-gated. AddOtOpcUaHealth takes no role argument
// and runs on every node; the check itself resolves ISyncStatus optionally and reports
// Healthy when LocalDb is absent (admin-only graphs) or replication is default-OFF, so a
// plain node is never degraded by it. A factory registration keeps this no-arg signature
// while still reading ISyncStatus + options + the sync port from the container.
// Registered on every node regardless of role (unlike the admin-only configdb probe above):
// the check itself resolves ISyncStatus optionally and reports Healthy when LocalDb is absent
// (admin-only graphs) or replication is default-OFF, so a plain node is never degraded by it.
// A factory registration reads ISyncStatus + options + the sync port from the container.
.Add(new HealthCheckRegistration(
"localdb-replication",
sp => new LocalDbReplicationHealthCheck(
+13 -3
View File
@@ -122,8 +122,16 @@ builder.AddZbSerilog(o => o.ServiceName = "otopcua");
// Windows-service registration is handled at install time by scripts/install/Install-Services.ps1
// rather than in-process, so the binary stays cross-platform-compilable.
// Shared services — always registered regardless of role. ConfigDb is required for everything.
builder.Services.AddOtOpcUaConfigDb(builder.Configuration);
// ConfigDb is admin-only now (per-cluster mesh Phase 4). Only the admin role owns a ConfigDb
// connection string; a driver-only node holds none at all and gets its deployed configuration from
// central over the Phase-3 fetch-and-cache path (ConfigSource:Mode=FetchAndCache). Registering it
// unconditionally would throw on a driver-only node, since AddOtOpcUaConfigDb requires the string.
// A fused admin+driver node keeps ConfigDb via its admin role — byte-for-byte unaffected.
if (hasAdmin)
builder.Services.AddOtOpcUaConfigDb(builder.Configuration);
else
Log.Information("Driver-only node — no ConfigDb registered; configuration via ConfigSource:Mode=FetchAndCache");
builder.Services.AddOtOpcUaCluster(builder.Configuration);
// Validate LdapOptions unconditionally so ANY role node (admin-only, driver-only, or fused)
@@ -392,7 +400,9 @@ if (hasAdmin)
// Cluster replication is opt-in behind Secrets:Replication:Enabled (default false) — see SecretsRegistration.
builder.Services.AddOtOpcUaSecrets(builder.Configuration);
builder.Services.AddOtOpcUaHealth();
// hasAdmin gates the configdb probe: a driver-only node registers no ConfigDb factory (see above), so
// probing it would throw when the readiness endpoint resolves the factory.
builder.Services.AddOtOpcUaHealth(hasAdmin);
builder.Services.AddOtOpcUaObservability(builder.Configuration);
// gRPC server plumbing shared by two endpoints: the LocalDb passive sync endpoint (driver-role,