From 850f55b9cf37fee207b8305bca44cb86e24d960d Mon Sep 17 00:00:00 2001 From: Tyler Kron Date: Sat, 18 Jul 2026 21:52:21 -0600 Subject: [PATCH] fix(firmware): cancel hard-timeout token before throwing TimeoutException (fixes flaky test + ordering race) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RunWithHardTimeoutAsync decided to time out via Task.Delay(hardTimeoutMs) winning the WhenAny race, but the worker's cancellation came from a SEPARATE hardTimeoutCts timer of the same duration. The two timers can fire in either order, so the delay could win a hair before hardTimeoutCts, letting a late-returning worker observe an as-yet-uncancelled token and run one further state-changing step after the caller already received a TimeoutException — the exact guarantee #326 finding #2 added. Now the abandon path calls hardTimeoutCts.Cancel() up front (idempotent), so the worker's linked token is deterministically cancelled before TimeoutException propagates. This also fixes the intermittent CI failures of RunWithHardTimeoutAsync_HardTimeoutElapses_StopsWorkerBeforeLateStep, which caught this race under load (observed failing on unrelated PRs #356 and #357). - Existing test is the regression guard (passed 30/30 locally after the fix); full suite 1635 pass. Co-Authored-By: Claude Opus 4.8 --- src/Daqifi.Core/Firmware/WifiBridgeActivator.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Daqifi.Core/Firmware/WifiBridgeActivator.cs b/src/Daqifi.Core/Firmware/WifiBridgeActivator.cs index b9b9ac6f..647cdbc9 100644 --- a/src/Daqifi.Core/Firmware/WifiBridgeActivator.cs +++ b/src/Daqifi.Core/Firmware/WifiBridgeActivator.cs @@ -216,6 +216,16 @@ internal static async Task RunWithHardTimeoutAsync(Action ope // same race SerialDeviceFinder guards against per #295). if (winner != workerTask && !workerTask.IsCompleted) { + // Cancel the hard-timeout source explicitly, up front, rather than relying on its + // independent timer having already fired. The Task.Delay(hardTimeoutMs) above and + // hardTimeoutCts's timer are two separate timers of the same duration, so the delay + // can win the race a hair before hardTimeoutCts fires — which would let a worker that + // returns late observe an as-yet-uncancelled linkedCts.Token and run one further + // state-changing step after the caller already got a TimeoutException (#326 finding + // #2). Cancelling here guarantees the worker's token is cancelled before we throw. + // Idempotent if the timer already fired. + hardTimeoutCts.Cancel(); + // Open() is uncancellable, so on timeout/cancellation the worker task // is ABANDONED rather than awaited — it may still be blocked in native // I/O. Observe its eventual fault so it can't surface as an