feat(alarms): DriverHostActor routes native-condition acks to the owning driver [H6d]
This commit is contained in:
@@ -40,6 +40,20 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
public sealed record ApplyResult(bool Success, string? Reason, CorrelationId Correlation);
|
||||
public sealed record WriteAttribute(string TagId, object Value);
|
||||
public sealed record WriteAttributeResult(bool Success, string? Reason);
|
||||
/// <summary>
|
||||
/// Sent by <see cref="DriverHostActor"/> when an OPC UA client Acknowledges a NATIVE Part 9
|
||||
/// condition (resolved from the condition NodeId to this driver via the host's alarm inverse map).
|
||||
/// The actor forwards it to the driver's <see cref="IAlarmSource.AcknowledgeAsync"/>, carrying the
|
||||
/// authored alarm full-reference (= the driver's <c>ConditionId</c>/AlarmFullReference) and the
|
||||
/// authenticated principal. Mirrors <see cref="WriteAttribute"/>, but the ack is fire-and-forget:
|
||||
/// the driver's <see cref="IAlarmSource.AcknowledgeAsync"/> returns no per-condition status and the
|
||||
/// OPC UA Part 9 ack already committed the local condition state, so there is no reply to surface.
|
||||
/// </summary>
|
||||
/// <param name="ConditionId">The authored alarm full-reference the driver correlates the ack on
|
||||
/// (= the equipment tag's <c>FullName</c>/AlarmFullReference).</param>
|
||||
/// <param name="Comment">Operator-supplied comment forwarded to the upstream alarm system; null when none.</param>
|
||||
/// <param name="OperatorUser">The authenticated principal performing the acknowledge.</param>
|
||||
public sealed record RouteAlarmAck(string ConditionId, string? Comment, string OperatorUser);
|
||||
public sealed record Subscribe(IReadOnlyList<string> FullReferences, TimeSpan PublishingInterval);
|
||||
/// <summary>
|
||||
/// Sets the set of references this driver should keep subscribed for the lifetime of the
|
||||
@@ -240,6 +254,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
Receive<InitializeRequested>(_ => { /* no-op */ });
|
||||
Receive<ApplyDelta>(msg => Sender.Tell(new ApplyResult(true, "stubbed", msg.Correlation)));
|
||||
Receive<WriteAttribute>(_ => Sender.Tell(new WriteAttributeResult(true, "stubbed")));
|
||||
// Stubbed drivers have no upstream alarm system — swallow the ack (it's fire-and-forget, no reply).
|
||||
Receive<RouteAlarmAck>(_ => { /* stubbed drivers have no alarm backend */ });
|
||||
Receive<DisconnectObserved>(_ => { /* stubbed drivers don't disconnect */ });
|
||||
Receive<ForceReconnect>(_ => { /* stubbed drivers don't reconnect */ });
|
||||
Receive<SetDesiredSubscriptions>(StoreDesiredSubscriptions);
|
||||
@@ -254,6 +270,10 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
// "write timeout". Synchronous Receive: Sender.Tell on the actor thread is safe (#4a-instance).
|
||||
Receive<WriteAttribute>(_ =>
|
||||
Sender.Tell(new WriteAttributeResult(false, "driver not connected")));
|
||||
// An ack arriving while still connecting can't reach the upstream alarm system; drop it (the ack is
|
||||
// fire-and-forget — no reply to surface — and the OPC UA condition state already committed locally).
|
||||
Receive<RouteAlarmAck>(_ =>
|
||||
_log.Debug("DriverInstance {Id}: alarm ack arrived during connect — dropped (driver not connected)", _driverInstanceId));
|
||||
Receive<ApplyDelta>(AdoptConfigDuringInit);
|
||||
Receive<InitializeSucceeded>(msg =>
|
||||
{
|
||||
@@ -314,6 +334,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
PublishHealthSnapshot();
|
||||
});
|
||||
ReceiveAsync<WriteAttribute>(HandleWriteAsync);
|
||||
ReceiveAsync<RouteAlarmAck>(HandleAcknowledgeAsync);
|
||||
ReceiveAsync<Subscribe>(HandleSubscribeAsync);
|
||||
ReceiveAsync<Unsubscribe>(_ => UnsubscribeAsync());
|
||||
Receive<SetDesiredSubscriptions>(msg =>
|
||||
@@ -354,6 +375,10 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
// timeout on an inbound write to a transiently-down driver). Synchronous Receive (#4a-instance).
|
||||
Receive<WriteAttribute>(_ =>
|
||||
Sender.Tell(new WriteAttributeResult(false, "driver not connected")));
|
||||
// An ack arriving while reconnecting can't reach the upstream alarm system; drop it (fire-and-forget,
|
||||
// no reply — the OPC UA condition state already committed locally on the Part 9 ack).
|
||||
Receive<RouteAlarmAck>(_ =>
|
||||
_log.Debug("DriverInstance {Id}: alarm ack arrived during reconnect — dropped (driver not connected)", _driverInstanceId));
|
||||
Receive<ApplyDelta>(AdoptConfigDuringInit);
|
||||
Receive<InitializeSucceeded>(msg =>
|
||||
{
|
||||
@@ -473,6 +498,46 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forwards an inbound native-condition acknowledge (routed by <see cref="DriverHostActor"/> from a
|
||||
/// resolved condition NodeId) to the driver's <see cref="IAlarmSource.AcknowledgeAsync"/>. The driver
|
||||
/// correlates on <see cref="AlarmAcknowledgeRequest.ConditionId"/> (= the authored alarm
|
||||
/// full-reference); <see cref="AlarmAcknowledgeRequest.SourceNodeId"/> carries the same reference (the
|
||||
/// driver's ack path keys on ConditionId). Bounded to 5s so a hung backend can't pin this actor.
|
||||
/// Fire-and-forget — the OPC UA Part 9 ack already committed the local condition state and
|
||||
/// <see cref="IAlarmSource.AcknowledgeAsync"/> returns no per-condition status — so there is no reply;
|
||||
/// a failure is logged and dropped (the local condition stays Acknowledged regardless).
|
||||
/// </summary>
|
||||
private async Task HandleAcknowledgeAsync(RouteAlarmAck msg)
|
||||
{
|
||||
if (_driver is not IAlarmSource src)
|
||||
{
|
||||
_log.Warning("DriverInstance {Id}: alarm ack dropped — driver does not implement IAlarmSource", _driverInstanceId);
|
||||
return;
|
||||
}
|
||||
|
||||
var request = new[]
|
||||
{
|
||||
new AlarmAcknowledgeRequest(
|
||||
SourceNodeId: msg.ConditionId,
|
||||
ConditionId: msg.ConditionId,
|
||||
Comment: msg.Comment,
|
||||
OperatorUser: msg.OperatorUser),
|
||||
};
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
try
|
||||
{
|
||||
await src.AcknowledgeAsync(request, cts.Token);
|
||||
_log.Info("DriverInstance {Id}: acknowledged native condition {Cond} by {User}",
|
||||
_driverInstanceId, msg.ConditionId, msg.OperatorUser);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warning(ex, "DriverInstance {Id}: native-alarm acknowledge of {Cond} failed",
|
||||
_driverInstanceId, msg.ConditionId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleSubscribeAsync(Subscribe msg)
|
||||
{
|
||||
// Capture Sender/Self BEFORE any await. The re-subscribe path below awaits
|
||||
|
||||
Reference in New Issue
Block a user