Skip to content

Commit

Permalink
Merge pull request #890 from Zac-HD/optionally-deterministic-scheduler
Browse files Browse the repository at this point in the history
Support deterministic scheduling
  • Loading branch information
Zac-HD authored Feb 3, 2019
2 parents 00ca910 + f532ebf commit 319327d
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 0 deletions.
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:
# 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

0 comments on commit 319327d

Please sign in to comment.