feat(mesh): route deploy, driver-control and alarm commands through the mesh router
Phase 2 Task 7. The five central->node publish sites -- ConfigPublishCoordinator's deploy dispatch, and AdminOperationsActor's restart, reconnect, acknowledge and shelve -- now hand a MeshCommand to the transport router instead of telling the DistributedPubSub mediator directly. Neither actor knows which transport is in force any more. _meshRouter is NULLABLE with a mediator fallback, and that is deliberate: about a dozen existing coordinator and admin-operations tests construct these actors without a router, and they must keep exercising the real DPS path rather than a silently-disabled one. The fallback is not a permanent seam -- Phase 6 deletes it with the DPS branch and the single mesh. Wiring note: the comm actor's WithActors block moved ABOVE the singleton registrations so both singletons can resolve it with registry.Get, which is the ordering-by-registration idiom AdminOperationsActor already uses for the coordinator. An earlier version used ActorSelection.ResolveOne().GetAwaiter() .GetResult() inside the props factory -- that blocks inside actor construction and races the very spawn it waits for. Sabotage-verified by inverting the branch so the mediator always wins: all six new tests go red, including the fallback test (which proves it is asserting the mediator path rather than passing by accident). Suites after: ControlPlane 118/118, Runtime 450/450, Host.IntegrationTests 196/202 -- sole failure AbCip_Green_AgainstSim, the fixture baseline that fails on master too. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -53,12 +53,49 @@ public static class ServiceCollectionExtensions
|
||||
var singletonOptions = new ClusterSingletonOptions { Role = AdminRole };
|
||||
var proxyOptions = new ClusterSingletonOptions { Role = AdminRole };
|
||||
|
||||
// Per-cluster mesh Phase 2: the central end of the central↔node command boundary.
|
||||
//
|
||||
// A plain WithActors, NOT a singleton — and that is the load-bearing part. A driver node's
|
||||
// ClusterClient rotates across contact points and must find a live comm actor at whichever
|
||||
// central node answers. As a singleton it would exist on one central node only, and the
|
||||
// rotation would fail silently against the other: acks would land sometimes, depending on
|
||||
// which contact the client happened to settle on.
|
||||
builder.WithActors((system, registry, resolver) =>
|
||||
{
|
||||
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
|
||||
var options = resolver.GetService<IOptions<MeshTransportOptions>>().Value;
|
||||
|
||||
var actor = system.ActorOf(
|
||||
CentralCommunicationActor.Props(
|
||||
dbFactory,
|
||||
options,
|
||||
new DefaultMeshClusterClientFactory(),
|
||||
// Resolved lazily, per message, so this actor never depends on the order in
|
||||
// which Akka.Hosting materialises the singleton proxy relative to this block.
|
||||
() => registry.TryGet<ConfigPublishCoordinatorKey>(out var coordinator)
|
||||
? coordinator
|
||||
: null),
|
||||
MeshPaths.CentralCommunicationName);
|
||||
|
||||
// Registered unconditionally, in both transport modes. Under Dps nothing dials it, and
|
||||
// an idle registration costs nothing — whereas registering only under ClusterClient
|
||||
// would mean flipping the flag on a running fleet requires a central restart before any
|
||||
// node can ack, turning a config change back into a deployment.
|
||||
ClusterClientReceptionist.Get(system).RegisterService(actor);
|
||||
registry.Register<CentralCommunicationActorKey>(actor);
|
||||
});
|
||||
|
||||
builder.WithSingleton<ConfigPublishCoordinatorKey>(
|
||||
ConfigPublishSingletonName,
|
||||
(system, registry, resolver) =>
|
||||
{
|
||||
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
|
||||
return ConfigPublishCoordinator.Props(dbFactory);
|
||||
// Registered above, so it is already in the registry — the same
|
||||
// ordering-by-registration the AdminOperations singleton relies on to resolve the
|
||||
// coordinator. Deliberately NOT an ActorSelection.ResolveOne here: that blocks
|
||||
// inside a props factory and races the spawn it is waiting for.
|
||||
var meshRouter = registry.Get<CentralCommunicationActorKey>();
|
||||
return ConfigPublishCoordinator.Props(dbFactory, applyDeadline: null, meshRouter: meshRouter);
|
||||
},
|
||||
singletonOptions);
|
||||
|
||||
@@ -74,7 +111,8 @@ public static class ServiceCollectionExtensions
|
||||
var mode = Enum.TryParse<TagConfigValidationMode>(
|
||||
resolver.GetService<IConfiguration>()?["Deployment:TagConfigValidationMode"],
|
||||
ignoreCase: true, out var m) ? m : TagConfigValidationMode.Warn;
|
||||
return AdminOperationsActor.Props(dbFactory, coordinator, probes, mode);
|
||||
var meshRouter = registry.Get<CentralCommunicationActorKey>();
|
||||
return AdminOperationsActor.Props(dbFactory, coordinator, probes, mode, meshRouter);
|
||||
},
|
||||
singletonOptions);
|
||||
|
||||
@@ -117,38 +155,6 @@ public static class ServiceCollectionExtensions
|
||||
singletonOptions,
|
||||
createProxyToo: false);
|
||||
|
||||
// Per-cluster mesh Phase 2: the central end of the central↔node command boundary.
|
||||
//
|
||||
// A plain WithActors, NOT a singleton — and that is the load-bearing part. A driver node's
|
||||
// ClusterClient rotates across contact points and must find a live comm actor at whichever
|
||||
// central node answers. As a singleton it would exist on one central node only, and the
|
||||
// rotation would fail silently against the other: acks would land sometimes, depending on
|
||||
// which contact the client happened to settle on.
|
||||
builder.WithActors((system, registry, resolver) =>
|
||||
{
|
||||
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
|
||||
var options = resolver.GetService<IOptions<MeshTransportOptions>>().Value;
|
||||
|
||||
var actor = system.ActorOf(
|
||||
CentralCommunicationActor.Props(
|
||||
dbFactory,
|
||||
options,
|
||||
new DefaultMeshClusterClientFactory(),
|
||||
// Resolved lazily, per message, so this actor never depends on the order in
|
||||
// which Akka.Hosting materialises the singleton proxy relative to this block.
|
||||
() => registry.TryGet<ConfigPublishCoordinatorKey>(out var coordinator)
|
||||
? coordinator
|
||||
: null),
|
||||
MeshPaths.CentralCommunicationName);
|
||||
|
||||
// Registered unconditionally, in both transport modes. Under Dps nothing dials it, and
|
||||
// an idle registration costs nothing — whereas registering only under ClusterClient
|
||||
// would mean flipping the flag on a running fleet requires a central restart before any
|
||||
// node can ack, turning a config change back into a deployment.
|
||||
ClusterClientReceptionist.Get(system).RegisterService(actor);
|
||||
registry.Register<CentralCommunicationActorKey>(actor);
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user