feat(grpc): central_control.proto + DTO mapper for the 7 site→central control RPCs (T1A.1)
Phase 1A of the ClusterClient→gRPC migration needs a wire contract for the seven messages SiteCommunicationActor forwards to /user/central-communication. This lands the contract and its mapper only — hosting (T1A.2) and the site-side transport seam (T1A.3) follow. `Protos/central_control.proto` (package scadabridge.centralcontrol.v1, service CentralControlService) declares SubmitNotification, QueryNotificationStatus, IngestAuditEvents, IngestCachedTelemetry, ReconcileSite, ReportSiteHealth and Heartbeat. Note the direction is the inverse of SiteStreamService: here the site dials and central serves. Decisions worth recording: - The two ingest RPCs IMPORT sitestream.proto and reuse AuditEventBatch / CachedTelemetryBatch / IngestAck rather than redeclaring them. The site telemetry actor already builds those messages, so a second copy would fork one wire contract into two kept in lockstep by hand. ForwardState / IngestedAtUtc stay off-wire exactly as they are today. - Heartbeat replies google.protobuf.Empty — it is fire-and-forget and must never surface a fault onto the heartbeat timer path. - The three NULLABLE SiteHealthReport collections travel inside single-field wrapper messages (ConnectionEndpointMapDto / TagQualityMapDto / NodeStatusListDto). proto3 cannot express presence on repeated/map fields, but null and empty genuinely differ here — SiteHealthCollector emits `ClusterNodes: _clusterNodes?.ToList()` and the central health surface reads null as "not reported", not as "reported empty". Same reasoning drives the BoolValue/Int64Value/DoubleValue wrappers on LocalDbReplicationConnected, LocalDbOplogBacklog and the two age gauges, whose docs are explicit that null is not zero/false. - ConnectionHealthEnum reserves 0 for UNSPECIFIED instead of mapping Connected onto it, and the decoder resolves anything unknown to ConnectionHealth.Error. An unrecognised connection state must not render as "healthy". - Guid? execution ids travel as "D" strings with empty meaning null; a malformed non-empty value throws rather than being laundered into "no correlation". - DateTimeOffset normalizes to a UTC instant (a protobuf Timestamp has no offset). Lossless in practice — every producer stamps UTC — and documented + asserted rather than left implicit. SiteCallDtoMapper gains a ToDto(SiteCall) overload. Its doc comment previously asserted such a method "would be dead code"; that held only while ClusterClient was the sole path from IngestCachedTelemetryCommand (which carries SiteCall, not SiteCallOperational) to central. Comment corrected alongside. Golden tests round-trip every message through a real protobuf encode/decode — DTO → proto → bytes → proto → DTO — with a fully-populated case and a null/empty/minimal case for every optional member. Verified to have teeth by mutation: dropping a scalar, collapsing an empty nullable collection to absent, and nulling a gauge each fail a test. Codegen stays CHECKED IN under CentralControlGrpc/ (protoc segfaults in the linux_arm64 Docker image); no active <Protobuf> item is committed. docker/regen-proto.sh is generalized to `regen-proto.sh [sitestream| centralcontrol|all]` — it now injects the ItemGroup rather than unwrapping a comment, so it no longer depends on there being exactly one Protobuf line, and it restores the csproj verbatim on every exit path.
This commit is contained in:
@@ -32,20 +32,30 @@
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.HealthMonitoring/ZB.MOM.WW.ScadaBridge.HealthMonitoring.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- gRPC proto generation. The compiled C# is checked in under
|
||||
SiteStreamGrpc/ (Sitestream.cs + SitestreamGrpc.cs) because protoc
|
||||
segfaults inside our linux_arm64 Docker build image. To regenerate
|
||||
after schema changes:
|
||||
1. Temporarily uncomment the Protobuf ItemGroup below.
|
||||
2. Delete SiteStreamGrpc/*.cs.
|
||||
<!-- gRPC proto generation. The compiled C# is checked in — SiteStreamGrpc/
|
||||
(Sitestream.cs + SitestreamGrpc.cs) for Protos/sitestream.proto, and
|
||||
CentralControlGrpc/ (CentralControl.cs + CentralControlGrpc.cs) for
|
||||
Protos/central_control.proto — because protoc segfaults inside our
|
||||
linux_arm64 Docker build image. To regenerate after schema changes run
|
||||
`docker/regen-proto.sh [sitestream|centralcontrol|all]`, which does all
|
||||
of the following and always leaves this file as it found it:
|
||||
1. Temporarily uncomment the Protobuf ItemGroup below (just the line
|
||||
for the proto you changed — the other file's checked-in C# is
|
||||
already compiled, so enabling both at once duplicates types).
|
||||
2. Delete the matching checked-in *.cs.
|
||||
3. `dotnet build` (on macOS) — Grpc.Tools writes fresh files to obj/.
|
||||
4. Copy obj/Debug/net10.0/Protos/*.cs into SiteStreamGrpc/.
|
||||
4. Copy obj/Debug/net10.0/Protos/*.cs into the matching folder.
|
||||
5. Re-comment the ItemGroup.
|
||||
Eventually we should switch the Docker build image to one with a
|
||||
working protoc on arm64. -->
|
||||
central_control.proto imports sitestream.proto, so protoc resolves it
|
||||
from the project-relative path without sitestream.proto needing its own
|
||||
Protobuf item.
|
||||
An ACTIVE Protobuf item must never be committed — it breaks the Docker
|
||||
image build. Eventually we should switch the Docker build image to one
|
||||
with a working protoc on arm64. -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos\sitestream.proto" GrpcServices="Both" />
|
||||
<Protobuf Include="Protos\central_control.proto" GrpcServices="Both" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user