fix(mesh): break Phase 6 boot-crash — cluster-redundancy singleton scope must not resolve IClusterRoleInfo eagerly
Phase 6 wired the re-homed redundancy singleton in Program.cs as `WithOtOpcUaClusterRedundancySingleton(sp.GetRequiredService<IClusterRoleInfo>())` inside the AddAkka configurator lambda. That lambda runs WHILE the ActorSystem is being built, but ClusterRoleInfo depends on the ActorSystem (it is a live Cluster.State view) — so resolving it there recurses into ActorSystem construction and every node dies at boot with StackOverflowException. It compiles cleanly (a runtime DI cycle) and RedundancyStateSingletonRehomeTests passed a hand-built FakeClusterRoleInfo straight into the extension, mocking around the composition-root line that overflows. The docker-dev live gate caught it on first boot. Derive the singleton's cluster-role scope from AkkaClusterOptions (pure config, no ActorSystem) instead: BuildClusterRedundancySingletonOptions / the extension now take AkkaClusterOptions and compute Role = Roles.FirstOrDefault(IsClusterRole) ?? driver — identical to ClusterRoleInfo.ClusterRole, which derives from the same config. Program.cs passes IOptions<AkkaClusterOptions>.Value, which has no ActorSystem dependency. Live-verified: rebuilt image boots with zero stack-overflow lines, cluster singletons form per mesh. 5/5 rehome tests pass; Host builds clean. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
+4
-4
@@ -36,7 +36,7 @@ public sealed class RedundancyStateSingletonRehomeTests
|
||||
public void Options_scope_the_singleton_to_the_nodes_cluster_role()
|
||||
{
|
||||
var options = ServiceCollectionExtensions.BuildClusterRedundancySingletonOptions(
|
||||
new FakeClusterRoleInfo(clusterRole: "cluster-SITE-A", clusterId: "SITE-A"));
|
||||
new AkkaClusterOptions { Roles = new[] { "driver", "cluster-SITE-A" } });
|
||||
|
||||
options.Role.ShouldBe("cluster-SITE-A");
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public sealed class RedundancyStateSingletonRehomeTests
|
||||
public void Missing_cluster_role_falls_back_to_the_driver_role()
|
||||
{
|
||||
var options = ServiceCollectionExtensions.BuildClusterRedundancySingletonOptions(
|
||||
new FakeClusterRoleInfo(clusterRole: null, clusterId: null));
|
||||
new AkkaClusterOptions { Roles = new[] { "admin", "driver" } });
|
||||
|
||||
options.Role.ShouldBe(RoleParser.Driver);
|
||||
}
|
||||
@@ -83,7 +83,7 @@ public sealed class RedundancyStateSingletonRehomeTests
|
||||
var registry = await BootAndGetRegistryAsync(
|
||||
roles: new[] { "driver", "cluster-SITE-A" },
|
||||
configure: ab => ab.WithOtOpcUaClusterRedundancySingleton(
|
||||
new FakeClusterRoleInfo(clusterRole: "cluster-SITE-A", clusterId: "SITE-A")));
|
||||
new AkkaClusterOptions { Roles = new[] { "driver", "cluster-SITE-A" } }));
|
||||
|
||||
registry.TryGet<RedundancyStateActorKey>(out _).ShouldBeTrue(
|
||||
"every driver node hosts its own mesh's redundancy singleton (Phase 6)");
|
||||
@@ -102,7 +102,7 @@ public sealed class RedundancyStateSingletonRehomeTests
|
||||
var registry = await BootAndGetRegistryAsync(
|
||||
roles: new[] { "driver" },
|
||||
configure: ab => ab.WithOtOpcUaClusterRedundancySingleton(
|
||||
new FakeClusterRoleInfo(clusterRole: null, clusterId: null)));
|
||||
new AkkaClusterOptions { Roles = new[] { "driver" } }));
|
||||
|
||||
registry.TryGet<RedundancyStateActorKey>(out _).ShouldBeTrue(
|
||||
"a driver node with no cluster role falls back to the driver role and still boots + registers");
|
||||
|
||||
Reference in New Issue
Block a user