fix(mesh): also scope the reconciler's observed membership to the own cluster

Task 3 review: scoping only the rows/enabled DB queries by ClusterId left
the observed set (from Cluster.State.Members) filtered by the driver role
alone. In the mixed-mesh window — DB scoping live via a cluster role, but
the physical Akka mesh not yet split so central still sees foreign members
via gossip — a foreign-cluster driver member stays in observed while its row
is filtered out, hitting the RunningNodeHasNoRow branch, which logs at ERROR.
That traded the Warning-level false positive for a worse Error-level one.

Filter observed by the node's own cluster ROLE too. Extracted the projection
into a static FilterOwnClusterDriverMembers(members, ownClusterRole) so the
membership filter is unit-testable without seeding a live cluster (the actor
harness joins as admin only, so observed is always empty). Sourced at
registration from IClusterRoleInfo.ClusterRole alongside ClusterId. Null role
(legacy, no cluster role) keeps every driver member — reconcile-all, unchanged.

Also clarified the ownClusterId doc wording (non-null-and-non-empty).

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-24 02:07:02 -04:00
parent a4d3ab4005
commit 1ce2042e14
4 changed files with 165 additions and 23 deletions
@@ -153,12 +153,16 @@ public static class ServiceCollectionExtensions
(system, registry, resolver) =>
{
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
// 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.
// Per-cluster mesh Phase 6: scope BOTH halves of the reconcile to the admin node's OWN
// cluster — rows by ClusterId, observed membership by cluster role. After the mesh split
// central sees only its own pair via gossip, so an unscoped sweep would false-report
// every other cluster's rows (EnabledRowNotInCluster). Scoping only the rows would swap
// that for a WORSE Error-level RunningNodeHasNoRow in the mixed-mesh window (rows already
// filtered, but foreign members still visible via gossip). A legacy node with no cluster
// role has null ClusterId/ClusterRole ⇒ full-fleet reconcile, unchanged.
var roleInfo = resolver.GetService<IClusterRoleInfo>();
return ClusterNodeAddressReconcilerActor.Props(dbFactory, roleInfo.ClusterId);
return ClusterNodeAddressReconcilerActor.Props(
dbFactory, roleInfo.ClusterId, roleInfo.ClusterRole);
},
singletonOptions,
createProxyToo: false);