Files
Sumi JeongandJeong, YunWon 9d9fc27082 _queue: check for pending signals during blocking get() (#8252)
* _queue: check for pending signals during blocking get()

Semaphore::acquire() waited on the condvar in a single uninterrupted call,
so a Python-level signal handler never ran until the wait itself returned.
check_signals() is only invoked from the bytecode dispatch loop, and this
native wait never passed through it -- CPython chunks its equivalent
blocking wait for the same reason.

Cap each wait at a short interval and call check_signals() between chunks,
only giving up once the caller's real deadline has actually elapsed. The
mutex guard must be dropped before check_signals() runs: a signal handler
that calls back into the same queue on the same thread would otherwise try
to relock a mutex it's still holding and deadlock against itself.

Add a regression snippet that arms a SIGALRM well before a helper thread
unblocks the queue, and asserts the handler ran near the alarm instead of
only once get() returned.

Refs #8250
Assisted-by: Claude

* _queue: correct SIGNAL_CHECK_INTERVAL doc comment

The old comment claimed this mirrors CPython's PyThread_acquire_lock_timed,
but CPython doesn't chunk its wait either -- it makes one sem_timedwait call
and relies on the OS reporting EINTR. parking_lot's Condvar has no equivalent
signal to surface, so polling is a workaround for that gap, not a mirror of
CPython's approach.

Assisted-by: Claude

* Apply suggestions from code review

Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>

* _queue: fix comment indentation (cargo fmt)

Assisted-by: Claude

---------

Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2026-07-16 17:39:48 +09:00

53 lines
1.4 KiB
Python

"""A blocking queue.SimpleQueue.get() must stay responsive to signals.
SimpleQueue.get() waits on a Condvar. A single uninterrupted wait blocks
Python-level signal handlers (including the default KeyboardInterrupt) until
the wait itself returns, since signals are only delivered at bytecode
safepoints. A regression shows up as the handler firing only once get()
unblocks, instead of promptly when the signal actually arrives.
"""
import queue
import signal
import sys
import threading
import time
if sys.platform.startswith("win"):
print("skipped (no SIGALRM)")
raise SystemExit(0)
q = queue.SimpleQueue()
start = time.time()
handled_at = []
def handler(signum, frame):
handled_at.append(time.time() - start)
signal.signal(signal.SIGALRM, handler)
signal.setitimer(signal.ITIMER_REAL, 0.3)
def unblock_later():
time.sleep(1.5)
q.put("unblock")
threading.Thread(target=unblock_later, daemon=True).start()
item = q.get() # blocks until unblock_later() wakes us up
elapsed = time.time() - start
assert item == "unblock", item
assert handled_at, "signal handler never ran"
# The handler should fire around t=0.3s, when the timer was started, not t=1.5s
# (when get() finally unblocked).
assert handled_at[0] < elapsed - 0.5, (
f"signal handled at {handled_at[0]:.2f}s but get() only returned at "
f"{elapsed:.2f}s -- signal was not processed while blocked"
)
print("ok")