# Scheduler interface and behavioral contracts
## Scope and provenance
The root `base-scheduler` crate defines the interaction between protocol libraries and their execution environment. It implements runtime-independent cancellation and buffer support, but contains no scheduler, executor, reactor, operating-system I/O, or runtime adapter.
Base's scheduler and protocol libraries use the `base-*` naming family. Applications choose concrete implementations such as `base-scheduler-direct` or `base-scheduler-tokio`; downstream libraries depend on the `base-scheduler` interface for their execution capabilities. Libraries may use concrete schedulers as development dependencies to test their behavior against different implementations. Future native implementations can use names such as `base-scheduler-native` or `base-scheduler-iouring` without changing that dependency boundary.
The initial extraction follows the Base ecosystem design and the [original interface proposal](proposal.md). Its source is [Socketry revision `b54b8d61bf80b71a1a5b97c3f5b80bae8a5ebabd`](https://github.com/base-rs/socketry-rust/tree/b54b8d61bf80b71a1a5b97c3f5b80bae8a5ebabd), specifically `crates/executor/src/scheduler/*.rs`, capability and shared-value modules in `crates/executor/src/`, and their runtime-independent tests. Existing MIT attribution is retained.
Applications use scheduler-bound `SocketStreamReader`, `SocketStreamWriter`, `SocketPacketReader`, `SocketPacketWriter`, `FileReader`, and `FileWriter` capabilities. Construction selects the scheduler and resource once; operations receive cancellation explicitly. Adapters implement these public resource contracts directly. Raw dispatch, monitors, receivers, and reusable registration and retirement state are private implementation details, retained only where they are shared internally. There is no public backend composition layer. Factory traits require only their resource and pool capabilities. Selector hooks live in `base-scheduler-selector`. There is no universal scheduler trait requiring unrelated facilities. A capability is a trait describing available behavior: scheduler capabilities construct resources or perform control operations, while bound reader/writer values provide stateful data-transfer capabilities. File allocation, advice, truncation, metadata, synchronization, and lifecycle operations remain separate scheduler capabilities. They do not implicitly synchronize with a reader or writer; callers order related operations explicitly.
The original proposal remains design history. The public Rust traits and this document describe the implemented initial contract. No crate publication or migration of Socketry is part of this extraction.
## Explicit execution services
Protocol values, parsing, and state transitions do not inherently need execution services. A protocol that performs I/O accepts the reader or writer capabilities it needs and may retain them alongside protocol state. The application constructs those handles from an explicitly selected scheduler and resource. Each operation receives its own cancellation token; a protocol never discovers an implicit current scheduler.
Socket reader/writer factories precisely capture the scheduler lifetime and their type parameters, excluding the temporary socket-reference lifetime. The returned handles retain access independently, allowing a connection owner to move its socket and directional handles together. This requires Rust 1.87. Backend implementations must declare compatible `use<...>` bounds; implementations that implicitly capture the socket-reference lifetime no longer satisfy the factory contract. Handle retirement, dropping directional handles, and exclusive socket closure remain separately ordered.
A bound handle retains its constructing scheduler for its lifetime. Moving a `Send` handle or its operation future between polling threads does not select another scheduler. Backend routing may use the current worker of that selected scheduler, while an accepted operation retains its original worker or ring through retirement. The backend performs any registration migration needed before a later operation. Changing execution services requires constructing a new compatible handle after retiring the previous one.
Implementations choose their own file, socket, metadata, signal-registration, task-handle, and registration representations. The interface imposes no `Arc<std::fs::File>`, native descriptor, Tokio handle, or ring-operation layout. Static resource traits describe stream, packet, and file behavior without requiring protocols to select readiness, completion, or buffer-registration mechanisms.
Factories establish scheduler/resource compatibility and return `InvalidInput` before payload submission when a pairing cannot be rejected by types. Direct resource types can be accepted by multiple scheduler instances, while other backends require the constructing instance or runtime. A bound handle fixes that compatible endpoint even where several instances could accept the resource. Keep its scheduler and drivers alive through operation completion and cleanup.
## Future and thread bounds
The extraction preserves Socketry's `Send` operation futures and `Send + Sync` file/socket resources and I/O capabilities. Static dispatch uses return-position `impl Future`; there is no mandatory allocation or dynamic dispatch for operations. Task and blocking-work handles are generic associated types, allowing different backends to retain their own representations.
Spawned futures, blocking closures, their outputs, and their handles are `Send + 'static`. This prevents borrowing caller stack storage into independently scheduled work. `Spawn` and `Blocking` do not require the scheduler object itself to be `Send + Sync`; an implementation may provide a thread-local submission endpoint for `Send` work. I/O and clock capabilities are shared services. Local-only futures and resources need a separate, deliberately designed interface later; they are not silently accepted by this API.
The production dependency is `event-listener`, used for runtime-independent cancellation and buffer-return notifications. There are no backend feature switches. Protocol libraries and backends can coexist as different concrete types in one program.
## Operation lifecycle, cancellation, and buffers
Cancellable operations construct lazy futures. Constructing an unpolled future submits no operation. An already-cancelled input prevents submission. After submission, cancellation is cooperative and best effort: an already-started blocking call may have to finish, and a completion backend must retire the original operation before releasing its resources. Successful progress or an established operation result wins concurrent cancellation, as specified by the capability.
`Cancellation` has persistent shared state. Child cancellation is isolated from ancestors and siblings; ancestor cancellation propagates to descendants. Dropping a token does not request cancellation. Cancelling is synchronous notification, not an assertion that work has stopped. Waiting for cancellation registers before checking state to preserve racing wakeups, and dropping a wait unregisters only that listener. `defer_cancel` notifies protected work once, then continues polling it so asynchronous draining remains possible.
Optimize ordinary progress without cancellation: checking existing cancellation state is an atomic read, and waiting retains pinned listener storage inline in the future rather than allocating a separate listener. Shared event state is initialized during token construction. Direct also uses inline capacity and drain listeners and avoids capacity registration when admission can complete immediately. Setup, child registration, actual cancellation propagation, and executor-specific waker behavior may still allocate; this does not impose a zero-allocation requirement on every backend. A parent records weak child registrations in slots indexed by each child, reusing the slots of dropped children, so steady child churn neither hashes nor grows the registry. Pinning preserves listener addresses without tying a `Send` future to a polling thread.
Worker-owned readiness operations with `Cancellation::never()` await the operation directly after the usual admission checks. Direction waits already wake and report driver closure, so neither a cancellation listener nor a separate closure listener contributes on that path. Independently driven selectors and cancellable tokens retain their existing wait logic.
Worker readiness uses a private broadcast notifier which registers a waiter and stores its waker together. Notification detaches all registered wakers under one lock, then invokes them outside it. Monotonic listener IDs and an atomic notification cutoff let completed waiters retire without reacquiring that lock, while cancellation removes only its own live entry. Independent simultaneous waiters remain supported. Lazy registration checks readiness and closure before and after registration; waker clone, replacement, destruction and wake callbacks run outside the notifier lock.
`IoBuf` and `IoBufMut` are unsafe contracts for stable, owned initialized storage. Their exposed allocation, range, and length stay stable while an operation owns them. Inline storage which moves with the owner cannot implement the traits safely. `BufferSlice` retains the owner and exposes a validated subrange without copying. Single-transfer buffer operations return that owner, including errors and cancellation. A stream writer's `submit` instead accepts ownership of a complete logical write and releases that owner independently when backend access ends.
Dropping an I/O future forfeits the buffer to backend retirement; it does not establish that kernel or worker access ended. Cancellation and future abandonment do not undo transferred bytes, changed file state, connection attempts, or cursor changes. A protocol must define when continuation is valid, rather than treating cancellation as rollback. Explicit offsets follow the documented platform cursor rules; protocol code requiring portable cursor ordering serializes related operations.
There is no global ordering between concurrent operations. Await related writes, synchronization, shutdown, or close in the order the protocol requires. Buffer reads and writes are single transfers, preserve short counts, and never grow the initialized buffer length. Datagram operations preserve message boundaries and report truncation where the capability supports it.
## Borrowed socket dispatch machinery
Bound socket handles implement borrowed transfers using private nonblocking dispatch, separately from operations which transfer stable ownership. Application code calls `read_into`, `read_vectored`, `write`, and `write_vectored` on its bound handle. Backend-private `_poll` helpers implement nonblocking readiness-driven I/O; the public borrowed methods return ordinary `Send` futures which callers `.await`, rather than exposing `Context` and `Poll`. Scalar operations accept borrowed initialized slices and the portable send/receive flags. Message operations accept borrowed standard-library `IoSlice`/`IoSliceMut` arrays and preserve the owned message operations' metadata and truncation semantics. Callers advance vectors explicitly after partial transfers.
Every payload and vector-array access finishes synchronously before the future returns from a poll, including `Pending`. Pending registrations retain no payload access, so even forgetting a pending future cannot leave a worker or kernel request accessing caller storage. Both payloads and vector arrays can live on the stack; no preparation allocation or stable owned allocation is required. Borrowed operations must never dispatch these references to independent workers or completion requests. Owned operations remain available for completion backends and independently retiring work.
Borrowed receives return a consumed message in the same poll rather than withholding a successful result across `Pending`, including empty datagrams and EOF. Message receives additionally attempt nonblocking I/O on their first poll after synchronous validation and cancellation checks. Private message-receive dispatch preserves this syscall-first contract so a pooled reader can probe once and release storage on `Pending`, without repeatedly discarding a deferred first attempt. Scalar borrowed reads can retain readiness-first optimizations.
Each attempt enforces nonblocking behavior. The current Unix implementations add `MSG_DONTWAIT` to recv/send-family calls. Windows enables `FIONBIO` before every native attempt and leaves that socket mode enabled, propagating setup failures before touching the payload. External aliases must not concurrently restore blocking mode; setting mode and issuing I/O are separate calls. Readiness is advisory and is followed by another nonblocking attempt. Existing sockets retain their backend compatibility requirements. Lazy construction and pre-cancellation perform no I/O or mode changes; successful results win concurrent cancellation.
Native and Tokio reuse their synchronous readiness callbacks, relaxing only private helper bounds to admit borrows. Direct's borrowed operations perform native calls in the polling future and wait on the existing retry pulse after `WouldBlock` or interruption; they never put references into its owned worker pool. Direct and Native wake pending borrowed operations on closure; Tokio closes admission without cancelling accepted borrowed operations. All reject operations first polled after admission closes. Native I/O-vector calls share the existing message syscall path with owned operations and use the borrowed arrays directly, without rebuilding or caching vectors.
On Unix, Tokio socket waits reuse an existing registration's advisory readiness directly, without additional native `poll` probes before or after the wait. Syscall-first probing remains for unregistered sockets so an unconnected stream's hangup does not become permanently cached before connect. Readiness-driven operations still retry their own nonblocking syscall and clear stale readiness after `WouldBlock`. One waiter per direction uses Tokio's lightweight poll slot; concurrent waits use its independent waiter list, and dropping a wait releases its reservation.
## Cleanup, tasks, and shutdown
Reader and writer `close()`, file/socket close, backend monitor/receiver close, socket shutdown, and file advice intentionally have no cancellation input. These operations remain available while ordinary operation tokens are cancelled. Their completion forms await backend execution and report its result; submission forms await bounded admission only. Accepted cleanup survives future abandonment and must be drained by graceful scheduler shutdown. Resource close does not cancel or join other operations; callers establish exclusive ownership and completion first. Dropping an unpolled owning close releases the resource using its ordinary destructor.
`Spawn::spawn` registers synchronously before a task becomes runnable. It has no hidden parent or task group. A caller passes cancellation into the future's closure explicitly. A closed scheduler rejects and drops the future without polling it. Task handles own execution lifetime: moving transfers ownership; awaiting returns normal output or `TaskError`; dropping requests destruction once any active poll returns. Destruction runs synchronous Rust destructors and does not execute the remainder of an asynchronous body. Host handles which detach on drop need an owning adapter. No independent forced-abort capability is added.
Graceful shutdown stops admission of new work, lets live owners coordinate cooperative cancellation and await completion, drains accepted cleanup, and retains all outstanding backend-accessed storage until retirement. An abrupt runtime teardown is a separate lifecycle event and must still preserve memory safety. This crate does not prescribe a runtime owner or a universal shutdown method; each backend documents how applications drive and shut it down.
## Clocks and optional services
`Clock::now` is monotonic within one clock domain. Use it rather than `Instant::now` or `Instant::elapsed` in generic consumers so deterministic and paused clocks remain substitutable. Deadlines from independently advancing domains are not interchangeable. Relative sleep starts on its first poll; overflow is an `InvalidInput` operation error. Pre-cancellation wins even for an elapsed deadline. Once waiting, expiry wins if observed together with cancellation. Dropping the wait releases its registration.
`Clock::timeout` is a default composition of an arbitrary `Send` operation future and relative sleep, implemented with standard future polling and inline storage. It polls the operation first on every poll. An immediately ready operation reads no clock and registers no timer, including with zero or overflowing durations; otherwise the duration starts when the operation first reports `Pending`. Operation completion wins simultaneous expiry or clock failure, preserving the operation's output unchanged inside `Ok`. `TimeoutError` distinguishes expiry from a clock operation error. Completion drops the losing future before returning. Timeout requests no cooperative cancellation and does not await backend retirement; accepted I/O can continue after its future is dropped. Use explicit cancellation and completion when protocol recovery requires them. Dropping the wrapper drops both futures. A timeout cannot preempt a long-running poll. The helper adds no backend requirement or production dependency; more general compositions can live in companion crates.
### Experimental reusable timers
`Timers` is an optional experimental capability extending `Clock`, rather than changing one-shot sleep semantics. Its associated `Timer: Send` retains backend timer state between calls to `timer_wait_until`. Construct it with `scheduler.timer()`, following the explicit service-construction convention used by I/O handles. Each wait explicitly receives the scheduler, absolute deadline, cancellation, and an exclusive mutable timer reference. Constructing an unpolled wait changes nothing. Pre-cancellation leaves retained state unchanged; cancellation observed after admission disarms the registration. Expiry observed together with cancellation wins once waiting begins.
Construction establishes scheduler compatibility without registering a deadline, reserving queue capacity, selecting a worker, or starting a timer service. It is infallible, including after scheduler closure; waits retain each backend's clock admission rules. Native and Direct require the same scheduler instance; Tokio accepts adapters of the same runtime. Incompatible pairings return `InvalidInput` before registration, including on the first wait. Resources may move between polling threads, but retained Native registrations keep their original worker, with waker updates and release routed to that worker. The resource retains backend ownership for lifetime safety, without replacing the operation's explicit scheduler argument. Its current representation needs no stable shared wrapper; private monitor storage serves its own registration lifetime requirements.
Dropping a wait leaves registration state in the resource for reuse; it can retain capacity and the previous task's waker, causing a stray wake after abandonment. Even expired Native registrations retain their capacity until reused or dropped. Dropping the resource retires its registration; replacing it with another scheduler's newly constructed timer explicitly selects a new owner. Forgetting it can leak capacity but cannot retain access to borrowed application memory. Tokio retains a pinned sleep allocation made on first pending use. These retention and allocation tradeoffs, and whether to expose explicit reset/disarm operations, remain experimental design questions.
Backends can privately keep an earlier armed deadline when a desired timeout moves later, then rearm on that earlier wake without completing before the desired deadline. Earlier desired deadlines replace the registration promptly. Native continues using its worker-local timer queues and timed driver waits, including with io\_uring; Direct retains its reference timer service, and Tokio reuses `Sleep::reset`. The timer benchmark compares fresh sleeps, reusable resources, and a portable `After` wrapper. Its measured refresh savings establish a bookkeeping improvement, not a network-throughput claim.
`Blocking` is an independent capability with lazy, cancellable, bounded admission. Running closures cannot be forcibly interrupted; they may capture a token to cooperate. Capacity counts queued and running work until execution ends, even after handle abandonment. Blocking job handles own queued work and report completion separately from admission errors. Thread-pool configuration and workers remain backend implementation details.
`Signals` is independently implementable and uses an associated signal identifier and owning `Subscription` guard. Subscription is synchronous and fallible. Notifications run in ordinary thread context, outside raw signal handlers and backend locks. A target can request cancellation or implement application policy. The synchronous `signal_subscribe` method returns that guard. Dropping a subscription stops new notifications without synchronously joining an in-progress callback; the backend retains its target until dispatch ends. This crate installs no signal handlers.
Address resolution, typed socket options, message I/O, file allocation/advice, and standard-library TCP resource registration remain separate extension capabilities. Portable consumers require them explicitly. File allocation/advice retain their Linux-style semantic contracts without requiring an implementation on unsupported platforms. There are no system resolver, socket-option syscalls, or signal constants in the interface crate.
Message I/O takes an owned `IoBufSequence` or `IoBufSequenceMut` and returns the same collection owner, including on errors and cancellation. Arrays, vectors, boxed slices, and pool-backed sequences can carry the buffer owners without requiring a separate vector allocation. Sequence traits are unsafe contracts: membership, order, and length remain stable while the backend owns the sequence, and each buffer retains its existing stable-byte-storage guarantees. An array of buffer owners may move even though inline byte arrays cannot implement `IoBuf` safely. Sequences expose indexed initialized byte ranges rather than mutable source owners, so advancing a prepared package cannot accidentally expose the original, unadvanced ranges. Backends retain the whole owner through retirement and can use their own reusable I/O-vector storage when a sequence does not provide prepared vectors.
`IoBuffers` and `IoBuffersMut` pair a complete sequence owner with a heap-stable array of standard-library I/O vectors. Construction allocates the vector array once; repeated operations and advancement allocate nothing and copy no payload bytes. The source owner remains private until `into_inner()` destroys the vectors and returns all original buffers and ranges. Prepared views borrow the package, and writable views require an exclusive borrow. Writable ranges must not overlap. A private initialized `MaybeUninit` owner wrapper prevents package moves from recursively retagging boxed allocations and invalidating cached pointers. Private lifetime erasure retains native vector layout without exposing a forged public lifetime; all byte access uses the cached pointers, avoiding fresh source-owner borrows which could invalidate writable pointer provenance. On Unix and Windows the standard types supply native `iovec` and `WSABUF` layout, allowing a writable package to expose the same array for sending through a shared view. Other targets still support indexed ranges and prepared writable views without assuming identical read/write layouts.
After an operation returns its owner, the caller may explicitly `advance(bytes)` using the reported transfer count. Advancement skips fully consumed vectors and updates only the first partially consumed vector. It does not resize, release, or copy source allocations, and over-advancement panics without modifying the package. Zero advancement skips leading empty ranges. Schedulers never advance implicitly; errors and cancellation return the current package unchanged. Any backend retaining a pointer to an I/O-vector array must stabilize that array as well as retaining payload owners until retirement, even if a custom sequence uses inline vector storage. Future abandonment forfeits the entire package to backend retirement; a destructor panic cannot replace that ownership requirement.
`AddressResolution::Addresses` chooses an owned `AddressInfoCollection`. Its `entries()` method yields borrowed `AddressInfoView` accessors with no required copied vector, per-entry reference count, or eager field conversion. Iterators and views are `Send`, and collection owners are `Send + Sync`, allowing generic consumers to continue work on another polling thread while preserving owner lifetimes. Native implementations can retain their original `addrinfo` list, traverse `ai_next`, validate it without copying, and invoke the matching deallocator exactly once on drop. Native wrapper code remains a backend responsibility; a Unix integration fixture demonstrates that this interface admits direct traversal and cleanup without a new Rust allocation. Arrays, vectors, boxed slices, and custom cached results compose through the same interface. `AddressInfo` remains a convenient portable value, and `to_address_info()` or `address()` performs conversion only when requested. Preserve candidate ordering, duplicates, scope identifiers, and native type/protocol values.
## Extraction decisions and follow-up
Concrete task types, native descriptors and metadata wrappers, blocking-pool options, workers, task-local lookup, timers, system resolution, selector code, runtime adapters, and OS feature flags stay in their implementation crates. The optional `Notify` integration for `futures-channel` stays out of the production dependency graph; notification implementations for cancellation, `Arc`, and standard unbounded channels are runtime-independent support.
Tests cover cancellation propagation and wakeups, buffer ownership and invalid ranges, error preservation, default relative-clock behavior, capability-limited consumers, and backend resource compatibility. Test-only implementations establish public API composition; they do not certify any real backend.
## Companion crates
The same workspace now contains `base-scheduler-conformance` and `base-scheduler-direct`. All public packages share a version, root release notes, and release tag. Keep production interface dependencies independent of both companions. Direct depends on conformance only for development; conformance depends on the interface and runtime-independent test helpers, never a backend.
Conformance supplies async capability-specific assertions and backend-controlled fixtures. Its initial file tests accept resources and paths supplied by the backend, respect partial transfers, and cover ownership, pre-cancellation, laziness, cursor operations, positioned I/O, metadata, truncation, synchronization, and errors. Blocking tests cover lazy/cancelled admission, outputs, panic payloads, queued cancellation, bounded admission, and handle abandonment. The blocking queue fixture holds workers at a known execution boundary and releases them on guard destruction; assertions do not require global FIFO execution. Backends drive their own executors and remain responsible for test deadlines and implementation-specific retirement/shutdown tests. No passing aggregate claims support for capabilities that were not tested.
Conformance also owns capability-specific case lists through exported macros that generate ordinary named `#[test]` functions. A backend supplies an async-closure runner with a fresh isolated fixture per case, drives its executor, enforces a deadline covering setup and cleanup, drains accepted work even after assertion failure, and propagates panics. Rust 1.85 async closures let cases borrow runner-owned fixtures without boxing futures or a custom harness. Updating conformance automatically extends each selected suite; normal test listing, filtering, and independent failure reporting remain available. Suite groups require only their documented capabilities; individual helpers remain available for smaller capability sets and existing callers. The full file suite uses `FileTestFixture` for independent backing-store observations.
Direct supplies the core file/socket aggregates, scatter/gather messages, `Blocking`, `Spawn`, `Clock`, native address resolution, typed socket options, and standard-library TCP registration on Unix and Windows. Unix additionally supports signal subscriptions, and Linux supports the interface's allocation/advice semantics; other platforms omit those optional traits. It is a reference implementation prioritizing clarity, simplicity, and behavioral conformance. Take straightforward performance improvements when they preserve those priorities; use implementation costs to expose avoidable interface-imposed allocations rather than requiring direct itself to eliminate every allocation. Ordinary work uses a bounded pool; cleanup uses a separate worker with its own bounded admission. Counts include both queued and running work. Admission is lazy and cancellation-aware. Accepted file operations retain their resource and owned input until retirement, even after waiter abandonment or scheduler destruction. A worker checks cancellation before executing queued I/O and returns the original input on cancellation. Started calls preserve their actual result. General blocking handles may abandon queued work, whereas accepted I/O and cleanup survive pool closure. Worker panic payloads from file operations are resumed in the waiting future, not disguised as I/O errors.
`DirectFile` owns a standard-library file with private reference retention and no scheduler affinity. Every direct instance accepts the same resource type; newly constructed bound handles may select another compatible instance after previous handles retire. Each live handle keeps its selected scheduler. Files are opened through `FileOpen`; native file import and access are deferred until descriptor flags and aliases can preserve positioned-write and exclusive-close guarantees. In particular, accepting append-mode descriptors would let Linux positioned writes append instead of honoring their offsets. Explicit close requires exclusive ownership, consumes the OS handle exactly once, reports errors without retrying, and uses the cleanup pool. This includes cleanup surviving abandoned completion waiters. Submitted cleanup admission reports only admission errors.
`DirectScheduler::close` stops admission to both pools, discards queued general blocking jobs, and leaves accepted I/O and cleanup to retire. `shutdown().await` additionally waits for all admitted jobs to finish. Applications coordinate cooperative cancellation and asynchronous cleanup before closing admission. Destruction closes admission without synchronously joining workers, so destructors can safely run in worker callbacks. A never-returning synchronous operation can prevent graceful shutdown. Waiting for a scheduler's shutdown from one of its own jobs is a caller-created dependency cycle. Direct uses no process-wide fallback pool or implicit current scheduler.
General blocking jobs reuse completion-channel receiver state to detect handle abandonment, avoiding a separate allocated flag. Accepted I/O and cleanup ignore receiver abandonment. Direct deliberately retains boxed jobs and completion channels; the interface's concrete futures and associated types permit other representations without imposing fixed limits on generic inputs or outputs.
`DirectSocket` privately retains a nonblocking `socket2::Socket`, with no public native access, cloning, or scheduler affinity. Standard-library TCP imports preserve existing endpoints and connections while selecting nonblocking mode; callers must not change that mode through external aliases. Every direct instance can operate on it. Scalar and message syscalls run synchronously in the ordinary bounded pool; native calls never retain payload, vector, or address pointers after returning. Prepared packages supply native arrays directly, while other sequences reuse per-operation scratch capacity across attempts. `WouldBlock` and interrupted attempts release their worker and admission slot before waiting for a scheduler-local retry pulse. The pool therefore bounds queued and running attempts, not the count of pending caller-owned socket futures. Reads waiting for peer traffic cannot reserve all workers or admission capacity and prevent writes from making progress. General blocking closures and synchronous file operations retain their existing limitations.
One lazy retry thread wakes pending socket futures approximately every two milliseconds and sleeps when none are waiting. It holds no socket or buffer owners. This reference policy deliberately avoids a reactor or per-operation threads; repeated attempts, latency, boxed worker jobs, and completion channels are accepted implementation costs. Each accepted attempt retains its entire input through syscall retirement after waiter abandonment. Between attempts only the waiting future retains the input; dropping it can end retries immediately because no backend access remains. Closing scheduler admission wakes retry waiters, and shutdown drains accepted attempts without waiting for unpolled caller-owned futures. Socket shutdown/close use independent cleanup admission, accepted cleanup survives abandonment, and explicit close checks exclusive ownership. Timers must likewise not consume ordinary blocking workers for their entire wait.
Conformance now supplies reusable scalar/socket-message assertions and backend-controlled I/O lifecycle fixtures, including deterministic cancellation races and retention after waiter abandonment. Direct exercises these with scalar reads and prepared sends/receives, in addition to implementation-specific retry, capacity, cleanup, and shutdown checks. Conformance also supplies clock, task, resolver, signal, typed socket-option, TCP registration, allocation, and advice suites. Controlled fixtures expose native/registration boundaries independently of the operations under test. Basic suites remain capability-specific; active task-poll and signal-dispatch cases use separate macros for fixtures shareable with scoped background drivers, without changing production trait bounds. Controlled test implementations and deliberately faulty variants validate the assertions, while native TCP tests exercise real resources. Direct now runs every applicable shared suite against its real implementations, using per-instance test-only observations and execution gates at native boundaries. Native resolver tests exercise numeric IPv4/IPv6, scoped results, structured errors, bounded retirement after abandonment, and allocation-free borrowed views. Signal bridge tests run in isolated child processes, and Linux filesystem tests invoke the real allocation/advice calls. These checks complement the controlled implementations that validate the assertion suites. The Native backend is described below; a smol adapter remains follow-up work. Keep backend code in companion or adapter crates rather than the interface package.
Direct task polling uses its own queue and workers backed by `async-task`, with synchronous registration and owning handles. Guards contain future and unconsumed-output destructor panics before they can unwind into the executor, including abandoned tasks. Unobserved panic payloads are leaked on this exceptional path because destroying the payload can itself panic. Awaited task results preserve the original payload. Task workers start during scheduler construction because `SpawnError` does not carry worker-start I/O failures. Timers use one lazy driver thread rather than holding a bounded syscall worker asleep. Closing admission leaves existing task owners valid, stops timer and signal services, and graceful shutdown additionally waits for task and signal-dispatch retirement. Callers coordinate task cancellation and completion before shutdown; a task cannot await the shutdown that must drain it. Unix signals bridge through `signal-hook`, with callback admission under a registry lock and callback execution outside it. Callback and dispatch-owned target destructor panics are contained so independent subscriptions continue receiving notifications; an exit guard releases shutdown waiters when dispatch ends. Native resolver results retain the original list and matching deallocator, including a Windows Winsock initialization guard. Linux advice uses independent cleanup admission, and allocation uses ordinary cancellable work.
## Tokio adapter
`base-scheduler-tokio` is a companion crate under `crates/tokio`, sharing workspace versions and releases. It adapts the Socketry Tokio implementation at revision `7a70586`, updating its buffers, resolver views, cancellation, lifecycle, and conformance tests to the current interface. The interface and conformance crates remain independent of Tokio, and concrete backends have no executor dependency on each other.
`TokioScheduler` receives an explicit runtime handle. The application owns and drives that runtime, with I/O and time enabled. Both runtime flavours are supported; operation futures can also be polled outside the selected runtime while its driver continues running. Task admission registers before Tokio submission, with the registry lock released before calling the host, since a stopped runtime can immediately destroy the submitted future. `TokioTask` wraps a Tokio join handle with abort-on-drop ownership and translates host task errors. Guards contain user future and unconsumed-output destructor panics, including during abandonment.
Adapter close preserves live task owners rather than aborting every task as Socketry's owning scheduler did. It stops new work and signal delivery without cancelling accepted socket or clock operations. These operations wait only on the explicit caller token, avoiding a scheduler-shutdown listener for every pending operation. With `Cancellation::never()`, no cancellation listener is registered. Closure uses an admission flag; operations first polled after closure are rejected, including futures constructed earlier. Applications coordinate explicit cancellation and await operations and tasks before graceful shutdown, which drains adapter-owned tasks, native work, and accepted cleanup without shutting down the host runtime. Socket and clock futures polled by external tasks are not separately tracked, so their owners must finish them; an adapter-owned task blocked on uncancelled I/O can prevent shutdown indefinitely. The host must remain alive and driven during this sequence. Abrupt host destruction retires task futures through Tokio while independently admitted native work retains its owned inputs until retirement.
Unix sockets retain the selected runtime identity and initialize their Tokio `AsyncFd` registration only when a native attempt returns `WouldBlock`; incompatible runtime pairings fail before submission. Registering an unconnected TCP socket eagerly can cache Linux hangup readiness which Tokio intentionally never clears, causing later connected I/O to spin within one poll. Deferred registration avoids that stale readiness while preserving one shared registration for concurrent operations and reporting registration failures through the waiting operation. Lifecycle operations, empty reads, and sockets without a successful stream read first attempt their nonblocking syscall, preserving immediate OS results independently of cached readiness. Nonempty scalar reads on a known-connected stream with an existing registration may wait on cached readiness first. Readiness is cleared only when the syscall returns `WouldBlock`; a short read alone does not prove that a TCP receive queue is drained, for example at an urgent-data boundary. After `WouldBlock`, one operation per direction reserves Tokio's lightweight polling waker; concurrent operations use Tokio's independent waiter list. Drop releases a reservation, and forgetting it leaves subsequent callers on the independent waiter path. The native descriptor stays owned and unchanged for the entire registration lifetime, including if forgotten, satisfying Tokio's explicit unsafe registration contract. Windows retains Socketry's `async-io` bridge for generic nonblocking sockets; this uses its own readiness reactor while task execution and timers remain on Tokio. Socket I/O makes synchronous readiness-driven calls, so a pending future owns its buffers with no outstanding kernel payload pointers. Prepared vector packages reach native syscalls directly. File, resolver, blocking, and cleanup work retain the bounded native-worker design and owned-input retirement contracts; files have no runtime affinity. Unix signals and Linux allocation/advice retain their platform-specific support.
## Native backend and reusable timers
See [the worker/reactor execution model](execution.md) for progress stages, batching and idle transitions, and the distinction between operation cost and queueing delay. [Worker progress requirements](../design/worker-progress.md) record the boundaries performance changes must preserve.
`base-scheduler-native` ports Socketry's default work-stealing executor and `async-io` readiness backend from revision `7a70586d5d37fb640d696d0c03a05b2ee3cbe06b`. Worker-local queues, preferred-worker remote inboxes, idle work stealing, external fairness, and the publication/parking fence remain. This branch experiments with alternating ready batches, as described under worker ownership below. Before another ready batch runs, the worker makes a nonblocking driver turn to publish available completions; when its queues are empty, the normal idle turn prepares and waits directly without a preceding nonblocking probe. Internal worker identity exists only for queue locality and blocking-wait detection; operations never find an implicit current scheduler. The root future can borrow local state and is polled on the caller's thread by `block_on`.
The adapter adopts Base's owning task handles, guarded user destructors, lazy cancellation, and asynchronous shutdown. A short admission lock serializes spawning with closure; no task-ID registry is needed. Separate counters track task-future ownership and queued or running runnables: shutdown needs both to retire, since destroying a task future can precede the return of its final runnable. Runnable quiescence notifications arm when the first drain wait registers, so ordinary dispatch avoids notifying an unused event. Arming shares the runnable counter's modification order and remains enabled thereafter, preserving concurrent and abandoned waits. Closing admission preserves live task owners and wakes socket and clock operations. Drop does not join. Explicit shutdown drains tasks, accepted blocking/file/resolver work, cleanup, timers, and signal delivery. Applications coordinate cooperative cancellation and await tasks before shutdown. A task must not await shutdown of its own scheduler.
New Native tasks select a preferred worker round-robin under the existing admission lock, including tasks spawned by another worker. Their initial runnable goes to that worker's queue before the first poll can attach persistent I/O state. There is no global unassigned-task queue or per-operation load counter. Subsequent wakes prefer the last worker to poll the task; idle workers can still steal queued tasks, including their initial runnable. This spreads initial placement rather than promising equal CPU or I/O load, pinning tasks, or migrating active ring resources. `block_on` continues polling its root on the caller's thread.
Sockets retain a `RegisteredSocket` native owner on Unix and Windows, without scheduler-instance affinity. It registers a borrowed descriptor with the portable `async-io` reactor only when a fallback wait first needs it, so worker-driven sockets never enter that reactor's poller; a registration failure surfaces at that wait rather than at socket creation. The default Native scheduler composes one readiness driver per task worker. On macOS this driver calls kqueue directly, using persistent `EV_CLEAR` read/write filters, stack-based change receipts, and `EVFILT_USER` interruption. On Linux the driver owns epoll directly, using persistent edge registrations and an eventfd interrupt. If its native eventfd or timerfd cannot be created, construction selects the existing `polling` backend before any socket registrations are exposed. Other platforms with edge support use `polling`; platforms without it retain independent `async-io` progress. Windows compiles only the retired-driver facade, with no unused worker-registration state or extra poller. There is no separate socket reactor handoff on the worker-driven paths. Files and other synchronous services still use bounded native pools independent of task workers. The separate io\_uring selector described below provides a completion-based alternative for owned file and socket transfers. Native kqueue and epoll event collection use one reusable vector of native records, decoded during dispatch without copying to a normalized event buffer. Linux drains eventfd only when its interrupt record is dispatched. Positive finite waits arm a monotonic timerfd at nanosecond precision and disarm it after every wait, including failed and interrupted waits; zero and indefinite waits do not change the timer. Timer records are filtered before pointer decoding, including records retained from earlier appended waits. Disarm clears unread expirations, and cleanup or interrupt-drain failures close the driver group. This uses `epoll_pwait` without probing newer syscalls; the compatibility backend retains `polling`'s timer and notification fallbacks. Only successfully initialized entries become iterable; internal timer and interrupt records are handled before pointer decoding, and batch clearing still precedes registration reclamation.
Unregistered worker-driven socket operations attempt their nonblocking syscall before creating a registration. On `WouldBlock`, a socket lazily obtains one persistent registration in the explicitly selected selector group. Its first compatible worker owns that registration for the socket's use of that group; root and external callers select workers round-robin. Pending futures and later operations can move between threads without changing that registration's owner. Using a compatible socket with another selector creates an independent registration. Retired groups are pruned on later registration lookup. Each registration has a monotonic diagnostic identity. On macOS and the Linux/Android epoll backend, the OS event token is the address of its stable registration state; dispatch updates readiness and wakes waiters without a routing lookup, table lock or reference-count upgrade. Other polling backends retain numeric-key lookup until their deletion semantics have been audited.
The controller anchors each registration with a strong reference before adding its kernel filters. Registration state retains only a weak controller reference and no native socket or payload. Deregistration marks the record inactive, deletes its filters synchronously and queues successful deletions for driver retirement. The driver clears its preceding event batch before reclaiming those records and polling again. A callback can therefore close a socket remotely without blocking on a live batch; a collected old event always inspects retained state and skips a record observed inactive. Publication already admitted before retirement can finish against the old record. Failed additions, including partial addition with failed rollback, and failed deletions remain quarantined until the driver stops. Normal group closure wakes waiters without freeing batch storage; sole-driver destruction releases its records after dispatch has ended. An idle driver is interrupted after sixteen pending retirements; a smaller group can remain retained until the next turn or stop. All waker callbacks and record destruction occur outside the routing lock.
Readability and writability each retain advisory readiness and a notification generation. A `WouldBlock` clears only the generation observed before its syscall. A wait registers its listener only after observing readiness clear, so publication notifies listeners only when it sets clear readiness; repeated edges on a ready direction, such as epoll's writable report with every readable event, skip the notification lock. Independent inline listeners support concurrent waits in the same direction; abandoning one wait cannot replace or remove another's waker. Bound scalar stream readers retain their registration and, after establishing progress, wait first when readiness is clear. Never-cancelled reads with an established registration poll a compact receive future directly, and never-cancelled worker-driven reads and writes skip the cancellation wrapper; driver closure still completes their waits. On macOS, kqueue also supplies the readable byte count and directional EOF. A positive short receive may clear its observed readiness only when it consumed at least the reported byte count and that event did not report EOF. An urgent-data boundary can stop a short receive before already-queued ordinary bytes, including when another alias consumed the urgent byte; the byte-count comparison preserves progress in that case. Publishing the count precedes advancing the notification generation, so a concurrent event either supplies a conservative byte hint or prevents the old generation from being cleared. On Linux, ordinary scalar stream readers default to OOB support disabled: a positive short receive clears the observed readiness unless the event reports EOF, and a reported urgent condition returns `Unsupported`. EOF and urgent conditions remain sticky across readiness clears. `reader.with_oob_support::<true>()` selects a compile-time specialization which retains Linux readiness until `WouldBlock`, preserving progress across urgent marks even if an alias consumes the urgent byte. OOB-disabled aliases must not consume urgent data. This setting is not a socket filter or a promise to detect every urgent byte: conservative paths may continue normal reads without rejecting it. The setting preserves the bound scheduler, pool, registration, and EOF/close state. Linux compatibility polling and other edge backends retain readiness until `WouldBlock`, and message receives preserve their first-poll syscall contract. EOF remains readable through the final data read. All payload access finishes inside the polling future; neither kqueue nor a readiness registration retains borrowed payload pointers. Registration owners retain no native-socket owner. The final socket alias synchronously deregisters its filters before releasing its descriptor, including on another thread, so an event batch temporarily retaining a record cannot prevent exclusive close. Closing or dropping a driver closes readiness admission across its group and wakes pending waits. Readiness records retire at a driver batch boundary; the kernel retains no payload storage for these waits.
Independent `async-io` socket fallback operations also attempt immediate progress first. After `WouldBlock`, they await descriptor readiness before another attempt. Repeated polling because a containing task wakes for another descriptor must not retry the blocked syscall. This matters for the io\_uring selector's fallback listener, which can share one supervisor task with active connections.
Bound borrowed stream writers attempt ready progress without a registration-table lookup. Their first `WouldBlock` acquires and retains the shared registration, retries across registration and then clears only the generation observed before a blocked send. Subsequent blocked operations reuse that registration. After writer/output-lane and operation admission checks, a successful send returns before constructing cancellation or readiness wait futures. Immediate errors and short counts also return directly. Cancellation and closure retain the same contract; owned output submission remains separately managed.
`base-scheduler-timers` is extracted as runtime-independent support with no dependency on the scheduler interface. It retains batched queues, stable slots, constant-time cancellation, optional sharded assistance and deadline notifications, and the optional bounded producer driver. Its `local` feature exposes the same queue engine with exclusive ownership and no synchronization or reference counting.
The Native clock has no dedicated timer thread or shared timer queue. Each task worker owns a reserved local queue alongside its I/O driver. Every loop collects expired timers before checking task quiescence; their wakers publish ordinary runnable tasks, which remain stealable. Timer collection never polls application futures inline. The earliest local deadline bounds the interruptible driver wait, or the thread park when there is no active driver. A pending timer command forces a nonblocking turn, including one published before the worker advertised its sleeping state.
A sleep first polled on the explicitly selected scheduler's worker registers directly, without a fresh allocation within reserved capacity. Root futures and callers on other executors select a worker by round robin and send a registration command with an acknowledgment. No external timer service is created. An acknowledgment owns the registration, so dropping a receiver also releases an accepted timer. The registration retains its original worker and generation key across future migration. Waker replacements, cancellation, and drop update that original queue locally or through its inbox; subsequent sleeps can register with the task's current worker. Wakers and acknowledgment callbacks run outside queue borrows and admission locks.
`timer_capacity` reserves capacity per worker. Full capacity on the selected owner returns `WouldBlock`, even if another worker has room; closure is `BrokenPipe`. Collected timers retain their key's capacity until the waiting future consumes or drops its registration. Capacity pressure reclaims canceled entries even when they are not at the heap root. Pre-cancellation wins before waiting; after waiting starts, expiry observed alongside cancellation or closure wins. Closing admission rejects new timer commands, drains accepted commands, and notifies local sleepers before a worker can exit. Shutdown waits for timer callbacks and driver destruction as well as task retirement.
## Selector composition
`base-scheduler-selector` separates native I/O services from task execution. Its `ReadinessSelector` owns bounded blocking and cleanup pools, socket readiness operations, native resolution, and platform-supported signals; it creates no task executor or clock. `new` and `with_options` keep independent `async-io` reactor progress. `ReadinessSelector::with_workers` returns caller-owned readiness drivers; those drivers must continue progressing through shutdown. `Selector` specifies only nonblocking admission closure and asynchronous retirement. Each selector implements the existing operation-level capabilities it supplies, preserving its associated resource types. Selectors do not need a second I/O interface or a universal set of capabilities.
`NativeScheduler<S = ReadinessSelector>` adds task workers and timers and forwards only the selected capabilities through static dispatch. Existing `new` and `with_options` construction selects readiness. `with_selector` accepts an owned selector and `NativeExecutorOptions`; I/O limits are configured separately when constructing the selector. Failed composition closes the supplied selector. Application shutdown coordinates tasks first, then closes admission and waits for selector, task, and timer retirement. A caller-owned driver must keep progressing during that wait.
The initial `base-scheduler-iouring` selector ports Socketry's owned request and cancellation engine for scalar file reads and writes (including positioned operations), socket reads/writes, and message sends/receives. Other operations delegate to an owned readiness selector. Ring setup probes required kernel support and fails explicitly if unavailable. Ring requests retain the file or socket and buffers until original completion; cancellation completion cannot release them. Admission counts queued plus in-flight original requests and is returned only when request ownership retires. Bound scalar stream reads and file resources reuse typed request and completion storage. Repeated scalar reads with the same owner type allocate nothing after warmup when backend capacity exists and no transient readiness retry is required; changing owner type replaces the cached request. Other ring paths may still allocate a request and completion channel. The public capability interface does not require these allocations. `NativeFile::retain_native` and `NativeSocket::retain_native` are unsafe backend-integration escape hatches with explicit retention, descriptor-flag, registration, and exclusive-close obligations.
Owned scalar socket writes first attempt `NativeSocket::try_send` after checking cancellation and selector closure. Successful attempts and terminal errors return the original buffer immediately, without a ring request or capacity permit. `WouldBlock` and `Interrupted` fall through to the completion path; scalar reads submit directly to the ring. Capacity bounds queued and in-flight ring requests, rather than completed synchronous attempts. Socket requests submit `Recv`, `Send`, `RecvMsg`, and `SendMsg`. A nonblocking `EAGAIN` completion retires the attempt before a readiness wait; retries retain the operation's original ring. Message requests derive pointers only after being boxed and retain the header, address, iovec array, and whole sequence until original completion. Prepared vectors are used directly, including inline vector arrays whose containing request stays immobile. Ordinary sequences build request-owned vectors. Kernel-accessible message storage uses `UnsafeCell` so shared cancellation bookkeeping cannot freeze writable output. Borrowed `_poll` operations call synchronous `NativeSocket::try_recv`, `try_send`, `try_recvmsg`, and `try_sendmsg` with enforced nonblocking flags. Ready operations bypass routing, admission, and request allocation. After `WouldBlock`, they submit a one-shot `PollAdd` readiness request on the current worker's ring and retry after completion; `socket_wait` uses the same ring wait. The first wait selects the ring, which remains fixed across spurious readiness and task migration. Readiness requests retain only the socket and never borrow payloads or vectors. Cancellation awaits readiness retirement before returning; dropping requests cancellation, while a forgotten future may leave its readiness request pending until an event or shutdown. Ordinary socket destruction is safe during retirement; exclusive close requires completion or draining first. Socket lifecycle operations still use the shared readiness fallback. Nonblocking owned-transfer retries wait on their original ring after the preceding attempt retires.
`IoUringSelector::new` returns a selector and a separate `IoUringDriver`, starting no ring thread. The driver exposes bounded `turn(timeout)` processing and a waker for caller-owned event loops; `run` is a convenience loop for a dedicated thread. Processing that wakes a caller prevents a subsequent blocking wait in the same turn, including wakes caused by command rejection, cancellation, or dropping retained owners. External submissions and wakeups signal a nonblocking eventfd watched by one outstanding ring poll. Idle turns publish submissions and wait in one io\_uring entry; productive turns skip empty submissions unless CQ overflow or deferred task work requires kernel progress. A fresh driver entering a thread-bound `IoUringWorker` attempts to replace its unused ring with one configured for a single issuer, deferred task work and the taskrun flag. This batches completion task work at explicit kernel entries. Nonblocking turns check the taskrun flag; the io\_uring submitter requests `GETEVENTS` when that flag is set, so busy workers publish completions without requiring a blocking wait. The io\_uring dependency minimum includes this submitter behavior; older submitters omit `GETEVENTS` for taskrun-only entries. If deferred setup is unavailable, the worker tries cooperative task running with the taskrun flag, then retains the original validated ring if both optional setups fail. Movable drivers and drivers that have already processed a turn retain their original ring: issuer-bound task work cannot safely follow a moving driver. Timed waits require extended wait arguments, which ring setup probes explicitly. The eventfd poll retains only a descriptor and no userspace destination buffer, including across timeouts and teardown. A notification CQE drains the counter before rearming, so the same readable counter cannot repeatedly complete new polls; notification errors are propagated only after publishing the whole CQ snapshot. Idle preparation consumes the local wake permit without speculatively reading the eventfd. Its armed poll is either submitted already or published by the wait entry, preserving remote notifications before and during that entry. Cancellation and payload SQ entries precede notification rearming so a repeatedly signalled eventfd cannot monopolize a one-entry queue. If no slot remains to arm the interrupt, the driver submits the queued work and returns before parking. Futures may move between polling threads while one driver exclusively owns the ring. Dropping an operation requests cancellation without reclaiming in-flight storage. Orderly closure keeps driving until original requests retire. Dropping or failing a driver with unknown kernel access is exceptional: queued requests return errors, in-flight storage is deliberately leaked, and polling its waiting future panics because its buffer cannot safely be returned. Native executor composition and caller-owned event-loop tests exercise the same selector implementation.
A nonblocking driver turn may stage one newly prepared batch when the SQ was initially empty and no processing could have woken a task. The next idle turn submits and waits together; the next nonblocking turn submits existing SQ entries even if the worker stays busy. CQ overflow and deferred task work always force kernel progress. A full command batch returns before waiting because remaining local commands have no interrupt. Owned request preparation conservatively returns to the executor when user buffer accessors can wake arbitrary tasks. Bound scalar stream reads capture their stable buffer pointer and length while polling the operation, before publishing the owned request, so later SQE preparation is callback-free. Their idle driver turn can prepare, submit, and wait together. Other request types retain their existing callback classification. After a blocking wait, the driver publishes the available CQ snapshot before returning, without polling application futures inline.
### Worker ownership and contention
Bound stream and packet readers receive on demand; constructing or retaining a pool does not start reception. The concrete `IoUringSelector::socket_recvmsg_multishot` experiment and its options provide explicit kernel controls for backend benchmarks. Its bound receiver returns immutable `SocketRecvMessage` values; cancelling a pending wait retires that receive sequence before returning. Internal strategies preserve the cancellation and retirement guarantees of their exposing resource handle.
The original ring owns pool registration, publication, and retirement. Kernel rearming preserves one request across CQEs with `IORING_CQE_F_MORE`; only a final original CQE permits deregistration or resubmission. Buffer exhaustion leaves a dormant request, rearmed when a lease returns, without busy polling or growing the pool. Local lease returns publish directly; remote returns enqueue commands. Cancellation, close, abandonment, and selector shutdown preserve the same original-ring lifetime. Each receiver holds one admission slot through retirement, including while dormant, so applications reserve headroom for other ring operations. Payload delivery uses a reserved queue bounded by the pool size. No application future is polled from CQ processing.
`IoUringSelector::with_workers` creates one ring per native worker; `NativeScheduler::with_drivers` assigns and drives them on its task threads. `SelectorDriver::enter` consumes a movable driver into a `SelectorWorker`; the io\_uring worker is thread-bound and privately owns its local routing guard. Each worker owns its SQ/CQ access, per-ring atomic admission state and request identifiers, pending queue, and in-flight operation storage. All rings share one fallback selector, so worker count does not multiply blocking or cleanup pools.
A private thread-local lookup selects the current ring only when it belongs to the explicitly supplied selector. Local submissions enqueue directly without using the synchronized inbox or writing the notification eventfd. Root futures and callers on other executors choose a ring by external-only round-robin; their submissions and cross-thread cancellation use that ring's inbox and interrupt eventfd. Owned operations that need a ring choose it on their first poll before waiting for admission; borrowed operations choose it only when a syscall needs a readiness wait. Submitted operations remain attached to it through original completion, including when their waiting future migrates. A later operation can use the task's new worker. Task futures retain no thread-local references. Protocols retain bound capabilities; the backend resolves their selected scheduler to its current worker internally.
The local and remote command queues are serviced with alternating preference and a bounded total batch. Cancellation may arrive through the other queue before an already-published submission is processed. An unknown cancellation is replayed once at the tail of the opposite FIFO, which places it after that submission; the replay never repeats. This handles migration without allocating a separate shared cancellation record or accumulating tombstones for already-completed operations.
CQ processing completes operations and publishes runnable tasks; it never polls application futures inline. The owning worker publishes the entire available CQ snapshot before polling any of those tasks. Other workers may steal published tasks immediately. The experimental worker loop alternates two reusable FIFO ready queues: it drains a finite current batch while new wakes enter the next queue, then runs a nonblocking driver turn before starting the next task batch. External wakes are admitted at batch boundaries, and both queue generations remain stealable. These are independent worker batches, not global scheduling rounds. Self-wakes cannot extend the owning worker's current batch. There is no fixed maximum batch size or preemption of an individual future poll, so a large batch can delay I/O checks. Driver command processing remains bounded independently. When idle, the worker publishes its sleeping state, rechecks runnable work, and enters an interruptible driver turn which checks both I/O queues before waiting. New runnable tasks, remote I/O, timers, and shutdown can interrupt the wait. Local submissions need no interrupt because their owning worker processes them before sleeping. A Native wake of its own executing worker clears the sleeping flag without calling thread unpark or the driver waker. Drivers must return after publishing completions or otherwise waking tasks, including cancellation and owner-drop paths, so those tasks can run; they must not wait for an interrupt from their own thread. Remote wakes retain the sleeping-flag exchange and interrupt path, including wakes racing the final queue recheck and wait. Explicit same-thread calls to an entered io\_uring driver's waker still record a coalescing local notification flag instead of writing the eventfd. Productive and nonblocking turns retain this flag until the selector reaches its actual wait decision, where it consumes the local flag and returns before waiting. Remote notifications remain in the eventfd until its poll CQE is collected; one successful read clears the accumulated counter before rearming. Wakes from another thread or for a controller not entered on this thread still use the eventfd; matching uses controller identity rather than selector-group identity.
Closing admission does not permit a worker to exit while its ring retains kernel-accessible requests, even if all task futures have been abandoned. Stopping or failing any ring closes admission across the selector group and its shared fallback; the other drivers continue retiring accepted work. Queued local owners are returned before signaling driver retirement. After a driver retires, live tasks can still progress using the executor's ordinary parking and wakeup path. Partial worker startup closes the selector and drops unstarted drivers, while started workers retire normally.
Owned scalar stream reads and file operations reuse request and completion storage; other ring operations may still allocate request records and completion channels. A stream reader initializes its receive cache lazily, so borrowed reads do not allocate it. After result consumption, a cached scalar-read request retains its bound socket but no permit or buffer. The reader retains its socket, so drop it before exclusive socket close. External notification writes are not yet coalesced beyond the eventfd's accumulated counter. Native's task ownership and runnable counters also remain shared. Per-worker rings remove shared I/O admission and submission contention; this is not a claim that the whole executor is contention-free or that every operation is allocation-free.
## Directional socket monitor machinery
Bound socket handles retain one private directional registration. Separate readable and writable records permit concurrent tasks without sharing one waiter slot. Direct, readiness, and Tokio retain their socket and delegate to their nonblocking paths. Native forwards the selected selector's factories directly. Stream and packet resource traits describe different application semantics without requiring separate public monitor types.
io\_uring retains stable shared monitor storage privately. Application ownership and kernel retention have separate lifecycle roles: the bound resource requests retirement when dropped, while the ring retains the record until the original and cancellation completions retire. An opaque `user_data` pointer must never outlive that retained state. Arbitrary borrowed stack storage does not satisfy asynchronous retirement.
The io\_uring backend lazily submits a multishot `PollAdd` when an operation needs readiness. Its stable monitor address is the SQE's `user_data`. Readiness CQEs with `MORE` keep that registration alive across successive operations; only a terminal CQE retires it. An unsupported multishot poll falls back to one-shot polling for that record. A terminal successful notification remains usable, and the next readiness wait can rearm it. Ordinary numeric request IDs are even; the low bit identifies monitor pointers and the existing high bit identifies cancellation CQEs. Supported Linux user-space addresses leave the high tag bit clear.
Readiness is cached per direction. A drained read clears only the generation observed before its syscall, preserving notifications racing it. Ordinary scalar stream reads with OOB support disabled wait on cached readiness after a short positive read instead of issuing an extra empty receive. Full reads keep probing; sticky closure flags inhibit short-read clearing to preserve EOF. OOB-enabled scalar reads, pooled reads, packet reads, and flagged/message receives retain syscall-first retries and clear only after `WouldBlock`. Urgent marks can leave ordinary data queued after a short receive, including when an alias consumes the urgent byte; use `with_oob_support::<true>()` when progress across those marks is required. Multishot ring poll completions can forward broad TCP wake keys containing `POLLPRI` for ordinary input. Only isolated priority notifications are unambiguous and become sticky `Unsupported` errors for disabled scalar readers; disabled support does not guarantee urgent-data detection. EOF also preserves readiness. Payload operations use direct nonblocking syscalls with no borrowed pointer retained by the kernel. `socket_monitor_wait` consumes neither bytes nor cached readiness and may report spurious readiness; callers retry their nonblocking operation.
Private monitor storage has one application owner, held by its bound resource, and independent backend retention. It retains a concrete shared record and an ownership-role flag, without generic projection, erased owners, or application-handle reference counting. `retained()` gives the backend an independently retained owner without extending the application's registration lifetime. The exclusive resource borrow serializes application waits. Dropping or cancelling an individual wait releases any cached waker, while leaving the poll armed. Notifications advance a readiness generation and consume that waker under the same lock; normal notification-driven completion therefore skips the cleanup lock. Dropping the resource requests registration retirement. The worker retains storage until the final original CQE and any issued cancellation CQE both arrive. Generation numbers drawn from the controller's unique request counter reject delayed cancellation commands, including after address reuse. Cancellation compares opaque tokens against live records instead of dereferencing a potentially stale command pointer.
io\_uring allocates shared retirement status once per monitor, independently of its resource-retaining storage. Submission fields drop that storage before publishing retirement or waking its waiter, including on rejection and driver teardown. This ordering allows an independently running caller to immediately drop its monitor and exclusively close its socket once close completes. Ordinary waits reuse the same status without allocating.
`socket_monitor_close` explicitly closes and awaits retirement of a persistent monitor before callers drop all its handles and exclusively close the socket. No other waiter may be active on that record. Close has no cancellation token. Dropping its future stops waiting while retirement continues; close can be awaited again. After first polling close, callers stop I/O through that monitor and its aliases. Fallbacks without persistent registrations need not enforce a post-close error. Backend teardown can release readiness-only records because the kernel never dereferences their opaque user-data pointers and no future CQE will be dispatched after driver destruction. Forgetting a future may leak the future and its captured ownership, but cannot invalidate a published pointer or prevent a later exclusive borrow from reusing the record.
Persistent registrations do not consume permits reserved for transient I/O operations: otherwise enough idle sockets could prevent another connection from making progress. Worker tables reserve initial capacity and can grow when a new registration needs space. Ordinary reuse allocates nothing after capacity exists. A directional record has at most one waiter, and independent reader/writer records support simultaneous tasks.
A suspended wait retains its original ring even if its polling task changes threads. A borrowed monitor operation likewise keeps the route selected by its first readiness wait through every retry. Generic monitor operations probe the nonblocking syscall on every poll; the ordinary scalar stream policy may wait first after a drained read. A later operation without cached readiness can select a changed compatible worker, retiring the former registration before rebinding the record to its own ring. This fixup is confined to migration; normal repeated waits keep their registration. Neither permanent task pinning nor a new stealing policy is introduced. Timers retain their existing worker-owned registration contract.
## Sequential file monitor machinery
`FileReading` and `FileWriting` select their handle families through `Reader<'scheduler>` and `Writer<'scheduler>`. The handles retain their constructing scheduler and resource independently of the constructor's file reference. Independent handles may share one file for concurrent operations. Ordinary reads and writes use the OS cursor shared with every alias; positioned operations preserve the documented platform cursor rules. Handles maintain no private logical position.
An exclusive handle borrow prevents overlapping operation futures, while accepted worker or kernel access can outlive an abandoned waiter. Private operation state tracks that access; reuse can return `WouldBlock` while access or an uncollected completion reserves the lane's reusable state. `close()` stops admission and awaits backend retirement without cancellation. It must not wait for the caller to collect a completed owner. A completion already returned to the caller remains collectible after handle close. Drop the closed handle before exclusively closing the underlying file.
Direct, readiness, and Tokio use their existing bounded worker pools. Their access guard retires when worker access ends, before the result is published; keeping a completed buffer in an unpolled completion does not keep I/O active. io\_uring binds private state to its constructing selector group, caches typed request storage and a completion slot, and retains the active operation's ring through retirement. Every operation has a fresh numeric identifier to isolate delayed cancellation. The completion slot uses separate result and retirement wakers, so an independently polled result and handle close cannot overwrite each other's notification. A mutex transfers the completed owner.
Each io\_uring lane lazily caches a fixed-file registration for its current ring, falling back to ordinary descriptors if disabled, unsupported, or out of slots. Moving a later operation to another ring releases and replaces that registration. Returning to an earlier worker registers again. `IoUringOptions::fixed_files` bounds each ring's sparse table independently of transient operation capacity. This registration cache and its request representation remain private.
The file benchmark compares reusing a bound handle with constructing fresh handles for page-cache-hot transfers. Backends, buffers, offsets, task counts, and optimized builds match; measurements isolate dispatch and bookkeeping rather than storage durability.
## io\_uring ring ownership and migration
Ring-specific state is represented by an internal `RingBinding<T>` pairing its value with the owning controller. Controller identity remains stable while this owner is retained; numeric slots and request generations are meaningful only in that ring. The bound handle's constructing scheduler validates the resource's driver group, then selects the current compatible worker through TLS, or an external round-robin route. TLS chooses a route within that scheduler; it never selects a new scheduler or changes an accepted operation's owner.
Distinguish active work from passive cached registrations. An accepted operation retains its original binding through terminal completion, cancellation, and abandonment, even if another thread polls its future. Cancellation and release commands go to the recorded controller. Only after retirement can a later operation choose a different ring. Persistent socket polls also have an active binding: when an uncached readiness wait needs another worker, retire the former poll before rebinding, including its issued cancellation completion. No automatic task pinning is introduced.
Passive file state caches only the current ring. `RingCache<T>` stores one binding inline. Before a new operation changes rings, the monitor requests release through the previous controller and creates a fresh lease on the selected ring. Returning to a visited worker registers again. Same-worker operations reuse their registration and typed request storage; migration allocates registration and retirement state. This avoids retaining monitors × visited-workers cache entries. Pending and failed releases remain tracked separately, so old registrations can temporarily coexist with the current one; a stalled original driver can delay their release. Cache entries are private backend resources, not application-selected schedulers.
File monitors use this cache for fixed-file leases. The owning worker lazily creates its sparse table and installs each new lease once. Warm requests share the cached lease instead of retaining another native-file owner on every operation, and submit a fixed-file index. Unsupported registration, failed insertion, and exhausted capacity cache an ordinary-descriptor fallback for that lease. The worker recycles a fixed-file slot only after the kernel successfully removes its registration. Failed removal is observable and does not recycle the slot; the ring still releases its remaining registered files on destruction.
Closing a file monitor first retires its accepted operation, then requests release of its current registration and awaits both that release and outstanding migration releases. Successfully released migration leases are reclaimed during subsequent operations; failed releases remain observable on repeated close. Release retains the native file and publishes completion outside the table lock. Dropping a pending close stops waiting without undoing those releases; another close observes their persistent results. Dropping a monitor queues release without synchronously waiting. Close and drop the monitor before exclusively closing its file, and close monitors while their drivers are still being driven. A driver disappearing before acknowledging release is reported as `BrokenPipe`. These lifecycle rules apply to future passive caches too; a cache must never treat cancellation as proof that kernel access ended.
A buffer cache additionally needs ownership of its backing allocation for the complete registration lifetime. The generic `IoBuf` contract only keeps bytes stable while an operation owns them. Returning an arbitrary `Vec` lets callers resize, replace, or free it; an address match is not sufficient to identify a reusable kernel registration. A future registered-buffer pool must retain its storage independently of application leases, bind each registration to its ring, allow completed leases to be parsed on another worker, and route recycling or release to the recorded owner. Existing provided-buffer multishot receivers follow their own pool-retention contract; they are not fixed-buffer file registrations.
## Shared receive storage and backend strategies
Bound stream and packet readers provide both borrowed reads and owned received data. Their synchronous factories retain directional registration state but allocate no payload pool. `read_into` and `read_vectored` therefore need no receive-pool allocation. The first owned `read` lazily constructs default storage. The `socket_stream_reader_with_pool` and `socket_packet_reader_with_pool` factories select shared storage at construction. Factory methods return the public reader/writer contracts and may be overridden by a backend to choose another statically dispatched implementation.
`BufferPool` is the portable pool contract: a shareable, bounded source of initialized, uniquely writable `ReceiveBuffer` owners. `BufferPooling::buffer_pool(options)` constructs an implementation-associated pool from an exact shared buffer count, usable size, and minimum power-of-two alignment. Worker count never multiplies the portable budget. Internal worker caches must make returned free capacity available to acquisitions on other workers. Elastic growth requires a separately defined construction and capacity policy; the existing contract waits at exhaustion.
`HeapBufferPool` reserves one initialized stable payload slab divided into aligned slots. Direct, readiness-native, and Tokio use it. io\_uring supplies its own associated pool type and uses demand-driven monitor reception. Bound readers do not receive ahead. Applications explicitly overlap independent input and output handles; the interface does not promise multishot dispatch or a performance benefit from kernel registration.
One owned stream read returns an independently retained initialized chunk, or `None` at EOF. One owned packet read returns an implementation-associated message with payload, source metadata, and truncation flags; empty datagrams remain messages. The backend may choose its allocation and metadata representation. `SocketRecvMessage::address()` borrows an optional `SocketAddressView`; `to_socket_address()` explicitly materializes a portable endpoint. `payload()` exposes completed bytes only, `payload_mut()` permits exclusive parsing or decryption, and `into_payload()` transfers an owned view of precisely that range. Metadata is excluded from payload access. Borrowed address views cannot survive extraction or conflicting mutable access.
`ReceiveBuffer::truncate` narrows a completed prefix without replacing its allocation. `freeze` transfers writable access to cloneable immutable storage. `FrozenBuffer::view` retains a range without payload copies. Slots return only after the final mutable lease or immutable view is destroyed; accepted I/O retains its lease independently until retirement. Parsers own framing and compaction: retain complete frames inside a chunk, and copy or assemble the unfinished tail only when contiguous records are required. Small retained views can pin entire slots, so selective copying can reduce memory pressure.
Heap acquisition uses an atomic availability bitmap and fixed slot indices. Checkout clears one available bit with acquire ordering; final ownership return publishes it with release ordering. Returned slots notify distinct registered capacity waiters, while ordinary returns skip notification when nobody waits. A persistent slab ownership claim travels between each slot's availability bit, mutable lease, and group of frozen views. Checkout, freeze, and uncontended return need no new per-buffer wrapper or slab reference-count update. Frozen clones update their preallocated slot reader count. Pool handles independently retain the slab; final handle destruction closes availability and retires idle claims. Outstanding chunks stay valid and retire rather than republish into a closed pool. The portable contract does not require a slab, bitmap, or this ownership representation.
Demand-driven owned socket reads acquire a buffer and attempt one borrowed nonblocking transfer. Immediate completion transfers the chunk or message to the application. A pending attempt consumes no bytes: the reader returns storage before awaiting readiness, then reacquires and retries. Idle connections therefore do not reserve the shared budget. Borrowed message operations guarantee a first-poll attempt, including immediate delivery of empty datagrams. Scalar reads may use readiness-first policies, but they must not leave borrowed bytes independently accessible after a poll.
Receive loops and registration machinery are private adapter implementation details. Explicit concrete multishot resources are optional backend experiments; their state is bound at construction. Ordinary protocols use the six reader/writer capabilities. Kernel registration, fixed-buffer indices, buffer publication, and zero-copy send choices remain behind backend boundaries. Required ownership, payload capacity, alignment, cancellation, and retained-view lifetime cannot silently change during an optimization fallback.
Reader cancellation ends the current wait without rolling back consumed data. `close()` has no cancellation argument, stops input, and awaits registration and any accepted backend access retiring. Cancel and await an active read before close; handle any delivery that wins the race. Once close begins, perform only close retries, then drop the handle before exclusively closing its resource. Returned chunks and messages survive that sequence. Leaked leases can retain capacity, but cannot permit overlapping mutable access.
### Future pool growth and worker caches
The ownership model does not require one fixed slab. A pool can retain multiple independent slabs or individually allocated buffers, distribute available storage to worker-local caches, and route remote returns to their recorded owners. Adding storage must never move an existing allocation while a lease, frozen view, registration, or accepted I/O can access it. Retained views can keep an older slab alive after newer slabs are added or idle slabs are retired. Buffer size and alignment remain the pool's advertised requirements.
A two-level allocator can keep ordinary checkout and return local. A shared backing allocator supplies batches or slabs to workers; workers refill below a low watermark and return surplus above a high watermark. A pool can identify compatible workers internally and provide a shared fallback for callers outside those workers, without adding worker IDs or registration choices to the portable operations. A local minimum is a cache preference, not an exclusive reservation: under the existing shared-budget contract, exhaustion must reclaim or expose another worker's free capacity before waiting indefinitely. `capacity()` counts all slots, including local caches, application-held buffers, and published buffers; it is not a free-buffer count. Applications retaining every slot still impose backpressure.
Elastic allocation is a future construction policy, not a reinterpretation of `BufferPoolOptions::buffers`. The current factory promises that exact total count, and the current pool contract waits at exhaustion instead of growing. A growth policy would need to distinguish initial provision from a maximum memory budget and define whether allocation happens on demand or during worker refill. It must also define what `capacity()` reports as storage grows or shrinks. No growth or cache-watermark options are introduced by this prototype.
Acquisition currently reports cancellation, not allocation failure: `acquire` returns `Result<Buffer, Cancelled>`, while `try_acquire` returns `Option<Buffer>`. Best-effort growth could fall back to reuse and backpressure when expansion fails, but callers requiring allocation errors would need an explicit fallible acquisition capability or a revised result type. The lease and receiver interfaces can remain unchanged; construction, capacity, and exhaustion semantics need an explicit extension before an elastic pool satisfies the portable contract. Provided-buffer groups and fixed-buffer registrations impose their own per-ring limits and retirement rules; adding a slab does not automatically publish or register it on every ring.
### Explicit network overlap
A stream reader starts each receive when its read future is polled. A stream writer's `submit` transfers an owned response to the backend, allowing the next read to overlap that accepted write. The next submission finishes its predecessor before accepting another owner, bounding outstanding output and preserving byte order. Call `flush` before successful close. Two reusable pool slots suffice for one read and one write; application-retained views need additional capacity. Ordinary packet reads and writes can be polled together on independent handles.
Pool construction does not select receive-ahead or multiply the shared payload budget by worker count. Bound readers keep no hidden message queue, provided-buffer registration, or partially consumed receive-ahead chunk. The standalone `socket_recvmsg_multishot` experiment retains its own bounded provided-buffer pool for backend comparisons; it is not a strategy selected by ordinary readers.
### Experimental registered storage and zero-copy sends
The receive/parse/send prototype under `benchmarks/buffers` separates allocation lifetime, ring registration, and access ownership. A registration retains the shared pool independently of individual leases. Multiple rings can register the same stable storage, including by cloning a fixed-buffer table where supported. This does not grant concurrent mutable access: receives require exclusive leases, and immutable parser views and sends prevent the slot from being recycled or published for another receive. Fixed-buffer registration is distinct from exclusive publication to a provided-buffer ring.
A zero-copy send retains an immutable buffer owner through its memory-retirement notification, when the kernel promises one, even after the send result is available. Short sends can retain an owner for the completed prefix and send the remaining view independently. Application views may outlive either event. Storage becomes writable again only after all application and kernel owners have retired. Production cancellation and abandonment must preserve this invariant without requiring callers to keep a future alive. The prototype's failure path deliberately leaks unretired owners; it is not a production cancellation implementation.
This experiment uses existing pool leases and frozen slices without extending the production capabilities. It compares ordinary sends, unregistered zero-copy sends, and fixed-buffer zero-copy sends on loopback and a physical network, recording actual copy fallback as well as throughput and CPU cost. Backend registration and send selection remain implementation details to evaluate before integration; ordinary provided-buffer receives still copy payload into userspace.
### File submission and completion machinery
`FileReader` and `FileWriter` retain sequential operation state. Ordinary pipelines overlap independent handles with `join!` or `zip`; constructing unpolled futures submits no work. Two reusable buffers suffice for one input read and one output write. When storage comes from a pool, release a completed write's returned owner inside its branch so a capacity-waiting read can progress. Merely separating acceptance from result collection does not solve memory-budget deadlocks or order concurrent writes.
`FileReader` and `FileWriter` expose `read_submit`, `read_at_submit`, `write_submit`, and `write_at_submit` alongside their ordinary operations. Awaiting admission returns a `Send + 'static` owning completion or rejects admission with the unchanged original owner. The returned completion borrows neither the handle nor cancellation reference. Accepted worker or ring requests progress independently of polling the completion; portable fallbacks may finish the ordinary operation before returning admission. A lane can reserve its reusable request state until result collection or abandonment; use independent handles for concurrent operations.
Close waits for backend access to retire, while preserving any owner in a live completion. It does not require that completion to be collected or dropped. Acceptance does not establish durability. Completed operations return the owner on success, error, or cancellation; waiter abandonment cannot release storage still in use.
Write-all compositions advance owned ranges after short writes while preserving the original owner. Zero progress on nonempty output is `WriteZero`. Explicit-submission helpers admit their first transfer; further short-write retries require polling the completion. Blocking backends check cancellation before executing queued calls. The current ring completion observes token cancellation while polled, and dropping an unpolled accepted completion also requests cancellation. Forgetting a completion may leak its owner while accepted work independently retires.
The buffer benchmark compares serial copies, joined pipelines, explicit submissions, and vector controls, plus borrowed, pooled, and retained-view TCP/UDP receives. File measurements are page-cache-hot rather than storage-durability results; network measurements are transport exercises rather than HTTP or TLS implementations.
## Bound stream output and backend progress
`SocketStreamWriting::socket_stream_writer(&socket)` constructs a bound `SocketStreamWriter` with borrowed scalar/vector writes and owned submission. Construction selects its scheduler once, sends no data, and requires a stream socket. The default owner type matches the scheduler pool; `socket_stream_writer_with_buffer::<B>` selects another stable owner without changing output semantics. The handle retains reusable directional monitoring and backend-owned output state. Operations take payloads and cancellation without another scheduler argument. Use one writer for each ordered output sequence; other writers and socket aliases do not participate in that ordering. Reading remains independent.
`submit(buffer, cancellation)` waits for the previous logical write to finish, then accepts the entire new owner. `Ok(())` reports admission, not byte completion or a forced kernel entry. Rejection returns the unchanged new buffer and guarantees none of its bytes were written. Once accepted, partial transfers and readiness retries progress independently of subsequent writer calls. Errors discovered after acceptance become terminal and are reported by `flush` or the next submission. Subsequent submissions return their new buffers without starting them. Successful empty submissions do not shut down the socket. Constructing an unpolled future changes nothing.
The supplied token governs an accepted logical write until its send result. Cancelling admission while it waits for an older write does not cancel that older write. Cancelling an accepted write cannot roll back a prefix already sent and makes the output sequence terminal. `flush(cancellation)` waits for all accepted bytes to enter the socket stream or returns the terminal error; it does not wait for peer acknowledgement. Its cancellation token cancels only the flush wait. `close()` has no cancellation input: it stops admission, cancels unfinished output, and awaits backend retirement. Cleanup does not implicitly flush, does not shut down the underlying socket, and does not repeat a previously reported send error. Call flush then close on success, and close even on failure. Once close begins, only close retries are valid. Drop requests cancellation without blocking; backend owners preserve memory safety even if futures or writer state are forgotten.
The writer accepts one unfinished logical write at a time. Ownership release does not require flush or result collection: pooled leases return as soon as backend access ends. A future zero-copy implementation may retain bounded completed owners after their send results and retire them on separate kernel notifications. Owned submission currently uses ordinary scalar `IoBuf` sends. Borrowed `write` and `write_vectored` first await prior accepted output, then make one possibly partial transfer through the persistent monitor. Callers advance their borrowed ranges after short results. File writers independently return owners alongside completed results; no zero-copy send dispatch is implemented.
Owned stream progress is private to each writer implementation. Immediate submissions attempt a direct nonblocking send. Partial writes and `WouldBlock` require backend-owned progress. The io\_uring implementation advances its accepted write through ring completions without an executor task per write. For a stalled write, its submit future returns only after the driver observes the kernel consume the first SQE, or after the accepted operation has already failed. This establishes a submission boundary before the caller continues computing, while preserving the portable admission contract. The driver tracks notification positions across partial submissions and forces a nonblocking turn while a receipt is outstanding; it cannot wait for I/O completion before allowing that caller to resume. This accounting relies on exclusive-owner rings without SQ polling. Ring sends use `MSG_WAITALL` so the kernel can continue short stream sends internally; the driver still handles partial results and cancellation. The direct syscall remains nonblocking. Tokio and Direct spawn a task only for a stalled logical write; their ready path uses neither a task nor a per-write allocation. These are implementation choices to measure, not requirements imposed on every backend. Performance evidence belongs with the write benchmark, rather than this shared contract.
## Interface reference
See [Scheduler interface reference](interface.md) for bound reader/writer methods, service capabilities, ownership conventions, and cleanup. This design records the lower-level backend composition and selector lifetime rules.
## Child process waits
`ProcessWait` is an optional scheduler capability, separate from spawning Rust futures and from signal subscriptions. It waits for one child status and preserves standard-library `ExitStatus`, including Unix signal, stopped, and continued status. Unix uses the native signed `pid_t` selector with `waitpid`'s individual, any-child, and group selection; Windows uses an individual unsigned process ID. Tracing-specific state changes are outside the portable contract.
The design follows io-event's process-wait mechanisms. io\_uring probes `WAITID` as an optional operation, not a minimum kernel requirement. Its backend-owned, stable `siginfo_t` storage remains retained through the original terminal CQE even when a cancellation CQE arrives first. Linux readiness and Tokio use pidfd notification; macOS and FreeBSD use `EVFILT_PROC` through a private kqueue descriptor watched by the selected reactor. Notification is advisory: the final reap is nonblocking, and a pool fallback bridges an exit notification which arrives before the process becomes reapable. These notification mechanisms serve individual exit waits; group and stopped/continued waits use io\_uring `WAITID` or the pool fallback.
Direct uses its bounded worker pool. Platforms and selectors lacking suitable native notification also use their existing pool. The fallback performs an ordinary blocking `waitpid` or `WaitForSingleObject`, retaining all status storage and the Windows process handle until completion. Cancellation can prevent queued work from starting; it cannot portably interrupt an already-started wait. Such a wait can reap after cancellation or abandonment, and its established status wins concurrent cancellation. No path terminates the process. An active blocking wait occupies pool capacity and can delay other work and scheduler shutdown indefinitely while the child remains alive. Applications should cancel and reap children before shutting down the pool. Windows checks signaled completion before reading the exit code; `259` is a valid completed exit status.
The caller exclusively owns reaping the selected Unix children, including every child covered by a group/any selector. Standard-library `Child` does not learn about an external reap, and must not subsequently be used for another wait or PID-based control after successful exit collection. A cancelled wait must be awaited before another reaper takes over. Dropped or forgotten requests retire independently; backend shutdown drains the pool and ring requests, and may need to wait for a child selected by a started blocking syscall to change state. A retained native-notification future checks closure before readiness, so polling it after shutdown does not initiate a reap. Concurrently polling a retained future is not an ownership handoff. Broad-selector conformance tests run in isolated subprocesses to avoid reaping another parallel test's children.