Repository navigation
sleep() hangs indefinitely when using a custom clock #3369
Copy link
Copy link
Open
Description
Tested on 0.32 and the current master.
MRE:
import trio
import trio.testing
class FooMockClock(trio.abc.Clock):
def __init__(self) -> None:
self._clock = trio.testing.MockClock(autojump_threshold=0)
def current_time(self) -> float:
return self._clock.current_time()
def deadline_to_sleep_time(self, deadline: float) -> float:
return self._clock.deadline_to_sleep_time(deadline)
def start_clock(self) -> None:
self._clock.start_clock()
async def main():
print(trio.current_time())
await trio.sleep(2)
print(trio.current_time())
if __name__ == '__main__':
print("MockClock")
trio.run(main, clock=trio.testing.MockClock(autojump_threshold=0))
print("FooMockClock")
trio.run(main, clock=FooMockClock())Output:
MockClock
0.0
2.0
FooMockClock
0.0
< hangs indefinitely >
I expected the second run() to behave exactly the same as the first. What am I doing wrong?
Also, if I copy-paste trio.testing.MockClock verbatim to this script and use that, the execution crashes - may be related:
Traceback (most recent call last):
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2813, in unrolled_run
assert isinstance(runner.clock, _core.MockClock)
AssertionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/bogdan/wb/github/nucypher-async/t_new.py", line 185, in <module>
trio.run(main, clock=MockClock(autojump_threshold=0))
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2538, in run
timeout = gen.send(next_send)
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2951, in unrolled_run
raise TrioInternalError("internal error in Trio - please file a bug!") from exc
trio.TrioInternalError: internal error in Trio - please file a bug!
Exception ignored in: <coroutine object Runner.init at 0x106a26500>
Traceback (most recent call last):
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2156, in init
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1112, in __aexit__
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1288, in _nested_child_finished
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1252, in _add_exc
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 933, in _cancel
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 501, in recalculate
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1671, in _attempt_delivery_of_any_pending_cancel
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1644, in _attempt_abort
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_io_kqueue.py", line 162, in abort
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_io_kqueue.py", line 184, in abort
ValueError: I/O operation on closed kqueue object
Exception ignored in: <function Nursery.__del__ at 0x105b208b0>
Traceback (most recent call last):
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1486, in __del__
AssertionError:
Exception ignored in: <coroutine object main at 0x106a26570>
Traceback (most recent call last):
File "/Users/bogdan/wb/github/nucypher-async/t_new.py", line 180, in main
File "/Users/bogdan/wb/repos/trio/src/trio/_timeouts.py", line 111, in sleep
File "/Users/bogdan/wb/repos/trio/src/trio/_timeouts.py", line 91, in sleep_until
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 714, in __exit__
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 640, in _close
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2993, in current_task
RuntimeError: must be called from async context
Exception ignored in: <function Nursery.__del__ at 0x105b208b0>
Traceback (most recent call last):
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1486, in __del__
AssertionError:
bogdan@lair ~/w/g/nucypher-async (todo-party) [1]> python t_new.py
0.0
Traceback (most recent call last):
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2813, in unrolled_run
assert isinstance(runner.clock, _core.MockClock)
AssertionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/bogdan/wb/github/nucypher-async/t_new.py", line 186, in <module>
trio.run(main, clock=MockClock(autojump_threshold=0))
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2538, in run
timeout = gen.send(next_send)
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2951, in unrolled_run
raise TrioInternalError("internal error in Trio - please file a bug!") from exc
trio.TrioInternalError: internal error in Trio - please file a bug!
Exception ignored in: <coroutine object Runner.init at 0x106ade500>
Traceback (most recent call last):
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2156, in init
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1112, in __aexit__
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1288, in _nested_child_finished
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1252, in _add_exc
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 933, in _cancel
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 501, in recalculate
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1671, in _attempt_delivery_of_any_pending_cancel
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1644, in _attempt_abort
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_io_kqueue.py", line 162, in abort
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_io_kqueue.py", line 184, in abort
ValueError: I/O operation on closed kqueue object
Exception ignored in: <function Nursery.__del__ at 0x105bd8820>
Traceback (most recent call last):
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1486, in __del__
AssertionError:
Exception ignored in: <coroutine object main at 0x106ade570>
Traceback (most recent call last):
File "/Users/bogdan/wb/github/nucypher-async/t_new.py", line 181, in main
File "/Users/bogdan/wb/repos/trio/src/trio/_timeouts.py", line 111, in sleep
File "/Users/bogdan/wb/repos/trio/src/trio/_timeouts.py", line 91, in sleep_until
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 714, in __exit__
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 640, in _close
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 2993, in current_task
RuntimeError: must be called from async context
Exception ignored in: <function Nursery.__del__ at 0x105bd8820>
Traceback (most recent call last):
File "/Users/bogdan/wb/repos/trio/src/trio/_core/_run.py", line 1486, in __del__
AssertionError:
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
A5rocks commentedon Dec 8, 2025
Note while debugging, this
trio.abc.Clockadapted from_run.pyseems to work:It looks like if
current_timeraises an error, this turns into aTrioInternalErrorwhich is not ideal! But it shows the error too.A5rocks commentedon Dec 9, 2025
I'm fairly certain the root cause is that we don't actually allow
MockClock-like clocks, so to accomplish it we do some funny things. Specifically, we tightly couple it with the loop, i.e. we haveMockClockset an attribute on the loop so the loop knows it has aMockClockand not anything else. So in this case, here's what I think is happening:deadline_to_sleep_timeFooMockClock.deadline_to_sleep_time, which in turn callsMockClock.deadline_to_sleep_timeratewasn't passed toMockClock's constructor,MockClock.deadline_to_sleep_timewill return999999999... so the loop will sleep999999999secondsrunner.clockis notself)999999999seconds...I haven't fully traced through everything so it's possible I missed something... but that seems reasonable to me. A fix is probably to check that
runner.clock_autojump_thresholdexists indeadline_to_sleep_timeifrate == 0.0. It's not an actual error in Trio, but it's nice to avoid confusing people!tl;dr Your custom clock is wrong which is causing long sleeps. However, Trio could and should help debug issues like this.
fjarri commentedon Dec 9, 2025
I would disagree with this assessment - my clock correctly impls
abc.Clock, so I've done my part. If there's something else the clock should do, it should be in the ABC as well.This part
seems hacky to me. Why have a separate
runner.clock_autojump_thresholdwhen it's a property of the clock? Should there be instead anautojump_thresholdproperty in the ABC which the runner will use?A5rocks commentedon Dec 9, 2025
Because designing a public API to satisfy all
MockClock-shaped usages is challenging (especially with n=1 examples to draw from)! Your clock is wrong in that you're using an API (MockClock) wrong within it, rather than the interface it exposes.What specific case are you trying to extend
MockClockto do? See #1587 for whyrunner.clock_autojump_thresholdis a thing rather than something more public.fjarri commentedon Dec 9, 2025
Well, that's technically true, but there's nothing in the documentation about
deadline_to_sleep_time()needing to use some undocumented ways to get the current runner and then setting an attribute in it.I have code that works with real time extensively (creation/expiration dates, internal timeouts for recurring tasks etc). So I cannot use
trio.current_time(). I also want to test it with an autojump clock, so I can't just query system time. So I pass a clock object as DI, which allows me to choose between the two. Now in tests, I don't want to have two fixtures,pytest_trio.autojump_clock, and my mock clock object, which essentially do the same thing and need to be always used together. So I want my mock clock object to impltrio.abc.Clock, so thatpytest_triocould pick it up and pass totrio.run().To summarize, there is a workaround for what I want to do, it'll just make the code more prone to future errors. But I do think that either the attribute setting hack should be fixed, or
abc.Clockshould be marked as private (if indeed the attribute setting is unavoidable), because as it stands it cannot be implemented by an external user.It seems that the issue listed exposing
autojump_thresholdas one of the options. And it is not like it's some internal thing - there is a description in the docs about what it does, and the user is allowed to change it (as a constructor argument, and later in a setter). The attribute is already public inMockClock, why not make it public inabc.Clock?A5rocks commentedon Dec 9, 2025
Hmm, I don't quite follow. It seems possible to implement the
abc.Clockinterface, just not to do something likeMockClock. I guess the docs are a little confusing about this (they do say "feel the need to take direct control over the PASSAGE OF TIME ITSELF" but that's more a joke, IMO :-).I think a
rate=1.0MockClockuses system time? I guess you mean that thecurrent_time()will be wrong if an autojump happens? I don't think that would be fixable with any proposed API, as I assume the loop checkscurrent_timeon wakeup, rather than taking on faith that the deadline has expired. Otherwise, it sounds to me like you can use this clock at runtime:And then use
MockClock(rate=1.0, autojump_threshold=0)at test time...Let me know if I'm misunderstanding your use case!
fjarri commentedon Dec 9, 2025
Well, what is possible then? I find the behavior where I literally just pass through all the ABC calls to
MockClock, and the result does not work, very surprising. It means the ABC is not actually A.It uses the system time rate, but
trio.current_time()still starts from 0. I don't need just real time intervals, I need real absolute dates in production.The code you provided does not have the autojump behavior, that is if I do
trio.sleep(), it actually waits the requested period in real time.A5rocks commentedon Dec 9, 2025
Ah, sorry, I misread the code. You can work around this with:
but that's certainly annoying. I'd be happy to add a parameter to
MockClockHowever, I still don't really get the use case here. So you can't use
trio.current_time()because it's wrong w/r/t real time? If so, you can replace the clock such that it is real time! The only problem is, yeah,MockClockwill be wrong. But you can use the above if that's the only issue. I don't really get the thing about passing around two things or whatever.A5rocks commentedon Dec 9, 2025
I think I can be a bit clearer. Here's a table where what Trio provides works without workarounds:
In this case, you would use
SystemClock(above) in production andMockClockin test.Here's a table where what Trio provides works, but in an annoying way (my latest comment is regarding this):
In this case, you would use
SystemClockin production andMockClockwith the above workaround in test. I think we can improve this by providingMockClock(based_on_real_time=True)or something.njsmith commentedon Dec 9, 2025
fjarri commentedon Dec 9, 2025
I've got a
Serverthat takes a clock object on initialization (because as I explained above I can use neithertrio.current_time()nor system time directly). So every test has to have two fixtures:autojump_clockcomes frompytest_trioand sets the clock oftrio.run(). I wantmy_clockto be passed totrio.run()as well, so that I don't need to mentionautojump_clock. For that it has to derive fromtrio.abc.Clock, which is currently impossible to implement correctly (using only public API).Using
trio.current_time()inside and passing a special clock totrio.run()in production is not a good solution, since it's easy to forget to do it - a user running aServerin their own event loop has to remember this caveat.And I find that the conversation is being steered into people trying to explain to me how I don't really want to do what I want to do, whereas my point is that
trio.abc.Clockis currently incomplete and dangerous to use, so it needs to be either amended, or marked as private. I say it with all possible respect (re-reading my previous messages, they are somewhat abrasive), as I have been enjoying usingtriofor years now, and hope to continue doing so.That would be ideal, and I can try and come up with a PR.
Edit: see #3371 as a draft of the proposed solution
A5rocks commentedon Dec 10, 2025
I'm a bit confused about what you mean here. I assume you mean that
trio.testing.MockClock's autojump functionality cannot be implemented correctly? (though it does sound like you are talking about howtrio.abc.Clockcannot be done. I think you might be misunderstanding the [current] model for it...)[re: your draft/PR]
Personally, in ranked order, I prefer:
trio.testing.MockClockto be based on system time (i.e. add a kwarg)jumpwould have to be a thing :/)real_baseontrio.testing.MockClocktrio.abc.ClockinterfaceA couple reasons against (4):
EDIT: I thought more and realized everything I can think of kinda sucks. (except (1), since that lets us delay solving this problem)
Thanks, this is the bit I was missing.
I hadn't quite realized what you were doing... I suppose your
Serveris callingclock.current_time()instead oftrio.current_time()?fjarri commentedon Dec 10, 2025
Yes, you're right.
MockClockis impossible to extend (or reimplement).It doesn't have to be,
autojump()andautojump_threshold()can have default implementations (do nothing, and returninf, respectively). Although seeing as howtriohas breaking releases every few months, with no releases in-between, I didn't think it would be a big deal.I think that mostly indicates a problem with the name... It's more of a
deadline_to_jump(), and should take adeadlineargument (that is, the runner will pass it, instead ofMockClockquerying it internally from_core.current_statistics()). Naturally, for a "real" clock it is a no-op. It also feels to me that three methods (deadline_to_sleep_time(),autojump(),autojump_threshold()) is too many, since their responsibilities intersect, but I need to think about it more. And we'd be getting into actual breaking change territory.Yes, exactly.