From beccfc640402dd83495b6da36a925e5d81378dff Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 8 Oct 2026 20:37:15 -0400 Subject: [PATCH] test(#606): native-ack routing waits for the driver child to be Connected; widen a starved poll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DriverHostActorNativeAlarmAckRoutingTests: Applied does not mean the child DriverInstanceActor is Connected, and an ack reaching a still-Connecting child is dropped by design — the single positive-control ack was itself dropped (4/20 under DOTNET_PROCESSOR_COUNT=1 x8, still failing with the presence budget dilated to 45 s, so not a budget problem). SpawnHostAndApply now waits for SubscribeAlarmsAsync, which only the Connected state calls. ScriptedAlarmEngine OnEvent_emission_happens_outside_evalGate: 2 s blocking poll -> 15 s (5/20 under starvation). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe --- .../ScriptedAlarmEngineTests.cs | 7 ++++-- ...iverHostActorNativeAlarmAckRoutingTests.cs | 24 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ScriptedAlarmEngineTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ScriptedAlarmEngineTests.cs index 1817003d..af5fae06 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ScriptedAlarmEngineTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ScriptedAlarmEngineTests.cs @@ -769,8 +769,11 @@ public sealed class ScriptedAlarmEngineTests // Drive to Active so Acknowledge has something to ack. up.Push("Temp", 150); // Use the same WaitForAsync that other tests use — synchronously - // here since this is a non-async test. - for (var i = 0; i < 80 && eng.GetState("HighTemp")!.Active != AlarmActiveState.Active; i++) + // here since this is a non-async test. A 15 s presence ceiling, not 2 s: this thread blocks a + // pool thread while the drain that activates the alarm needs another, and under thread-pool + // starvation (DOTNET_PROCESSOR_COUNT=1, 8 parallel copies) 2 s failed 5 runs in 20 (#606). + var activeBy = DateTime.UtcNow.AddSeconds(15); + while (DateTime.UtcNow < activeBy && eng.GetState("HighTemp")!.Active != AlarmActiveState.Active) Thread.Sleep(25); eng.GetState("HighTemp")!.Active.ShouldBe(AlarmActiveState.Active); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorNativeAlarmAckRoutingTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorNativeAlarmAckRoutingTests.cs index c74619fa..08d2f449 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorNativeAlarmAckRoutingTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorNativeAlarmAckRoutingTests.cs @@ -157,6 +157,15 @@ public sealed class DriverHostActorNativeAlarmAckRoutingTests : RuntimeActorTest actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId())); coordinator.ExpectMsg(Timeout).Outcome.ShouldBe(ApplyAckOutcome.Applied); + // Applied does NOT mean the driver child is Connected: the apply acks once the child is spawned, + // and a RouteAlarmAck reaching a still-Connecting child is dropped by design (fire-and-forget, no + // upstream to reach). Each test sends its positive-control ack exactly once, so on a loaded runner + // the control itself was dropped and the wait read [] — 4 of 20 runs under thread-pool starvation, + // and on Gitea (#606). SubscribeAlarmsAsync is only ever called from the Connected state (a + // pre-connect request is swallowed and re-issued on the Connected entry), so wait for it. + if (factory is RecordingAlarmDriverFactory recording) + AwaitCondition(() => recording.AlarmsSubscribed, Timeout); + return actor; } @@ -234,6 +243,9 @@ public sealed class DriverHostActorNativeAlarmAckRoutingTests : RuntimeActorTest /// off the actor thread). public IReadOnlyList Acks => _driver.Acks; + /// True once the driver child has reached Connected and opened its alarm subscription. + public bool AlarmsSubscribed => _driver.AlarmsSubscribed; + /// public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson) { @@ -269,8 +281,16 @@ public sealed class DriverHostActorNativeAlarmAckRoutingTests : RuntimeActorTest /// public Task SubscribeAlarmsAsync( - IReadOnlyList sourceNodeIds, CancellationToken cancellationToken) => - Task.FromResult(new StubAlarmHandle()); + IReadOnlyList sourceNodeIds, CancellationToken cancellationToken) + { + Volatile.Write(ref _alarmsSubscribed, true); + return Task.FromResult(new StubAlarmHandle()); + } + + private bool _alarmsSubscribed; + + /// True once has been called (only ever from Connected). + public bool AlarmsSubscribed => Volatile.Read(ref _alarmsSubscribed); /// public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) =>