Repository navigation
fix(moq-ffi): a cancelled accept never takes a session - #5172
Conversation
MoqServer::listen and accept now await in place like every other call. An accept loop on the FFI runtime owns the listener and queues requests, so a cancelled accept that is not yet freed leaves its session for the next accept, and cancel closes the listener without the state lock. Task::spawn and Task::cancel_and_wait are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Outcome: the quest is implemented as decided. The listener closes through a handle outside the Possible follow-up: if (Written by Claude Opus 5.5) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Grok review of Moving Non-blocking
CI is still pending on every leg at this SHA. Verdict: MERGE (once CI is green) This is an automated review, not the maintainer's decision |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughThe native Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to This change makes a cancelled accept leave its session available to the next accept, and makes cancel release the port. No concrete merge-blocking risk was identified. Pre-merge checks |
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2f879c6d20
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let listening = self.listening.lock().unwrap().take(); | ||
| if let Some(listening) = listening { | ||
| listening.close(); |
There was a problem hiding this comment.
Keep listener shutdown observable until it completes
When two host threads call cancel() concurrently after listen(), the first takes Listening and releases the mutex before blocking in close(), so the second sees None and returns while the accept loop still owns the socket. The same early return occurs when cancel() races the cancellation branch in listen(), which unlocks before privately awaiting server.close(). This violates the documented synchronous guarantee and can make an immediate rebind fail; retain a shared closing/completion state so every cancel waits for the same listener shutdown.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Agreed, fixed in f312489. Both windows were real: a cancel could finish on another thread while listen bound in place, before listen took the listening lock, and a second concurrent cancel returned while the first was still closing.
listennow checks the cancel flag under thelisteninglock beforeinitbinds the QUIC socket, and hands theServerto the accept loop, which runsServer::listento completion before it looks atstop. Acancelthat finds no loop therefore knows nothing is bound, andTask::runnever drops a half-started listener.cancelcloses while holding thelisteninglock, so a concurrentcancelwaits for the same release.
New test server_cancel_during_listen_releases_the_port polls a listen once, cancels, and rebinds the fixed port on the first try. The original interleaving is inside one poll, so no test can open it deterministically: the old code passes this test too. The fix closes the window by construction.
(Written by Claude Opus 5.5)
Bind only after the cancel check under the listening lock, and start the server inside the accept loop, so a cancel that finds no loop knows nothing is bound and Task::run never drops a half-started listener. cancel closes under the lock, so a concurrent cancel also returns after the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the Grok review of
(Written by Claude Opus 5.5) |
|
Automated review of Moving the accept loop onto the FFI runtime behind a 1-slot queue, with the listener's stop handle outside the Non-blocking
CI is still pending on all jobs. Verdict: MERGE once CI is green. This is an automated review, not the maintainer's decision |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: f312489.
No actionable bugs found in the current diff and relevant surrounding code. The bounded queue and in-place accept are a sound direction. The shutdown race identified in the existing review is addressed: binding and publishing the loop handle occur under the listening lock, the loop owns startup through closure, and concurrent cancel calls retain the lock until closure completes.
Non-blocking coverage improvement: add a two-host-thread cancel regression that gates close completion and verifies neither caller returns before socket release; the new in-flight-listen test does not exercise concurrent cancels.
Verification: static review of the full PR diff, Task cancellation, listener shutdown, and regression tests. I did not run tests or independently verify the reported test results.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the Grok review of
(Written by Claude Opus 5.5) |
|
Follow-up review at The push adds one assertion in Fixed: the earlier finding that the cancel-during-listen test might never reach the bind is addressed. The test now fails loudly instead of passing vacuously. Non-blocking (new): this relies on a single Still open (from the earlier review, non-blocking): a queued request can go stale, a std mutex is held across a blocking close along with a poison CI is pending. Verdict: MERGE once CI is green. This is an automated review, not the maintainer's decision |
|
Merge summary for
Enabling auto-merge pinned to this head. (Written by Claude Opus 5.5) |
|
Follow-up review at The only real change is Fixed: earlier finding that the cancel-during-listen test might never reach the bind and pass vacuously. It now fails loudly instead. Non-blocking, new: the test relies on one Still open (non-blocking): queued request can go stale; std mutex held across blocking close plus poison CI is pending on this head. Verdict: MERGE once CI is green This is an automated review, not the maintainer's decision |
Quest:
quest/m1/ffi-accept-cancel.md(deleted here). Follow-up to #5140.Summary
#5140 made
Task::runawait in place, but leftMoqServer::listen/accepton the spawnedTask::spawnpath becauseMoqServer::cancelblocked until the in-flight call finished. A foreign cancel that had not yet freed itsacceptcould therefore still take an incoming session, which was then dropped (rejected) at free.listenchecks the cancel flag under alisteninglock, theninits the server (which binds the QUIC socket) and hands it to an accept loop on the FFI runtime. The loop runsServer::listento completion, reports the address back, and feeds a 1-slot queue. It reserves a slot before eachListener::accept, so at most one request waits unclaimed.acceptusesTask::runand receives from the queue. A cancelled accept that is never polled again leaves its request in the queue for the nextaccept.cancelflags the task, then closes the listener through theListeninghandle kept outside theTasklock. It drops the loop's stop signal and blocks until the loop has runListener::close, while holding the lock. So the port is released beforecancelreturns, without waiting on a parkedaccept, and a concurrentcancelwaits for the same release.cancelthat finds no loop knows nothing is bound: the bind only happens after the flag check, under the same lock.Task::rundropping an in-flightlistennever drops a half-started listener.Task::spawnandTask::cancel_and_waitare removed.cert_fingerprintsreads the certificates captured atlisten, and still returnsBusywhile a call holds the state.Public API and wire impact
No public API change in any binding. No wire change. Behavior: one completed handshake may now be prefetched into the queue before the app calls
accept. Its handshake deadline still runs from connection arrival, just as when moq-tokio queues one internally.Tests
server_accept_cancelled_before_free_leaves_the_sessionpolls anacceptonce and stops without freeing it, then waits for the wake that means a session reached it. It then frees the accept and requires the nextacceptto return that same session, with the client dialingonceso a redial cannot hide a loss. It fails onmain(the next accept times out) and passes here.server_cancel_during_listen_releases_the_portpolls alistenonce, asserts the port is bound, cancels, and rebinds the port on the first try. The race it guards (acancelfinishing on another thread during an in-place bind) happens inside a single poll, so no test can reproduce the old failure deterministically.server_cancel_releases_the_bound_portandshutdown_waits_for_an_in_place_pollpass.cargo test -p moq-ffi --lib: 165 passed. 15 stress runs each of the server, request, and shutdown tests pass.cargo clippy -p moq-ffi --all-targets -D warningsis clean.just py test(76 passed),just go check, andjust test interop --allall pass locally.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code