From 0dd158a761cc9778e7a20ef2214f99e5922aa93b Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sun, 19 Jul 2026 17:56:26 +0300 Subject: [PATCH] gh-153005: Use a monotonic clock for concurrent.interpreters Queue timeouts Queue.get() and Queue.put() computed their timeout deadline from time.time(), the wall clock. If the system clock was stepped (NTP, a manual change) while a call was blocked, the timeout could over- or under-wait. queue.Queue uses time.monotonic() for the same reason. Compute the deadline and check it against time.monotonic() instead. --- Lib/concurrent/interpreters/_queues.py | 8 +++---- Lib/test/test_interpreters/test_queues.py | 23 +++++++++++++++++++ ...-07-19-17-45-00.gh-issue-153005.F6WH92.rst | 4 ++++ 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-19-17-45-00.gh-issue-153005.F6WH92.rst diff --git a/Lib/concurrent/interpreters/_queues.py b/Lib/concurrent/interpreters/_queues.py index 5f3ee0934de59d6..fc4ee595f3aa995 100644 --- a/Lib/concurrent/interpreters/_queues.py +++ b/Lib/concurrent/interpreters/_queues.py @@ -220,12 +220,12 @@ def put(self, obj, block=True, timeout=None, *, timeout = int(timeout) if timeout < 0: raise ValueError(f'timeout value must be non-negative') - end = time.time() + timeout + end = time.monotonic() + timeout while True: try: _queues.put(self._id, obj, unboundop) except QueueFull: - if timeout is not None and time.time() >= end: + if timeout is not None and time.monotonic() >= end: raise # re-raise time.sleep(_delay) else: @@ -255,12 +255,12 @@ def get(self, block=True, timeout=None, *, timeout = int(timeout) if timeout < 0: raise ValueError(f'timeout value must be non-negative') - end = time.time() + timeout + end = time.monotonic() + timeout while True: try: obj, unboundop = _queues.get(self._id) except QueueEmpty: - if timeout is not None and time.time() >= end: + if timeout is not None and time.monotonic() >= end: raise # re-raise time.sleep(_delay) else: diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py index 77334aea3836b98..85f88e9ddbb9b23 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -3,6 +3,7 @@ import threading from textwrap import dedent import unittest +from unittest import mock from test.support import import_helper, Py_DEBUG # Raise SkipTest if subinterpreters not supported. @@ -354,6 +355,28 @@ def test_get_timeout(self): with self.assertRaises(queues.QueueEmpty): queue.get(HUGE_TIMEOUT, 0.1) + def test_timeout_uses_monotonic_clock(self): + # gh-153005: the timeout deadline must be based on time.monotonic(), + # not the wall clock, so adjusting the system clock during the call + # cannot make get()/put() over- or under-wait. + class FakeClock: + def __init__(self): + self.now = 1000.0 + def monotonic(self): + return self.now + def sleep(self, delay): + self.now += delay + def time(self): + raise AssertionError('the wall clock must not be used') + + with mock.patch.object(queues, 'time', FakeClock()): + queue = queues.create(1) + with self.assertRaises(queues.QueueEmpty): + queue.get(timeout=1, _delay=0.4) + queue.put(None) + with self.assertRaises(queues.QueueFull): + queue.put(None, timeout=1, _delay=0.4) + def test_get_nowait(self): queue = queues.create() with self.assertRaises(queues.QueueEmpty): diff --git a/Misc/NEWS.d/next/Library/2026-07-19-17-45-00.gh-issue-153005.F6WH92.rst b/Misc/NEWS.d/next/Library/2026-07-19-17-45-00.gh-issue-153005.F6WH92.rst new file mode 100644 index 000000000000000..7f3d10b3072ef94 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-19-17-45-00.gh-issue-153005.F6WH92.rst @@ -0,0 +1,4 @@ +:meth:`!concurrent.interpreters.Queue.get` and +:meth:`!concurrent.interpreters.Queue.put` now compute their ``timeout`` +deadline from :func:`time.monotonic` instead of the wall clock, so adjusting +the system clock during the call no longer makes them over- or under-wait.