async: always defer task wakes via ngx_post_event#295
Conversation
`schedule()` ran `runnable.run()` synchronously when a task was woken from outside its own poll (`woken_while_running == false`). That violates the `Waker::wake()` contract (wakes must be non-blocking and non-re-entrant): when a wake fires from a `Drop` that holds a lock the woken task also needs — e.g. h2's `Streams::drop` waking its `Connection` task while holding `Arc<Mutex<Inner>>` — the synchronous re-poll re-enters and deadlocks on that lock. Always defer the wake via `ngx_post_event` instead; the runnable is re-polled on the next event-loop tick by `ngx_event_process_posted`. On the single-threaded event loop that is one worker-local list insert — one tick of latency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a freestanding test in `async_::spawn` (no deps beyond `async_task`) reproducing the deadlock fixed by the previous commit: a `Drop` impl wakes a parked task while holding a lock. With synchronous re-poll the re-poll finds the lock still held — the deadlock signature, surfaced via `Mutex::try_lock` returning `WouldBlock` so the test cannot hang; with deferred wakes the re-poll acquires the lock cleanly. The test supplies its own `schedule` functions, so no NGINX event loop is required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
I was asked to review this to identify if it could cause any headaches within NGINX WASM. I appreciate that this change will also make the interleaving of WASM guest execution (or any rust async code) with other components in the NGINX worker process easier to reason about. |
|
What are the chances 😲? I can't do an official review here but: lgtm, and thanks! |
Fixes #294.
schedule()ranrunnable.run()synchronously when a task was woken fromoutside its own poll, violating the
Wakercontract (wakes must be non-blockingand non-re-entrant). When a wake fires from a
Dropholding a lock the task alsoneeds — e.g. h2's
Streams::drop— the re-poll re-enters and deadlocks. Thisalways defers the wake via
ngx_post_event(one event-loop tick).runnable.run(); alwaysSCHEDULER.schedule().async_task) — synchronous re-pollreproduces the held-lock deadlock signature, deferred re-poll avoids it; no
NGINX event loop needed.
Verified on Linux + macOS aarch64:
cargo test/clippy --all-targets -Dwarnings/
fmt --checkall clean.