diff --git a/tests/test_base.py b/tests/test_base.py index b226a84f..e66f3180 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -220,6 +220,24 @@ def cb(): finished = int(round(self.loop.time() * 1000)) self.assertGreaterEqual(finished - started, 69) + def test_call_later_not_early(self): + # Refs #739: asyncio.sleep()/call_later() must never fire before + # the requested delay has actually elapsed, as measured by a + # wall-clock source independent of the loop's own (millisecond- + # truncated) internal clock. The underlying bug only manifests + # when the loop's internal clock happens to be sampled close to + # a millisecond boundary, so run enough iterations to make a + # regression here reliably observable. + async def main(): + delay = 0.01 + for _ in range(300): + started = time.monotonic() + await asyncio.sleep(delay) + elapsed = time.monotonic() - started + self.assertGreaterEqual(elapsed, delay) + + self.loop.run_until_complete(main()) + def test_call_at(self): if (os.environ.get('TRAVIS_OS_NAME') or os.environ.get('GITHUB_WORKFLOW')): diff --git a/uvloop/handles/timer.pyx b/uvloop/handles/timer.pyx index 86d46ef0..d6e43424 100644 --- a/uvloop/handles/timer.pyx +++ b/uvloop/handles/timer.pyx @@ -50,9 +50,19 @@ cdef class UVTimer(UVHandle): uv.uv_update_time(self._loop.uvloop) # void self.start_t = uv.uv_now(self._loop.uvloop) + # libuv's internal clock (loop->time, just refreshed above) is + # the current monotonic time truncated ("floored") to whole + # milliseconds, discarding up to just under 1ms. uv_timer_start() + # schedules the callback for loop->time + timeout, so without + # compensation the timer can fire up to ~1ms sooner than + # `self.timeout` milliseconds of actual wall-clock time have + # elapsed. Pad the timeout given to libuv by 1ms so the timer + # never fires earlier than requested; self.timeout (and thus + # get_when()/TimerHandle.when()) is left untouched so it keeps + # reporting the originally requested deadline. err = uv.uv_timer_start(self._handle, __uvtimer_callback, - self.timeout, 0) + self.timeout + 1, 0) if err < 0: exc = convert_error(err) self._fatal_error(exc, True)