Skip to content

Commit 0dd158a

Browse files
committed
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.
1 parent 1604043 commit 0dd158a

3 files changed

Lines changed: 31 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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import threading
44
from textwrap import dedent
55
import unittest
6+
from unittest import mock
67

78
from test.support import import_helper, Py_DEBUG
89
# Raise SkipTest if subinterpreters not supported.
@@ -354,6 +355,28 @@ def test_get_timeout(self):
354355
with self.assertRaises(queues.QueueEmpty):
355356
queue.get(HUGE_TIMEOUT, 0.1)
356357

358+
def test_timeout_uses_monotonic_clock(self):
359+
# gh-153005: the timeout deadline must be based on time.monotonic(),
360+
# not the wall clock, so adjusting the system clock during the call
361+
# cannot make get()/put() over- or under-wait.
362+
class FakeClock:
363+
def __init__(self):
364+
self.now = 1000.0
365+
def monotonic(self):
366+
return self.now
367+
def sleep(self, delay):
368+
self.now += delay
369+
def time(self):
370+
raise AssertionError('the wall clock must not be used')
371+
372+
with mock.patch.object(queues, 'time', FakeClock()):
373+
queue = queues.create(1)
374+
with self.assertRaises(queues.QueueEmpty):
375+
queue.get(timeout=1, _delay=0.4)
376+
queue.put(None)
377+
with self.assertRaises(queues.QueueFull):
378+
queue.put(None, timeout=1, _delay=0.4)
379+
357380
def test_get_nowait(self):
358381
queue = queues.create()
359382
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)