feat(mesh): mesh_transport.v1 proto contract

This commit is contained in:
Joseph Doherty
2026-08-06 02:53:50 -04:00
parent 9e9a963255
commit c0ccfa27fb
4 changed files with 201 additions and 0 deletions
@@ -0,0 +1,33 @@
using ZB.MOM.WW.OtOpcUa.Commons.Protos.MeshTransport.V1;
namespace ZB.MOM.WW.OtOpcUa.Commons.Protos;
/// <summary>
/// The single source of truth for which <see cref="CommandEnvelope.CommandOneofCase"/> variants
/// the cross-mesh command transport actually handles. Both the contract-lock test (which proves
/// this list stays in lock-step with the generated oneof) and the oneof↔domain mapper reference
/// this array, so adding a fifth command to <c>mesh_transport.proto</c> forces a matching entry
/// here or the build/test goes red.
/// </summary>
/// <remarks>
/// Mirrors <see cref="TelemetryProtoContract"/>, and for a sharper reason: a telemetry variant
/// nobody handles loses an observation, whereas a command variant nobody handles loses a
/// deployment, a driver restart or an alarm acknowledgement — silently, since the sender's ack
/// only ever meant "received".
/// </remarks>
public static class MeshTransportProtoContract
{
/// <summary>
/// Exactly the four cross-mesh command oneof cases mirrored from the domain records:
/// DispatchDeployment / RestartDriver / ReconnectDriver / AlarmCommand. The fifth message on
/// this contract, <c>ApplyAckMessage</c>, travels the opposite direction on its own RPC and is
/// not a oneof member, so it is deliberately absent from this list.
/// </summary>
public static readonly CommandEnvelope.CommandOneofCase[] HandledCases =
[
CommandEnvelope.CommandOneofCase.DispatchDeployment,
CommandEnvelope.CommandOneofCase.RestartDriver,
CommandEnvelope.CommandOneofCase.ReconnectDriver,
CommandEnvelope.CommandOneofCase.AlarmCommand,
];
}
@@ -0,0 +1,118 @@
syntax = "proto3";
package mesh_transport.v1;
option csharp_namespace = "ZB.MOM.WW.OtOpcUa.Commons.Protos.MeshTransport.V1";
import "google/protobuf/timestamp.proto";
// ClusterClient→gRPC migration Phase 0: the cross-mesh command plane. The five messages that cross
// an application-Cluster boundary today ride an Akka ClusterClient receptionist; this contract is
// the replacement carrier. Four are central→node commands, one is the node→central deployment ack.
//
// The C# namespace deliberately says "MeshTransport", never "MeshCommand" — the latter is already
// the CLR envelope record ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh.MeshCommand, and a generated type
// of the same name would collide on every file that touches both sides of the mapper.
//
// Additive-only field evolution: never renumber or reuse a tag; a pre-field peer must read a new
// field's proto3 default correctly (which is why nullable domain members are modelled with `optional`
// for explicit presence, and why every enum reserves 0 as UNSPECIFIED). The oneof is locked by
// MeshTransportProtoContract + its test in the Commons test project.
// Central → node. Delivered to the node's NodeCommunicationActor, which re-emits the decoded message
// on the node's EventStream. ONE-WAY semantics, preserved verbatim from the ClusterClient transport:
// the ack means "received and re-emitted", nothing more — not applied, not acknowledged by any
// subscriber. No retry is layered on a failure; a failed Execute is logged per node and dropped,
// which is an honest upgrade over the receptionist's silent drop rather than a semantic change.
service NodeCommandService {
rpc Execute (CommandEnvelope) returns (ExecuteAck);
}
// Node → central. Delivered to central's CentralCommunicationActor, which forwards to the
// ConfigPublishCoordinator singleton proxy — so an ack landing on either node of the central pair
// routes identically, exactly as the receptionist made arrival-node-agnostic. ONE-WAY semantics: the
// ack means "handed to the coordinator", not "the deployment is complete".
service CentralCommandService {
rpc ReportApply (ApplyAckMessage) returns (ReportAck);
}
message CommandEnvelope {
// Transport-level correlation, carried for every command including AlarmCommandMessage, which is
// the one CLR record with no correlation member of its own. The other three ALSO carry their own
// domain correlation field-for-field below; the duplication is deliberate, so each command stays
// independently round-trippable through the mapper and the envelope stays loggable on its own.
string correlation_id = 1;
oneof command {
DispatchDeploymentCommand dispatch_deployment = 2; // <- Messages.Deploy.DispatchDeployment
RestartDriverCommand restart_driver = 3; // <- Messages.Admin.RestartDriver
ReconnectDriverCommand reconnect_driver = 4; // <- Messages.Admin.ReconnectDriver
AlarmCommandMessage alarm_command = 5; // <- OpcUa.AlarmCommand
}
}
// Deliberately empty. A non-OK gRPC status is the ONLY failure signal on this call — putting a
// success flag in the body would invite a caller to read OK+false as a distinct outcome the
// fire-and-forget contract does not have. Extensible later without a version bump.
message ExecuteAck {
}
// Deliberately empty, for the same reason as ExecuteAck.
message ReportAck {
}
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy.DispatchDeployment.
message DispatchDeploymentCommand {
string deployment_id = 1; // DeploymentId (Guid) in its "N" hex form
string revision_hash = 2; // RevisionHash — lowercase 64-char SHA-256 hex
string correlation_id = 3; // CorrelationId (Guid) in its "N" hex form
}
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin.RestartDriver.
message RestartDriverCommand {
string cluster_id = 1;
string driver_instance_id = 2;
string actor_by_user_name = 3;
string correlation_id = 4; // Guid in the record; "N" hex form
}
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin.ReconnectDriver.
message ReconnectDriverCommand {
string cluster_id = 1;
string driver_instance_id = 2;
string actor_by_user_name = 3;
string correlation_id = 4; // Guid in the record; "N" hex form
}
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.OpcUa.AlarmCommand.
message AlarmCommandMessage {
string alarm_id = 1;
// The Part 9 operation (Acknowledge / Confirm / OneShotShelve / TimedShelve / Unshelve / Enable /
// Disable / AddComment). A `string` in the CLR record, and kept as one here — the same call the
// telemetry contract makes for transition_kind / state / level. Promoting it to a proto enum would
// put a lossy translation in the mapper for a set the CLR side does not constrain: an operation the
// enum did not know would decode to UNSPECIFIED and the command would be dropped, which is the
// exact silent-drop class this migration exists to remove.
string operation = 2;
string user = 3;
optional string comment = 4; // nullable in the record — presence distinguishes null from ""
google.protobuf.Timestamp unshelve_at_utc = 5; // DateTime? — absent Timestamp encodes null
}
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy.ApplyAck.
message ApplyAckMessage {
string deployment_id = 1; // DeploymentId (Guid) in its "N" hex form
string node_id = 2; // NodeId — a logical host name, already a string
ApplyOutcome outcome = 3;
optional string failure_reason = 4; // nullable in the record — presence distinguishes null from ""
string correlation_id = 5; // CorrelationId (Guid) in its "N" hex form
}
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy.ApplyAckOutcome, shifted by one so 0 stays
// meaningless. proto3 decodes an absent enum field as 0; if 0 were Applied, an ack that lost the
// field would report a deployment applied when the node said nothing at all — and the deploy path
// seals on exactly that signal.
enum ApplyOutcome {
APPLY_OUTCOME_UNSPECIFIED = 0;
APPLY_OUTCOME_APPLIED = 1;
APPLY_OUTCOME_FAILED = 2;
}
@@ -28,6 +28,7 @@
<ItemGroup>
<Protobuf Include="Protos\deployment_artifact.proto" GrpcServices="Both"/>
<Protobuf Include="Protos\telemetry.proto" GrpcServices="Both"/>
<Protobuf Include="Protos\mesh_transport.proto" GrpcServices="Both"/>
</ItemGroup>
<ItemGroup>