A.3 (worker IPC slice): proto SubscribeAlarms/Acknowledge/QueryActive commands + executor routing

Adds the worker-side IPC surface for the alarm subsystem so the gateway
can drive the AlarmDispatcher across the named-pipe boundary. Adds four
proto MxCommandKind values + matching command messages and two
MxCommandReply payload variants:

- SubscribeAlarmsCommand(subscription_expression)
- UnsubscribeAlarmsCommand
- AcknowledgeAlarmCommand(alarm_guid, comment, operator_user/node/domain/full_name)
- QueryActiveAlarmsCommand(alarm_filter_prefix)
- AcknowledgeAlarmReplyPayload(native_status)
- QueryActiveAlarmsReplyPayload(repeated ActiveAlarmSnapshot snapshots)

Worker plumbing:

- New IAlarmCommandHandler interface + AlarmCommandHandler production
  impl. Lazy-creates an AlarmDispatcher (with a wnwrap-backed consumer
  by default) on the first SubscribeAlarms; routes Acknowledge / QueryActive /
  Unsubscribe through it. Idempotent under repeated Unsubscribe; rejects
  a second Subscribe without an intervening Unsubscribe; cleans up the
  consumer if the underlying Subscribe call throws.
- MxAccessCommandExecutor: 4 new switch arms map MxCommandKind values to
  IAlarmCommandHandler calls. Acknowledge surfaces the AVEVA native
  status into both MxCommandReply.Hresult and the dedicated
  AcknowledgeAlarmReplyPayload.NativeStatus so gateway-side consumers
  can echo it without unpacking the outer envelope. Invalid GUIDs and
  missing payloads return InvalidRequest; handler exceptions return
  MxaccessFailure with the exception message in DiagnosticMessage.
- MxAccessStaSession: new constructor overload accepts an
  alarmCommandHandlerFactory; it's invoked on the STA thread during
  StartAsync and the resulting handler is passed into the executor.
  ShutdownGracefullyAsync + Dispose tear it down on the STA before the
  data-side cleanup runs.

Tests: 20 new unit tests covering AlarmCommandHandler lazy lifecycle
(Subscribe/Unsubscribe/Acknowledge/Query/Dispose, error paths) and the
executor's 4 alarm switch arms (OK/InvalidRequest/MxaccessFailure paths,
hresult propagation, prefix filtering). Worker test suite total: 192
passed / 3 skipped (live probes) / 1 pre-existing structure-test fail
(untouched).

Deferred to next slice: gateway-side WorkerAlarmRpcDispatcher that
replaces NotWiredAlarmRpcDispatcher, builds + sends these commands across
the IPC, and unwraps the resulting MxCommandReply into AcknowledgeAlarmReply
/ ActiveAlarmSnapshot stream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-01 10:52:04 -04:00
parent 82eb0ad569
commit 01f5e6ad91
7 changed files with 3170 additions and 352 deletions
File diff suppressed because it is too large Load Diff
@@ -88,6 +88,10 @@ message MxCommand {
UnAdviseItemBulkCommand un_advise_item_bulk = 31;
SubscribeBulkCommand subscribe_bulk = 32;
UnsubscribeBulkCommand unsubscribe_bulk = 33;
SubscribeAlarmsCommand subscribe_alarms = 34;
UnsubscribeAlarmsCommand unsubscribe_alarms = 35;
AcknowledgeAlarmCommand acknowledge_alarm_command = 36;
QueryActiveAlarmsCommand query_active_alarms_command = 37;
PingCommand ping = 100;
GetSessionStateCommand get_session_state = 101;
GetWorkerInfoCommand get_worker_info = 102;
@@ -122,6 +126,10 @@ enum MxCommandKind {
MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK = 22;
MX_COMMAND_KIND_SUBSCRIBE_BULK = 23;
MX_COMMAND_KIND_UNSUBSCRIBE_BULK = 24;
MX_COMMAND_KIND_SUBSCRIBE_ALARMS = 25;
MX_COMMAND_KIND_UNSUBSCRIBE_ALARMS = 26;
MX_COMMAND_KIND_ACKNOWLEDGE_ALARM = 27;
MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS = 28;
MX_COMMAND_KIND_PING = 100;
MX_COMMAND_KIND_GET_SESSION_STATE = 101;
MX_COMMAND_KIND_GET_WORKER_INFO = 102;
@@ -263,6 +271,42 @@ message SubscribeBulkCommand {
repeated string tag_addresses = 2;
}
// Subscribe the worker's alarm consumer to an AVEVA alarm provider.
// Subscription expression follows the canonical
// `\\<machine>\Galaxy!<area>` format (literal "Galaxy" provider). The
// worker spins up a wnwrapConsumer-backed subscription on its STA on
// first call; subsequent calls are an error (use UnsubscribeAlarms then
// SubscribeAlarms to reconfigure).
message SubscribeAlarmsCommand {
string subscription_expression = 1;
}
// Tear down the worker's alarm consumer. No-op if no subscription is
// currently active.
message UnsubscribeAlarmsCommand {
}
// Acknowledge a single alarm by its GUID. Operator identity fields are
// recorded atomically with the ack transition in the alarm-history log.
// The reply's hresult / native_status surfaces AVEVA's
// AlarmAckByGUID return code.
message AcknowledgeAlarmCommand {
// Canonical 8-4-4-4-12 GUID string (e.g. "BCC47053-9542-4D65-BDAA-BCDEA6A32A73").
string alarm_guid = 1;
string comment = 2;
string operator_user = 3;
string operator_node = 4;
string operator_domain = 5;
string operator_full_name = 6;
}
// Snapshot the currently-active alarm set. Optional filter prefix scopes
// the snapshot to alarms whose alarm_full_reference starts with the
// supplied string (matches QueryActiveAlarmsRequest.alarm_filter_prefix).
message QueryActiveAlarmsCommand {
string alarm_filter_prefix = 1;
}
message UnsubscribeBulkCommand {
int32 server_handle = 1;
repeated int32 item_handles = 2;
@@ -314,6 +358,8 @@ message MxCommandReply {
BulkSubscribeReply un_advise_item_bulk = 31;
BulkSubscribeReply subscribe_bulk = 32;
BulkSubscribeReply unsubscribe_bulk = 33;
AcknowledgeAlarmReplyPayload acknowledge_alarm = 34;
QueryActiveAlarmsReplyPayload query_active_alarms = 35;
SessionStateReply session_state = 100;
WorkerInfoReply worker_info = 101;
DrainEventsReply drain_events = 102;
@@ -379,6 +425,24 @@ message DrainEventsReply {
repeated MxEvent events = 1;
}
// Reply payload for AcknowledgeAlarmCommand. Surfaces AVEVA's native
// AlarmAckByGUID return code; 0 means success. The MxCommandReply's
// hresult field carries the same value and is preferred for protocol
// consumers — this payload exists so the gateway-side
// WorkerAlarmRpcDispatcher can echo native_status into
// AcknowledgeAlarmReply.hresult without unpacking the outer envelope.
message AcknowledgeAlarmReplyPayload {
int32 native_status = 1;
}
// Reply payload for QueryActiveAlarmsCommand. The worker walks
// IMxAccessAlarmConsumer.SnapshotActiveAlarms and packs each record as
// an ActiveAlarmSnapshot proto for the gateway-side ConditionRefresh
// stream.
message QueryActiveAlarmsReplyPayload {
repeated ActiveAlarmSnapshot snapshots = 1;
}
message MxEvent {
MxEventFamily family = 1;
string session_id = 2;