From 67ba575ae0af932c523750180c6d21d91dc2bbcc Mon Sep 17 00:00:00 2001 From: Neko Asakura Date: Fri, 23 Oct 2026 16:43:19 -0400 Subject: [PATCH 1/2] gh-160414: pass a notification on when `Condition.wait()` times out --- Lib/test/lock_tests.py | 33 +++++++++++++++++++ Lib/threading.py | 14 +++++--- ...-10-23-09-46-09.gh-issue-160414.Vq8sLm.rst | 2 ++ 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-23-09-46-09.gh-issue-160414.Vq8sLm.rst diff --git a/Lib/test/lock_tests.py b/Lib/test/lock_tests.py index 47025c041daf1e..8bbc1dec0f4df8 100644 --- a/Lib/test/lock_tests.py +++ b/Lib/test/lock_tests.py @@ -772,6 +772,39 @@ def test_nan_timeout(self): self.assertRaises(ValueError, cond.wait_for, lambda: False, nan) self.assertEqual(len(cond._waiters), 0) + def test_notify_after_timeout(self): + # gh-160414: check that notify() still wakes a waiter when the one + # it picks has just timed out. + cond = self.condtype() + waiting = threading.Event() + results = [] + def short(): + with cond: + waiting.set() + results.append(("short", cond.wait(0.01))) + def long(): + waiting.wait() + with cond: + results.append(("long", cond.wait(support.SHORT_TIMEOUT))) + t1 = threading.Thread(target=short) + t2 = threading.Thread(target=long) + t1.start() + t2.start() + try: + waiting.wait() + # Hold the lock until the short timeout has expired. + for _ in support.sleeping_retry(support.SHORT_TIMEOUT): + with cond: + if len(cond._waiters) == 2: + break + with cond: + time.sleep(0.1) + cond.notify() + finally: + t1.join() + t2.join() + self.assertEqual(sorted(results), [("long", True), ("short", False)]) + def test_waitfor(self): cond = self.condtype() state = 0 diff --git a/Lib/threading.py b/Lib/threading.py index 507d40a1baa275..e2302df229a896 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -368,14 +368,18 @@ def wait(self, timeout=None): else: # A NaN timeout is rejected by acquire(). gotit = waiter.acquire(True, timeout) - return gotit finally: self._acquire_restore(saved_state) if not gotit: - try: - self._waiters.remove(waiter) - except ValueError: - pass + if waiter.acquire(False): + # gh-160414: notified after the timeout; pass it on. + self.notify() + else: + try: + self._waiters.remove(waiter) + except ValueError: + pass + return gotit def wait_for(self, predicate, timeout=None): """Wait until a condition evaluates to True. diff --git a/Misc/NEWS.d/next/Library/2026-10-23-09-46-09.gh-issue-160414.Vq8sLm.rst b/Misc/NEWS.d/next/Library/2026-10-23-09-46-09.gh-issue-160414.Vq8sLm.rst new file mode 100644 index 00000000000000..892b97eb52894e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-23-09-46-09.gh-issue-160414.Vq8sLm.rst @@ -0,0 +1,2 @@ +Fix :meth:`threading.Condition.notify` waking no thread when the waiter it +picks has just timed out; the notification now goes to the next waiter. From 0b38f46a0b19a9d4ee09ecfdfcdd436e958067e5 Mon Sep 17 00:00:00 2001 From: Neko Asakura Date: Fri, 23 Oct 2026 19:49:37 -0400 Subject: [PATCH 2/2] gh-160414: make `test_notify_after_timeout` deterministic --- Lib/test/lock_tests.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Lib/test/lock_tests.py b/Lib/test/lock_tests.py index 8bbc1dec0f4df8..5264b2ba5c5b70 100644 --- a/Lib/test/lock_tests.py +++ b/Lib/test/lock_tests.py @@ -777,10 +777,17 @@ def test_notify_after_timeout(self): # it picks has just timed out. cond = self.condtype() waiting = threading.Event() + notified = threading.Event() results = [] + acquire_restore = cond._acquire_restore + def hold_t1(state): + if threading.current_thread() is t1: + waiting.set() + notified.wait(support.SHORT_TIMEOUT) + acquire_restore(state) + cond._acquire_restore = hold_t1 def short(): with cond: - waiting.set() results.append(("short", cond.wait(0.01))) def long(): waiting.wait() @@ -792,15 +799,13 @@ def long(): t2.start() try: waiting.wait() - # Hold the lock until the short timeout has expired. for _ in support.sleeping_retry(support.SHORT_TIMEOUT): with cond: if len(cond._waiters) == 2: + cond.notify() break - with cond: - time.sleep(0.1) - cond.notify() finally: + notified.set() t1.join() t2.join() self.assertEqual(sorted(results), [("long", True), ("short", False)])