fix(alarms): subscribe native alarms to un-gate the IAlarmSource feed
Phase B native alarms never fired end-to-end: GalaxyDriver suppresses OnAlarmEvent until an alarm subscription exists (_alarmSubscriptions.Count > 0), but the runtime only attached the OnAlarmEvent handler and never called SubscribeAlarmsAsync — so the central feed stayed gated and no transition reached the Part 9 condition / /alerts. Unit tests passed because they inject through the IAlarmSource seam directly; the deferred live /run surfaced it. DriverHostActor computes per-driver alarm refs (alarm-bearing tags' FullNames) and hands them via SetDesiredSubscriptions; DriverInstanceActor calls SubscribeAlarmsAsync for IAlarmSource drivers on Connected entry and whenever alarm refs are pushed while Connected (the deploy path), idempotent via a cached handle reset on detach so reconnect re-subscribes.
This commit is contained in:
+51
-2
@@ -64,6 +64,42 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase
|
||||
.FullReference.ShouldBe("tag-z");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The native-alarm path is gated at the driver: an <see cref="IAlarmSource"/> suppresses
|
||||
/// <c>OnAlarmEvent</c> until at least one alarm subscription exists (e.g. GalaxyDriver gates its
|
||||
/// central feed). When the host pushes a <see cref="DriverInstanceActor.SetDesiredSubscriptions"/>
|
||||
/// carrying native-alarm refs to a <c>Connected</c> driver — the live deploy path — the actor must
|
||||
/// call <see cref="IAlarmSource.SubscribeAlarmsAsync"/> with those refs to un-gate the feed, and must
|
||||
/// NOT re-subscribe when the same set is redeployed (the feed is already un-gated).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Alarm_subscription_is_established_when_alarm_refs_are_pushed_while_connected()
|
||||
{
|
||||
var driver = new AlarmSourceStubDriver();
|
||||
var parent = CreateTestProbe();
|
||||
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));
|
||||
|
||||
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||
AwaitCondition(() => driver.AlarmSubscriberCount == 1, TimeSpan.FromSeconds(2)); // reached Connected
|
||||
driver.SubscribeAlarmsCallCount.ShouldBe(0, "no alarm subscription before any alarm refs are pushed");
|
||||
|
||||
// A deploy delivers the desired set with a native-alarm ref while the driver is already Connected.
|
||||
actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(
|
||||
Array.Empty<string>(), TimeSpan.FromMilliseconds(100), new[] { "Temp.HiHi" }));
|
||||
|
||||
AwaitCondition(() => driver.SubscribeAlarmsCallCount == 1, TimeSpan.FromSeconds(2));
|
||||
driver.LastAlarmRefs.ShouldBe(new[] { "Temp.HiHi" });
|
||||
|
||||
// Redeploying the same alarm set must NOT re-subscribe (idempotent — already un-gated). Round-trip a
|
||||
// data Subscribe to flush the mailbox so the second SetDesiredSubscriptions is fully processed first.
|
||||
actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(
|
||||
Array.Empty<string>(), TimeSpan.FromMilliseconds(100), new[] { "Temp.HiHi" }));
|
||||
await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
|
||||
new DriverInstanceActor.Subscribe(new[] { "tag-z" }, TimeSpan.FromMilliseconds(100)),
|
||||
TimeSpan.FromSeconds(3));
|
||||
driver.SubscribeAlarmsCallCount.ShouldBe(1, "an already-established alarm subscription is not re-issued");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A driver that is NOT an <see cref="IAlarmSource"/> (only <see cref="ISubscribable"/>) connects
|
||||
/// and serves data changes normally — <c>AttachAlarmSource</c> is a safe no-op (no crash) and no
|
||||
@@ -266,10 +302,23 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase
|
||||
public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
private int _subscribeAlarmsCallCount;
|
||||
|
||||
/// <summary>Number of <see cref="SubscribeAlarmsAsync"/> calls — proves the actor established the
|
||||
/// native-alarm subscription that un-gates the feed (incremented on the actor dispatcher thread).</summary>
|
||||
public int SubscribeAlarmsCallCount => Volatile.Read(ref _subscribeAlarmsCallCount);
|
||||
|
||||
/// <summary>The node set handed to the most recent <see cref="SubscribeAlarmsAsync"/> call.</summary>
|
||||
public IReadOnlyList<string>? LastAlarmRefs { get; private set; }
|
||||
|
||||
/// <summary>Subscribes to alarm events for the specified node set.</summary>
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<IAlarmSubscriptionHandle>(new StubAlarmHandle());
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
{
|
||||
LastAlarmRefs = sourceNodeIds;
|
||||
Interlocked.Increment(ref _subscribeAlarmsCallCount);
|
||||
return Task.FromResult<IAlarmSubscriptionHandle>(new StubAlarmHandle());
|
||||
}
|
||||
|
||||
/// <summary>Cancels an alarm subscription.</summary>
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) =>
|
||||
|
||||
Reference in New Issue
Block a user