22from math import inf
33
44from .. import _core
5+ from ._run import GLOBAL_RUN_CONTEXT
56from .._abc import Clock
67from .._util import SubclassingDeprecatedIn_v0_15_0
78
@@ -51,32 +52,13 @@ class MockClock(Clock, metaclass=SubclassingDeprecatedIn_v0_15_0):
5152 above) then just set it to zero, and the clock will jump whenever all
5253 tasks are blocked.
5354
54- .. warning::
55-
56- If you're using :func:`wait_all_tasks_blocked` and
57- :attr:`autojump_threshold` together, then you have to be
58- careful. Setting :attr:`autojump_threshold` acts like a background
59- task calling::
60-
61- while True:
62- await wait_all_tasks_blocked(
63- cushion=clock.autojump_threshold, tiebreaker=float("inf"))
64-
65- This means that if you call :func:`wait_all_tasks_blocked` with a
66- cushion *larger* than your autojump threshold, then your call to
67- :func:`wait_all_tasks_blocked` will never return, because the
68- autojump task will keep waking up before your task does, and each
69- time it does it'll reset your task's timer. However, if your cushion
70- and the autojump threshold are the *same*, then the autojump's
71- tiebreaker will prevent them from interfering (unless you also set
72- your tiebreaker to infinity for some reason. Don't do that). As an
73- important special case: this means that if you set an autojump
74- threshold of zero and use :func:`wait_all_tasks_blocked` with the
75- default zero cushion, then everything will work fine.
76-
77- **Summary**: you should set :attr:`autojump_threshold` to be at
78- least as large as the largest cushion you plan to pass to
79- :func:`wait_all_tasks_blocked`.
55+ .. note:: If you use ``autojump_threshold`` and
56+ `wait_all_tasks_blocked` at the same time, then you might wonder how
57+ they interact, since they both cause things to happen after the run
58+ loop goes idle for some time. The answer is:
59+ `wait_all_tasks_blocked` takes priority. If there's a task blocked
60+ in `wait_all_tasks_blocked`, then the autojump feature treats that
61+ as active task and does *not* jump the clock.
8062
8163 """
8264
@@ -88,8 +70,6 @@ def __init__(self, rate=0.0, autojump_threshold=inf):
8870 self ._virtual_base = 0.0
8971 self ._rate = 0.0
9072 self ._autojump_threshold = 0.0
91- self ._autojump_task = None
92- self ._autojump_cancel_scope = None
9373 # kept as an attribute so that our tests can monkeypatch it
9474 self ._real_clock = time .perf_counter
9575
@@ -124,56 +104,39 @@ def autojump_threshold(self):
124104 @autojump_threshold .setter
125105 def autojump_threshold (self , new_autojump_threshold ):
126106 self ._autojump_threshold = float (new_autojump_threshold )
127- self ._maybe_spawn_autojump_task ()
128- if self ._autojump_cancel_scope is not None :
129- # Task is running and currently blocked on the old setting, wake
130- # it up so it picks up the new setting
131- self ._autojump_cancel_scope .cancel ()
132-
133- async def _autojumper (self ):
134- while True :
135- with _core .CancelScope () as cancel_scope :
136- self ._autojump_cancel_scope = cancel_scope
137- try :
138- # If the autojump_threshold changes, then the setter does
139- # cancel_scope.cancel(), which causes the next line here
140- # to raise Cancelled, which is absorbed by the cancel
141- # scope above, and effectively just causes us to skip back
142- # to the start the loop, like a 'continue'.
143- await _core .wait_all_tasks_blocked (self ._autojump_threshold , inf )
144- statistics = _core .current_statistics ()
145- jump = statistics .seconds_to_next_deadline
146- if 0 < jump < inf :
147- self .jump (jump )
148- else :
149- # There are no deadlines, nothing is going to happen
150- # until some actual I/O arrives (or maybe another
151- # wait_all_tasks_blocked task wakes up). That's fine,
152- # but if our threshold is zero then this will become a
153- # busy-wait -- so insert a small-but-non-zero _sleep to
154- # avoid that.
155- if self ._autojump_threshold == 0 :
156- await _core .wait_all_tasks_blocked (0.01 )
157- finally :
158- self ._autojump_cancel_scope = None
159-
160- def _maybe_spawn_autojump_task (self ):
161- if self ._autojump_threshold < inf and self ._autojump_task is None :
162- try :
163- clock = _core .current_clock ()
164- except RuntimeError :
165- return
166- if clock is self :
167- self ._autojump_task = _core .spawn_system_task (self ._autojumper )
107+ self ._try_resync_autojump_threshold ()
108+
109+ # runner.clock_autojump_threshold is an internal API that isn't easily
110+ # usable by custom third-party Clock objects. If you need access to this
111+ # functionality, let us know, and we'll figure out how to make a public
112+ # API. Discussion:
113+ #
114+ # https://github.com/python-trio/trio/issues/1587
115+ def _try_resync_autojump_threshold (self ):
116+ try :
117+ runner = GLOBAL_RUN_CONTEXT .runner
118+ if runner .is_guest :
119+ runner .force_guest_tick_asap ()
120+ except AttributeError :
121+ pass
122+ else :
123+ runner .clock_autojump_threshold = self ._autojump_threshold
124+
125+ # Invoked by the run loop when runner.clock_autojump_threshold is
126+ # exceeded.
127+ def _autojump (self ):
128+ statistics = _core .current_statistics ()
129+ jump = statistics .seconds_to_next_deadline
130+ if 0 < jump < inf :
131+ self .jump (jump )
168132
169133 def _real_to_virtual (self , real ):
170134 real_offset = real - self ._real_base
171135 virtual_offset = self ._rate * real_offset
172136 return self ._virtual_base + virtual_offset
173137
174138 def start_clock (self ):
175- token = _core .current_trio_token ()
176- token .run_sync_soon (self ._maybe_spawn_autojump_task )
139+ self ._try_resync_autojump_threshold ()
177140
178141 def current_time (self ):
179142 return self ._real_to_virtual (self ._real_clock ())
0 commit comments