Skip to content

Commit

Permalink
Support deterministic scheduling
Browse files Browse the repository at this point in the history
Very important for Hypothesis, and arguably for debugging in general.
  • Loading branch information
Zac-HD committed Jan 30, 2019
1 parent 0b8f377 commit c2d6660
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 0 deletions.
4 changes: 4 additions & 0 deletions newsfragments/890.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The Trio scheduler can now be made deterministic by seeding the internal
random.Random instance that is used to shuffle available tasks.
This is not public API, but is important for some tools such as
pytest-trio's `Hypothesis <https://hypothesis.readthedocs.io>`_ integration.
15 changes: 15 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 @@ -63,6 +64,10 @@
# Used to log exceptions in instruments
INSTRUMENT_LOGGER = logging.getLogger("trio.abc.Instrument")

# Private flag to sort tasks before shuffling them, so that seeding
# the Random instance `_r` can make scheduling deterministic.
_ALLOW_DETERMINISTIC_SCHEDULING = False


# On 3.7+, Context.run() is implemented in C and doesn't show up in
# tracebacks. On 3.6 and earlier, we use the contextvars backport, which is
Expand Down Expand Up @@ -682,6 +687,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 @@ -1556,7 +1562,16 @@ def run_impl(runner, async_fn, args):
# scheduling, then there are other things that will probably need to
# change too, like the deadlines tie-breaker and the non-deterministic
# ordering of task._notify_queues.)
#
# We sort tasks before shuffling them so that seeding the Random
# instance can make the scheduler deterministic, which is important
# for testing and debugging, especially with tools such as Hypothesis,
# without giving up the advantages of sets everywhere else. This is
# optional, and disabled by default, due to the performance impact in
# Trio's inner loop when there is a large number of tasks (PR #890).
batch = list(runner.runq)
if _ALLOW_DETERMINISTIC_SCHEDULING:
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

0 comments on commit c2d6660

Please sign in to comment.