Repository navigation
embassy-sync: wake the next waiter when a FairSemaphore acquire is canceled - #6964
Merged
lulf merged 1 commit intoSep 14, 2026
Merged
Conversation
…nceled FairSemaphore hands out permits in FIFO order, so a waiter that asks for more permits than are available parks at the head of the queue and holds back the waiters behind it. Dropping that head waiter's future left the rest of the queue pending even when enough permits were free, so anything that cancels an acquire (select, with_timeout) could strand the next waiter until an unrelated release happened to come along. FairSemaphoreState::cancel in embassy-sync/src/semaphore.rs only cleared the canceled waiter's slot with set_waker(ticket, None). Every other place that changes who is at the head of the queue wakes the new head: take() does it after popping a satisfied waiter, and release() and set() do it after adding permits. Cancel was the only path that skipped it. Wake the new head from cancel() when the head slot is now empty. wake() already pops the canceled entries off the front, and checking the head first keeps canceling a non-head waiter free of spurious wakes.
|
👋 Welcome, @MaxFreedomPollard, and thanks for opening your first pull request here! If you haven't already, please give the contributor guide a read. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FairSemaphorehands out permits in FIFO order, so a task asking for more permits than are currently available parks at the head of the queue and holds back everyone behind it. That part is intended. The problem is what happens when that head waiter goes away without ever acquiring: if itsacquirefuture is dropped, nothing wakes the waiters behind it, and they stay pending even though there are already enough permits to satisfy them. Anything that cancels an acquire runs into this,selectorwith_timeoutaroundsema.acquire(n)being the obvious cases. The queue only starts moving again if some unrelatedreleasehappens to come along.The cause is
FairSemaphoreState::cancelat embassy-sync/src/semaphore.rs:429. It clears the canceled waiter's slot withset_waker(ticket, None)and stops there. Every other place that changes who is at the head of the queue wakes the new head:takedoes it after popping a satisfied waiter,releaseandsetdo it after adding permits. Cancel was the one path that skipped it. #2766 fixed the same shape of lost wakeup on the completion path in 2024, so this is the half that was left.The fix is to wake the new head from
cancelwhen the head slot has become empty.wake()already pops canceled entries off the front and wakes whoever is next, so it comes to three lines. Checking the head first keeps cancelling a waiter that was not at the head free of spurious wakes.The regression test is
cancel_wakes_next_waiter, next to the existingfairnesstest. It builds a semaphore with one permit, queues a waiter for two permits and then a waiter for one, drops the first, and asserts the second gets woken and then completes. It uses a counting waker fromfutures-testinstead of threads or timers, so there is nothing timing or platform dependent in it.Verification, on an aarch64 Mac with the pinned 1.97 toolchain.
cargo test --manifest-path ./embassy-sync/Cargo.toml, the command.github/ci/test.shruns for this crate, gives 108 passed and 0 failed, plus the trybuild ui test and 9 doctests. Reverting only thecancelchange and keeping the test givescancel_wakes_next_waiter ... FAILEDwithassertion left == right failed, left: 0, right: 1on the wake count, so the test does pin the behaviour.rustfmt --check --skip-children --unstable-features --edition 2024under nightly-2026-08-26 fromrust-toolchain-nightly.tomlreports no changes.cargo build --target thumbv7em-none-eabiandcargo build --target thumbv6m-none-eabi --features defmtboth succeed, so the crate still cross compiles.