Repository navigation
Conversation
1c98666 to
f3cf3d2
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3553 +/- ##
================================================
Coverage 100.00000% 100.00000%
================================================
Files 128 128
Lines 19475 19784 +309
Branches 1323 1327 +4
================================================
+ Hits 19475 19784 +309
🚀 New features to boost your workflow:
|
This comment was marked as resolved.
This comment was marked as resolved.
A task blocked in to_thread.run_sync looks idle to the run loop: it is parked in wait_task_rescheduled, and the worker thread only touches the loop when it finishes. So with a finite autojump_threshold, MockClock could advance virtual time while a thread was still doing real work, making concurrent tasks observe spuriously-fired timeouts. Count the work that is in flight off the event loop in a plain counter on the Runner, autojump_holds (the same place clock_autojump_threshold lives, avoiding an import cycle with trio._threads), and don't arm the autojump while it is nonzero -- mirroring the existing special case for tasks blocked in wait_all_tasks_blocked. When the check suppresses arming, the IO wait just sleeps until a thread reports back, so there is no polling or busy-wait. Each to_thread.run_sync call owns one unit of the count through a small private object, _AutojumpThreadHold, which only acts inside the run it belongs to, so the count is only ever changed on that run's thread. Deliberate details: * A thread that has re-entered Trio via from_thread.run is NOT counted while the re-entrant call runs: the thread is parked and the real work is back on the Trio side, where the run loop can already see it. Without this, a virtual-time sleep inside the re-entrant call would deadlock. This applies on both dispatch routes -- the waiting task's message loop, and the system task that runs the call when the thread was started with abandon_on_cancel=True or passes an explicit trio_token. The unit is taken back when the re-entrant call returns. * When a task with abandon_on_cancel=True is cancelled, its unit is given up for good even though the abandoned OS thread is still running, and a re-entrant call still in flight at that moment will not take it back. The clock therefore tracks "threads some task is waiting on", which preserves today's cancel-then-jump behavior after abandonment. * A worker thread that re-enters a *different* run (explicit trio_token) stays counted in its home run, whose clock cannot see the work being done in the other run, and the other run's count is never touched. * The threads Trio itself uses only to wait for something outside the process -- a child exiting (the waitid backend) or a Windows handle -- are not counted. Where the platform allows, those waits are I/O waits that leave the run loop idle, and the clock must not behave differently because a platform needs a thread for them. They go through a private _to_thread_run_sync(..., hold_autojump=False); the public function's signature is unchanged. The cost: a thread can now deadlock under an autojump clock. If it blocks, other than through from_thread.run, waiting for a task that first has to sleep in virtual time (queue.get() on a queue a task fills after a sleep), the clock waits for the thread and the thread for the clock, where the clock used to jump and let it finish. Fixes python-trio#456. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USWcRiSmFFfbTeL9SNRFE1
The autojump clock now waits for to_thread.run_sync threads, but a task can also be waiting for other work that happens outside the run loop: a task parked in wait_task_rescheduled until a foreign thread, or another event loop, wakes it through TrioToken.run_sync_soon. To the run loop such a task looks idle, so virtual time can jump past its timeouts while the work is still in progress, and code outside Trio had no way to say otherwise. hold_autojump() is a context manager over the same Runner.autojump_holds count that to_thread.run_sync uses: the autojump is not armed while any unit is held. It is synchronous, counted (so it nests), does nothing under a real clock, and does not hold up wait_all_tasks_blocked. It can only be entered from inside a task; run_sync_soon callbacks qualify because they run in a system task. The run loop re-checks the count after every batch of task steps and runs none between arming the autojump and firing it, so the count cannot rise in between, and no check is needed at the firing site. The runner is bound on entry and released on exit, so exiting the block from inside a different run cannot touch that run's count. to_thread.run_sync keeps its own private holder on the same count, because it has to let go of its unit and re-take it around from_thread re-entry, which a with block cannot express.
5f18da3 to
33ba976
Compare
Basically I wonder how much we benefit from exposing this. I guess it's good to be general? But I can't really think of other situations where this is useful? (I think this makes sense for |
I'm about 50/50 "might as well be general", and thinking about specific cases like interprocess stuff like https://github.com/goodboy/tractor, interop between Trio and other async loops (eg js/pyodide), etc. |
|
Makes sense! I only just realized that the semantics of an operation matter for this (since either an operation is blocked on another task that is alive or some external thing; and we only want to prevent autojump for that external thing). I'll review the code at some later point but on a high level this makes sense to me. |
|
Ah yeah I'm not sure if there's much demand for using it directly. But our
general design idea has been that you can use `trio.lowlevel` to build all
the stuff in `trio.*` (including `trio.to_thread`) and I don't see a reason
not to expose it, it's a pretty simple self-contained concept.
Could nitpick the name I guess, like should it refer to autojump or
wait_all_tasks_blocked, idk, probably doesn't matter too much. The concept
doesn't seem risky tho.
…On Thu, Oct 22, 2026, 20:21 A5rocks ***@***.***> wrote:
*A5rocks* left a comment (python-trio/trio#3553)
<#3553 (comment)>
It turns out tokio's equivalent of autojump_clock works this way
unconditionally (any running delegation-to-thread -> clock stays frozen
until complete), and ime it's been a good default, useful and I haven't run
into any problems.
Basically I wonder how much we benefit from exposing this. I guess it's
good to be general? But I can't really think of other situations where this
is useful?
(I think this makes sense for to_thread.run_sync() though!)
—
Reply to this email directly, view it on GitHub
<#3553?email_source=notifications&email_token=AAEU42HTO726IKC34RTQLKD5VLFEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTMMZVGY2TQNZUGYZKM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-6356587462>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAEU42E54YHD4YJPXQSFJV35VLFEVAVCNFSNUABEKJSXA33TNF2G64TZHM3TSMBYGM3DCNB3JFZXG5LFHM2TSOBZGEYTKNBTGGQXMAQ>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AAEU42BQCBECLLLRBYAGW435VLFEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTMMZVGY2TQNZUGYZKM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJKTGN5XXIZLSL5UW64Y>
and Android
<https://github.com/notifications/mobile/android/AAEU42B7UZZTU6NCAWEFJ5T5VLFEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTMMZVGY2TQNZUGYZKM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLTGN5XXIZLSL5QW4ZDSN5UWI>.
Download it today!
You are receiving this because you commented.Message ID:
***@***.***>
|
I've regularly run into painful situations in tests where I'd like to use the
autojump_clockto speed things up, but it doesn't play nicely with work delegated to threads.This PR proposes a new mechanism to avoid jumping the clock forward while there's work happening in a thread (or in general elsewhere; it could be another async loop too); instead waiting in real time for it to finish. Details are described in the documentation in the diff.
Thoughts?