From d08abb61c212c984b047bdcfa3d29d048b161f72 Mon Sep 17 00:00:00 2001 From: Praneet Kacha Date: Wed, 8 Jul 2026 11:22:32 -0700 Subject: [PATCH] Fix infinite WaitTimer spin when the delayed-callback timer is disarmed (UINT64_MAX) PR #983 removed the `if (dueTime == UINT64_MAX) return;` guard from the top of TaskQueuePortImpl::SubmitPendingCallbacks and routed the early (now < dueTime) path through the new RearmTimerIfDueTimeUnchanged() helper, which calls m_timer.Start(dueTime) unconditionally. Only ArmTimerIfEarlier() kept a UINT64_MAX guard. UINT64_MAX is the "nothing armed" sentinel for m_timerDue. When a timer callback fires while the timer is disarmed, SubmitPendingCallbacks re-arms the sentinel: WaitTimerImpl::Start(UINT64_MAX) builds Deadline(Deadline::duration(UINT64_MAX)), and because the STL backend's duration rep is a signed int64, UINT64_MAX becomes -1 -- a deadline 1ns before the steady_clock epoch, i.e. perpetually in the past. The wait-timer thread then fires immediately, calls SubmitPendingCallbacks, sees m_timerDue == UINT64_MAX (now < UINT64_MAX is always true), re-arms the sentinel, and fires again: an infinite fire/re-arm spin that pins the timer thread and starves every real pending delayed callback (they sit unpromoted in m_pendingList because PromoteReadyPendingCallbacks is never reached). Observed on PS5 as an intermittent hang where a delayed websocket send-completion poll is never promoted, so the operation never completes. Fix: restore the pre-#983 sentinel guard at the top of SubmitPendingCallbacks and add the same guard to RearmTimerIfDueTimeUnchanged, so the disarm sentinel can never be programmed as a real deadline. --- Source/Task/TaskQueue.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Source/Task/TaskQueue.cpp b/Source/Task/TaskQueue.cpp index 252f8a21..59549581 100644 --- a/Source/Task/TaskQueue.cpp +++ b/Source/Task/TaskQueue.cpp @@ -1212,6 +1212,14 @@ bool TaskQueuePortImpl::ArmTimerForNextPendingDueTime( // must re-evaluate instead of resurrecting it. bool TaskQueuePortImpl::RearmTimerIfDueTimeUnchanged(uint64_t dueTime) { + // UINT64_MAX is the "nothing armed" sentinel and must never be programmed + // as a real deadline (see SubmitPendingCallbacks). Mirrors the guard in + // ArmTimerIfEarlier. + if (dueTime == UINT64_MAX) + { + return true; + } + while (true) { uint64_t currentDue = m_timerDue.load(); @@ -1402,6 +1410,17 @@ void TaskQueuePortImpl::SubmitPendingCallbacks() { uint64_t dueTime = m_timerDue.load(); + // UINT64_MAX is the "nothing armed" sentinel. It must never be programmed + // as a real deadline: WaitTimer::Start(UINT64_MAX) converts to a duration + // of -1 (before the steady_clock epoch), a perpetually-past deadline that + // fires immediately and re-arms the sentinel in an infinite loop, starving + // every real pending callback. If the timer is disarmed there is nothing + // to sweep, so return. + if (dueTime == UINT64_MAX) + { + return; + } + // Timer callbacks are advisory: a threadpool fire can arrive after // retargeting, or slightly before the steady-clock deadline due to // clock-source differences on Win32. If the deadline hasn't arrived,