From a4971450d8551202168b80f5164fdd9a247c34d7 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Sat, 4 Jul 2026 16:39:43 +0800 Subject: [PATCH] gh-153005: Fix timeout handling in concurrent.interpreters.Queue Queue.get() and Queue.put() converted the timeout argument with int(), truncating a floating-point timeout to whole seconds, so a timeout below one second became non-blocking. Use the value as given, reject a negative or NaN timeout with ValueError, and base the deadline on time.monotonic(), matching queue.Queue. --- Lib/concurrent/interpreters/_queues.py | 20 +++++----- Lib/test/test_interpreters/test_queues.py | 37 +++++++++++++++++++ ...-07-04-16-44-43.gh-issue-153005.lWdjWt.rst | 3 ++ 3 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-04-16-44-43.gh-issue-153005.lWdjWt.rst diff --git a/Lib/concurrent/interpreters/_queues.py b/Lib/concurrent/interpreters/_queues.py index 5f3ee0934de59d6..608032f46761b6f 100644 --- a/Lib/concurrent/interpreters/_queues.py +++ b/Lib/concurrent/interpreters/_queues.py @@ -217,15 +217,15 @@ def put(self, obj, block=True, timeout=None, *, else: unboundop, = _serialize_unbound(unbounditems) if timeout is not None: - timeout = int(timeout) - if timeout < 0: - raise ValueError(f'timeout value must be non-negative') - end = time.time() + timeout + timeout = float(timeout) + if not timeout >= 0: # also rejects NaN + raise ValueError('timeout value must be non-negative') + 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: @@ -252,15 +252,15 @@ def get(self, block=True, timeout=None, *, if not block: return self.get_nowait() if timeout is not None: - timeout = int(timeout) - if timeout < 0: - raise ValueError(f'timeout value must be non-negative') - end = time.time() + timeout + timeout = float(timeout) + if not timeout >= 0: # also rejects NaN + raise ValueError('timeout value must be non-negative') + 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..e34d1159892a388 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -1,6 +1,7 @@ import importlib import pickle import threading +import time from textwrap import dedent import unittest @@ -354,6 +355,42 @@ def test_get_timeout(self): with self.assertRaises(queues.QueueEmpty): queue.get(HUGE_TIMEOUT, 0.1) + def test_put_timeout_fractional(self): + # A fractional timeout must not be truncated to whole seconds. + queue = queues.create(1) + queue.put(None) + start = time.monotonic() + with self.assertRaises(queues.QueueFull): + queue.put(None, timeout=0.1) + self.assertGreaterEqual(time.monotonic() - start, 0.05) + + def test_get_timeout_fractional(self): + # A fractional timeout must not be truncated to whole seconds. + queue = queues.create() + start = time.monotonic() + with self.assertRaises(queues.QueueEmpty): + queue.get(timeout=0.1) + self.assertGreaterEqual(time.monotonic() - start, 0.05) + + def test_timeout_zero_does_not_block(self): + queue = queues.create(1) + queue.put(None) + start = time.monotonic() + with self.assertRaises(queues.QueueFull): + queue.put(None, timeout=0) + self.assertLess(time.monotonic() - start, 1.0) + empty = queues.create() + with self.assertRaises(queues.QueueEmpty): + empty.get(timeout=0) + + def test_timeout_invalid(self): + queue = queues.create() + for bad in (-0.5, float('nan')): + with self.assertRaisesRegex(ValueError, 'non-negative'): + queue.get(timeout=bad) + with self.assertRaisesRegex(ValueError, 'non-negative'): + queue.put(None, timeout=bad) + def test_get_nowait(self): queue = queues.create() with self.assertRaises(queues.QueueEmpty): diff --git a/Misc/NEWS.d/next/Library/2026-07-04-16-44-43.gh-issue-153005.lWdjWt.rst b/Misc/NEWS.d/next/Library/2026-07-04-16-44-43.gh-issue-153005.lWdjWt.rst new file mode 100644 index 000000000000000..94d3052650b403f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-04-16-44-43.gh-issue-153005.lWdjWt.rst @@ -0,0 +1,3 @@ +Fix :meth:`concurrent.interpreters.Queue.get` and +:meth:`concurrent.interpreters.Queue.put` truncating a floating-point *timeout* +to whole seconds. A negative or NaN timeout now raises :exc:`ValueError`.