test(alarms): fix reconnect-drop test to use mailbox-ordering approach

This commit is contained in:
Joseph Doherty
2026-06-14 22:48:36 -04:00
parent 49d98cba31
commit d8129e5ab7
@@ -98,38 +98,32 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase
/// re-delivers active alarms once the actor re-enters <c>Connected</c>, so dropping here is safe.
/// (<see cref="DriverInstanceActor"/> line ~345: the <c>Reconnecting</c> state's
/// <c>Receive&lt;NativeAlarmRaised&gt;</c> logs a debug message and discards.)
///
/// <para>
/// To reproduce the race deterministically: a <c>ForceReconnect</c> is enqueued first, then
/// the driver fires an alarm while its <c>OnAlarmEvent</c> handler is still attached (the actor
/// hasn't yet processed <c>ForceReconnect</c>). The handler's <c>self.Tell(NativeAlarmRaised)</c>
/// lands second in the mailbox. The actor then processes <c>ForceReconnect</c> → Reconnecting
/// (detaches handler), then processes the queued <c>NativeAlarmRaised</c> in Reconnecting
/// → drops it. The default 10 s reconnect interval ensures no retry fires during the check.
/// </para>
/// </summary>
[Fact]
public void Native_alarm_during_reconnect_is_dropped_not_forwarded()
{
// Long reconnect interval (default 10 s) so the retry doesn't fire during the assertion window.
var driver = new AlarmSourceStubDriver();
var parent = CreateTestProbe();
var actor = parent.ChildActorOf(DriverInstanceActor.Props(
driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));
// Drive the actor to Connected first, then push it into Reconnecting via DisconnectObserved.
// Drive to Connected; confirm the alarm handler is attached.
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
AwaitCondition(() => driver.AlarmSubscriberCount == 1, TimeSpan.FromSeconds(2));
// One successful alarm-forward from Connected, to confirm the happy path is hot before the test.
driver.RaiseAlarm(new AlarmEventArgs(
new StubAlarmHandle(),
SourceNodeId: "src-node-reconnect",
ConditionId: "cond-reconnect",
AlarmType: "T",
Message: "pre-reconnect raise",
Severity: AlarmSeverity.High,
SourceTimestampUtc: DateTime.UtcNow,
Kind: AlarmTransitionKind.Raise));
parent.ExpectMsg<DriverInstanceActor.AttributeAlarmPublished>(TimeSpan.FromSeconds(2));
// Force Connected → Reconnecting; the alarm handler is detached during this transition.
actor.Tell(new DriverInstanceActor.DisconnectObserved("test-induced disconnect"));
// Wait until the actor detaches the alarm handler (AlarmSubscriberCount drops to 0).
AwaitCondition(() => driver.AlarmSubscriberCount == 0, TimeSpan.FromSeconds(2));
// Now tell the actor a NativeAlarmRaised (the driver thread can fire this at any time).
// In Reconnecting the actor handles NativeAlarmRaised with a debug log and drops it.
// Enqueue ForceReconnect FIRST — the actor hasn't processed it yet, so the handler is
// still wired. The test thread then immediately fires the alarm on the driver; the handler's
// self.Tell(NativeAlarmRaised) lands SECOND in the mailbox.
actor.Tell(new DriverInstanceActor.ForceReconnect());
driver.RaiseAlarm(new AlarmEventArgs(
new StubAlarmHandle(),
SourceNodeId: "src-node-reconnect",
@@ -140,12 +134,15 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase
SourceTimestampUtc: DateTime.UtcNow,
Kind: AlarmTransitionKind.Raise));
// The parent must NOT receive an AttributeAlarmPublished while the actor is Reconnecting.
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
// The actor processes: (1) ForceReconnect → Reconnecting (handler detached);
// (2) NativeAlarmRaised → dropped (debug log, no forward).
// The parent must NOT receive AttributeAlarmPublished from that alarm.
// Wait generously — the default reconnect interval of 10 s means no retry fires here.
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
// The actor must still be alive (not crashed/dead-lettered) — a Watch + no Terminated proves it.
// The actor must still be alive — Watch + no Terminated (not crashed or dead-lettered).
Watch(actor);
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); // still no Terminated
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100));
}
/// <summary>