alarms-over-gateway: full pipeline (wnwrap consumer + dispatcher + IPC + auto-subscribe + ack-by-name + live smoke) #118
File diff suppressed because it is too large
Load Diff
@@ -92,6 +92,7 @@ message MxCommand {
|
|||||||
UnsubscribeAlarmsCommand unsubscribe_alarms = 35;
|
UnsubscribeAlarmsCommand unsubscribe_alarms = 35;
|
||||||
AcknowledgeAlarmCommand acknowledge_alarm_command = 36;
|
AcknowledgeAlarmCommand acknowledge_alarm_command = 36;
|
||||||
QueryActiveAlarmsCommand query_active_alarms_command = 37;
|
QueryActiveAlarmsCommand query_active_alarms_command = 37;
|
||||||
|
AcknowledgeAlarmByNameCommand acknowledge_alarm_by_name_command = 38;
|
||||||
PingCommand ping = 100;
|
PingCommand ping = 100;
|
||||||
GetSessionStateCommand get_session_state = 101;
|
GetSessionStateCommand get_session_state = 101;
|
||||||
GetWorkerInfoCommand get_worker_info = 102;
|
GetWorkerInfoCommand get_worker_info = 102;
|
||||||
@@ -130,6 +131,7 @@ enum MxCommandKind {
|
|||||||
MX_COMMAND_KIND_UNSUBSCRIBE_ALARMS = 26;
|
MX_COMMAND_KIND_UNSUBSCRIBE_ALARMS = 26;
|
||||||
MX_COMMAND_KIND_ACKNOWLEDGE_ALARM = 27;
|
MX_COMMAND_KIND_ACKNOWLEDGE_ALARM = 27;
|
||||||
MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS = 28;
|
MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS = 28;
|
||||||
|
MX_COMMAND_KIND_ACKNOWLEDGE_ALARM_BY_NAME = 29;
|
||||||
MX_COMMAND_KIND_PING = 100;
|
MX_COMMAND_KIND_PING = 100;
|
||||||
MX_COMMAND_KIND_GET_SESSION_STATE = 101;
|
MX_COMMAND_KIND_GET_SESSION_STATE = 101;
|
||||||
MX_COMMAND_KIND_GET_WORKER_INFO = 102;
|
MX_COMMAND_KIND_GET_WORKER_INFO = 102;
|
||||||
@@ -307,6 +309,27 @@ message QueryActiveAlarmsCommand {
|
|||||||
string alarm_filter_prefix = 1;
|
string alarm_filter_prefix = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Acknowledge a single alarm by its (name, provider, group) tuple. Used
|
||||||
|
// when the public RPC's AlarmFullReference (Provider!Group.Tag) cannot
|
||||||
|
// be resolved to a GUID directly. The worker invokes
|
||||||
|
// wwAlarmConsumerClass.AlarmAckByName which reaches the same alarm
|
||||||
|
// history path as AlarmAckByGUID.
|
||||||
|
message AcknowledgeAlarmByNameCommand {
|
||||||
|
// Tag/alarm name (e.g. "TestMachine_001.TestAlarm001"). Tag itself
|
||||||
|
// may contain dots; the gateway-side parser splits on the first dot
|
||||||
|
// after the '!' separator.
|
||||||
|
string alarm_name = 1;
|
||||||
|
// AVEVA alarm-provider name (literal "Galaxy" for ArchestrA Galaxies).
|
||||||
|
string provider_name = 2;
|
||||||
|
// Area/group name (e.g. "TestArea").
|
||||||
|
string group_name = 3;
|
||||||
|
string comment = 4;
|
||||||
|
string operator_user = 5;
|
||||||
|
string operator_node = 6;
|
||||||
|
string operator_domain = 7;
|
||||||
|
string operator_full_name = 8;
|
||||||
|
}
|
||||||
|
|
||||||
message UnsubscribeBulkCommand {
|
message UnsubscribeBulkCommand {
|
||||||
int32 server_handle = 1;
|
int32 server_handle = 1;
|
||||||
repeated int32 item_handles = 2;
|
repeated int32 item_handles = 2;
|
||||||
|
|||||||
@@ -45,6 +45,39 @@ public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher
|
|||||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a full alarm reference of the form <c>Provider!Group.Tag</c>
|
||||||
|
/// into its components. Convention: the first <c>!</c> separates
|
||||||
|
/// provider from <c>Group.Tag</c>; the first <c>.</c> after the
|
||||||
|
/// <c>!</c> separates group from tag (the tag itself may contain
|
||||||
|
/// more dots — e.g. <c>TestMachine_001.TestAlarm001</c>).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>true on a well-formed reference; false otherwise.</returns>
|
||||||
|
public static bool TryParseAlarmReference(
|
||||||
|
string? reference,
|
||||||
|
out string providerName,
|
||||||
|
out string groupName,
|
||||||
|
out string alarmName)
|
||||||
|
{
|
||||||
|
providerName = string.Empty;
|
||||||
|
groupName = string.Empty;
|
||||||
|
alarmName = string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(reference)) return false;
|
||||||
|
|
||||||
|
int bang = reference!.IndexOf('!');
|
||||||
|
if (bang <= 0 || bang == reference.Length - 1) return false;
|
||||||
|
|
||||||
|
string left = reference[..bang];
|
||||||
|
string right = reference[(bang + 1)..];
|
||||||
|
int dot = right.IndexOf('.');
|
||||||
|
if (dot <= 0 || dot == right.Length - 1) return false;
|
||||||
|
|
||||||
|
providerName = left;
|
||||||
|
groupName = right[..dot];
|
||||||
|
alarmName = right[(dot + 1)..];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AcknowledgeAlarmReply> AcknowledgeAsync(
|
public async Task<AcknowledgeAlarmReply> AcknowledgeAsync(
|
||||||
AcknowledgeAlarmRequest request,
|
AcknowledgeAlarmRequest request,
|
||||||
@@ -64,25 +97,10 @@ public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!System.Guid.TryParse(request.AlarmFullReference, out System.Guid guid))
|
WorkerCommand workerCommand;
|
||||||
|
if (System.Guid.TryParse(request.AlarmFullReference, out System.Guid guid))
|
||||||
{
|
{
|
||||||
// Reference→GUID lookup not yet implemented. Surface a clear
|
workerCommand = new WorkerCommand
|
||||||
// diagnostic so client teams can plumb the reference parser
|
|
||||||
// when the worker AlarmAckByName command lands.
|
|
||||||
return new AcknowledgeAlarmReply
|
|
||||||
{
|
|
||||||
SessionId = request.SessionId,
|
|
||||||
CorrelationId = request.ClientCorrelationId,
|
|
||||||
ProtocolStatus = new ProtocolStatus
|
|
||||||
{
|
|
||||||
Code = ProtocolStatusCode.InvalidRequest,
|
|
||||||
Message = "AlarmFullReference must currently be a canonical GUID; reference→GUID lookup is pending the AlarmAckByName worker command.",
|
|
||||||
},
|
|
||||||
DiagnosticMessage = $"AcknowledgeAlarm received non-GUID reference '{request.AlarmFullReference}'.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkerCommand workerCommand = new WorkerCommand
|
|
||||||
{
|
{
|
||||||
Command = new MxCommand
|
Command = new MxCommand
|
||||||
{
|
{
|
||||||
@@ -102,6 +120,47 @@ public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher
|
|||||||
},
|
},
|
||||||
EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
|
EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
else if (TryParseAlarmReference(
|
||||||
|
request.AlarmFullReference,
|
||||||
|
out string providerName,
|
||||||
|
out string groupName,
|
||||||
|
out string alarmName))
|
||||||
|
{
|
||||||
|
workerCommand = new WorkerCommand
|
||||||
|
{
|
||||||
|
Command = new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AcknowledgeAlarmByName,
|
||||||
|
AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
|
||||||
|
{
|
||||||
|
AlarmName = alarmName,
|
||||||
|
ProviderName = providerName,
|
||||||
|
GroupName = groupName,
|
||||||
|
Comment = request.Comment ?? string.Empty,
|
||||||
|
OperatorUser = request.OperatorUser ?? string.Empty,
|
||||||
|
OperatorNode = string.Empty,
|
||||||
|
OperatorDomain = string.Empty,
|
||||||
|
OperatorFullName = string.Empty,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new AcknowledgeAlarmReply
|
||||||
|
{
|
||||||
|
SessionId = request.SessionId,
|
||||||
|
CorrelationId = request.ClientCorrelationId,
|
||||||
|
ProtocolStatus = new ProtocolStatus
|
||||||
|
{
|
||||||
|
Code = ProtocolStatusCode.InvalidRequest,
|
||||||
|
Message = "AlarmFullReference must be a canonical GUID or 'Provider!Group.Tag' format.",
|
||||||
|
},
|
||||||
|
DiagnosticMessage = $"AcknowledgeAlarm received unrecognized reference '{request.AlarmFullReference}'.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken)
|
WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|||||||
@@ -36,33 +36,6 @@ public sealed class WorkerAlarmRpcDispatcherTests
|
|||||||
Assert.Equal(ProtocolStatusCode.SessionNotFound, reply.ProtocolStatus.Code);
|
Assert.Equal(ProtocolStatusCode.SessionNotFound, reply.ProtocolStatus.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task AcknowledgeAsync_returns_invalid_request_when_reference_is_not_a_guid()
|
|
||||||
{
|
|
||||||
SessionRegistry registry = new SessionRegistry();
|
|
||||||
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient();
|
|
||||||
GatewaySession session = NewSession("s1");
|
|
||||||
session.AttachWorkerClient(worker);
|
|
||||||
session.MarkReady();
|
|
||||||
registry.TryAdd(session);
|
|
||||||
|
|
||||||
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
|
|
||||||
|
|
||||||
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
|
|
||||||
new AcknowledgeAlarmRequest
|
|
||||||
{
|
|
||||||
SessionId = "s1",
|
|
||||||
ClientCorrelationId = "c1",
|
|
||||||
AlarmFullReference = "Galaxy!Area.Tag", // not a GUID
|
|
||||||
Comment = "x",
|
|
||||||
OperatorUser = "u",
|
|
||||||
},
|
|
||||||
CancellationToken.None);
|
|
||||||
|
|
||||||
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
|
||||||
Assert.Equal(0, worker.InvokeCount); // dispatcher short-circuits before the IPC.
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AcknowledgeAsync_forwards_guid_and_returns_native_status()
|
public async Task AcknowledgeAsync_forwards_guid_and_returns_native_status()
|
||||||
{
|
{
|
||||||
@@ -148,6 +121,107 @@ public sealed class WorkerAlarmRpcDispatcherTests
|
|||||||
Assert.Contains("-123", reply.DiagnosticMessage);
|
Assert.Contains("-123", reply.DiagnosticMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Galaxy!TestArea.TestMachine_001.TestAlarm001", "Galaxy", "TestArea", "TestMachine_001.TestAlarm001")]
|
||||||
|
[InlineData("Galaxy!Area.Tag", "Galaxy", "Area", "Tag")]
|
||||||
|
[InlineData("Provider!Group.Tag.With.Dots", "Provider", "Group", "Tag.With.Dots")]
|
||||||
|
public void TryParseAlarmReference_decomposes_provider_group_tag(
|
||||||
|
string reference, string expectedProvider, string expectedGroup, string expectedName)
|
||||||
|
{
|
||||||
|
Assert.True(WorkerAlarmRpcDispatcher.TryParseAlarmReference(
|
||||||
|
reference, out string provider, out string group, out string name));
|
||||||
|
Assert.Equal(expectedProvider, provider);
|
||||||
|
Assert.Equal(expectedGroup, group);
|
||||||
|
Assert.Equal(expectedName, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("no-bang-here")]
|
||||||
|
[InlineData("!Group.Tag")] // empty provider
|
||||||
|
[InlineData("Galaxy!")] // bang at end
|
||||||
|
[InlineData("Galaxy!Group")] // missing dot
|
||||||
|
[InlineData("Galaxy!.Tag")] // empty group
|
||||||
|
[InlineData("Galaxy!Group.")] // empty tag
|
||||||
|
public void TryParseAlarmReference_rejects_malformed_references(string? reference)
|
||||||
|
{
|
||||||
|
Assert.False(WorkerAlarmRpcDispatcher.TryParseAlarmReference(
|
||||||
|
reference, out _, out _, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AcknowledgeAsync_routes_provider_group_tag_via_AckByName()
|
||||||
|
{
|
||||||
|
SessionRegistry registry = new SessionRegistry();
|
||||||
|
AcknowledgeAlarmByNameCommand? observed = null;
|
||||||
|
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient
|
||||||
|
{
|
||||||
|
ReplyFactory = command =>
|
||||||
|
{
|
||||||
|
Assert.Equal(MxCommandKind.AcknowledgeAlarmByName, command.Command.Kind);
|
||||||
|
observed = command.Command.AcknowledgeAlarmByNameCommand;
|
||||||
|
return new MxCommandReply
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AcknowledgeAlarmByName,
|
||||||
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" },
|
||||||
|
Hresult = 0,
|
||||||
|
AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
GatewaySession session = NewSession("s1");
|
||||||
|
session.AttachWorkerClient(worker);
|
||||||
|
session.MarkReady();
|
||||||
|
registry.TryAdd(session);
|
||||||
|
|
||||||
|
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
|
||||||
|
|
||||||
|
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
|
||||||
|
new AcknowledgeAlarmRequest
|
||||||
|
{
|
||||||
|
SessionId = "s1",
|
||||||
|
ClientCorrelationId = "c1",
|
||||||
|
AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001",
|
||||||
|
Comment = "ack-by-name",
|
||||||
|
OperatorUser = "bob",
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||||
|
Assert.NotNull(observed);
|
||||||
|
Assert.Equal("TestMachine_001.TestAlarm001", observed!.AlarmName);
|
||||||
|
Assert.Equal("Galaxy", observed.ProviderName);
|
||||||
|
Assert.Equal("TestArea", observed.GroupName);
|
||||||
|
Assert.Equal("bob", observed.OperatorUser);
|
||||||
|
Assert.Equal("ack-by-name", observed.Comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AcknowledgeAsync_returns_invalid_request_for_unparseable_reference()
|
||||||
|
{
|
||||||
|
SessionRegistry registry = new SessionRegistry();
|
||||||
|
FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient();
|
||||||
|
GatewaySession session = NewSession("s1");
|
||||||
|
session.AttachWorkerClient(worker);
|
||||||
|
session.MarkReady();
|
||||||
|
registry.TryAdd(session);
|
||||||
|
|
||||||
|
WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry);
|
||||||
|
|
||||||
|
AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync(
|
||||||
|
new AcknowledgeAlarmRequest
|
||||||
|
{
|
||||||
|
SessionId = "s1",
|
||||||
|
AlarmFullReference = "no-bang-no-dot",
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
||||||
|
Assert.Equal(0, worker.InvokeCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task QueryActiveAlarmsAsync_yields_each_snapshot_from_payload()
|
public async Task QueryActiveAlarmsAsync_yields_each_snapshot_from_payload()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -164,6 +164,62 @@ public sealed class AlarmCommandExecutorTests
|
|||||||
Assert.Contains("-123", reply.DiagnosticMessage);
|
Assert.Contains("-123", reply.DiagnosticMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AcknowledgeAlarmByName_routes_tuple_to_handler()
|
||||||
|
{
|
||||||
|
FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = 0 };
|
||||||
|
MxAccessCommandExecutor executor = NewExecutor(handler);
|
||||||
|
|
||||||
|
StaCommand command = new StaCommand(
|
||||||
|
SessionId, CorrelationId,
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AcknowledgeAlarmByName,
|
||||||
|
AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
|
||||||
|
{
|
||||||
|
AlarmName = "TestMachine_001.TestAlarm001",
|
||||||
|
ProviderName = "Galaxy",
|
||||||
|
GroupName = "TestArea",
|
||||||
|
Comment = "ack",
|
||||||
|
OperatorUser = "alice",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
MxCommandReply reply = executor.Execute(command);
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||||
|
Assert.NotNull(reply.AcknowledgeAlarm);
|
||||||
|
Assert.Equal(0, reply.AcknowledgeAlarm.NativeStatus);
|
||||||
|
Assert.NotNull(handler.LastAckByNameTuple);
|
||||||
|
Assert.Equal("TestMachine_001.TestAlarm001", handler.LastAckByNameTuple!.Value.Name);
|
||||||
|
Assert.Equal("Galaxy", handler.LastAckByNameTuple!.Value.Provider);
|
||||||
|
Assert.Equal("TestArea", handler.LastAckByNameTuple!.Value.Group);
|
||||||
|
Assert.Equal("alice", handler.LastAckOperatorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AcknowledgeAlarmByName_with_empty_name_returns_invalid_request()
|
||||||
|
{
|
||||||
|
MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler());
|
||||||
|
|
||||||
|
StaCommand command = new StaCommand(
|
||||||
|
SessionId, CorrelationId,
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AcknowledgeAlarmByName,
|
||||||
|
AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand
|
||||||
|
{
|
||||||
|
AlarmName = " ",
|
||||||
|
ProviderName = "Galaxy",
|
||||||
|
GroupName = "TestArea",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
MxCommandReply reply = executor.Execute(command);
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void QueryActiveAlarms_returns_payload_with_snapshots()
|
public void QueryActiveAlarms_returns_payload_with_snapshots()
|
||||||
{
|
{
|
||||||
@@ -373,6 +429,18 @@ public sealed class AlarmCommandExecutorTests
|
|||||||
return AcknowledgeReturn;
|
return AcknowledgeReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int AcknowledgeByName(
|
||||||
|
string alarmName, string providerName, string groupName,
|
||||||
|
string comment, string operatorUser, string operatorNode,
|
||||||
|
string operatorDomain, string operatorFullName)
|
||||||
|
{
|
||||||
|
LastAckByNameTuple = (alarmName, providerName, groupName);
|
||||||
|
LastAckOperatorName = operatorUser;
|
||||||
|
return AcknowledgeReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
|
||||||
|
|
||||||
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
|
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
|
||||||
{
|
{
|
||||||
LastFilterPrefix = alarmFilterPrefix;
|
LastFilterPrefix = alarmFilterPrefix;
|
||||||
|
|||||||
@@ -222,6 +222,18 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
return AcknowledgeReturn;
|
return AcknowledgeReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int AcknowledgeByName(
|
||||||
|
string alarmName, string providerName, string groupName,
|
||||||
|
string ackComment, string ackOperatorName, string ackOperatorNode,
|
||||||
|
string ackOperatorDomain, string ackOperatorFullName)
|
||||||
|
{
|
||||||
|
LastAckByNameTuple = (alarmName, providerName, groupName);
|
||||||
|
LastAckOperatorName = ackOperatorName;
|
||||||
|
return AcknowledgeReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
|
||||||
|
|
||||||
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms() => SnapshotResult;
|
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms() => SnapshotResult;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -158,6 +158,32 @@ public sealed class AlarmDispatcherTests
|
|||||||
Assert.Equal("Alice Smith", consumer.LastAckOperatorFullName);
|
Assert.Equal("Alice Smith", consumer.LastAckOperatorFullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AcknowledgeByName_forwards_to_consumer_with_full_tuple()
|
||||||
|
{
|
||||||
|
FakeAlarmConsumer consumer = new FakeAlarmConsumer { AcknowledgeReturn = 0 };
|
||||||
|
using AlarmDispatcher dispatcher = new AlarmDispatcher(
|
||||||
|
consumer,
|
||||||
|
new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()),
|
||||||
|
SessionId);
|
||||||
|
|
||||||
|
int rc = dispatcher.AcknowledgeByName(
|
||||||
|
alarmName: "TestMachine_001.TestAlarm001",
|
||||||
|
providerName: "Galaxy",
|
||||||
|
groupName: "TestArea",
|
||||||
|
ackComment: "ack",
|
||||||
|
ackOperatorName: "alice",
|
||||||
|
ackOperatorNode: "WS",
|
||||||
|
ackOperatorDomain: "CORP",
|
||||||
|
ackOperatorFullName: "Alice Smith");
|
||||||
|
|
||||||
|
Assert.Equal(0, rc);
|
||||||
|
Assert.NotNull(consumer.LastAckByNameTuple);
|
||||||
|
Assert.Equal("TestMachine_001.TestAlarm001", consumer.LastAckByNameTuple!.Value.Name);
|
||||||
|
Assert.Equal("Galaxy", consumer.LastAckByNameTuple!.Value.Provider);
|
||||||
|
Assert.Equal("TestArea", consumer.LastAckByNameTuple!.Value.Group);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SnapshotActiveAlarms_maps_records_to_protos()
|
public void SnapshotActiveAlarms_maps_records_to_protos()
|
||||||
{
|
{
|
||||||
@@ -275,6 +301,18 @@ public sealed class AlarmDispatcherTests
|
|||||||
return AcknowledgeReturn;
|
return AcknowledgeReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int AcknowledgeByName(
|
||||||
|
string alarmName, string providerName, string groupName,
|
||||||
|
string ackComment, string ackOperatorName, string ackOperatorNode,
|
||||||
|
string ackOperatorDomain, string ackOperatorFullName)
|
||||||
|
{
|
||||||
|
LastAckByNameTuple = (alarmName, providerName, groupName);
|
||||||
|
LastAckOperatorName = ackOperatorName;
|
||||||
|
return AcknowledgeReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
|
||||||
|
|
||||||
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
|
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
|
||||||
{
|
{
|
||||||
return SnapshotResult;
|
return SnapshotResult;
|
||||||
|
|||||||
@@ -120,6 +120,29 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
|
|||||||
operatorFullName ?? string.Empty);
|
operatorFullName ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int AcknowledgeByName(
|
||||||
|
string alarmName,
|
||||||
|
string providerName,
|
||||||
|
string groupName,
|
||||||
|
string comment,
|
||||||
|
string operatorUser,
|
||||||
|
string operatorNode,
|
||||||
|
string operatorDomain,
|
||||||
|
string operatorFullName)
|
||||||
|
{
|
||||||
|
AlarmDispatcher? d = GetDispatcherOrThrow();
|
||||||
|
return d.AcknowledgeByName(
|
||||||
|
alarmName ?? string.Empty,
|
||||||
|
providerName ?? string.Empty,
|
||||||
|
groupName ?? string.Empty,
|
||||||
|
comment ?? string.Empty,
|
||||||
|
operatorUser ?? string.Empty,
|
||||||
|
operatorNode ?? string.Empty,
|
||||||
|
operatorDomain ?? string.Empty,
|
||||||
|
operatorFullName ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
|
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
|
||||||
{
|
{
|
||||||
@@ -184,6 +207,20 @@ public interface IAlarmCommandHandler : IDisposable
|
|||||||
string operatorDomain,
|
string operatorDomain,
|
||||||
string operatorFullName);
|
string operatorFullName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledge a single alarm by (name, provider, group) — used when
|
||||||
|
/// the caller has the human-readable reference but not the GUID.
|
||||||
|
/// </summary>
|
||||||
|
int AcknowledgeByName(
|
||||||
|
string alarmName,
|
||||||
|
string providerName,
|
||||||
|
string groupName,
|
||||||
|
string comment,
|
||||||
|
string operatorUser,
|
||||||
|
string operatorNode,
|
||||||
|
string operatorDomain,
|
||||||
|
string operatorFullName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Snapshot the currently-active alarm set, optionally scoped to a
|
/// Snapshot the currently-active alarm set, optionally scoped to a
|
||||||
/// prefix matched against <c>AlarmFullReference</c>.
|
/// prefix matched against <c>AlarmFullReference</c>.
|
||||||
|
|||||||
@@ -92,6 +92,33 @@ public sealed class AlarmDispatcher : IDisposable
|
|||||||
ackOperatorFullName);
|
ackOperatorFullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledge an alarm by its (name, provider, group) tuple.
|
||||||
|
/// Routes to the consumer's <c>AcknowledgeByName</c> path which
|
||||||
|
/// maps to <c>wwAlarmConsumerClass.AlarmAckByName</c>.
|
||||||
|
/// </summary>
|
||||||
|
public int AcknowledgeByName(
|
||||||
|
string alarmName,
|
||||||
|
string providerName,
|
||||||
|
string groupName,
|
||||||
|
string ackComment,
|
||||||
|
string ackOperatorName,
|
||||||
|
string ackOperatorNode,
|
||||||
|
string ackOperatorDomain,
|
||||||
|
string ackOperatorFullName)
|
||||||
|
{
|
||||||
|
if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher));
|
||||||
|
return consumer.AcknowledgeByName(
|
||||||
|
alarmName,
|
||||||
|
providerName,
|
||||||
|
groupName,
|
||||||
|
ackComment,
|
||||||
|
ackOperatorName,
|
||||||
|
ackOperatorNode,
|
||||||
|
ackOperatorDomain,
|
||||||
|
ackOperatorFullName);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Snapshot the currently-active alarm set as
|
/// Snapshot the currently-active alarm set as
|
||||||
/// <see cref="ActiveAlarmSnapshot"/> protos for the
|
/// <see cref="ActiveAlarmSnapshot"/> protos for the
|
||||||
|
|||||||
@@ -61,6 +61,23 @@ public interface IMxAccessAlarmConsumer : IDisposable
|
|||||||
string ackOperatorDomain,
|
string ackOperatorDomain,
|
||||||
string ackOperatorFullName);
|
string ackOperatorFullName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledge a single alarm by its (name, provider, group) tuple.
|
||||||
|
/// Reaches AVEVA's <c>AlarmAckByName</c> on
|
||||||
|
/// <c>wwAlarmConsumerClass</c>; same alarm-history outcome as
|
||||||
|
/// <see cref="AcknowledgeByGuid"/>, used when the caller has the
|
||||||
|
/// human-readable reference but not the canonical GUID.
|
||||||
|
/// </summary>
|
||||||
|
int AcknowledgeByName(
|
||||||
|
string alarmName,
|
||||||
|
string providerName,
|
||||||
|
string groupName,
|
||||||
|
string ackComment,
|
||||||
|
string ackOperatorName,
|
||||||
|
string ackOperatorNode,
|
||||||
|
string ackOperatorDomain,
|
||||||
|
string ackOperatorFullName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the consumer's most recently parsed snapshot of currently
|
/// Returns the consumer's most recently parsed snapshot of currently
|
||||||
/// active alarms. Used by the gateway's QueryActiveAlarms (PR A.7)
|
/// active alarms. Used by the gateway's QueryActiveAlarms (PR A.7)
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
|||||||
MxCommandKind.SubscribeAlarms => ExecuteSubscribeAlarms(command),
|
MxCommandKind.SubscribeAlarms => ExecuteSubscribeAlarms(command),
|
||||||
MxCommandKind.UnsubscribeAlarms => ExecuteUnsubscribeAlarms(command),
|
MxCommandKind.UnsubscribeAlarms => ExecuteUnsubscribeAlarms(command),
|
||||||
MxCommandKind.AcknowledgeAlarm => ExecuteAcknowledgeAlarm(command),
|
MxCommandKind.AcknowledgeAlarm => ExecuteAcknowledgeAlarm(command),
|
||||||
|
MxCommandKind.AcknowledgeAlarmByName => ExecuteAcknowledgeAlarmByName(command),
|
||||||
MxCommandKind.QueryActiveAlarms => ExecuteQueryActiveAlarms(command),
|
MxCommandKind.QueryActiveAlarms => ExecuteQueryActiveAlarms(command),
|
||||||
_ => CreateInvalidRequestReply(command, $"Unsupported MXAccess command kind {command.Kind}."),
|
_ => CreateInvalidRequestReply(command, $"Unsupported MXAccess command kind {command.Kind}."),
|
||||||
};
|
};
|
||||||
@@ -402,6 +403,54 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MxCommandReply ExecuteAcknowledgeAlarmByName(StaCommand command)
|
||||||
|
{
|
||||||
|
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AcknowledgeAlarmByNameCommand)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "AcknowledgeAlarmByName command payload is required.");
|
||||||
|
}
|
||||||
|
if (alarmCommandHandler is null)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(
|
||||||
|
command,
|
||||||
|
"AcknowledgeAlarmByName requires an alarm command handler; the worker was constructed without one.");
|
||||||
|
}
|
||||||
|
|
||||||
|
AcknowledgeAlarmByNameCommand payload = command.Command.AcknowledgeAlarmByNameCommand;
|
||||||
|
if (string.IsNullOrWhiteSpace(payload.AlarmName))
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "AcknowledgeAlarmByName.alarm_name is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int rc = alarmCommandHandler.AcknowledgeByName(
|
||||||
|
payload.AlarmName,
|
||||||
|
payload.ProviderName,
|
||||||
|
payload.GroupName,
|
||||||
|
payload.Comment,
|
||||||
|
payload.OperatorUser,
|
||||||
|
payload.OperatorNode,
|
||||||
|
payload.OperatorDomain,
|
||||||
|
payload.OperatorFullName);
|
||||||
|
MxCommandReply reply = CreateOkReply(command);
|
||||||
|
reply.Hresult = rc;
|
||||||
|
reply.AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload
|
||||||
|
{
|
||||||
|
NativeStatus = rc,
|
||||||
|
};
|
||||||
|
if (rc != 0)
|
||||||
|
{
|
||||||
|
reply.DiagnosticMessage = $"AVEVA AlarmAckByName returned non-zero status {rc}.";
|
||||||
|
}
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateAlarmFailureReply(command, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private MxCommandReply ExecuteQueryActiveAlarms(StaCommand command)
|
private MxCommandReply ExecuteQueryActiveAlarms(StaCommand command)
|
||||||
{
|
{
|
||||||
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.QueryActiveAlarmsCommand)
|
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.QueryActiveAlarmsCommand)
|
||||||
|
|||||||
@@ -172,6 +172,33 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
|||||||
szOprFullName: ackOperatorFullName ?? string.Empty);
|
szOprFullName: ackOperatorFullName ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int AcknowledgeByName(
|
||||||
|
string alarmName,
|
||||||
|
string providerName,
|
||||||
|
string groupName,
|
||||||
|
string ackComment,
|
||||||
|
string ackOperatorName,
|
||||||
|
string ackOperatorNode,
|
||||||
|
string ackOperatorDomain,
|
||||||
|
string ackOperatorFullName)
|
||||||
|
{
|
||||||
|
if (disposed) throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer));
|
||||||
|
|
||||||
|
wwAlarmConsumerClass com = client
|
||||||
|
?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer));
|
||||||
|
|
||||||
|
return com.AlarmAckByName(
|
||||||
|
szAlarmName: alarmName ?? string.Empty,
|
||||||
|
szProviderName: providerName ?? string.Empty,
|
||||||
|
szGroupName: groupName ?? string.Empty,
|
||||||
|
szComment: ackComment ?? string.Empty,
|
||||||
|
szOprName: ackOperatorName ?? string.Empty,
|
||||||
|
szNode: ackOperatorNode ?? string.Empty,
|
||||||
|
szDomainName: ackOperatorDomain ?? string.Empty,
|
||||||
|
szOprFullName: ackOperatorFullName ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
|
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user