diff --git a/Lib/concurrent/interpreters/_queues.py b/Lib/concurrent/interpreters/_queues.py index 5f3ee0934de59d..fc4ee595f3aa99 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 77334aea3836b9..baa772b3b36795 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -2,7 +2,9 @@ import pickle import threading from textwrap import dedent +import time import unittest +from unittest import mock from test.support import import_helper, Py_DEBUG # Raise SkipTest if subinterpreters not supported. @@ -354,6 +356,19 @@ 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 deadline must be computed from the monotonic clock, + # since the wall clock can be adjusted while the call is blocked. + queue = queues.create(1) + with mock.patch.object(queues, 'time', wraps=time) as fake_time: + with self.assertRaises(queues.QueueEmpty): + queue.get(timeout=0) + queue.put(None) + with self.assertRaises(queues.QueueFull): + queue.put(None, timeout=0) + fake_time.monotonic.assert_called() + fake_time.time.assert_not_called() + 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 00000000000000..7f3d10b3072ef9 --- /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.