Skip to content
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

Support deterministic scheduling #890

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions newsfragments/890.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added an internal mechanism for pytest-trio's
`Hypothesis <https://hypothesis.readthedocs.io>`__ integration
to make the task scheduler reproducible and avoid flaky tests.
14 changes: 14 additions & 0 deletions trio/_core/_run.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import functools
import itertools
import logging
import os
import random
Expand Down Expand Up @@ -58,6 +59,12 @@
else: # pragma: no cover
raise NotImplementedError("unsupported platform")

# When running under Hypothesis, we want examples to be reproducible and
# shrinkable. pytest-trio's Hypothesis integration monkeypatches this
# variable to True, and registers the Random instance _r for Hypothesis
# to manage for each test case, which together should make Trio's task
# scheduling loop deterministic. We have a test for that, of course.
_ALLOW_DETERMINISTIC_SCHEDULING = False
_r = random.Random()

# Used to log exceptions in instruments
Expand Down Expand Up @@ -682,6 +689,7 @@ class Task:
name = attr.ib()
# PEP 567 contextvars context
context = attr.ib()
_counter = attr.ib(init=False, factory=itertools.count().__next__)

# Invariant:
# - for unscheduled tasks, _next_send is None
Expand Down Expand Up @@ -1557,6 +1565,12 @@ def run_impl(runner, async_fn, args):
# change too, like the deadlines tie-breaker and the non-deterministic
# ordering of task._notify_queues.)
batch = list(runner.runq)
if _ALLOW_DETERMINISTIC_SCHEDULING:
pquentin marked this conversation as resolved.
Show resolved Hide resolved
# We're running under Hypothesis, and pytest-trio has patched this
# in to make the scheduler deterministic and avoid flaky tests.
# It's not worth the (small) performance cost in normal operation,
# since we'll shuffle the list and _r is only seeded for tests.
batch.sort(key=lambda t: t._counter)
runner.runq.clear()
_r.shuffle(batch)
while batch:
Expand Down
42 changes: 42 additions & 0 deletions trio/tests/test_scheduler_determinism.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import trio


async def scheduler_trace():
"""Returns a scheduler-dependent value we can use to check determinism."""
trace = []

async def tracer(name):
for i in range(10):
trace.append((name, i))
await trio.sleep(0)

async with trio.open_nursery() as nursery:
for i in range(5):
nursery.start_soon(tracer, i)

return tuple(trace)


def test_the_trio_scheduler_is_not_deterministic():
# At least, not yet. See https://github.com/python-trio/trio/issues/32
traces = []
for _ in range(10):
traces.append(trio.run(scheduler_trace))
assert len(set(traces)) == len(traces)


def test_the_trio_scheduler_is_deterministic_if_seeded(monkeypatch):
monkeypatch.setattr(
trio._core._run, "_ALLOW_DETERMINISTIC_SCHEDULING", True
)
traces = []
for _ in range(10):
state = trio._core._run._r.getstate()
try:
trio._core._run._r.seed(0)
traces.append(trio.run(scheduler_trace))
finally:
trio._core._run._r.setstate(state)

assert len(traces) == 10
assert len(set(traces)) == 1