feat(driver): async backpressure via tokio Semaphore, permit released at the CQE (backlog#1102)#2
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR productionizes async backpressure for the io_uring driver by replacing the blocking Mutex/Condvar backpressure with a tokio::sync::Semaphore, ensuring permit acquisition never parks a Tokio runtime worker while preserving the “release at CQE” invariant.
Changes:
- Replaced the blocking backpressure mechanism with
tokio::sync::Semaphore, deferring submission to first poll when saturated and carryingOwnedSemaphorePermitintoPendingfor CQE-scoped release. - Updated
ReadHandlelifecycle to support deferred submission (WaitingPermit) and to only issue async-cancel on drop when actually submitted. - Adjusted cancel-stress accounting and added a saturation regression test ensuring saturated
read_atreturns immediately without submitting.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/driver.rs |
Implements async backpressure via Tokio semaphore; enforces CQE-scoped permit release and deferred submission under saturation. |
tests/cancel.rs |
Updates invariants for async backpressure and adds a saturation test to ensure submit does not block. |
docs/DESIGN.md |
Documents the new semaphore-based backpressure and CQE-release enforcement. |
… at the CQE (rustfs/backlog#1102) The hand-rolled Mutex+Condvar semaphore blocked the calling thread in `acquire()` whenever `entries` ops were already in flight — in an async caller that parks a runtime worker, exactly the concern behind rustfs/backlog#1060. Replace it with a `tokio::sync::Semaphore`: - `submit` takes the permit with `try_acquire_owned()` on the common unsaturated path: no allocation, no await, and the op is in flight the moment `submit` returns — behavior identical to before. - When saturated it never blocks. The acquire future is handed to the returned `ReadHandle`, which awaits it on its first poll and submits then. - The `OwnedSemaphorePermit` travels with `Msg::Read` into the `Pending` entry, so it is dropped exactly when the entry is removed at the final CQE. The load-bearing "release at the CQE, never at future drop" rule is now enforced by the type system instead of a manual `release()` that could be forgotten. A short-read resubmit keeps the entry, and thus the permit. - The driver closes the semaphore on exit, so a handle still awaiting a permit resolves with a driver-gone error instead of hanging. Public API is unchanged (`read_at`/`read_current` still return `ReadHandle`), so ecstore needs no change. `cancel_stress` no longer asserts `submitted == OPS`: that was an artifact of the blocking acquire, which submitted every op before its handle could be dropped. A handle dropped before its first poll now never submits and never allocates a buffer — strictly better — while the conservation identity `delivered + orphan_reclaimed == submitted` still holds exactly. Adds `saturated_submit_defers_instead_of_blocking`, which would hang under the old implementation. All 12 tests pass under real io_uring (both run-docker.sh legs); clippy -D warnings clean. Co-Authored-By: heihutu <heihutu@gmail.com>
edc143c to
76619b6
Compare
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.
What
Second productionization step for rustfs/backlog#1102: replace the hand-rolled
Mutex+Condvarbackpressure semaphore withtokio::sync::Semaphore, so acquiring a permit never parks a runtime worker.Why
Backpressure::acquire()blocked the calling thread wheneverentriesops were already in flight. In an async caller (ecstore'sUringBackend) that parks a tokio worker under saturation — exactly the concern behind rustfs/backlog#1060.Design
submittakes the permit withtry_acquire_owned(); when a permit is free (the common, unsaturated case) there is no allocation, no await, and the op is in flight the momentsubmitreturns — byte-for-byte the old behavior.ReadHandle, which awaits it on its first poll and submits then.OwnedSemaphorePermittravels withMsg::Readinto thePendingentry and is dropped exactly when that entry is removed at the final CQE — no manualrelease()to forget. A short-read resubmit keeps the entry, and thus the permit.close()s the semaphore on exit, so a handle still awaiting a permit resolves with a driver-gone error instead of hanging.Compatibility
read_at/read_currentstill returnReadHandle;ReadHandleis still the awaitable. ecstore needs no change, and there is zero S3/client impact.cancel_stressno longer assertssubmitted == OPS. That was an artifact of the blocking acquire, which forced every op to be submitted before its handle could be dropped. A handle dropped before its first poll now never acquires a permit, never submits, and never allocates a buffer — strictly better for memory — while the load-bearing conservation identitydelivered + orphan_reclaimed == submittedstill holds exactly.Verification
run-docker.shlegs (leg 1 seccomp-blocked → graceful degradation; leg 2 unconfined → real io_uring).saturated_submit_defers_instead_of_blocking: saturates every permit with blocked pipe reads, then assertsread_atreturns without submitting (this test would hang under the old blocking implementation), and that the deferred handle submits and returns correct bytes once a permit frees.cargo fmt --check+clippy --all-targets -D warningsclean.Remaining in #1102
tokio
AsyncFdreaping (removing the dedicated driver thread), O_DIRECT aligned buffers, the three read shapes, and the process-level singleton ring.