Repository navigation
feat(engine): preallocated command queue; zero-allocation send through a ready gate (#76) - #197
Conversation
…h a ready gate (#76)
joaquinbejar
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES. Two independent controls confirm sender starvation and pooled buffer retention after runtime shutdown. The allocation gate reproduces zero blocks for 1024 ready gate sends, and normal close, cancellation and backpressure checks pass. Submitted as COMMENT because GitHub prevents the author account from requesting changes on its own pull request.
| // The guard lives in this block only, so the future holds no lock | ||
| // across the wait below (and stays `Send`). | ||
| let pushed = { | ||
| let mut ring = self.shared.ring(); |
There was a problem hiding this comment.
🟠 Warning: A fresh sender can take this slot before an older notified sender resumes. With capacity 1, the probe completes 1000 fresh sends while the older send is repolled each time and stays pending. Tokio reserves the freed slot for the older waiter. Under sustained producers this can indefinitely delay a waiting send or Logout. Preserve FIFO capacity reservation, including cancellation handoff, rather than relying on Notify ordering.
| } | ||
|
|
||
| impl<T> Drop for CommandReceiver<T> { | ||
| fn drop(&mut self) { |
There was a problem hiding this comment.
🟠 Warning: Closing here leaves queued commands in Shared while Connection handles retain its Arc. Dropping the runtime with two pooled sends queued leaves their buffers unavailable even though the surviving connections report closed. The identical engine probe returns both buffers on PR196. Drain queued commands when the receiver is destroyed and release their payloads outside the queue mutex.
Codecov Report❌ Patch coverage is
🚀 New features to boost your workflow:
|
Summary
Closes #76. Its acceptance criterion, zero heap allocations per steady-state send with no store, was not met: even a pooled message sent through a gate that decides at once cost about one block per 128 sends, because the
tokio::sync::mpsccommand channel grows its slot list. The channel is replaced by a ring allocated once, so that path now allocates nothing.Changes
ironfix-engine/src/command_queue.rs:CommandSender/CommandReceiverover aVecDequeallocated with the session's outbound capacity at session start and never grown.std::sync::Mutexheld only to push or pop one command, never across an.await, as the governance list allows for short, synchronous sections.tokio::sync::Notifywakes the other side; its waiters live in the waiting futures, so waiting allocates nothing.recvreturnsNoneonce every handle is dropped and the ring is empty, and the last drop wakes a waiting receiver;Send.Connectionand the reactor use it. Nothing else changes: same capacity knob (with_outbound_capacity), same backpressure, same close behaviour.Measured
From
ironfix-engine/tests/allocations.rswith dhat on macOS, block counts, not time:to_app_checkedasync_traitto_appfuture)The full profile was recorded in #153 and is unchanged apart from the queue's blocks: TLS,
FileStore, observer and Reject path, each attributed to a component in the test's table.MemoryStorestays at 5 blocks per send, as documented in ADR-0008.Public / contract impact
None: the queue is private, and
Connection's API is unchanged.Testing
cargo test --workspace --all-features: 2655 passed, 0 failed. fmt, clippy -D warnings, doc and check-spanish are clean.Closes #76