Skip to content

Commit b94b9c8

Browse files
authored
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.
1 parent bf2c053 commit b94b9c8

3 files changed

Lines changed: 23 additions & 4 deletions

File tree

Lib/concurrent/interpreters/_queues.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,12 @@ def put(self, obj, block=True, timeout=None, *,
220220
timeout = int(timeout)
221221
if timeout < 0:
222222
raise ValueError(f'timeout value must be non-negative')
223-
end = time.time() + timeout
223+
end = time.monotonic() + timeout
224224
while True:
225225
try:
226226
_queues.put(self._id, obj, unboundop)
227227
except QueueFull:
228-
if timeout is not None and time.time() >= end:
228+
if timeout is not None and time.monotonic() >= end:
229229
raise # re-raise
230230
time.sleep(_delay)
231231
else:
@@ -255,12 +255,12 @@ def get(self, block=True, timeout=None, *,
255255
timeout = int(timeout)
256256
if timeout < 0:
257257
raise ValueError(f'timeout value must be non-negative')
258-
end = time.time() + timeout
258+
end = time.monotonic() + timeout
259259
while True:
260260
try:
261261
obj, unboundop = _queues.get(self._id)
262262
except QueueEmpty:
263-
if timeout is not None and time.time() >= end:
263+
if timeout is not None and time.monotonic() >= end:
264264
raise # re-raise
265265
time.sleep(_delay)
266266
else:

Lib/test/test_interpreters/test_queues.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import pickle
33
import threading
44
from textwrap import dedent
5+
import time
56
import unittest
7+
from unittest import mock
68

79
from test.support import import_helper, Py_DEBUG
810
# Raise SkipTest if subinterpreters not supported.
@@ -354,6 +356,19 @@ def test_get_timeout(self):
354356
with self.assertRaises(queues.QueueEmpty):
355357
queue.get(HUGE_TIMEOUT, 0.1)
356358

359+
def test_timeout_uses_monotonic_clock(self):
360+
# gh-153005: the deadline must be computed from the monotonic clock,
361+
# since the wall clock can be adjusted while the call is blocked.
362+
queue = queues.create(1)
363+
with mock.patch.object(queues, 'time', wraps=time) as fake_time:
364+
with self.assertRaises(queues.QueueEmpty):
365+
queue.get(timeout=0)
366+
queue.put(None)
367+
with self.assertRaises(queues.QueueFull):
368+
queue.put(None, timeout=0)
369+
fake_time.monotonic.assert_called()
370+
fake_time.time.assert_not_called()
371+
357372
def test_get_nowait(self):
358373
queue = queues.create()
359374
with self.assertRaises(queues.QueueEmpty):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:meth:`!concurrent.interpreters.Queue.get` and
2+
:meth:`!concurrent.interpreters.Queue.put` now compute their ``timeout``
3+
deadline from :func:`time.monotonic` instead of the wall clock, so adjusting
4+
the system clock during the call no longer makes them over- or under-wait.

0 commit comments

Comments
 (0)