From 5c1c1c57d17856f793911e00e82e4ebb0741a1d8 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 17 Aug 2026 22:20:07 +0300 Subject: [PATCH] gh-153005: Use a monotonic clock for concurrent.interpreters Queue timeouts (GH-154156) 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. (cherry picked from commit b94b9c8886a987a324a677b5fda5bef27f15cb14) --- Lib/concurrent/interpreters/_queues.py | 8 ++++---- Lib/test/test_interpreters/test_queues.py | 15 +++++++++++++++ ...2026-07-19-17-45-00.gh-issue-153005.F6WH92.rst | 4 ++++ 3 files changed, 23 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 56c947f7d2aa12d..f5655a8fdc85150 100644 --- a/Lib/concurrent/interpreters/_queues.py +++ b/Lib/concurrent/interpreters/_queues.py @@ -221,12 +221,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 as exc: - 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: @@ -256,12 +256,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 as exc: - 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 8c6a0efbb920d82..cdd0de614353c05 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. @@ -355,6 +357,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 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.