Skip to content

Commit 5c1c1c5

Browse files
fedonmanBHUVANSH855
authored andcommitted
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 b94b9c8)
1 parent 90a1f02 commit 5c1c1c5

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
@@ -221,12 +221,12 @@ def put(self, obj, block=True, timeout=None, *,
221221
timeout = int(timeout)
222222
if timeout < 0:
223223
raise ValueError(f'timeout value must be non-negative')
224-
end = time.time() + timeout
224+
end = time.monotonic() + timeout
225225
while True:
226226
try:
227227
_queues.put(self._id, obj, unboundop)
228228
except QueueFull as exc:
229-
if timeout is not None and time.time() >= end:
229+
if timeout is not None and time.monotonic() >= end:
230230
raise # re-raise
231231
time.sleep(_delay)
232232
else:
@@ -256,12 +256,12 @@ def get(self, block=True, timeout=None, *,
256256
timeout = int(timeout)
257257
if timeout < 0:
258258
raise ValueError(f'timeout value must be non-negative')
259-
end = time.time() + timeout
259+
end = time.monotonic() + timeout
260260
while True:
261261
try:
262262
obj, unboundop = _queues.get(self._id)
263263
except QueueEmpty as exc:
264-
if timeout is not None and time.time() >= end:
264+
if timeout is not None and time.monotonic() >= end:
265265
raise # re-raise
266266
time.sleep(_delay)
267267
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.
@@ -355,6 +357,19 @@ def test_get_timeout(self):
355357
with self.assertRaises(queues.QueueEmpty):
356358
queue.get(HUGE_TIMEOUT, 0.1)
357359

360+
def test_timeout_uses_monotonic_clock(self):
361+
# gh-153005: the deadline must be computed from the monotonic clock,
362+
# since the wall clock can be adjusted while the call is blocked.
363+
queue = queues.create(1)
364+
with mock.patch.object(queues, 'time', wraps=time) as fake_time:
365+
with self.assertRaises(queues.QueueEmpty):
366+
queue.get(timeout=0)
367+
queue.put(None)
368+
with self.assertRaises(queues.QueueFull):
369+
queue.put(None, timeout=0)
370+
fake_time.monotonic.assert_called()
371+
fake_time.time.assert_not_called()
372+
358373
def test_get_nowait(self):
359374
queue = queues.create()
360375
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)