Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')):
Expand Down
12 changes: 11 additions & 1 deletion uvloop/handles/timer.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<uv.uv_timer_t*>self._handle,
__uvtimer_callback,
self.timeout, 0)
self.timeout + 1, 0)
if err < 0:
exc = convert_error(err)
self._fatal_error(exc, True)
Expand Down
Loading