-
-
Notifications
You must be signed in to change notification settings - Fork 378
Make autojump/autojump_threshold methods of abc.Clock
#3371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. ❌ Your project check has failed because the head coverage (99.99484%) is below the target coverage (100.00000%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #3371 +/- ##
====================================================
- Coverage 100.00000% 99.99484% -0.00517%
====================================================
Files 128 128
Lines 19407 19398 -9
Branches 1318 1316 -2
====================================================
- Hits 19407 19397 -10
- Partials 0 1 +1
🚀 New features to boost your workflow:
|
a666494 to
25289f1
Compare
for more information, see https://pre-commit.ci
|
Following some discussion in #3369, the first commit now represents the minimum change - the new methods in the ABC have default implementations, so the PR is not breaking. But the whole handling of the clock in the event loops feels a little weird to me. So I tried to re-imagine the logic in the second commit. The idea is, we have real time (system time), and virtual time (clock time). These should not be mixed. The runner's statistics work with virtual times. In Thus the responsibility of the clock is to keep virtual time, and to convert it to real time when necessary. When we
The second argument in |
|
Sorry, I've been out of commission for a while... Strictly looking at the code this is still a breaking change (
This would be so simple if only we had no |
|
OK I played around with things until I have something that passes our test suite, with a bit of cheating: I added a new statistic tracking how many tasks are waiting for an idle period (i.e. in Here's the very hacky diff if you want to see: Details
diff --git a/src/trio/_abc.py b/src/trio/_abc.pyindex abb6824381..2c3982d055 100644
--- a/src/trio/_abc.py
+++ b/src/trio/_abc.py
@@ -65,6 +65,9 @@
"""
+ def propagate(self, timeout: float) -> None: # TODO: think about migration
+ pass
+
class Instrument(ABC): # noqa: B024 # conceptually is ABC
"""The interface for run loop instrumentation.
diff --git a/src/trio/_core/_mock_clock.py b/src/trio/_core/_mock_clock.py
index e437464b65..0bf738a61c 100644
--- a/src/trio/_core/_mock_clock.py
+++ b/src/trio/_core/_mock_clock.py
@@ -70,6 +70,7 @@
self._real_base = 0.0
self._virtual_base = 0.0
self._rate = 0.0
+ self._jump_to: float | None = None
# kept as an attribute so that our tests can monkeypatch it
self._real_clock = time.perf_counter
@@ -118,9 +119,9 @@
runner.force_guest_tick_asap()
except AttributeError:
pass
- else:
- if runner.clock is self:
- runner.clock_autojump_threshold = self._autojump_threshold
+ # else:
+ # if runner.clock is self:
+ # runner.clock_autojump_threshold = self._autojump_threshold
# Invoked by the run loop when runner.clock_autojump_threshold is
# exceeded.
@@ -145,6 +146,12 @@
virtual_timeout = deadline - self.current_time()
if virtual_timeout <= 0:
return 0
+ elif (
+ self.autojump_threshold * self._rate <= virtual_timeout and
+ _core.current_statistics().idle_waiters == 0
+ ):
+ self._jump_to = deadline
+ return self.autojump_threshold
elif self._rate > 0:
return virtual_timeout / self._rate
else:
@@ -163,3 +170,11 @@
if seconds < 0:
raise ValueError("time can't go backwards")
self._virtual_base += seconds
+
+ def propagate(self, timeout: float) -> None:
+ # we base most parts off a system clock, so this is unnecessary
+ # except for:
+ if self._jump_to:
+ # we really should just rely on `self._jump_to`...
+ self._autojump() # temp
+ self._jump_to = None
diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py
index 8a7ddc1dc3..25f262fa59 100644
--- a/src/trio/_core/_run.py
+++ b/src/trio/_core/_run.py
@@ -1709,6 +1709,8 @@
backend. This always has an attribute ``backend`` which is a string
naming which operating-system-specific I/O backend is in use; the
other attributes vary between backends.
+ * ``idle_waiters`` (int): The number of tasks waiting in
+ `Runner.wait_all_tasks_blocked`.
"""
tasks_living: int
@@ -1716,6 +1718,7 @@
seconds_to_next_deadline: float
io_statistics: IOStatistics
run_sync_soon_queue_size: int
+ idle_waiters: int
# This holds all the state that gets trampolined back and forth between
@@ -1817,7 +1820,7 @@
asyncgens: AsyncGenerators = attrs.Factory(AsyncGenerators)
# If everything goes idle for this long, we call clock._autojump()
- clock_autojump_threshold: float = inf
+ # clock_autojump_threshold: float = inf
# Guest mode stuff
is_guest: bool = False
@@ -1869,6 +1872,7 @@
seconds_to_next_deadline=seconds_to_next_deadline,
io_statistics=self.io_manager.statistics(),
run_sync_soon_queue_size=self.entry_queue.size(),
+ idle_waiters=len(self.waiting_for_idle),
)
@_public
@@ -2758,9 +2762,9 @@
# We use 'elif' here because if there are tasks in
# wait_all_tasks_blocked, then those tasks will wake up without
# jumping the clock, so we don't need to autojump.
- elif runner.clock_autojump_threshold < timeout:
- timeout = runner.clock_autojump_threshold
- idle_primed = IdlePrimedTypes.AUTOJUMP_CLOCK
+ # elif runner.clock_autojump_threshold < timeout:
+ # timeout = runner.clock_autojump_threshold
+ # idle_primed = IdlePrimedTypes.AUTOJUMP_CLOCK
if "before_io_wait" in runner.instruments:
runner.instruments.call("before_io_wait", timeout)
@@ -2773,6 +2777,8 @@
if "after_io_wait" in runner.instruments:
runner.instruments.call("after_io_wait", timeout)
+ runner.clock.propagate(timeout)
+
# Process cancellations due to deadline expiry
now = runner.clock.current_time()
if runner.deadlines.expire(now):
@@ -2808,10 +2814,6 @@
runner.reschedule(task)
else:
break
- else:
- assert idle_primed is IdlePrimedTypes.AUTOJUMP_CLOCK
- assert isinstance(runner.clock, _core.MockClock)
- runner.clock._autojump()
# Process all runnable tasks, but only the ones that are already
# runnable now. Anything that becomes runnable during this cycle
diff --git a/src/trio/_core/_tests/test_guest_mode.py b/src/trio/_core/_tests/test_guest_mode.py
index 743eddc846..649f71d6ab 100644
--- a/src/trio/_core/_tests/test_guest_mode.py
+++ b/src/trio/_core/_tests/test_guest_mode.py
@@ -655,7 +655,7 @@
assert signal.getsignal(signal.SIGINT) is signal.default_int_handler
-
+@pytest.mark.skip("this breaks")
def test_guest_mode_autojump_clock_threshold_changing() -> None:
# This is super obscure and probably no-one will ever notice, but
# technically mutating the MockClock.autojump_threshold from the host
diff --git a/src/trio/_core/_tests/test_mock_clock.py b/src/trio/_core/_tests/test_mock_clock.py
index 6131e7ba3f..26776f442a 100644
--- a/src/trio/_core/_tests/test_mock_clock.py
+++ b/src/trio/_core/_tests/test_mock_clock.py
@@ -177,7 +177,7 @@
assert record == ["waiter done", "yawn"]
-
+@pytest.mark.skip("is no longer necessary")
async def test_initialization_doesnt_mutate_runner() -> None:
before = (
GLOBAL_RUN_CONTEXT.runner.clock,If it's fine by you (I assume you've already added a workaround), I'd like to propose this design in the issue and get others to weigh in? |
The full PR yes, but but just the first commit is non-breaking.
Oh yes, it's not a blocker or anything. We can take our time.
Sounds good to me. I assume your diff is based off trunk? Seems like a simpler solution, and it does get rid of the I noticed though that you commented out the test that modifies the autojump threshold from a host runner (which was an issue I didn't know how to resolve) - does that mean this functionality can be dropped? |
Yeah, in my opinion this is not an important property (we don't promise it in the docstrings I checked). But I guess I should also mention that in a proposal. (I guess this is also breaking :P)
My thought process was that An alternative to Hopefully I get the motivation to write up the proposal promptly! |
Fixes #3369 by exposing
autojumpandautojump_thresholdas methods ofabc.Clock.runner.clock_autojump_thresholdis removed, and the runner now gets it fromclock.TODO: adjust documentation (
autoump_thresholddescription can be moved fromMockClocktoabc.Clock)Note that
MockClockstill has some internal trickery related to guest mode, but it is only necessary if you want to modifyautojump_thresholdwhile the loop is running. I am not sure how to resolve that at the moment, but I'll think about it.