Skip to content

embassy-sync: wake the next waiter when a FairSemaphore acquire is canceled - #6964

Merged
lulf merged 1 commit into
embassy-rs:mainfrom
MaxFreedomPollard:fair-semaphore-cancel-wake
Sep 14, 2026
Merged

lulf merged 1 commit into
embassy-rs:mainfrom
MaxFreedomPollard:fair-semaphore-cancel-wake

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

FairSemaphore hands 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 its acquire future 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, select or with_timeout around sema.acquire(n) being the obvious cases. The queue only starts moving again if some unrelated release happens to come along.

The cause is FairSemaphoreState::cancel at embassy-sync/src/semaphore.rs:429. It clears the canceled waiter's slot with set_waker(ticket, None) and stops there. 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, release and set do 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 cancel when 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 existing fairness test. 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 from futures-test instead 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.sh runs for this crate, gives 108 passed and 0 failed, plus the trybuild ui test and 9 doctests. Reverting only the cancel change and keeping the test gives cancel_wakes_next_waiter ... FAILED with assertion left == right failed, left: 0, right: 1 on the wake count, so the test does pin the behaviour. rustfmt --check --skip-children --unstable-features --edition 2024 under nightly-2026-08-26 from rust-toolchain-nightly.toml reports no changes. cargo build --target thumbv7em-none-eabi and cargo build --target thumbv6m-none-eabi --features defmt both succeed, so the crate still cross compiles.

…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.
@embassy-bot

embassy-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown

👋 Welcome, @MaxFreedomPollard, and thanks for opening your first pull request here!

If you haven't already, please give the contributor guide a read.

@leftger leftger added the e-sync label Sep 7, 2026
@leftger
leftger requested a review from alexmoon September 7, 2026 06:49

@lulf lulf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable to me

@lulf
lulf added this pull request to the merge queue Sep 14, 2026
Merged via the queue into embassy-rs:main with commit 2cad35b Sep 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants