Skip to content

Commit 69107e8

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 acdecb3 commit 69107e8

3 files changed

Lines changed: 23 additions & 5 deletions

File tree

Lib/test/support/interpreters/queues.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,14 @@ def put(self, obj, timeout=None, *,
230230
timeout = int(timeout)
231231
if timeout < 0:
232232
raise ValueError(f'timeout value must be non-negative')
233-
end = time.time() + timeout
233+
end = time.monotonic() + timeout
234234
if fmt is _PICKLED:
235235
obj = pickle.dumps(obj)
236236
while True:
237237
try:
238238
_queues.put(self._id, obj, fmt, unboundop)
239239
except QueueFull as exc:
240-
if timeout is not None and time.time() >= end:
240+
if timeout is not None and time.monotonic() >= end:
241241
raise # re-raise
242242
time.sleep(_delay)
243243
else:
@@ -271,12 +271,12 @@ def get(self, timeout=None, *,
271271
timeout = int(timeout)
272272
if timeout < 0:
273273
raise ValueError(f'timeout value must be non-negative')
274-
end = time.time() + timeout
274+
end = time.monotonic() + timeout
275275
while True:
276276
try:
277277
obj, fmt, unboundop = _queues.get(self._id)
278278
except QueueEmpty as exc:
279-
if timeout is not None and time.time() >= end:
279+
if timeout is not None and time.monotonic() >= end:
280280
raise # re-raise
281281
time.sleep(_delay)
282282
else:

Lib/test/test_interpreters/test_queues.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import pickle
33
import threading
44
from textwrap import dedent
5-
import unittest
65
import time
6+
import unittest
7+
from unittest import mock
78

89
from test.support import import_helper, Py_DEBUG
910
# Raise SkipTest if subinterpreters not supported.
@@ -383,6 +384,19 @@ def test_get_timeout(self):
383384
with self.assertRaises(queues.QueueEmpty):
384385
queue.get(timeout=0.1)
385386

387+
def test_timeout_uses_monotonic_clock(self):
388+
# gh-153005: the deadline must be computed from the monotonic clock,
389+
# since the wall clock can be adjusted while the call is blocked.
390+
queue = queues.create(1)
391+
with mock.patch.object(queues, 'time', wraps=time) as fake_time:
392+
with self.assertRaises(queues.QueueEmpty):
393+
queue.get(timeout=0)
394+
queue.put(None)
395+
with self.assertRaises(queues.QueueFull):
396+
queue.put(None, timeout=0)
397+
fake_time.monotonic.assert_called()
398+
fake_time.time.assert_not_called()
399+
386400
def test_get_nowait(self):
387401
queue = queues.create()
388402
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)