From c72d7b790289237aa17c0e5b101082d27ac174a6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 19 Jun 2026 05:01:42 -0400 Subject: [PATCH] test(dcl): de-race MxGateway Unsubscribe stops-routing under load (#288) --- .../Adapters/MxGatewayDataConnectionTests.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/MxGatewayDataConnectionTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/MxGatewayDataConnectionTests.cs index a6548105..b8db9d87 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/MxGatewayDataConnectionTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/MxGatewayDataConnectionTests.cs @@ -5,6 +5,12 @@ using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Adapters; +// Non-parallel with the rest of the DataConnectionManagerActor collection: these adapter +// tests pump a fire-and-forget event loop (ConnectAsync → Task.Run(RunEventLoopAsync)) whose +// thread-pool continuation must run before the WaitUntil attach-barrier observes +// fake.OnUpdate. Serializing within the assembly trims in-assembly thread-pool contention so +// that barrier (and the negative Unsubscribe assertion that follows it) is not starved. +[Collection("DataConnectionManagerActor")] public class MxGatewayDataConnectionTests { private static MxGatewayDataConnection NewAdapter(FakeMxGatewayClient fake) => @@ -249,7 +255,17 @@ public class MxGatewayDataConnectionTests () => adapter.BrowseChildrenAsync(null)); } - private static async Task WaitUntil(Func condition, int timeoutMs = 2000) + // Generous ceiling (30 s, matching the DataConnectionManagerActorCollection rationale): + // every caller polls a *monotonic, positive* barrier — fake.OnUpdate becoming non-null + // (the fire-and-forget event-loop task has attached) or a Disconnected count reaching its + // target. Both only ever transition once and never reset, so a longer ceiling merely + // tolerates thread-pool starvation under full-solution CPU oversubscription; it cannot + // produce a false pass. In Unsubscribe_stops_routing_updates this barrier establishes the + // happens-before that the loop is attached BEFORE the test removes the subscription; the + // negative assertion (hits == 0) runs afterwards and is independent of this timeout — + // the update is delivered synchronously by the test thread, so widening here neither + // weakens nor races the negative check. + private static async Task WaitUntil(Func condition, int timeoutMs = 30_000) { var sw = System.Diagnostics.Stopwatch.StartNew(); while (!condition() && sw.ElapsedMilliseconds < timeoutMs)