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 28, 2019
1 parent 0b8f377 commit c93347f
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 1 deletion.
4 changes: 4 additions & 0 deletions newsfragments/889.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.
14 changes: 13 additions & 1 deletion trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,7 @@ class Task:
name = attr.ib()
# PEP 567 contextvars context
context = attr.ib()
_creation_time = attr.ib(init=False, factory=perf_counter)

# Invariant:
# - for unscheduled tasks, _next_send is None
Expand All @@ -704,6 +705,12 @@ class Task:
def __repr__(self):
return ("<Task {!r} at {:#x}>".format(self.name, id(self)))

def sort_key(self):
return (
self._creation_time, self.name, self._cancel_points,
self._schedule_points
)

@property
def parent_nursery(self):
"""The nursery this task is inside (or None if this is the "init"
Expand Down Expand Up @@ -1556,7 +1563,12 @@ 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.)
batch = list(runner.runq)
#
# 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.
batch = sorted(runner.runq, key=Task.sort_key)
runner.runq.clear()
_r.shuffle(batch)
while batch:
Expand Down
39 changes: 39 additions & 0 deletions trio/tests/test_scheduler_determinism.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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():
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 c93347f

Please sign in to comment.