docs(mesh): fix stale redundancy-singleton comments + prove the driver fallback end-to-end

Review follow-ups on the redundancy re-home (Phase 6, Task 2):

- Program.cs: the hasDriver comment claimed IClusterRoleInfo "throws at host
  start if this driver node carries no cluster role" — false since the fallback
  landed. Rewrite it to describe the cluster-{ClusterId} scope with the driver-role
  fallback for legacy single-mesh / not-yet-migrated nodes.
- ClusterRoleInfo.cs: the SubscriberActor comment described RedundancyStateActor as
  the "admin-role singleton" — stale. Note it is now a cluster-{ClusterId}-scoped
  singleton spawned on every driver node (LeaderChanged stays a no-op here).
- Add a host/registry-level test for the driver-role FALLBACK path (roles ["driver"],
  no cluster role) closing the coverage asymmetry — it was proven only in the pure
  helper. Asserts the node boots and registers RedundancyStateActorKey, i.e. the boot
  the earlier throw-based version would have aborted.

The boot helper now supplies an in-memory IDbContextFactory and a fake IClusterRoleInfo:
Akka.Hosting invokes singleton props factories eagerly at StartAsync, and the admin
ClusterNodeAddressReconciler factory reads both (the latter for its per-cluster reconcile
scope, added by a sibling Phase 6 task) — without them the admin boot NREs.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-24 01:58:48 -04:00
parent 6531ec1984
commit a4d3ab4005
3 changed files with 40 additions and 7 deletions
+7 -6
View File
@@ -375,12 +375,13 @@ builder.Services.AddAkka("otopcua", (ab, sp) =>
{
ab.WithOtOpcUaRuntimeActors();
// Per-cluster mesh Phase 6: re-home the redundancy-state singleton off the admin node onto
// EVERY driver node, scoped to the node's own cluster-{ClusterId} role, so each mesh elects
// its own pair-local Primary (a driver-only site pair has no admin node to do it). Runs on
// the fused central node too (it is hasDriver), where it scopes to cluster-MAIN — exactly one
// redundancy singleton per node, since the admin branch above no longer registers one.
// IClusterRoleInfo is registered by AddOtOpcUaCluster (resolved lazily here); it throws at
// host start if this driver node carries no cluster-{ClusterId} role.
// EVERY driver node, so each mesh elects its own pair-local Primary (a driver-only site pair
// has no admin node to do it). It scopes to this node's own cluster-{ClusterId} role, falling
// back to the driver role for legacy single-mesh / not-yet-migrated nodes (already pair-local
// on a split mesh, since the mesh IS the pair). Runs on the fused central node too (it is
// hasDriver), where it scopes to cluster-MAIN — exactly one redundancy singleton per node,
// since the admin branch above no longer registers one. IClusterRoleInfo is registered by
// AddOtOpcUaCluster and resolved lazily here.
ab.WithOtOpcUaClusterRedundancySingleton(sp.GetRequiredService<IClusterRoleInfo>());
}
});