feat(alarms): DriverInstanceActor forwards native OnAlarmEvent to parent (Phase B WS-4b)
This commit is contained in:
@@ -64,6 +64,11 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
/// <see cref="ISubscribable.OnDataChange"/>. The parent forwards to OpcUaPublishActor.</summary>
|
||||
public sealed record AttributeValuePublished(string DriverInstanceId, string FullReference, object? Value, OpcUaQuality Quality, DateTime TimestampUtc);
|
||||
private sealed record DataChangeForward(string FullReference, DataValueSnapshot Snapshot);
|
||||
/// <summary>Published to the parent whenever the subscribed driver (an <see cref="IAlarmSource"/>) fires
|
||||
/// <see cref="IAlarmSource.OnAlarmEvent"/>. The parent (<see cref="DriverHostActor"/>) projects + routes it
|
||||
/// to the materialised Part 9 condition. Parallels <see cref="AttributeValuePublished"/>.</summary>
|
||||
public sealed record AttributeAlarmPublished(string DriverInstanceId, AlarmEventArgs Args);
|
||||
private sealed record NativeAlarmRaised(AlarmEventArgs Args);
|
||||
public sealed class RetryConnect
|
||||
{
|
||||
public static readonly RetryConnect Instance = new();
|
||||
@@ -94,6 +99,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
/// do not need to re-send subscription requests after a reconnect.</summary>
|
||||
private ISubscriptionHandle? _subscriptionHandle;
|
||||
private EventHandler<DataChangeEventArgs>? _dataChangeHandler;
|
||||
private EventHandler<AlarmEventArgs>? _alarmEventHandler;
|
||||
|
||||
/// <summary>The references the host wants kept subscribed (set by <see cref="SetDesiredSubscriptions"/>).
|
||||
/// Re-applied on every entry into <c>Connected</c> so values resume after a reconnect or redeploy.</summary>
|
||||
@@ -222,6 +228,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
Become(Connected);
|
||||
PublishHealthSnapshot();
|
||||
ResubscribeDesired();
|
||||
AttachAlarmSource();
|
||||
});
|
||||
Receive<InitializeFailed>(msg =>
|
||||
{
|
||||
@@ -241,6 +248,10 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
// to Self (HandleSubscribeAsync already logged the underlying cause). Swallow it so it doesn't dead-letter.
|
||||
Receive<SubscriptionFailed>(msg =>
|
||||
_log.Debug("DriverInstance {Id}: resubscribe reported failure: {Reason}", _driverInstanceId, msg.Reason));
|
||||
// A native alarm transition can race in while still (re)connecting (the driver's feed runs on its
|
||||
// own thread); drop it — the feed re-delivers active alarms once Connected. Trace only.
|
||||
Receive<NativeAlarmRaised>(_ =>
|
||||
_log.Debug("DriverInstance {Id}: native alarm arrived during connect — dropped (feed re-delivers)", _driverInstanceId));
|
||||
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
||||
}
|
||||
|
||||
@@ -273,6 +284,9 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
else if (_subscriptionHandle is not null) Self.Tell(new Unsubscribe());
|
||||
});
|
||||
Receive<DataChangeForward>(OnDataChangeForward);
|
||||
// Native alarm transition marshaled onto the actor thread from the driver's OnAlarmEvent;
|
||||
// project it to the parent the same way DataChangeForward projects AttributeValuePublished.
|
||||
Receive<NativeAlarmRaised>(m => Context.Parent.Tell(new AttributeAlarmPublished(_driverInstanceId, m.Args)));
|
||||
// ResubscribeDesired self-Tells Subscribe; HandleSubscribeAsync replies SubscriptionEstablished to the
|
||||
// sender, which on the self-resubscribe path is Self. Swallow it (trace only) so it doesn't dead-letter.
|
||||
Receive<SubscriptionEstablished>(msg =>
|
||||
@@ -299,6 +313,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
Become(Connected);
|
||||
PublishHealthSnapshot();
|
||||
ResubscribeDesired();
|
||||
AttachAlarmSource();
|
||||
});
|
||||
Receive<InitializeFailed>(_ => { /* keep retrying via timer */ });
|
||||
Receive<SetDesiredSubscriptions>(StoreDesiredSubscriptions);
|
||||
@@ -312,6 +327,10 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
// to Self (HandleSubscribeAsync already logged the underlying cause). Swallow it so it doesn't dead-letter.
|
||||
Receive<SubscriptionFailed>(msg =>
|
||||
_log.Debug("DriverInstance {Id}: resubscribe reported failure: {Reason}", _driverInstanceId, msg.Reason));
|
||||
// A native alarm transition can race in while still reconnecting (the driver's feed runs on its
|
||||
// own thread); drop it — the feed re-delivers active alarms once Connected. Trace only.
|
||||
Receive<NativeAlarmRaised>(_ =>
|
||||
_log.Debug("DriverInstance {Id}: native alarm arrived during reconnect — dropped (feed re-delivers)", _driverInstanceId));
|
||||
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
||||
Timers.StartPeriodicTimer("retry-connect", RetryConnect.Instance, _reconnectInterval);
|
||||
}
|
||||
@@ -446,9 +465,9 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Tear down the event handler + null the handle. Called from Unsubscribe path, on
|
||||
/// PostStop, and on Connected → Reconnecting transitions so a stale handler doesn't push
|
||||
/// data-change events to an actor that has lost its driver connection.</summary>
|
||||
/// <summary>Tear down the data-change + native-alarm event handlers + null the handle. Called from the
|
||||
/// Unsubscribe path, on PostStop, and on Connected → Reconnecting transitions so a stale handler doesn't
|
||||
/// push data-change / alarm events to an actor that has lost its driver connection.</summary>
|
||||
private void DetachSubscription()
|
||||
{
|
||||
if (_driver is ISubscribable subscribable && _dataChangeHandler is not null)
|
||||
@@ -457,6 +476,26 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
_dataChangeHandler = null;
|
||||
_subscriptionHandle = null;
|
||||
DetachAlarmSource();
|
||||
}
|
||||
|
||||
/// <summary>Subscribe the driver's native alarm event (if it is an <see cref="IAlarmSource"/>),
|
||||
/// marshaling each transition to the actor thread. Idempotent; mirrors the OnDataChange attach.</summary>
|
||||
private void AttachAlarmSource()
|
||||
{
|
||||
if (_driver is not IAlarmSource src || _alarmEventHandler is not null) return;
|
||||
var self = Self;
|
||||
_alarmEventHandler = (_, e) => self.Tell(new NativeAlarmRaised(e));
|
||||
src.OnAlarmEvent += _alarmEventHandler;
|
||||
}
|
||||
|
||||
/// <summary>Symmetric teardown — called from <see cref="DetachSubscription"/> and PostStop so a stale
|
||||
/// handler never pushes to a disconnected actor.</summary>
|
||||
private void DetachAlarmSource()
|
||||
{
|
||||
if (_driver is IAlarmSource src && _alarmEventHandler is not null)
|
||||
src.OnAlarmEvent -= _alarmEventHandler;
|
||||
_alarmEventHandler = null;
|
||||
}
|
||||
|
||||
/// <summary>Records the host's desired subscription set without touching the live subscription.
|
||||
|
||||
Reference in New Issue
Block a user