Repository navigation
Description
Summary
Replace the pool scheduler's Arc<Mutex<SchedulerState>> plus atomic service guard and transient tokio::spawn calls with one long-lived scheduler task that owns the fairness and waiter state.
This is the final structural implementation slice for ADR 013 proposal #638. It follows the stable slot graph in #645 and should consume the scoped permit-selection API from #646.
Current behaviour
The current scheduler:
- shares
SchedulerStatethrough a synchronous mutex; - uses
AtomicBool::is_servicingas a distributed task-lifecycle guard; - calls
kickfrom acquire and lease-drop paths; - spawns
service_waiterswhen the guard transitions from idle; - may spawn a task that immediately discovers no waiter and exits;
- divides shutdown, waiter selection, restart recovery, and capacity notification among several caller contexts.
This topology makes one conceptual state machine look like shared mutable data. It also complicates #550's bounded-waiter admission and leaves adversarial interleavings from #593/#535 difficult to test directly.
Proposed architecture
Create one scheduler task during pool construction. It owns SchedulerState directly and receives commands through a Tokio channel.
A representative protocol is:
enum SchedulerCommand<S, P, C> {
Acquire {
handle_id: u64,
reply: oneshot::Sender<Result<PooledClientLease<S, P, C>, ClientError>>,
},
DeregisterHandle {
handle_id: u64,
},
CapacityAvailable,
Shutdown {
reply: oneshot::Sender<()>,
},
}The exact types may differ, but the protocol and ownership must remain explicit.
Ownership split
Keep scheduler control and physical slot storage separate:
Arc<PoolCore>from Introduce PoolCore and index-based pooled leases #645 owns fixed slots and shared physical resources;- a cloneable
SchedulerHandleowns only the command sender and synchronous handle-ID allocation if retained; SchedulerTaskowns the receiver,SchedulerState, fairness rotation, waiter counts, and shutdown state;- the task may own
Arc<PoolCore>, butPoolCoremust not own the command sender in a way that creates a strong cycle.
Dropping the final public scheduler sender must eventually close the mailbox, terminate the task, and release the task's pool-core reference.
Responsibilities of the actor
Admission
- reject new acquire commands after shutdown begins;
- enforce the waiter bound and/or acquisition deadline required by [security][low] Client pool admits unbounded blocked waiters under backpressure #550;
- return a typed error when admission fails;
- prune cancelled oneshot replies so abandoned callers do not consume waiter capacity indefinitely.
Fairness
- preserve FIFO arrival order for live admitted waiters;
- preserve round-robin turns among logical handles;
- handle deregistration idempotently;
- never grant a lease to a cancelled or deregistered waiter.
Capacity
- attempt an immediate grant when an acquire command arrives and capacity exists;
- wait for capacity without spawning a task per lease release;
- use Race borrowed pool-slot permits without per-slot Arc cloning #646's slot-index/permit selection;
- treat duplicate
CapacityAvailablemessages as harmless hints, not extra permits.
Shutdown
- reject later acquires;
- resolve every queued waiter with
ClientError::disconnectedor the chosen typed shutdown error; - cancel or drop any pending permit-selection future safely;
- acknowledge explicit
close()only after scheduler state has drained and the actor can terminate; - terminate on mailbox closure even when callers omit explicit close.
Public API behaviour
Preserve existing WireframeClientPool, PoolHandle, and PooledClientLease APIs where practical.
WireframeClientPool::handle() may remain synchronous by allocating a logical ID outside the actor. The implementation must prove command ordering for acquire and deregistration from one logical handle. A registration acknowledgement is acceptable if the public API change is justified, but do not quietly assume cross-sender global ordering that Tokio does not promise.
WireframeClientPool::close(self) becomes an awaitable scheduler shutdown rather than shutdown(); yield_now(); drop(self).
Drop behaviour
PooledClientLease::drop cannot await. It should:
- release its
OwnedSemaphorePermitby ordinary drop; - send or coalesce a non-blocking capacity hint to the actor;
- tolerate an already-closed scheduler mailbox during shutdown;
- avoid cloning a large ownership graph;
- never spawn a task.
If permit release can be observed without a per-drop command, for example through a shared Notify polled by the actor, that is acceptable if it preserves the single-owner scheduler state and has no lost-wakeup race.
Integration with existing issues
- Simplify client pool waiter state transitions #535: reuse its state-transition simplification inside the actor or close it as superseded with an explicit explanation.
- Add pool poison recovery integration coverage #539: reassess whether scheduler poison-recovery integration remains relevant once scheduler state is no longer behind
std::sync::Mutex; retain slot-bookkeeping scope if still applicable. - [security][medium] Cancelled pooled client operations can return dirty sockets as reusable #548: do not grant reuse of a dirty physical connection merely because capacity returned. Cancellation safety remains a separate invariant.
- [security][low] Client pool admits unbounded blocked waiters under backpressure #550: implement bounded waiter admission in the actor rather than adding another shared counter outside it.
- Mutation testing: client pool lifecycle and slot rotation survivors #593: preserve and extend shutdown, rotation, and waiter behavioural tests.
Acceptance criteria
- Exactly one scheduler task is created per client pool.
- Repeated acquire/drop operations create no additional scheduler tasks.
-
SchedulerStateis owned by the actor and is not behindArc<Mutex<_>>. - The
is_servicingatomic/spawn-restart protocol is removed. - Acquire, deregister, capacity, and shutdown transitions flow through an explicit command or wake protocol.
- FIFO and round-robin semantics match the existing public contract.
- Waiter admission is bounded according to [security][low] Client pool admits unbounded blocked waiters under backpressure #550.
- Cancelled acquire futures are pruned and receive no lease.
- Blocked waiters resolve promptly during shutdown.
-
close()waits for scheduler shutdown rather than yielding speculatively. - Dropping all public pool/control handles without
close()terminates the actor and releasesPoolCore. - Lease drop never calls
tokio::spawn. - No strong-reference cycle retains the pool.
- [security][medium] Cancelled pooled client operations can return dirty sockets as reusable #548's dirty-socket invariant is preserved.
- Establish runtime ownership and task-churn baselines #639 benchmarks record uncontended/contended latency, allocations, and task-count changes.
Tests
Add deterministic state-machine and runtime tests covering:
- immediate uncontended grant;
- FIFO ordering under saturation;
- round-robin ordering across several logical handles;
- cancelled waiter before and during grant;
- handle deregistration with queued waiters;
- bounded waiter rejection;
- capacity release racing with acquire and shutdown;
- explicit close with blocked waiters;
- implicit shutdown when all senders drop;
- actor task count remaining one under a long acquire/drop loop;
- no PoolCore retention after final handle/lease drop.
Use loom or a small pure scheduler-state model for event-order races that cannot be forced reliably with wall-clock Tokio tests.
Non-goals
- Moving socket request/response I/O into the scheduler actor.
- Dynamically resizing the slot collection.
- Replacing Tokio channels or semaphores.
- Weakening cancellation safety to simplify capacity accounting.
Dependencies
- ADR 013 proposal ADR 013 proposal: Single-owner client-pool scheduler and slot graph #638.
- Baseline issue Establish runtime ownership and task-churn baselines #639.
- Pool core Introduce PoolCore and index-based pooled leases #645.
- Scoped permit selection Race borrowed pool-slot permits without per-slot Arc cloning #646.
References
- Epic Epic: Align Tokio task boundaries with ownership boundaries #635
- ADR 011 proposal ADR 011 proposal: Runtime ownership and task-lifetime boundaries #636
- ADR 013 proposal ADR 013 proposal: Single-owner client-pool scheduler and slot graph #638
- Simplify client pool waiter state transitions #535
- Add pool poison recovery integration coverage #539
- [security][medium] Cancelled pooled client operations can return dirty sockets as reusable #548
- [security][low] Client pool admits unbounded blocked waiters under backpressure #550
- Mutation testing: client pool lifecycle and slot rotation survivors #593
- Establish runtime ownership and task-churn baselines #639
- Introduce PoolCore and index-based pooled leases #645
- Race borrowed pool-slot permits without per-slot Arc cloning #646
src/client/pool/scheduler.rssrc/client/pool/client_pool.rssrc/client/pool/handle.rssrc/client/pool/lease.rs
Activity
coderabbitai commentedon Aug 8, 2026
🔗 Related PRs
#540 - Approve the actor and codec-driver boundary (10.1.3) [merged]
leynos/pg-embed-setup-unpriv#157 - Add verification tests for lifecycle invariants (
#152) [merged]#552 - Align roadmap footnotes with the roadmap grammar [open]
#612 - Kill mutation-testing survivors and widen scaffolding excludes (
#566,#567,#569,#570,#593,#595,#598) [merged]leynos/cuprum#232 - docs: mark
#125FD-ownership hazard resolved in roadmap 8.1 (#125) [merged]📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
Record the pool acquire-path defects instead of patching them (#639)
Report contended pool allocations as an observation
Separate the two pool acquire-path defects in the recorded baselines
Record the pool acquire-path defects instead of patching them (#639)
1 remaining item
Separate the two pool acquire-path defects in the recorded baselines
Record the pool acquire-path defects instead of patching them (#639)
Report contended pool allocations as an observation
Separate the two pool acquire-path defects in the recorded baselines
Establish runtime ownership and task-churn baselines (#639) (#756)
Document borrowed permit selection and isolate flaky size-16 case (#646)
Document borrowed permit selection and isolate flaky size-16 case (#646)
Document borrowed permit selection and isolate flaky size-16 case (#646)
Race borrowed pool-slot permits without per-slot Arc cloning (#646) (#…
leynos commentedon Oct 30, 2026
@coderabbitai plan
coderabbitai commentedon Oct 30, 2026
Coding Plan
Summary
SchedulerStateinto one persistentSchedulerTaskspawned at pool construction. The task receivesAcquireandDeregisterHandlethrough an unbounded mailbox. It enforces themax_waitersadmission bound itself.acquire_slot_permitrace. Leases then hold no scheduler reference. Lease drop releases only its permit and never sends or spawns.PoolCoreshutdownNotify. Await the actorJoinHandleinclose(). ReportPoolClosedfor closure andSchedulerFailedfor a supervised actor panic. Terminate the actor when the final sender drops, so thatPoolCoreis released.#639baselines with updated actor-era assertions.Design Choices
Design Choice 1: Bounded or unbounded command mailbox
Options Considered:
mpscmailbox.Acquireusessend().awaitandDeregisterHandleusestry_send. A lost deregistration needs delayed cleanup through sweep pruning.mpscmailbox. The actor enforces themax_waitersadmission limit separately. Drop-path sends never fail while the actor is alive.Chosen Option: 2
Rationale: Deregistration cannot be lost and per-sender order is preserved. Admission stays bounded inside the actor, which is the exception that ADR 013 accepts.
Design Choice 2: How the actor learns that capacity returned
Options Considered:
PooledClientLease::dropsends a coalescedCapacityAvailablehint through a sender that the lease holds.PoolCore::acquire_slot_permitrace while waiters are queued. The semaphore then signals capacity at level, so no lost wakeup can occur.Chosen Option: 2
Rationale: It removes
release_controlfrom leases and removes every send from lease drop. Tokio semaphore fairness delivers the next released permit to the queued race.Design Choice 3: Shutdown delivery mechanism
Options Considered:
Shutdown { reply }command through the mailbox.PoolCore::shutdownflag and itsNotifyas a dedicated channel. Set a shared status first. The close owner then awaits the actorJoinHandle.CancellationTokenfromtokio-util.Chosen Option: 2
Rationale: It reuses the lost-wakeup-safe
shutdown_notified_after_checkpath. It adds no dependency. Awaiting theJoinHandleis the acknowledgement that ADR 013 pins.Design Choice 4: Error reported for shutdown
Options Considered:
ClientError::disconnected().ClientError::PoolClosedandClientError::SchedulerFailed. Neither variant is recyclable.Chosen Option: 2
Rationale: Scheduler closure is not a broken socket, and the ADR verification requires
SchedulerFailedfor an actor panic. Document the visible behaviour change.Design Choice 5: Handle registration for round-robin
Options Considered:
RegisterHandlecommand fromhandle(). This depends on cross-clone ordering.Acquirefor it, and remove it onDeregisterHandle. This relies only on the per-sender order of each handle's own sender.Chosen Option: 2
Rationale: It needs no API change and no ordering assumption across clones. It replaces the
UnregisteredHandlerejection.Design Choice 6: Uncontended acquisition path
Options Considered:
try_acquire_permitfast path that bypasses the actor.Chosen Option: 1
Rationale: Admission and fairness then stay with one owner, as the ticket and
#550require. Record the latency cost in the#639benchmarks.Design Choice 7: Acquisition deadline (`#550`)
Options Considered:
max_waitersbound into the actor only.AcquireTimeoutand deadline configuration as well.Chosen Option: 1
Rationale: The waiter bound satisfies the acceptance criterion. Leave the deadline as follow-up work under
#550.💡 User Tips
Regenerate the plan with different choices with
@coderabbitai <feedback>.Implementation Steps
Phase 1: Typed errors and an actor-owned scheduler model
Define the error vocabulary. Turn the scheduler state into a plain struct with synchronous transition handlers. Define the command protocol and the cloneable scheduler handle. These tasks fix the protocol and the pure model that Phase 2 drives and Phase 3 tests.
Task 1: Task 1.1: Add `PoolClosed` and `SchedulerFailed` errors
Give callers typed, non-recyclable errors for pool closure and actor failure.
src/client/error.rs:ClientError::PoolClosed.ClientError::SchedulerFailed.pool_saturated.ClientError::should_recycle_connectionto returnfalsefor both new variants.closedoc example insrc/client/pool/client_pool.rsand the related doc text. ShowPoolClosedinstead of the disconnected I/O error.Task 2: Task 1.2: Convert `SchedulerState` into an actor-owned model
Remove the shared-mutex model. Keep the FIFO and round-robin semantics. Make every transition a synchronous method.
SchedulerStateout ofscheduler/mod.rsinto a newsrc/client/pool/scheduler/state.rs. Store it by value. Do not wrap it inMutexorArc.servicing_waiters. A waiter stays queued until the actor holds a permit for it. The waiter count is therefore the FIFO length plus the per-handle queue lengths. This matches the old bound, which also counted the waiter held by the worker.UnregisteredHandlerejection with lazy registration:Acquirefor an ID inserts the ID intoround_robin_orderandhandle_waiters.DeregisterHandleremoves the ID and drops its queued senders.DeregisterHandleis idempotent. An unknown ID is a no-op.prune_cancelled_waitersand run it before every admission check.take_next_waiterskip senders whoseis_closed()is true, for both policies. This grant-sweep pruning means a cancelled waiter never receives a lease.WaiterEnqueueResult::Saturatedand themax_waiterslimit. Remove theUnregisteredHandlevariant.Task 3: Task 1.3: Define the command protocol, shared status and `SchedulerHandle`
Make the actor protocol and its ownership explicit.
src/client/pool/scheduler/command.rswith a crate-privateSchedulerCommand<S, P, C>enum:Acquire { handle_id, reply }, wherereplyis the existing oneshot lease reply type.DeregisterHandle { handle_id }.cfg(test)) variant that injects a panic for failure tests.CapacityAvailableorShutdownvariants. Assumptions 2 and 3 give the reasons.src/client/pool/scheduler/handle.rswith a cloneable crate-privateSchedulerHandle. It holds:mpsccommand sender;Arcto a small shared status record.AtomicU64handle-ID counter, which starts at 1;Running,Closed,Failed);PoolCore.SchedulerHandlethese operations:register_handleallocates the next ID synchronously.acquiresendsAcquireand awaits the reply.deregistersendsDeregisterHandlewithout blocking and ignores a closed mailbox.close_errormaps the shared status toSchedulerFailedorPoolClosed.close_error. Do not map it todisconnected().acquireif the status is notRunning.🤖 Prompt for AI agents
Phase 2: Persistent actor task and lifecycle wiring
Run one actor task per pool. Wire construction, acquisition, deregistration, close and implicit termination to it. Remove the spawn-on-demand machinery. These tasks change the runtime ownership graph together.
Task 1: Task 2.1: Implement the scheduler actor loop with a pending-grant guard
Give one task ownership of the receiver, the state and the permit race.
src/client/pool/scheduler/actor.rswith aSchedulerTaskthat owns:SchedulerState;Arc<PoolCore>;Arcto the shared status.handle_commandhandlesAcquireandDeregisterHandle.grant_sweephandles immediate grants.on_permit_wonhandles race results.drain_for_shutdownhandles shutdown.grant_sweeploops while live waiters exist andPoolCore::try_acquire_permitsucceeds. For each permit it:PooledClientLease::new;Acquire, the actor runs these steps:PoolSaturated { limit }when the queue is full;grant_sweep;record_capacity_wait.PoolCore::acquire_slot_permitrace in aPendingGrantRAII guard:grant_sweep.tokio::select!with these branches in this order:core.shutdown_notified_after_check);on_permit_wonand thengrant_sweep;recv().recv()returnsNone. All senders are then gone. Drop the state, drop the guard and return, which releases the actor'sArc<PoolCore>.Closed;PoolClosedto every queued waiter;receiver.close();PoolClosedto eachAcquire;src/client/pool/scheduler/service.rs.is_servicing,try_begin_servicing,stop_servicing,restart_if_waiters,kick,notify_capacity_available,resolve_immediate_leaseandPoolSchedulerfromscheduler/mod.rs. Keepscheduler/mod.rsas the module root and the spawn entry point.Task 2: Task 2.2: Add termination supervision and failure reporting
Make abnormal actor termination permanent and visible, and keep it distinct from closure.
scheduler/actor.rs.std::thread::panicking()is true, set the status toFailed.tracingevent with the pool identifier.Failedbefore the reply senders drop lets waiting callers observeSchedulerFailed.wireframe_pool_scheduler_failures_totalinsrc/metrics/client_pool.rs. Label it with the pool identifier. Use the existingcfg(all(feature = "metrics", feature = "pool", not(loom)))gate and the existing no-op fallback.SchedulerFailedfrom the status check.Task 3: Task 2.3: Wire pool construction and an awaitable `close`
Create exactly one actor per pool. Hold the
JoinHandleonly in the non-shared close owner.WireframeClientPool::connectinsrc/client/pool/client_pool.rs:PoolCore.SchedulerTaskonce.record_scheduler_spawnonce.SchedulerHandleand theJoinHandlein the pool.PoolSchedulerControlwithSchedulerHandlein the pool struct. Do not givePoolCoreany sender orJoinHandle.close(self)with these steps:Closed.core.shutdown(). This wakes the actor and any permit waits.JoinHandle.JoinErrorreports a panic, log it.yield_now()fromclose.TestPoolconstructor used byclient_pool_tests.rsso that it spawns the actor in the same way. Make sure its callers run inside a Tokio runtime.Task 4: Task 2.4: Update `PoolHandle` and `PooledClientLease`
Route acquisition and deregistration through the actor. Strip the scheduler from leases.
src/client/pool/handle.rs:PoolHandleholds its ownSchedulerHandleclone and itshandle_id.corefield if no other handle method reads it.acquire(&mut self)delegates toSchedulerHandle::acquire.DropcallsSchedulerHandle::deregister. The handle's own sender carries both commands, so per-sender FIFO placesDeregisterHandleafter that handle'sAcquire.pool.handle()synchronous. Keeppool.acquire()as a temporary handle.src/client/pool/lease.rs:release_controland thenotify_capacity_availablecall fromDrop.PooledClientLease::new.pending_responses > 0discard before_permitdrops, which preserves#548.tokio::spawn.try_acquire_immediatelyandacquire_for_handlepaths.Task 5: Task 2.5: Re-point task-churn counters at the actor model
Keep
PoolTaskChurnmeaningful for a persistent task.src/client/pool/churn.rs:scheduler_spawnsis 1 per pool, recorded at construction.scheduler_spawns.capacity_waitscounts admitted waiters that the actor could not grant at once.PoolTaskChurnObserverunchanged.lock_or_recoverinsrc/client/pool/sync.rs. Keeplock_or_recoverforlast_returned_at, and update its doc comment to list only the remaining callers.🤖 Prompt for AI agents
Phase 3: Deterministic tests for the actor protocol
Prove the state machine, the runtime lifecycle and the ownership release. Adapt existing tests to the new errors and the new task model.
Task 1: Task 3.1: Pure state and handler-model tests
Cover event-order races by driving the synchronous handlers in chosen orders.
src/client/pool/scheduler/tests.rs:round_robin_enqueue_rejects_unknown_handleswith lazy-registration and idempotent-deregistration tests.take_next_waiterskips closed senders under both policies.scheduler/actor.rs, for examplescheduler/actor_tests.rs. Use theTestCorealias andtest_pool_slot. Use oneshot receivers and call the handlers with no spawned task. Cover these cases:PoolSaturatedatmax_waiters;PoolClosed.src/client/pool/core_tests/transition_model.rswith actor event steps:Acquire,Cancel,Deregister,Release,ShutdownandSenderClose. Use them to enumerate orders in which capacity release races with acquisition and shutdown. Assert that capacity is preserved and that no cancelled or deregistered waiter receives a lease.Task 2: Task 3.2: Runtime lifecycle tests
Prove task count, close and implicit termination on a real current-thread runtime.
src/client/tests/pool_lifecycle.rsor a new sibling file. Do not let any file exceed 400 lines. Cover these cases:Handle::metrics().num_alive_tasks().scheduler_spawnsstays at 1 across that loop.close()with blocked waiters resolves every waiter withPoolClosed. Afterclose()returns, the actor task is gone.close(), made through a survivingPoolHandle, returnsPoolClosed.close()terminates the actor. Prove it with the task count.DropProbefires after the final lease drops.SchedulerFailedto the queued waiters and to later acquisitions. It does not surfacePoolClosedand does not hang.wait_for_capacity_waitsstyle helpers. Do not use sleeps.pool_and_handle_drop_keep_lease_slot_alive_until_final_lease_dropinclient_pool_tests.rsto run with the actor started.Task 3: Task 3.3: Adapt existing tests and BDD steps
Align existing coverage with the new errors and remove scheduler poison coverage.
src/client/pool/scheduler_tests.rs. The scheduler state no longer sits behind a mutex. This retires the scheduler part of#539. Keep the slotlast_returned_atpoison coverage.disconnectedtoPoolClosed:close_refuses_later_handle_acquisitions;shutdown_drains_a_queued_waiter;close_rejects_new_and_queued_acquisitions_while_an_existing_lease_keeps_its_slot;tests/features/client_pool*.feature.shutdown_drains_a_queued_waiterstill fails if the drain is a no-op.#548tests inlease_tests.rsandtests/pool_concurrency.rsunchanged. Confirm that they pass withoutrelease_control.scripts/check-feature-gate.shand thetests/uicompile-fail suite. Confirm that the pool code stays underfeature = "pool".🤖 Prompt for AI agents
Phase 4: Benchmarks and documentation
Record the
#639evidence and update the architecture records.Task 1: Task 4.1: Update `#639` benchmark assertions and capture new baselines
Replace the spawn-on-demand witnesses with persistent-task evidence.
wireframe_testing/src/runtime_ownership_benchmarks/client_pool_benchmark_support/(measurement/harness.rs,tests/uncontended.rs,tests/contended.rs,tests/sampling.rs,measurement/distribution/tests.rs);tests/runtime_ownership_benchmark_helpers/pool.rs.>= 1spawn witnesses withcapacity_waitswitnesses.uncontended_acquisition_spawns_at_least_one_scheduler_taskandcontended_acquisition_does_spawn_pool_scheduler_tasksto match the new meaning.<= 0.close().benches/runtime_ownership_baselines/driver.rsandobserve.rsconsistent with the new counter meaning.make bench-runtime-ownership. Add uncontended and contended latency, allocation and task-count captures todocs/runtime-ownership-baselines.md, labelled with the commit and toolchain. State that the workload parameters did not change.Task 2: Task 4.2: Update the architecture and developer documentation
Record the implemented protocol and the resolved decisions.
docs/adr-013-client-pool-scheduler-and-slot-ownership.md. Record these items:#647) is implemented.JoinHandleacknowledgement.#535is superseded by the actor's synchronous handlers.#539has slot-only scope.AcquireTimeoutdeadline remains open work.docs/wireframe-client-design.mdsections 17.2.1 and 17.2.2. Describe thePoolClosedandSchedulerFailedsemantics and the actor ownership graph.docs/developers-guide.md: tests drive synchronous handlers for event-order races and use runtime tests for lifecycle.make fmt,make markdownlintandmake nixie.🤖 Prompt for AI agents
Research
Ticket Summary
Replace the spawn-on-demand pool scheduler with one persistent scheduler actor per client pool. The actor owns all waiter and fairness state. It receives commands through a Tokio channel and observes slot capacity directly. Remove the shared
Mutex<SchedulerState>, theis_servicingguard and every lease-droptokio::spawn. Makeclose()await actor termination. Make implicit sender loss terminate the actor and releasePoolCore. Preserve FIFO and round-robin order, bounded admission (#550) and the dirty-socket invariant (#548).Codebase Summary
The pool lives under
src/client/pool/.WireframeClientPool::connect(client_pool.rs) builds two independent roots:Arc<PoolCore>(core.rs) ownsBox<[PoolSlot]>, the shutdown flag and itsNotify.Arc<PoolSchedulerControl>holdsArc<PoolScheduler>and the churn counters.The scheduler code is in
scheduler/mod.rsandscheduler/service.rs.PoolSchedulerkeepsSchedulerStatebehindstd::sync::Mutex(recovered bysync.rs::lock_or_recover).SchedulerStateholds the FIFO queue, the round-robin order, the per-handle queues andservicing_waiters.The current servicing flow:
kickuses a compare-and-swap onis_servicingto spawn a transientservice_waitersworker.kick.PooledClientLease::drop(lease.rs) callskickthroughrelease_control.PoolCoreexposes the#646permit-selection API:try_acquire_permitis the synchronous rotation probe.acquire_slot_permitis the async race. It usesFuturesUnorderedonly when every slot is blocked.Dirty-socket safety does not depend on admission permits. It depends on four mechanisms:
protocol_gatesemaphore;SlotConnection;PoolHandle(handle.rs) is notClone. Itsacquiretakes&mut self, and itsDropderegisters the handle ID.ClientError(error.rs) is#[non_exhaustive]and hasPoolSaturated { limit }. It has noPoolClosedorSchedulerFailedvariant. Shutdown currently surfaces as the recyclabledisconnected()error.Test layers:
scheduler/tests.rs;scheduler_tests.rs;core_tests/;src/client/tests/pool_lifecycle.rs;client_pool_tests.rs;tests/features/.The
#639benchmarks andPoolTaskChurncounters (churn.rs) assume spawn-on-demand servicing. ADR 013 pins these requirements:PoolClosedandSchedulerFailederrors;JoinHandleheld only by the close owner;PoolCoreor in leases.Repository rules:
.expect()in production code.make check-fmt,make lintandmake testbefore committing.🚀 Next Steps
🤖 All AI agent prompts combined
💡 Iterate on the plan with:
@coderabbitai <feedback>Start Coding →
Give each client pool a persistent scheduler task
Address review findings on the pool scheduler
Address review findings on the pool scheduler