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:
Joseph Doherty
2026-07-23 12:54:32 -04:00
parent 3a8ddb7087
commit 7fd5cb2b56
42 changed files with 479 additions and 1295 deletions
+15 -4
View File
@@ -1,10 +1,21 @@
# gRPC Streaming Channel: Site → Central Real-Time Data
> **Status update (2026-07-23) — the "ClusterClient keeps command/control" framing is SUPERSEDED.**
> This plan introduced gRPC for *streaming only*, leaving command/control on Akka ClusterClient. That
> split no longer holds: **Phase 4 of the ClusterClient→gRPC migration moved command/control to gRPC
> too and deleted the ClusterClient site↔central transport entirely.** Command/control now rides gRPC
> in both directions — site→central to the central-hosted `CentralControlService` (`GrpcCentralTransport`)
> and central→site to the site-hosted `SiteCommandService` (`GrpcSiteTransport`) — alongside the
> streaming `SiteStreamService` this plan describes. Wherever the text below says "ClusterClient handles
> command/control", read it as "gRPC command/control (`CentralControlService` / `SiteCommandService`)".
> See `docs/requirements/Component-Communication.md` and
> `docs/plans/2026-07-22-clusterclient-to-grpc-plan.md`.
## Context
Debug streaming events currently flow through Akka.NET ClusterClient (`InstanceActor → SiteCommunicationActor → ClusterClient.Send → CentralCommunicationActor → bridge actor`). ClusterClient wasn't built for high-throughput value streaming — it's a cluster coordination tool with gossip-based routing. As we scale beyond debug view to health streaming, alarm feeds, or future live dashboards, pushing all real-time data through ClusterClient will become a bottleneck.
**Goal**: Add a dedicated gRPC server-streaming channel on each site node. Central subscribes to sites over gRPC for real-time data. ClusterClient continues to handle command/control (subscribe, unsubscribe, deploy, lifecycle) but all streaming values flow through the gRPC channel.
**Goal**: Add a dedicated gRPC server-streaming channel on each site node. Central subscribes to sites over gRPC for real-time data. Command/control (subscribe, unsubscribe, deploy, lifecycle) and streaming values flow over gRPC. *(As originally written, command/control stayed on ClusterClient; Phase 4 later moved it to gRPC — see the status note above.)*
**Scope**: General-purpose site→central streaming transport. Debug view is the first consumer, but the proto and server are designed so future features (health streaming, alarm feeds, live dashboards) can subscribe with different event types and filters.
@@ -57,7 +68,7 @@ flowchart TD
class PB dec
```
**Key separation**: ClusterClient handles subscribe/unsubscribe/snapshot (request-response). gRPC handles the ongoing value stream (server-streaming).
**Key separation**: command/control handles subscribe/unsubscribe/snapshot (request-response); the ongoing value stream is server-streaming. Both ride gRPC after Phase 4 (subscribe/unsubscribe/snapshot over `SiteCommandService`, the value stream over `SiteStreamService`); as originally drawn, the request-response leg was ClusterClient.
## Port & Address Configuration
@@ -164,7 +175,7 @@ Add corresponding columns to the sites list table. Wire `_formGrpcNodeAAddress`
### SiteStreamGrpcClientFactory
Reads `GrpcNodeAAddress` / `GrpcNodeBAddress` from the `Site` entity (loaded by `CentralCommunicationActor.LoadSiteAddressesFromDb()`) when creating per-site gRPC channels. Falls back to NodeB if NodeA connection fails (same pattern as ClusterClient dual-contact-point failover).
Reads `GrpcNodeAAddress` / `GrpcNodeBAddress` from the `Site` entity when creating per-site gRPC channels. As of Phase 4, `CentralCommunicationActor.LoadSiteAddressesFromDb()` no longer builds Akka ClusterClient contacts at all — it builds a **per-site gRPC-endpoint cache** from those same `GrpcNodeAAddress` / `GrpcNodeBAddress` columns, feeding a `SitePairChannelProvider` that both this streaming factory and the command/control `GrpcSiteTransport` (`SiteCommandService`) dial. Falls back to NodeB if NodeA connection fails.
### Docker Compose Port Allocation
@@ -364,7 +375,7 @@ public async Task<StreamSubscription> SubscribeAsync(
### Client Factory
`SiteStreamGrpcClientFactory` caches per-site `GrpcChannel` instances (same pattern as `CentralCommunicationActor._siteClients` caching per-site ClusterClient instances).
`SiteStreamGrpcClientFactory` caches per-site `GrpcChannel` instances. (As originally written this mirrored `CentralCommunicationActor._siteClients` caching per-site ClusterClient instances; after Phase 4 that dictionary caches per-site gRPC endpoints/channels, not ClusterClients.)
## Failover & Reconnection