feat(comm): Phase 4 — delete Akka ClusterClient site↔central transport, gRPC-only
ClusterClient→gRPC migration Phase 4 (docs/plans/2026-07-22-clusterclient-to-grpc-plan.md). Phases 2/3 proved both directions on gRPC; this removes the Akka transport underneath. Deleted: - AkkaCentralTransport, AkkaSiteTransport (+ their dedicated tests) - ISiteClientFactory + DefaultSiteClientFactory; CentralCommunicationActor legacy ctor + SelectTransport (Host now builds GrpcSiteTransport and injects it) - ClusterClient creation + both ClusterClientReceptionist.RegisterService calls in AkkaHostedService; the RegisterCentralClient message + receive block - CommunicationOptions.CentralContactPoints; the CentralTransport/SiteTransport coexistence flags; the CentralTransportMode/SiteTransportKind enums gRPC is now the only site↔central transport (site→central CentralControlService via GrpcCentralTransport; central→site SiteCommandService via GrpcSiteTransport), both built unconditionally by the Host. NoOpCentralTransport is the fail-loud null-default so TestKit command-dispatch suites still construct the site actor without a wired transport; production always injects GrpcCentralTransport. Config: CentralGrpcEndpoints is now unconditional — CommunicationOptionsValidator rejects blank entries (role-agnostic), and StartupValidator requires a Site node to list >=1 endpoint (fail-fast, mirrors GrpcPsk). Rig configs moved CentralContactPoints -> CentralGrpcEndpoints (docker x6, docker-env2 x2, Host default, deploy/wonder-app-vd03). Kept Akka.Cluster.Tools (ClusterSingleton still used). Tests: build 0/0; Communication.Tests 640, Host.Tests 421 green. Removed the ClusterClient.Send per-site-routing tests (covered by the transport suites), swapped the ISiteClientFactory-based ctors to a substitute ISiteCommandTransport, converted the audit-push integration relay to an in-process bridge transport. Docs: Component-Communication/Host/StoreAndForward, components/Communication, topology-guide, grpc_streams (SUPERSEDED note), the frame-size known-issue (retired amendment), and CLAUDE.md transport decisions. Not included: the dead IntegrationCallRequest path (#32) is a separate user-owned behavioral decision — SiteEnvelope routing is transport-agnostic so it still compiles.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Cluster.Tools.Client;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
|
||||
@@ -109,100 +109,11 @@ public class SiteCommunicationActorTests : TestKit
|
||||
handlerProbe.ExpectMsg<IntegrationCallRequest>(msg => msg.CorrelationId == "corr1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationSubmit_WithCentralClient_ForwardedToCentralAndAckRoutedBack()
|
||||
{
|
||||
// The site forwards a buffered notification to central over the ClusterClient
|
||||
// command/control transport; the central ack must route back to the original
|
||||
// sender (the S&F forwarder's Ask), not to the SiteCommunicationActor.
|
||||
var dmProbe = CreateTestProbe();
|
||||
var centralClientProbe = CreateTestProbe();
|
||||
var siteActor = Sys.ActorOf(Props.Create(() =>
|
||||
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
||||
|
||||
siteActor.Tell(new RegisterCentralClient(centralClientProbe.Ref));
|
||||
|
||||
var submit = new NotificationSubmit(
|
||||
"notif-1", "Operators", "Subj", "Body", "site1", "inst1", "alarmScript",
|
||||
DateTimeOffset.UtcNow);
|
||||
siteActor.Tell(submit);
|
||||
|
||||
// Central client (acting as ClusterClient) receives a ClusterClient.Send wrapping
|
||||
// the NotificationSubmit, addressed to the central communication actor. Fish past
|
||||
// any periodic HeartbeatMessage the actor's timer may interleave.
|
||||
var send = centralClientProbe.FishForMessage<ClusterClient.Send>(
|
||||
s => s.Message is NotificationSubmit);
|
||||
Assert.Equal("/user/central-communication", send.Path);
|
||||
var forwarded = Assert.IsType<NotificationSubmit>(send.Message);
|
||||
Assert.Equal("notif-1", forwarded.NotificationId);
|
||||
|
||||
// The ack is sent to the ClusterClient.Send's Sender — replying as that probe
|
||||
// must land back at the test actor (the original Tell sender).
|
||||
centralClientProbe.Reply(new NotificationSubmitAck("notif-1", Accepted: true, Error: null));
|
||||
ExpectMsg<NotificationSubmitAck>(ack => ack.NotificationId == "notif-1" && ack.Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationSubmit_WithoutCentralClient_RepliesWithNonAccepted()
|
||||
{
|
||||
// No ClusterClient registered yet: the submit cannot be forwarded, so the actor
|
||||
// replies with a non-accepted ack and the S&F forwarder treats it as transient.
|
||||
var dmProbe = CreateTestProbe();
|
||||
var siteActor = Sys.ActorOf(Props.Create(() =>
|
||||
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
||||
|
||||
var submit = new NotificationSubmit(
|
||||
"notif-2", "Operators", "Subj", "Body", "site1", null, null,
|
||||
DateTimeOffset.UtcNow);
|
||||
siteActor.Tell(submit);
|
||||
|
||||
ExpectMsg<NotificationSubmitAck>(ack => ack.NotificationId == "notif-2" && !ack.Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationStatusQuery_WithCentralClient_ForwardedToCentralAndResponseRoutedBack()
|
||||
{
|
||||
// Notify.Status(id) issues a NotificationStatusQuery; the site actor forwards it
|
||||
// to central over the ClusterClient command/control transport and the central
|
||||
// response must route back to the original sender (the helper's Ask).
|
||||
var dmProbe = CreateTestProbe();
|
||||
var centralClientProbe = CreateTestProbe();
|
||||
var siteActor = Sys.ActorOf(Props.Create(() =>
|
||||
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
||||
|
||||
siteActor.Tell(new RegisterCentralClient(centralClientProbe.Ref));
|
||||
|
||||
var query = new NotificationStatusQuery("corr-99", "notif-1");
|
||||
siteActor.Tell(query);
|
||||
|
||||
var send = centralClientProbe.FishForMessage<ClusterClient.Send>(
|
||||
s => s.Message is NotificationStatusQuery);
|
||||
Assert.Equal("/user/central-communication", send.Path);
|
||||
var forwarded = Assert.IsType<NotificationStatusQuery>(send.Message);
|
||||
Assert.Equal("notif-1", forwarded.NotificationId);
|
||||
|
||||
// The response is sent to the ClusterClient.Send's Sender — replying as that
|
||||
// probe must land back at the test actor (the original Tell sender).
|
||||
centralClientProbe.Reply(new NotificationStatusResponse(
|
||||
"corr-99", Found: true, Status: "Delivered", RetryCount: 0,
|
||||
LastError: null, DeliveredAt: DateTimeOffset.UtcNow));
|
||||
ExpectMsg<NotificationStatusResponse>(r => r.CorrelationId == "corr-99" && r.Found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotificationStatusQuery_WithoutCentralClient_RepliesWithNotFound()
|
||||
{
|
||||
// No ClusterClient registered yet: the query cannot reach central, so the actor
|
||||
// replies Found: false. Notify.Status then falls back to the site S&F buffer.
|
||||
var dmProbe = CreateTestProbe();
|
||||
var siteActor = Sys.ActorOf(Props.Create(() =>
|
||||
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
||||
|
||||
siteActor.Tell(new NotificationStatusQuery("corr-100", "notif-2"));
|
||||
|
||||
ExpectMsg<NotificationStatusResponse>(
|
||||
r => r.CorrelationId == "corr-100" && !r.Found);
|
||||
}
|
||||
// The site→central send-forwarding tests that used to live here (NotificationSubmit /
|
||||
// NotificationStatusQuery, with and without a registered central ClusterClient) were removed
|
||||
// with the Akka transport in the ClusterClient→gRPC migration's Phase 4: the actor no longer
|
||||
// owns the wire, it delegates to an injected ICentralTransport, and those seven sends (plus the
|
||||
// reply routing and the fail-loud fallback) are now covered by SiteCommunicationActorTransportTests.
|
||||
|
||||
[Fact]
|
||||
public void EventLogQuery_WithoutHandler_ReturnsFailure()
|
||||
@@ -355,22 +266,22 @@ public class SiteCommunicationActorTests : TestKit
|
||||
// leader check in production); tests inject a stub so they do not need
|
||||
// to bring up a full cluster in the TestKit ActorSystem.
|
||||
var dmProbe = CreateTestProbe();
|
||||
var centralClientProbe = CreateTestProbe();
|
||||
var transport = Substitute.For<ICentralTransport>();
|
||||
var fastHeartbeatOptions = new CommunicationOptions
|
||||
{
|
||||
TransportHeartbeatInterval = TimeSpan.FromMilliseconds(50)
|
||||
ApplicationHeartbeatInterval = TimeSpan.FromMilliseconds(100)
|
||||
};
|
||||
|
||||
var siteActor = Sys.ActorOf(Props.Create(() =>
|
||||
new SiteCommunicationActor("site1", fastHeartbeatOptions, dmProbe.Ref, () => isActive)));
|
||||
Sys.ActorOf(Props.Create(() =>
|
||||
new SiteCommunicationActor("site1", fastHeartbeatOptions, dmProbe.Ref, () => isActive, null, transport)));
|
||||
|
||||
siteActor.Tell(new RegisterCentralClient(centralClientProbe.Ref));
|
||||
|
||||
var send = centralClientProbe.FishForMessage<ClusterClient.Send>(
|
||||
s => s.Message is HeartbeatMessage, TimeSpan.FromSeconds(3));
|
||||
var heartbeat = Assert.IsType<HeartbeatMessage>(send.Message);
|
||||
Assert.Equal(isActive, heartbeat.IsActive);
|
||||
Assert.Equal("site1", heartbeat.SiteId);
|
||||
// The heartbeat now rides the injected ICentralTransport (SendHeartbeat), not a
|
||||
// ClusterClient. The captured message must carry the injected active/standby flag.
|
||||
AwaitAssert(
|
||||
() => transport.Received().SendHeartbeat(
|
||||
Arg.Is<HeartbeatMessage>(h => h.IsActive == isActive && h.SiteId == "site1"),
|
||||
Arg.Any<IActorRef>()),
|
||||
TimeSpan.FromSeconds(3));
|
||||
}
|
||||
|
||||
// ---- Central→site failover relay (Task 10) ----------------------------------
|
||||
|
||||
Reference in New Issue
Block a user