fix(mesh): scope ClusterNode address reconcile to the admin node's own cluster

Phase 6 splits the fleet into one mesh per Cluster, so an admin (central)
node's Cluster.State.Members shows only its own pair — it no longer sees
site members via gossip. The ClusterNodeAddressReconciler singleton compared
live membership against EVERY ClusterNode row fleet-wide, so post-split every
foreign-cluster row would log EnabledRowNotInCluster forever.

Scope the actor's two DB queries to rows whose ClusterId matches the admin
node's own (IClusterRoleInfo.ClusterId, sourced at registration). A legacy
admin node with no cluster role (null ClusterId) still reconciles the whole
fleet — it genuinely sees every member via gossip. The pure Reconcile
function and AddressMismatchKind semantics are unchanged; scoping lives
entirely in the actor's queries.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-24 01:39:06 -04:00
parent a2c5d57fa0
commit 2535df019b
6 changed files with 149 additions and 40 deletions
@@ -153,7 +153,12 @@ public static class ServiceCollectionExtensions
(system, registry, resolver) =>
{
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
return ClusterNodeAddressReconcilerActor.Props(dbFactory);
// Per-cluster mesh Phase 6: scope the reconcile to the admin node's OWN cluster's rows.
// After the mesh split central sees only its own pair via gossip, so a fleet-wide sweep
// would false-report every other cluster's rows as EnabledRowNotInCluster forever. A
// legacy node with no cluster role has null ClusterId ⇒ full-fleet reconcile, unchanged.
var roleInfo = resolver.GetService<IClusterRoleInfo>();
return ClusterNodeAddressReconcilerActor.Props(dbFactory, roleInfo.ClusterId);
},
singletonOptions,
createProxyToo: false);