_queue: check for pending signals during blocking get()#8252
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthrough
ChangesSimpleQueue signal handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant MainThread
participant SimpleQueue
participant Semaphore
participant SignalHandler
participant WorkerThread
MainThread->>SimpleQueue: get()
SimpleQueue->>Semaphore: acquire()
loop bounded wait
Semaphore->>Semaphore: wait up to SIGNAL_CHECK_INTERVAL
Semaphore->>SignalHandler: vm.check_signals()
end
SignalHandler-->>Semaphore: handler completes
WorkerThread->>SimpleQueue: put("unblock")
SimpleQueue-->>MainThread: return "unblock"
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/stdlib/src/_queue.rs (1)
254-268: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAlign the non-blocking
getpath with the semaphore.
get(block=False)bypassesself.sem.acquire(...), whileget_nowait()consumes a permit before popping frombuf. That leaves the semaphore count ahead of the buffer after a non-blockingget, so a later blockingget()can consume a stale permit and then raiseEmptyon an empty queue.get(block=False)andget_nowait()should use the same path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/stdlib/src/_queue.rs` around lines 254 - 268, The non-blocking get path does not synchronize with the semaphore, causing permit and buffer counts to diverge. Update the `get` method’s `block == false` handling to call the same semaphore-acquisition logic as `get_nowait`, preserving the existing empty-queue error behavior; use `get_nowait` or a shared helper to ensure both paths consume a permit before removing from `self.buf`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@crates/stdlib/src/_queue.rs`:
- Around line 254-268: The non-blocking get path does not synchronize with the
semaphore, causing permit and buffer counts to diverge. Update the `get`
method’s `block == false` handling to call the same semaphore-acquisition logic
as `get_nowait`, preserving the existing empty-queue error behavior; use
`get_nowait` or a shared helper to ensure both paths consume a permit before
removing from `self.buf`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 4d087bfc-0d93-42ab-a54a-88410b6fb0f5
📒 Files selected for processing (2)
crates/stdlib/src/_queue.rsextra_tests/snippets/stdlib_queue_signal.py
| /// Wait chunk size so blocking waits stay responsive to signals, mirroring | ||
| /// CPython's `PyThread_acquire_lock_timed`. | ||
| #[cfg(feature = "threading")] | ||
| const SIGNAL_CHECK_INTERVAL: Duration = Duration::from_millis(50); |
There was a problem hiding this comment.
does this line has CPython reference?
There was a problem hiding this comment.
No CPython citation for 50ms, It was empirical choice.
CPython doesn't chunk either; it makes one sem_timedwait call and relies on EINTR.
parking_lot's condvar doesn't expose signal interruptions to us the way CPython does, So I think polling is the workaround. Comment updated to reflect this.
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 RustPython#8250 Assisted-by: Claude
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
256f201 to
a8ae1e3
Compare
_queue.SimpleQueue.get()doesn't check for pending signals while blocked #8250Summary
queue.SimpleQueue.get()blocked via a single uninterruptedparking_lot::Condvarwait, 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 avoids this by chunking its equivalent blocking wait (PyThread_acquire_lock_timed).This PR caps each wait at a short interval and calls
check_signals()between chunks, only giving up once the caller's real deadline has actually elapsed. The mutex guard is dropped beforecheck_signals()runs, since a signal handler that calls back into the same queue on the same thread would otherwise deadlock trying to relock a mutex it's stillholding (a self-deadlock I hit and fixed while developing this).
[Before]

[After]

Test plan
extra_tests/snippets/stdlib_queue_signal.py: arms aSIGALRMwell before a helper thread unblocks the queue, and asserts the handler ran near the alarm instead of only onceget()returned. Verified it fails with anAssertionErroragainst the pre-fix code and passes after the fix.cargo run -- -m test test_queue -j 1: 162 passed, 6 skipped (unrelated, memory-gated).cargo clippy/cargo fmt --check: clean.AI disclosure
This change was developed with assistance from Claude Code(claude-sonnet-5): investigating the root cause, drafting the fix andregression test, and iterating after I found a self-deadlock in an early
version of the fix. I reviewed the diff, ran the build/lints/tests myself, and verified the fix and regression test against both RustPython and real CPython before opening this PR.
Assisted-by: Claude Code:claude-sonnet-5
Summary by CodeRabbit
Summary by CodeRabbit
Bug Fixes
queue.SimpleQueue.get()/get_nowait()while waiting for items.Tests
get()call, confirms the call unblocks as expected, and verifies the signal handler runs within a reasonable time (skips on platforms without the required signal support).