From c93347fa152d79c737909a8407ebf4bcec04d731 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Mon, 28 Jan 2019 09:34:34 -1000 Subject: [PATCH] Support deterministic scheduling Very important for Hypothesis, and arguably for debugging in general. --- newsfragments/889.feature.rst | 4 +++ trio/_core/_run.py | 14 ++++++++- trio/tests/test_scheduler_determinism.py | 39 ++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 newsfragments/889.feature.rst create mode 100644 trio/tests/test_scheduler_determinism.py diff --git a/newsfragments/889.feature.rst b/newsfragments/889.feature.rst new file mode 100644 index 0000000000..ed1de6b623 --- /dev/null +++ b/newsfragments/889.feature.rst @@ -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 `_ integration. diff --git a/trio/_core/_run.py b/trio/_core/_run.py index f96f6c9181..c7f33e3e95 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -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 @@ -704,6 +705,12 @@ class Task: def __repr__(self): return ("".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" @@ -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: diff --git a/trio/tests/test_scheduler_determinism.py b/trio/tests/test_scheduler_determinism.py new file mode 100644 index 0000000000..d7e82649df --- /dev/null +++ b/trio/tests/test_scheduler_determinism.py @@ -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