diff --git a/context/design.md b/context/design.md index 0b043ad..ba1785a 100644 --- a/context/design.md +++ b/context/design.md @@ -103,13 +103,13 @@ Tokio and futures-io expose different AsyncRead and AsyncWrite traits. `tokio-ut Entering a Tokio runtime context provides access to its services; entering alone does not drive the runtime. Some mixed execution is possible when the required services are running, but must be established for the concrete APIs being used. Do not advertise universal Tokio compatibility from a Waker or stream adapter alone. -The optional `scheduler::tokio` adapter implements Socket, File, Clock and Spawn against an existing runtime. It preserves scheduler task ownership and joins owned task destruction on asynchronous shutdown. The same generic TCP program runs on Socketry and Tokio. The adapter scopes runtime context to individual polls when registering resources; its futures can also be polled by Socketry workers while Tokio drives the underlying services. Socketry contextual lookups still identify Socketry execution; portable code passes handles explicitly. +The optional `scheduler::tokio` adapter implements SocketScheduler, FileScheduler, Clock and Spawn against an existing runtime. It preserves scheduler task ownership and joins owned task destruction on asynchronous shutdown. The same generic TCP program runs on Socketry and Tokio. The adapter scopes runtime context to individual polls when registering resources; its futures can also be polled by Socketry workers while Tokio drives the underlying services. Socketry contextual lookups still identify Socketry execution; portable code passes handles explicitly. For existing libraries tied to Tokio, either keep their work on Tokio and bridge owned messages/results, or provide the particular trait adapter they consume. Avoid a broad imitation of Tokio's API. ## Address resolution -`AddressResolution` is independent of `Socket` and returns `Vec` using an optional hostname, numeric or named service, typed `AddressHints`, and explicit cancellation. Hints select family, socket type/protocol, passive binding, numeric-only hosts, and configured families during native resolution. Each candidate retains socket creation parameters, including raw or platform-specific values when transports are unspecified. Typed resolver errors retain native codes separately from errno. Native and Tokio schedulers use their own bounded blocking pools, sharing capacity with `spawn_blocking`; the simulator uses exact-name fixtures. System lookups preserve native `getaddrinfo`/`GetAddrInfoW` behavior, including hosts-file policy. Cancellation can prevent queued execution, but started lookups preserve their actual result and retain their host, service, and capacity until completion, even after waiter abandonment. Await them before shutdown for graceful completion. Connection retries, DNS-specific APIs, caching, canonical names, and further resolver flags remain above or outside this initial capability. +`AddressResolution` is independent of `SocketScheduler` and returns `Vec` using an optional hostname, numeric or named service, typed `AddressHints`, and explicit cancellation. Hints select family, socket type/protocol, passive binding, numeric-only hosts, and configured families during native resolution. Each candidate retains socket creation parameters, including raw or platform-specific values when transports are unspecified. Typed resolver errors retain native codes separately from errno. Native and Tokio schedulers use their own bounded blocking pools, sharing capacity with `spawn_blocking`; the simulator uses exact-name fixtures. System lookups preserve native `getaddrinfo`/`GetAddrInfoW` behavior, including hosts-file policy. Cancellation can prevent queued execution, but started lookups preserve their actual result and retain their host, service, and capacity until completion, even after waiter abandonment. Await them before shutdown for graceful completion. Connection retries, DNS-specific APIs, caching, canonical names, and further resolver flags remain above or outside this initial capability. ## I/O and io\_uring @@ -137,7 +137,9 @@ Tokio-uring is useful reference code for owned-buffer operations. It has its own The implementations live under `scheduler/selector/`, selected with Cargo features and target configuration. epoll, kqueue and iocp expose shared async-io readiness operations; the process-wide reactor owns persistent registrations. Windows uses IOCP/AFD socket readiness. Regular-file fallback operations use a blocking pool, because readiness does not make ordinary file I/O nonblocking. -`File::file_stat` queries an open descriptor and returns an owned associated metadata type. `FileMetadata` defines portable optional accessors for type, size, and access/modification/creation times, allowing native-backed results and simple simulated records. Readiness/Tokio wrap `std::fs::Metadata`; io_uring retains Linux `statx` directly in the request allocation, with native fields exposed on the concrete type. Avoid eager field normalization and additional output allocations. Returning by value keeps the interface simple; copy elimination is an optimization rather than a contract. The file and output survive waiter destruction until backend access ends. Metadata does not change the cursor or order concurrent modifications, and path-based lookup remains separate. +`FileStat::file_stat` queries an open descriptor and returns an owned associated metadata type. `FileMetadata` defines portable optional accessors for type, size, and access/modification/creation times, allowing native-backed results and simple simulated records. Readiness/Tokio wrap `std::fs::Metadata`; io_uring retains Linux `statx` directly in the request allocation, with native fields exposed on the concrete type. Avoid eager field normalization and additional output allocations. Returning by value keeps the interface simple; copy elimination is an optimization rather than a contract. The file and output survive waiter destruction until backend access ends. Metadata does not change the cursor or order concurrent modifications, and path-based lookup remains separate. + +File operation traits share `FileOperation::File`; `FileStat` declares `Metadata`. `FileScheduler` is an automatically implemented aggregate of the core file capabilities, while `FileAllocate` and `FileAdvice` remain optional and depend only on `FileOperation`. Use narrow bounds for generic consumers and import individual operation traits for concrete method calls. The corresponding `SocketOperation` split is agreed follow-up work; `SocketScheduler` still declares its operations directly. The Linux `io-uring` feature selects a dedicated ring for socket/file reads and writes, scatter/gather message sends/receives, and `file_sync`, plus descriptor-based `file_stat` using required `Statx` support. `file_truncate` uses the ring when its opcode is available, with a blocking-file fallback on older kernels. Optional `FileAllocate` adds Linux range reservation with `AllocationMode::Extend` or `KeepSize`, using a probed `Fallocate` request or the blocking syscall fallback. Other platforms currently omit that capability. Synchronization, allocation, and native truncation retain the file without a payload buffer and use the same original-completion retirement contract as transfers. Connect, accept, explicit readiness waits and sleep still use async-io. Initialization probes required opcodes and reliable completion overflow; it returns an error if unavailable rather than silently changing implementation. Shutdown cancels and drains original completions. Operation identifiers are never reused, and cancellation completions cannot release original buffers. Unexpected selector failure retains resources whose kernel lifetime cannot be established and panics their waiting operations. Normal cancellation does not retain completed resources. @@ -182,7 +184,7 @@ The algorithm's existing Ruby performance motivates the port. Rust performance c 1. Record boundaries and reusable Rust conventions (implemented). 2. Replace stackful execution with async-task and Crossbeam worker queues (implemented). Keep the coroutine prototype in its saved branch. 3. Implement primitive task dispatch, scheduler ownership, cancellation and shutdown (implemented). Runtime-independent cooperative signals and deferred cancellation are implemented; cancellation-aware file/socket operations and transferable task ownership are implemented. Clock cancellation is implemented; exclusive file/socket close, directional socket shutdown, and bounded submission-only cleanup are implemented; optional Linux file advice shares the bounded cleanup admission and draining contract. Higher-level task groups are outside the executor for now. -4. Establish minimal clock and I/O contracts with concrete consumers (implemented with Socket, File, Clock and a portable TCP example). +4. Establish minimal clock and I/O contracts with concrete consumers (implemented with SocketScheduler, FileScheduler, Clock and a portable TCP example). 5. Implement native readiness and the Tokio adapter, running the same consumers with both (implemented). Native sleep uses async-io until scheduler integration with `socketry-timers`. 6. Implement io\_uring's owned-buffer lifecycle, socket/file operations, cancellation and runtime probing (implemented). Improve operation reuse, buffer registration and submission backpressure in subsequent work. 7. Port the timer queue with upstream attribution and deterministic verification (implemented in `socketry-timers`). Integrate a driven sleep future with the scheduler in subsequent work; see the [timer package](../crates/timers/readme.md) and [benchmarks](../crates/timers/context/benchmarks.md). diff --git a/context/gaps.md b/context/gaps.md index 375c25e..9dc2a75 100644 --- a/context/gaps.md +++ b/context/gaps.md @@ -18,7 +18,7 @@ The executor does not expose a Barrier or require a group/owner argument. Librar ## 3. The I/O boundary is still narrow -`Socket` uses one associated resource type for unconnected, bound, listening, connected, and datagram sockets. It exposes `socket_open`, `socket_bind`, syscall-like `socket_listen`, `socket_connect`, `socket_accept`, address inspection, reads, writes, and readiness. The initial API supports IPv4/IPv6 and stream/datagram sockets with optional TCP/UDP protocol selection. OS-resource import is a separate `SocketRegistration` capability. Socket close/shutdown and their admission-only variants are implemented; typed option get/set supports address reuse, keepalive, and TCP no-delay; `SocketMessages` implements owned scatter/gather payloads, addresses, peeking, routing flags, and receive truncation; ancillary data and further options remain future work. `File` opens associated resources and borrows them for seeking, truncation, synchronization, shared-cursor and positioned I/O. Explicit close/submission now consumes an exclusively owned handle, and optional `FileAllocate` supplies Linux allocation and `FileAdvice` supplies hints with completion/submission forms. `file_stat` returns owned associated metadata with native storage and portable accessors; path-based metadata remains future work. File/socket operations support cancellation and owned buffer views. `Clock` supplies monotonic time and cancellable absolute and relative waits. +`SocketScheduler` uses one associated resource type for unconnected, bound, listening, connected, and datagram sockets. It exposes `socket_open`, `socket_bind`, syscall-like `socket_listen`, `socket_connect`, `socket_accept`, address inspection, reads, writes, and readiness. The initial API supports IPv4/IPv6 and stream/datagram sockets with optional TCP/UDP protocol selection. OS-resource import is a separate `SocketRegistration` capability. Socket close/shutdown and their admission-only variants are implemented; typed option get/set supports address reuse, keepalive, and TCP no-delay; `SocketMessages` implements owned scatter/gather payloads, addresses, peeking, routing flags, and receive truncation; ancillary data and further options remain future work. `FileScheduler` opens associated resources and borrows them for seeking, truncation, synchronization, shared-cursor and positioned I/O. Explicit close/submission now consumes an exclusively owned handle, and optional `FileAllocate` supplies Linux allocation and `FileAdvice` supplies hints with completion/submission forms. `file_stat` returns owned associated metadata with native storage and portable accessors; path-based metadata remains future work. File/socket operations support cancellation and owned buffer views. `Clock` supplies monotonic time and cancellable absolute and relative waits. `Blocking` now adds bounded asynchronous admission for owned synchronous closures, implemented through dedicated native and Tokio pools. `AddressResolution` uses the same bounded pools for system hostname lookup and returns owned address/type/protocol candidates with numeric or named services and native resolver hints; the simulator uses an explicit table. Started system lookups finish independently of cancellation. Existing file fallbacks still use backend pools. Ancillary data, further socket options, randomness, and processes remain outside the portable boundary. Deadline policies can now compose `Clock` and cancellation; higher-level timeout helpers remain separate. Ordinary synchronization uses futures and wakers above scheduler primitives. Expand capabilities as real consumers need them, keeping the application's implementation choice explicit. A broader boundary would also allow more operating-system behavior to be substituted in deterministic tests. diff --git a/context/implementation.md b/context/implementation.md index 7100ae4..3096de8 100644 --- a/context/implementation.md +++ b/context/implementation.md @@ -57,12 +57,13 @@ This guide describes the current implementation and its boundaries. Read [the de ## I/O and runtime boundaries -- `scheduler.rs` re-exports Socket, File, Interest and Clock from `scheduler/socket.rs`, `file.rs`, `interest.rs` and `clock.rs`. Operations return concrete Send futures; portable consumers receive the required capabilities. -- `File` declares an associated resource and opens it with `FileOptions`; native/Tokio choose `Arc` internally. `Socket` creates IP socket descriptors with explicit bind/listen/connect and address inspection; standard-library resource import belongs to `SocketRegistration`. Blocking truncation, synchronization, shared-cursor, and positioned file helpers live in `scheduler/file_io.rs`. `file_seek` accepts `std::io::SeekFrom` and returns the resulting offset through a cancellable blocking fallback on all current native backends and the Tokio runtime pool; await it and other cursor operations in order. `file_read`/`file_write` advance the OS cursor with no extra per-file state or locking; io\_uring represents current-position requests with `None` internally and encodes them as offset `-1`. Await cursor operations sequentially when ordering matters, and retain buffers through terminal completion as for positioned I/O. -- `FileAllocate: File` optionally reserves space with `AllocationMode::Extend`/`KeepSize`. Native readiness and Tokio implement it on Linux using `fallocate64` on their file pools; io\_uring probes optional `Fallocate` and falls back when absent. Other targets currently omit the implementation. Positive length and a range end within `i64` are required; data and cursor are preserved, and filesystem support errors remain runtime results. No resize/zero-write emulation, unsharing, or durability promise. The simulator reserves vector capacity within its 1 MiB bound. -- `File::file_truncate` changes the logical length with zero-filled extension, requiring write access and preserving the cursor. Lengths must fit in `i64`. Readiness and Tokio use `set_len` on their file pools; io\_uring probes optional `Ftruncate` support and falls back to the blocking implementation when absent. Buffer-free ring requests share resource retention and cancellation with sync. Await related operations sequentially; truncation provides neither space reservation nor durability. The simulator uses its 1 MiB bound. -- `File::file_stat` returns owned associated `File::Metadata: FileMetadata` by value without moving the cursor. Common accessors return optional type, size, and access/modification/creation times. Readiness/Tokio use `SystemFileMetadata` to retain `std::fs::Metadata` and expose `as_std()` for platform extensions. io_uring requires `Statx` support and returns `StatxMetadata` with `as_statx()`; `scheduler/selector/io_uring/stat.rs` keeps native output inline in the existing boxed request until original completion. Its mask preserves missing fields, and timestamps convert only on access. No separate output allocation, eager normalization, or caller-buffer API; copies through completion storage may remain. Shared contracts and `examples/file_stat.rs` cover ownership, cursor preservation, cancellation, and immediate close. The simulator provides its own length-only metadata record with absent timestamps. Path-based lookup remains separate. -- `File::file_sync` takes `SyncMode::All` or `SyncMode::Data` and waits for OS completion. io\_uring submits buffer-free `Fsync` requests through the same cancellation/completion machinery as transfers and requires opcode support at initialization. Readiness and Tokio use their existing file pools with `sync_all`/`sync_data`; the simulator completes immediately without modeling durability. Await preceding writes first; sync leaves the cursor unchanged and does not flush application buffers or synchronize directory entries. File ownership is retained through terminal completion even after waiter abandonment. +- `scheduler.rs` re-exports SocketScheduler, FileScheduler, Interest and Clock from `scheduler/socket_scheduler.rs`, `file_scheduler.rs`, `interest.rs` and `clock.rs`. Operations return concrete Send futures; portable consumers receive the required capabilities. +- File operation traits share `FileOperation::File`; `FileStat` declares `Metadata`. `FileScheduler` is an automatically implemented aggregate of the core file capabilities, while `FileAllocate` and `FileAdvice` remain optional and depend only on `FileOperation`. Use narrow bounds for generic consumers and import individual operation traits for concrete method calls. The corresponding `SocketOperation` split is agreed follow-up work; `SocketScheduler` still declares its operations directly. +- `FileOperation` declares an associated resource and `FileOpen` opens it with `FileOptions`; native/Tokio choose `Arc` internally. `SocketScheduler` creates IP socket descriptors with explicit bind/listen/connect and address inspection; standard-library resource import belongs to `SocketRegistration`. Blocking truncation, synchronization, shared-cursor, and positioned file helpers live in `scheduler/file_io.rs`. `file_seek` accepts `std::io::SeekFrom` and returns the resulting offset through a cancellable blocking fallback on all current native backends and the Tokio runtime pool; await it and other cursor operations in order. `file_read`/`file_write` advance the OS cursor with no extra per-file state or locking; io\_uring represents current-position requests with `None` internally and encodes them as offset `-1`. Await cursor operations sequentially when ordering matters, and retain buffers through terminal completion as for positioned I/O. +- `FileAllocate: FileOperation` optionally reserves space with `AllocationMode::Extend`/`KeepSize`. Native readiness and Tokio implement it on Linux using `fallocate64` on their file pools; io\_uring probes optional `Fallocate` and falls back when absent. Other targets currently omit the implementation. Positive length and a range end within `i64` are required; data and cursor are preserved, and filesystem support errors remain runtime results. No resize/zero-write emulation, unsharing, or durability promise. The simulator reserves vector capacity within its 1 MiB bound. +- `FileTruncate::file_truncate` changes the logical length with zero-filled extension, requiring write access and preserving the cursor. Lengths must fit in `i64`. Readiness and Tokio use `set_len` on their file pools; io\_uring probes optional `Ftruncate` support and falls back to the blocking implementation when absent. Buffer-free ring requests share resource retention and cancellation with sync. Await related operations sequentially; truncation provides neither space reservation nor durability. The simulator uses its 1 MiB bound. +- `FileStat::file_stat` returns owned associated `FileStat::Metadata: FileMetadata` by value without moving the cursor. Common accessors return optional type, size, and access/modification/creation times. Readiness/Tokio use `SystemFileMetadata` to retain `std::fs::Metadata` and expose `as_std()` for platform extensions. io_uring requires `Statx` support and returns `StatxMetadata` with `as_statx()`; `scheduler/selector/io_uring/stat.rs` keeps native output inline in the existing boxed request until original completion. Its mask preserves missing fields, and timestamps convert only on access. No separate output allocation, eager normalization, or caller-buffer API; copies through completion storage may remain. Shared contracts and `examples/file_stat.rs` cover ownership, cursor preservation, cancellation, and immediate close. The simulator provides its own length-only metadata record with absent timestamps. Path-based lookup remains separate. +- `FileSync::file_sync` takes `SyncMode::All` or `SyncMode::Data` and waits for OS completion. io\_uring submits buffer-free `Fsync` requests through the same cancellation/completion machinery as transfers and requires opcode support at initialization. Readiness and Tokio use their existing file pools with `sync_all`/`sync_data`; the simulator completes immediately without modeling durability. Await preceding writes first; sync leaves the cursor unchanged and does not flush application buffers or synchronize directory entries. File ownership is retained through terminal completion even after waiter abandonment. - `scheduler/socketry.rs` owns the executor; `socketry/operations.rs` forwards capabilities to its lazily initialized, compile-time selected selector. - `scheduler/selector/` contains readiness, epoll, kqueue, iocp and io\_uring. Platform readiness modules share async-io's persistent registrations and process-wide reactor. Registered sockets remain usable as tasks migrate. - Default feature `native` provides TCP, stateful and positioned files, and sleep. Feature `io-uring` selects Linux completion reads/writes; other supported platforms retain readiness. Feature `tokio` enables the separate runtime adapter. No default features builds the executor and contracts without native I/O. @@ -84,7 +85,7 @@ This guide describes the current implementation and its boundaries. Read [the de ## Interface experiment -`crates/executor/examples/interface_probe.rs` runs generic file-copy and echo-server functions against native, Tokio, and an in-memory implementation. It uses the exported `File` and `Socket` traits for associated resources, creation, cancellation and address inspection. Socket setup follows open, bind, and listen/connect steps; no listener-specific resource type is required. It uses scheduler implementations directly and retries partial writes through owned buffer views. The simulator exercises short transfers and ordinary resource destruction without native I/O dependencies. It supports cancellation but does not model timers, kernel completion tracking, or a simulated task scheduler; findings and next changes are recorded in `crates/executor/design.md`. +`crates/executor/examples/interface_probe.rs` runs generic file-copy and echo-server functions against native, Tokio, and an in-memory implementation. It uses the exported `FileScheduler` and `SocketScheduler` traits for associated resources, creation, cancellation and address inspection. Socket setup follows open, bind, and listen/connect steps; no listener-specific resource type is required. It uses scheduler implementations directly and retries partial writes through owned buffer views. The simulator exercises short transfers and ordinary resource destruction without native I/O dependencies. It supports cancellation but does not model timers, kernel completion tracking, or a simulated task scheduler; findings and next changes are recorded in `crates/executor/design.md`. ## Verification @@ -96,7 +97,7 @@ Coverage measures source regions for native and Tokio schedulers on each support ## Explicit file closure -- Native and Tokio `File::File` is an owned, non-`Clone` `FileDescriptor`. Backend retention uses a private `Arc`; consuming it for close uses `Arc::try_unwrap` and panics on outstanding use. Do not cancel or wait for other operations during close. +- Native and Tokio `FileOperation::File` is an owned, non-`Clone` `FileDescriptor`. Backend retention uses a private `Arc`; consuming it for close uses `Arc::try_unwrap` and panics on outstanding use. Do not cancel or wait for other operations during close. - `file_close` awaits bounded admission and execution; `file_close_submit` awaits only admission. Neither accepts cancellation. Accepted close jobs survive waiter destruction and run on a dedicated cleanup pool independent of user blocking closures. Native shutdown outside future and blocking-pool workers and awaited Tokio shutdown drain them. Completion and capacity wakeups run in blocking-pool worker context, so reentrant scheduler destruction requests shutdown without waiting on its own cleanup queue. - All operation result notifications must follow release of backend file references, including cancellation and requests discarded before submission. io_uring still retains published requests until original terminal CQEs; cancellation CQEs cannot release them. - `examples/file_close.rs` explains cancellation clones, keeping the operation future alive through cancellation, completion races, and explicit close before propagating an I/O result. Dropping an operation future abandons completion and may leave backend work running; io_uring requests cancellation, but immediate close can still panic until backend access ends. @@ -104,7 +105,7 @@ Coverage measures source regions for native and Tokio schedulers on each support ## Explicit socket lifecycle -- `Socket` requires directional shutdown with `std::net::Shutdown`, consuming close, and `_submit` variants. Ordinary futures await OS completion; `_submit` awaits admission and discards execution errors. These lazy cleanup operations have no cancellation input. +- `SocketScheduler` requires directional shutdown with `std::net::Shutdown`, consuming close, and `_submit` variants. Ordinary futures await OS completion; `_submit` awaits admission and discards execution errors. These lazy cleanup operations have no cancellation input. - Native and Tokio descriptors privately retain their persistent registration through `Arc`, without exposing `Clone` or a common file/socket resource trait. Readiness reads/writes borrow it; accepted shutdown jobs and io_uring operations retain it. Explicit close checks exclusivity before admission and panics on outstanding use; the cleanup job then deregisters and closes on its worker. - File and socket cleanup share `scheduler/cleanup.rs` and each scheduler's existing bounded cleanup pool. Admission, abandonment, error reporting, and graceful draining use the same contract. Native socket close/shutdown currently use this pool even with io_uring selected; native SQEs are deferred. - Await prior writes before `Shutdown::Write`, and await shutdown completion before explicit close. `_submit` supplies no completion fence. Completion notifications follow release of retained backend ownership. Request and await I/O cancellation before close; never equate dropping an operation future with completion. @@ -113,7 +114,7 @@ Coverage measures source regions for native and Tokio schedulers on each support ## Typed socket options -- `SocketOption` describes an identifier's associated `Value`; `SocketOptions: Socket` adds lazy cancellable `socket_get_option` and `socket_set_option`. Consumers require each supported option through trait bounds. The core `Socket` trait is unchanged. +- `SocketOption` describes an identifier's associated `Value`; `SocketOptions: SocketScheduler` adds lazy cancellable `socket_get_option` and `socket_set_option`. Consumers require each supported option through trait bounds. The core `SocketScheduler` trait is unchanged. - `SocketReuseAddress`, `SocketKeepalive`, and `TcpNoDelay` use boolean values and preserve their OS meanings. Keepalive timing, additional options, and read-only/write-only controls remain separate extensions. Invalid resource protocols or states return OS errors even when the capability exists. - Native mappings live privately in `scheduler/native_socket_option.rs`, using socket2's typed accessors. Readiness and Tokio execute them directly during a poll; io_uring delegates to readiness with its open check. Scheduler handles preserve shutdown and Tokio runtime validation. There is no readiness wait, pool dispatch, raw public ABI, or per-socket locking for these controls. - Cancellation before execution prevents the syscall. Once executed, its result wins cancellation; an unpolled setter has no effect. Await configuration before dependent operations. Address reuse and accepted-socket inheritance vary by OS; configure accepted resources explicitly. @@ -121,14 +122,14 @@ Coverage measures source regions for native and Tokio schedulers on each support ## Message/address I/O -- `SocketMessages: Socket` adds `socket_sendmsg` and `socket_recvmsg`. `SocketMessage` owns scatter/gather buffers and an optional destination; receive metadata returns copied bytes, sender address when available, and payload/control truncation. Buffer owners return on success, error, or cancellation, and initialized lengths remain unchanged. No `Sync` bound is added to `IoBuf`. +- `SocketMessages: SocketScheduler` adds `socket_sendmsg` and `socket_recvmsg`. `SocketMessage` owns scatter/gather buffers and an optional destination; receive metadata returns copied bytes, sender address when available, and payload/control truncation. Buffer owners return on success, error, or cancellation, and initialized lengths remain unchanged. No `Sync` bound is added to `IoBuf`. - `scheduler/message_io.rs` owns native metadata and validates representable lengths. Readiness/Tokio execute nonblocking syscalls; Windows uses vectored Winsock payload/address calls and normalizes message-too-large to truncation. Zero-capacity receives preserve OS differences; use nonempty storage for portable empty-message reception. `peek` and `dont_route` are the initial portable input flags. - `selector/io_uring/message.rs` submits native message opcodes through the existing completion/cancellation machinery. Stable payload, header, address, and vector allocations survive until the original terminal CQE. Descriptor retention ends before completion notification. Close still requires completion of outstanding operations. - Ancillary data is not supported by this capability. Unix reports control truncation; the Windows fallback cannot observe control metadata. Do not silently extend that fallback to ancillary-capable operations. The stream-only simulator has no `SocketMessages` implementation; shared message contracts run against the native backends and Tokio. ## Address resolution -- `AddressResolution`, `AddressHints`, `AddressService`, `AddressInfo`, and resolver error types are exported by the executor and facade. The trait has no `Socket` or `Blocking` supertrait; concrete implementations choose their lookup mechanism. +- `AddressResolution`, `AddressHints`, `AddressService`, `AddressInfo`, and resolver error types are exported by the executor and facade. The trait has no `SocketScheduler` or `Blocking` supertrait; concrete implementations choose their lookup mechanism. - Native and Tokio scheduler/handle implementations delegate to `scheduler/address_resolution/system.rs`, using their existing bounded blocking pool. The I/O selector is not initialized for resolution. Family/type/protocol and flag hints apply inside native resolution. Preserve order, duplicates, IPv6 scope/flow metadata, and native socket values. `SocketType::Raw` and `Other(i32)` plus `SocketProtocol::Other(i32)` let results feed socket creation without loss. - Keep system lookup off future workers. Cancellation before admission/worker execution prevents lookup; once started, await the actual result. A dropped future abandons its owning job handle, while a running worker retains inputs and its admission slot. Pool shutdown discards queued work and reports `BrokenPipe` unless cooperative cancellation is set; it does not join started lookups. - The private lookup boundary accepts an injected function for deterministic cancellation/error/lifetime tests. Production uses Unix `getaddrinfo` or Windows `GetAddrInfoW`, with RAII cleanup and independent Winsock initialization. Native EAI/Winsock errors become `AddressResolveError` inside `io::Error`; capture errno immediately for `EAI_SYSTEM`. Shared contracts run through native, Tokio, and the example simulator, whose exact-name and fixed service tables do not call the system resolver. The simulator models both families as configured, supports numeric raw candidates, and retains stream-only socket execution. Hostname-based connection tests exercise composition through public traits. diff --git a/crates/executor/design.md b/crates/executor/design.md index 764c48e..e82fb2a 100644 --- a/crates/executor/design.md +++ b/crates/executor/design.md @@ -40,7 +40,9 @@ Dedicated acceptors may reduce scheduling interference, and blocking or multisho ## Capability traits and static dispatch -`File`, `Socket`, and `Clock` name scheduler capabilities. `Spawn` provides primitive task dispatch and returns an owning task handle. `Blocking` adds optional execution of owned synchronous closures with bounded asynchronous admission. `AddressResolution` adds hostname lookup independently of `Socket`. `File::File` and `Socket::Socket` are associated resource types chosen by each implementation. Native and Tokio use concrete `FileDescriptor` and `SocketDescriptor` types. `Socket` replaces the earlier capability name `Network`. +`FileScheduler`, `SocketScheduler`, and `Clock` name scheduler capabilities. `Spawn` provides primitive task dispatch and returns an owning task handle. `Blocking` adds optional execution of owned synchronous closures with bounded asynchronous admission. `AddressResolution` adds hostname lookup independently of `SocketScheduler`. `FileOperation::File` and `SocketScheduler::Socket` are associated resource types chosen by each implementation. Native and Tokio use concrete `FileDescriptor` and `SocketDescriptor` types. + +Use the `Scheduler` suffix for the broad file and socket interfaces: `FileScheduler` and `SocketScheduler` replace the former `File` and `Socket` trait names. This identifies the scheduler providing those capabilities and distinguishes it from the resource and individual operation traits. `FileScheduler` is already a blanket aggregate; `SocketScheduler` still declares its methods directly until the operation-level socket split. Individual capabilities retain names such as `FileRead`, `Clock`, and `Spawn`. Prefer generic trait bounds, concrete resource types, and concrete future types. These allow compile-time capability checking, monomorphization, and inlining without requiring a boxed future for each operation. The compiler decides which calls to inline; the interface should make that optimization possible. @@ -48,19 +50,19 @@ Keep core traits small and dependable. Implementing a core trait requires workin Additional capabilities belong in extension traits where their semantics or platform availability differ. A consumer requiring an extension declares that requirement through its trait bounds. An incompatible scheduler then fails at compile time. Implemented examples include `S: FileAllocate` for allocation and `S: SocketOptions` for a particular socket option. -Prefer operation-level capability traits so consumers can require just the operations they use. Keep completion and `_submit` variants of the same operation together, and keep inherently platform-specific types and flags in platform extensions. The existing broad `File` and `Socket` traits have not yet been decomposed; the following refactor is agreed follow-up work. +Prefer operation-level capability traits so consumers can require just the operations they use. Keep completion and `_submit` variants of the same operation together, and keep inherently platform-specific types and flags in platform extensions. The file capability split is implemented; the equivalent socket refactor remains follow-up work. -### Follow-up: operation-level capabilities +### Operation-level capabilities -Split resource identity from the operations available on that resource. Introduce small scheduler traits, provisionally named `FileResource` and `SocketResource`, that declare only the associated file or socket type and its existing bounds. Every file operation on a scheduler shares that scheduler's one file type, and every socket operation shares its one socket type. These are scheduler capabilities, not a common descriptor hierarchy, and they do not prescribe `Arc`, dynamic dispatch, or a concrete resource representation. +`FileOperation` declares the scheduler's associated `File` resource type and its existing `Send + Sync` bounds. It is implemented on the scheduler, not the descriptor. Every file operation on that scheduler shares this one type. This is a scheduler capability, not a common descriptor hierarchy, and it does not prescribe `Arc`, dynamic dispatch, or a concrete resource representation. The corresponding socket base trait will be named `SocketOperation`; it is not implemented yet. Existing `FileType` and `SocketType` enums retain their classification meanings, and concrete native resources remain `FileDescriptor` and `SocketDescriptor`. -Move each independently useful operation into its own capability: for example, `FileOpen`, `FileRead`, `FileReadAt`, `FileWrite`, `FileWriteAt`, `FileStat`, and `FileClose`; similarly, `SocketOpen`, `SocketBind`, `SocketListen`, `SocketAccept`, `SocketRead`, and `SocketWrite`. Keep trait names aligned with current method names: the current single-buffer operations are `socket_read` and `socket_write`, while message operations are `socket_recvmsg` and `socket_sendmsg`. Split `SocketMessages` into receive and send capabilities so a reader need not implement sending. This refactor does not rename the operations or add a new `socket_recv` method. +The implemented core file capabilities are `FileOpen`, `FileClose`, `FileStat`, `FileTruncate`, `FileSync`, `FileSeek`, `FileRead`, `FileWrite`, `FileReadAt`, and `FileWriteAt`. Each depends only on `FileOperation`. Optional `FileAdvice` and `FileAllocate` use that same base, without requiring unrelated open, read, write, and close operations. `FileStat` declares the operation-specific `Metadata: FileMetadata` output, so read-only implementations do not need a metadata representation. -Operation traits depend on the minimal resource trait, not the broad aggregate. In particular, change `FileAdvice: File` and `FileAllocate: File` to depend on the shared file resource capability; otherwise they still require unrelated open, read, write, and close operations. Apply the same rule to socket extensions. Move operation-specific associated outputs, such as the current `File::Metadata`, onto the corresponding operation trait (`FileStat`) rather than requiring them of every file-capable scheduler. +A generic consumer can express its requirement as follows: -A generic consumer could then express its requirement as follows. This is a proposed interface example, not code that compiles against the current broad traits: +```rust +use socketry_executor::{BufferResult, Cancellation, FileRead}; -```rust,ignore async fn read_chunk( scheduler: &S, file: &S::File, @@ -71,31 +73,39 @@ async fn read_chunk( } ``` -This function needs neither file creation nor writing, metadata, or close. A copy loop could require `S: FileRead + FileWrite`; a receive-only protocol could require just the relevant socket receive capability. Partial and simulated backends can implement those capabilities without unrelated methods or `Unsupported` stubs. Preserve the current owned-buffer, cancellation, resource-retention, and concrete `Send` future contracts. +This function needs neither file creation nor writing, metadata, or close. A copy loop using existing files can require `S: FileRead + FileWrite`. Partial and simulated backends can implement those capabilities without unrelated methods or `Unsupported` stubs. The owned-buffer, cancellation, resource-retention, and concrete `Send` future contracts are preserved. -Keep `file_close` and `file_close_submit` in `FileClose`, `file_advise` and `file_advise_submit` in `FileAdvice`, and the corresponding socket pairs together. Each pair describes the same underlying operation with different completion boundaries. Audit the remaining grouped capabilities, including typed socket get/set options, for independent consumer requirements; retain small cohesive groups where their shared contract is useful, such as a clock's instant type and monotonic deadline operations. +`FileClose` keeps `file_close` and `file_close_submit` together, and `FileAdvice` keeps both advice forms together. Each pair describes the same underlying operation with different completion boundaries. Retain this grouping for the socket equivalents. Audit remaining grouped capabilities, including typed socket get/set options, for independent consumer requirements; retain small cohesive groups where their shared contract is useful, such as a clock's instant type and monotonic deadline operations. -Retain `File` and `Socket` as convenience aggregate traits built from the required operation traits, with blanket implementations for schedulers satisfying those bounds. Do not repeat method declarations on the aggregates or make operation traits depend on them. Optional capabilities such as allocation and advice remain outside the core aggregate so requiring `File` does not accidentally require a Linux-only operation. The aggregates use ordinary supertraits and blanket implementations, not unstable trait aliases. +`FileScheduler` is a convenience aggregate of all core file operation traits, with a blanket implementation for any scheduler satisfying those bounds. It declares no methods or associated types itself. Optional allocation and advice remain outside the aggregate. Generic `S: FileScheduler` code can use all core methods; narrower consumers select individual capabilities. This uses ordinary supertraits and a blanket implementation, not unstable trait aliases. These bounds are explicit capability requirements, not inferred effect tracking. Rust checks calls through the generic scheduler against its declared bounds; it does not automatically attach an I/O effect set to the returned `Future` or infer all transitive operations. A bound permits an operation without promising that every execution performs it, and it does not constrain calls through unrelated concrete APIs. No new computation trait or marker on every future is required. -Implement this as a separate public-interface refactor: migrate native, Tokio, and simulated implementations together, update exports and examples, and organize conformance tests by operation capability. Include a minimal partial backend and compile-time checks showing that a consumer accepts exactly its required capabilities and rejects a missing one. Moving methods and associated types changes implementor code and may change fully qualified paths, so preserving the aggregate names alone does not guarantee source compatibility. Record the migration explicitly when the refactor lands. +Native, Tokio, and simulated implementations use the file operation traits. Shared file conformance tests declare their required setup, operation, and observation capabilities. A minimal read-only implementation exercises a read-only consumer, and compile-fail documentation checks reject missing operations and optional extensions. The copy failure adapter implements only file creation and positioned reads/writes. + +This is a source-breaking implementor refactor. Replace the former `impl File` with `impl FileOperation` for `type File`, `impl FileStat` for `type Metadata` and `file_stat`, and an implementation of each supported operation trait. Do not implement the aggregate explicitly. Fully qualified associated paths become `::File` and `::Metadata`; method paths become, for example, `FileRead::file_read`. Concrete callers import the individual operation traits for method lookup; importing only `FileScheduler` does not bring those methods into scope. Generic callers replace `S: File` with `S: FileScheduler` or narrow their bounds. Consumers of `FileAdvice` or `FileAllocate` must now explicitly require any additional operations they use. Socket implementations and consumers rename `Socket` to `SocketScheduler`, including `::Socket`; their operation requirements are unchanged. + +### Follow-up: socket operation capabilities + +Extract `SocketOperation` and split the current `SocketScheduler` operations into capabilities such as `SocketOpen`, `SocketBind`, `SocketListen`, `SocketAccept`, `SocketRead`, and `SocketWrite`. Keep trait names aligned with current method names: single-buffer operations are `socket_read` and `socket_write`, while message operations are `socket_recvmsg` and `socket_sendmsg`. Split `SocketMessages` into receive and send capabilities so a reader need not implement sending. This refactor does not rename the operations or add a new `socket_recv` method. + +Make each socket operation and extension depend on `SocketOperation`, and retain `SocketScheduler` as a convenience aggregate. Migrate all backends, exports, examples, and conformance tests together. Include partial-backend and missing-capability checks, and document associated-path and concrete-method-import migration as for files. ### Common scheduler dependency and resource creation -Generic code takes a scheduler such as `&S`, with bounds describing the portion of the interface it uses. A task group may require `S: Spawn`; a timed primitive may additionally require `Clock`; an HTTP server may require `Spawn + Socket`. These bounds describe the same chosen scheduler implementation. A named aggregate trait can make a standard bundle convenient once its required capabilities are settled; it should not force a task-only primitive to require file or socket support. The current public `Scheduler` is the concrete native implementation, not an aggregate scheduler trait. +Generic code takes a scheduler such as `&S`, with bounds describing the portion of the interface it uses. A task group may require `S: Spawn`; a timed primitive may additionally require `Clock`; an HTTP server may require `Spawn + SocketScheduler`. These bounds describe the same chosen scheduler implementation. A named aggregate trait can make a standard bundle convenient once its required capabilities are settled; it should not force a task-only primitive to require file or socket support. The current public `Scheduler` is the concrete native implementation, not an aggregate scheduler trait. -Resource creation belongs inside the corresponding capability. `File` declares an associated file type, and file creation returns that type. `Socket` declares one associated socket type for unconnected, bound, listening, connected, and datagram sockets. Its `socket_open`, `socket_bind`, `socket_listen`, and `socket_connect` operations follow the corresponding system-call boundaries; `socket_accept` returns another value of that same type. `socket_listen` takes an existing socket and backlog and leaves bind behavior to the operating system, including implicit binding where supported. Generic consumers use these associated resources rather than creating a `std::fs::File` or `std::net::TcpListener` themselves. +Resource creation belongs inside the corresponding capability. `FileOperation` declares an associated file type, and `FileOpen` returns that type. `SocketScheduler` declares one associated socket type for unconnected, bound, listening, connected, and datagram sockets. Its `socket_open`, `socket_bind`, `socket_listen`, and `socket_connect` operations follow the corresponding system-call boundaries; `socket_accept` returns another value of that same type. `socket_listen` takes an existing socket and backlog and leaves bind behavior to the operating system, including implicit binding where supported. Generic consumers use these associated resources rather than creating a `std::fs::File` or `std::net::TcpListener` themselves. -The initial socket arguments select IPv4 or IPv6, stream or datagram, and an optional TCP or UDP protocol. Address parameters use `std::net::SocketAddr`. `SocketOptions` adds typed configuration without extending the required `Socket` surface. `SocketMessages` adds owned scatter/gather payloads, IP addresses, and portable flags. Non-IP families and ancillary data remain future extensions. File and socket descriptors remain distinct; no common descriptor trait is required. Existing std TCP resources can still enter through the optional `SocketRegistration` interoperability trait. +The initial socket arguments select IPv4 or IPv6, stream or datagram, and an optional TCP or UDP protocol. Address parameters use `std::net::SocketAddr`. `SocketOptions` adds typed configuration without extending the required `SocketScheduler` surface. `SocketMessages` adds owned scatter/gather payloads, IP addresses, and portable flags. Non-IP families and ancillary data remain future extensions. File and socket descriptors remain distinct; no common descriptor trait is required. Existing std TCP resources can still enter through the optional `SocketRegistration` interoperability trait. The interface does not require `Arc` around every resource. Concrete implementations choose their resource representation and how to retain it during in-flight work. They may use OS handles, registered-resource tables, shared ownership, or simulated resource identifiers. Taking a resource by reference does not remove the backend's obligation to retain everything still accessible by a submitted operation after its waiting future is dropped. -These resource contracts are implemented. `File::file_open` accepts public `FileOptions`; seeking, stateful, and positioned operations borrow `&Self::File`. The native and Tokio implementations return an owned, non-`Clone` `FileDescriptor`, with private `Arc` retention for backend access. Their socket operations use one non-`Clone` `SocketDescriptor` for every socket state. Optional `SocketRegistration` imports existing `std::net` TCP resources and is not required of simulated implementations. +These resource contracts are implemented. `FileOpen::file_open` accepts public `FileOptions`; seeking, stateful, and positioned operations borrow `&Self::File`. The native and Tokio implementations return an owned, non-`Clone` `FileDescriptor`, with private `Arc` retention for backend access. Their socket operations use one non-`Clone` `SocketDescriptor` for every socket state. Optional `SocketRegistration` imports existing `std::net` TCP resources and is not required of simulated implementations. ### Typed socket options -`SocketOption` associates an option identifier with its `Value` type; `SocketOptions: Socket` supplies `socket_get_option(socket, option, cancellation)` and `socket_set_option(socket, option, value, cancellation)`. Both return concrete `Send` futures with `OperationResult` outputs. The getter returns `O::Value`; the setter returns `()`. Generic code requires exactly the options it uses: +`SocketOption` associates an option identifier with its `Value` type; `SocketOptions: SocketScheduler` supplies `socket_get_option(socket, option, cancellation)` and `socket_set_option(socket, option, value, cancellation)`. Both return concrete `Send` futures with `OperationResult` outputs. The getter returns `O::Value`; the setter returns `()`. Generic code requires exactly the options it uses: ```rust,ignore async fn configure + SocketOptions>( @@ -119,7 +129,7 @@ Shared tests exercise round trips, cancellation, unpolled setters, configuration ### Message/address I/O -`SocketMessages: Socket` is an optional capability; existing implementations of `Socket` gain no required methods. The native readiness selectors, native scheduler/handle, io_uring selector, and Tokio scheduler/handle implement it. The stream-only example simulator does not currently implement this capability or UDP. +`SocketMessages: SocketScheduler` is an optional capability; existing implementations of `SocketScheduler` gain no required methods. The native readiness selectors, native scheduler/handle, io_uring selector, and Tokio scheduler/handle implement it. The stream-only example simulator does not currently implement this capability or UDP. ```rust,ignore let message = SocketMessage { @@ -149,7 +159,7 @@ The [message example](examples/socket_messages.rs) demonstrates a generic scatte ## Address resolution -`AddressResolution` is an optional capability implemented by native and Tokio schedulers and their handles, plus the example simulator. It is independent of `Socket`, `Blocking`, and the I/O selector at the public boundary. A consumer can require `S: AddressResolution + Socket` without imposing how lookup is implemented. +`AddressResolution` is an optional capability implemented by native and Tokio schedulers and their handles, plus the example simulator. It is independent of `SocketScheduler`, `Blocking`, and the I/O selector at the public boundary. A consumer can require `S: AddressResolution + SocketScheduler` without imposing how lookup is implemented. ```rust,ignore let addresses = scheduler.address_resolve( @@ -346,7 +356,7 @@ scheduler.sleep_until(deadline, cancellation) scheduler.sleep(duration, cancellation) ``` -This list includes proposed extensions. The implemented `File` methods are `file_open`, `file_stat`, `file_seek`, `file_sync`, `file_truncate`, shared-cursor `file_read`/`file_write`, and explicit-offset `file_read_at`/`file_write_at`. The implemented `Socket` methods are `socket_open`, `socket_bind`, `socket_listen`, `socket_connect`, `socket_accept`, `socket_read`, `socket_write`, and `socket_wait`, plus synchronous `socket_local_addr` and `socket_peer_addr`. Initial creation supports IPv4/IPv6 stream and datagram sockets with TCP/UDP protocol selection. Connected UDP preserves datagram boundaries. Optional `SocketMessages` adds address-based message operations; ancillary data remains an extension. Async I/O methods take `&Cancellation`; buffer operations return `(OperationResult, B)` on success, error, and cancellation. +This list includes proposed extensions. The implemented `FileScheduler` methods are `file_open`, `file_stat`, `file_seek`, `file_sync`, `file_truncate`, shared-cursor `file_read`/`file_write`, and explicit-offset `file_read_at`/`file_write_at`. The implemented `SocketScheduler` methods are `socket_open`, `socket_bind`, `socket_listen`, `socket_connect`, `socket_accept`, `socket_read`, `socket_write`, and `socket_wait`, plus synchronous `socket_local_addr` and `socket_peer_addr`. Initial creation supports IPv4/IPv6 stream and datagram sockets with TCP/UDP protocol selection. Connected UDP preserves datagram boundaries. Optional `SocketMessages` adds address-based message operations; ancillary data remains an extension. Async I/O methods take `&Cancellation`; buffer operations return `(OperationResult, B)` on success, error, and cancellation. An optional context pairing a scheduler reference with a cancellation signal could reduce repetitive arguments. Such a context should preserve explicit dependency and cancellation boundaries; it is a convenience, not a new computation abstraction. @@ -448,58 +458,74 @@ The sketches borrow resource handles and cancellation signals. In-flight operati Keep the receiver, resource, operation arguments, and cancellation input explicit: ```rust -pub trait File: Send + Sync { +pub trait FileOperation: Send + Sync { type File: Send + Sync; - type Metadata: FileMetadata; - - fn file_stat( - &self, - file: &Self::File, - cancellation: &Cancellation, - ) -> impl Future> + Send; +} +pub trait FileOpen: FileOperation { fn file_open( &self, path: &Path, options: FileOptions, cancellation: &Cancellation, ) -> impl Future> + Send; +} + +pub trait FileStat: FileOperation { + type Metadata: FileMetadata; + fn file_stat( + &self, + file: &Self::File, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} +pub trait FileTruncate: FileOperation { fn file_truncate( &self, file: &Self::File, length: u64, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait FileSync: FileOperation { fn file_sync( &self, file: &Self::File, mode: SyncMode, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait FileSeek: FileOperation { fn file_seek( &self, file: &Self::File, - position: std::io::SeekFrom, + position: SeekFrom, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait FileRead: FileOperation { fn file_read( &self, file: &Self::File, buffer: B, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait FileWrite: FileOperation { fn file_write( &self, file: &Self::File, buffer: B, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait FileReadAt: FileOperation { fn file_read_at( &self, file: &Self::File, @@ -507,7 +533,9 @@ pub trait File: Send + Sync { offset: u64, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait FileWriteAt: FileOperation { fn file_write_at( &self, file: &Self::File, @@ -517,7 +545,29 @@ pub trait File: Send + Sync { ) -> impl Future> + Send; } -pub trait Socket: Send + Sync { +pub trait FileClose: FileOperation { + fn file_close(&self, file: Self::File) -> impl Future> + Send; + fn file_close_submit( + &self, + file: Self::File, + ) -> impl Future> + Send; +} + +pub trait FileScheduler: + FileOpen + + FileClose + + FileStat + + FileTruncate + + FileSync + + FileSeek + + FileRead + + FileWrite + + FileReadAt + + FileWriteAt +{ +} + +pub trait SocketScheduler: Send + Sync { type Socket: Send + Sync; fn socket_open( @@ -647,7 +697,7 @@ Dropping an in-flight io_uring operation requests cancellation, but its file and File close initially uses a bounded blocking cleanup pool for native and Tokio schedulers, including the io_uring scheduler. Both variants use the same path. This pool reuses the internal blocking-pool implementation but has separate workers from arbitrary `spawn_blocking` closures, so shutdown draining does not depend on a user closure that might itself destroy the scheduler. Both pools start lazily and each uses the configured `BlockingOptions` limits. Accepted close jobs survive waiter and scheduler destruction. Native shutdown/destruction outside future and blocking-pool workers waits for cleanup; native destruction on either kind of worker only requests shutdown. Explicit synchronous shutdown panics on either kind of worker to avoid waiting for its own completion. Tokio destruction requests shutdown, and its awaited `shutdown` drains cleanup. Standalone io_uring selectors own a cleanup pool drained by their shutdown; standalone readiness selectors use a process-wide bounded fallback, like their other file operations. The simulator completes close immediately. `_submit` execution errors are currently discarded; use ordinary close for diagnostics. Native close/shutdown SQEs and CQE suppression remain later optimizations. -`Socket::socket_shutdown` takes `std::net::Shutdown::{Read, Write, Both}` and borrows the descriptor. Await preceding writes before shutting down `Write`; the other direction can remain usable for the response. Repeated shutdown and I/O on disabled directions retain OS-specific outcomes. `socket_close` consumes the descriptor and rejects retained backend use before admission; it never coordinates or interrupts other operations. Deregistration and close run together on the cleanup worker. Deregistration errors are execution results where the backend exposes them; Tokio's Unix `AsyncFd::into_inner()` discards them internally. The ordinary future reports available execution errors; `_submit` discards them. +`SocketScheduler::socket_shutdown` takes `std::net::Shutdown::{Read, Write, Both}` and borrows the descriptor. Await preceding writes before shutting down `Write`; the other direction can remain usable for the response. Repeated shutdown and I/O on disabled directions retain OS-specific outcomes. `socket_close` consumes the descriptor and rejects retained backend use before admission; it never coordinates or interrupts other operations. Deregistration and close run together on the cleanup worker. Deregistration errors are execution results where the backend exposes them; Tokio's Unix `AsyncFd::into_inner()` discards them internally. The ordinary future reports available execution errors; `_submit` discards them. Native and Tokio socket descriptors privately share their persistent registration through `Arc`. Ordinary readiness operations borrow it without cloning; io_uring and accepted shutdown jobs retain references until completion. Cleanup releases those references before notifying completion, allowing an immediately following close. `_submit` alone cannot establish this ordering: use ordinary shutdown when subsequent explicit close depends on its completion. The in-memory backend completes immediately while preserving admission-only error reporting for `_submit`. @@ -670,7 +720,7 @@ Keep `_submit` exceptional rather than adding a submission-only variant to every A pool receive returns a selected filled lease, rather than accepting an already-selected buffer: ```rust -pub trait PoolReceive: Socket { +pub trait PoolReceive: SocketScheduler { type Pool: Send + Sync; type Lease: IoBufMut; @@ -697,7 +747,7 @@ The returned lease exposes only the initialized received payload for sending, an ### Example: positioned file read ```rust -async fn read_header( +async fn read_header( scheduler: &S, path: &Path, cancellation: &Cancellation, @@ -721,7 +771,7 @@ This deliberately performs one read and permits a short result. A caller needing ### Example: forwarding pooled socket data ```rust -async fn forward( +async fn forward( scheduler: &S, source: &S::Socket, destination: &S::Socket, @@ -890,13 +940,13 @@ Not every useful scheduler operation has an io\_uring opcode; seek and address r ### File semantics -Implemented `FileAdvice: File` is an optional Linux capability with `file_advise(file, offset, length, AdviceMode)` and `file_advise_submit(...)`, returning `io::Result<()>` without a cancellation input. `Normal`, `Sequential`, `Random`, `WillNeed`, `DontNeed`, and `NoReuse` follow `posix_fadvise` hints. Zero length means through EOF; offsets and range ends must fit in `i64`. Advice preserves contents, size, and cursor, but access-policy hints may apply to the entire open file description. Success establishes neither completed prefetch nor eviction, and advice does not guarantee durability. +Implemented `FileAdvice: FileOperation` is an optional Linux capability with `file_advise(file, offset, length, AdviceMode)` and `file_advise_submit(...)`, returning `io::Result<()>` without a cancellation input. `Normal`, `Sequential`, `Random`, `WillNeed`, `DontNeed`, and `NoReuse` follow `posix_fadvise` hints. Zero length means through EOF; offsets and range ends must fit in `i64`. Advice preserves contents, size, and cursor, but access-policy hints may apply to the entire open file description. Success establishes neither completed prefetch nor eviction, and advice does not guarantee durability. Both forms share bounded cleanup admission and retained ownership. The ordinary form reports the syscall result after releasing the retained file. `_submit` reports only validation/admission errors, then discards execution errors; accepted jobs survive waiter abandonment and graceful shutdown drains them. A caller may drop its descriptor after submission, but explicit close requires completion and may panic while advice retains it. Use the ordinary form when an immediate close is needed. Native readiness, Tokio, and io_uring initially run `posix_fadvise64` on the existing cleanup pool. This preserves the accepted-work contract independently of ordinary ring-request cancellation during shutdown. Native advice SQEs remain a later optimization alongside bounded ring admission and retirement. The syscall's directly returned error number is preserved instead of consulting errno. Other platforms omit this capability; the simulator validates ranges and accepts hints without modeling a page cache. See [Linux advice semantics](https://man7.org/linux/man-pages/man2/posix_fadvise.2.html) and the [commented example](examples/file_advice.rs). -Implemented `FileAllocate: File` is an optional extension with `file_allocate(file, offset, length, AllocationMode, cancellation) -> OperationResult<()>`. `Extend` reserves backing space and grows the file to the end of the range when necessary, without shrinking it. `KeepSize` reserves space without changing logical length, including beyond EOF. Existing data and the shared cursor are preserved. Length must be positive and the range end must fit in `i64`; filesystem limits, write-access failures, and other OS errors remain observable. This does not unshare copy-on-write extents or guarantee durability. Await related operations in order; errors and cancellation need not roll back partial allocation. +Implemented `FileAllocate: FileOperation` is an optional extension with `file_allocate(file, offset, length, AllocationMode, cancellation) -> OperationResult<()>`. `Extend` reserves backing space and grows the file to the end of the range when necessary, without shrinking it. `KeepSize` reserves space without changing logical length, including beyond EOF. Existing data and the shared cursor are preserved. Length must be positive and the range end must fit in `i64`; filesystem limits, write-access failures, and other OS errors remain observable. This does not unshare copy-on-write extents or guarantee durability. Await related operations in order; errors and cancellation need not roll back partial allocation. Native readiness and Tokio provide this capability on Linux by running `fallocate` on their existing file pools. io\_uring probes optional `Fallocate` support and uses the same syscall fallback when the opcode is absent. It retains the file through original completion with the other buffer-free requests. The simulator reserves vector capacity within its existing 1 MiB bound and models the logical length and data contract, without modeling filesystem extents or disk exhaustion. @@ -906,7 +956,7 @@ Implemented `file_truncate(file, length, cancellation)` sets the logical file le Truncation is lazy and checks cancellation before dispatch; blocking workers check again before execution. Started blocking calls preserve their result. Native io\_uring requests use best-effort cancellation and retain the file through the original completion, with success winning a concurrent cancellation request. Dropping a waiter cannot undo a resize. Readiness and Tokio use `std::fs::File::set_len` on existing file pools. io\_uring probes `Ftruncate` once at initialization and uses it when available; older kernels use the blocking fallback without disabling the ring or exposing `Unsupported` for this operation. No extra cursor tracking or per-file serialization is introduced. The simulator implements resizing within its existing 1 MiB storage bound and reports `FileTooLarge` when truncation would exceed it. -Implemented `file_stat(file, cancellation)` queries an already-open resource without moving its cursor and returns an owned `File::Metadata` value. The associated type implements `FileMetadata`, with `file_type() -> Option`, `size() -> Option`, and `accessed()`/`modified()`/`created() -> Option`. `None` means unavailable or unrepresentable, not zero. `FileType` distinguishes regular files, directories, symlinks, and other kinds; native fields retain finer distinctions. Creation time is never substituted with Unix status-change time. Metadata fields are observations that may reflect different instants, not an atomic snapshot. Await modifications first when ordering matters. Path lookup, relative paths, and symlink-following policy remain separate APIs. +Implemented `file_stat(file, cancellation)` queries an already-open resource without moving its cursor and returns an owned `FileStat::Metadata` value. The associated type implements `FileMetadata`, with `file_type() -> Option`, `size() -> Option`, and `accessed()`/`modified()`/`created() -> Option`. `None` means unavailable or unrepresentable, not zero. `FileType` distinguishes regular files, directories, symlinks, and other kinds; native fields retain finer distinctions. Creation time is never substituted with Unix status-change time. Metadata fields are observations that may reflect different instants, not an atomic snapshot. Await modifications first when ordering matters. Path lookup, relative paths, and symlink-following policy remain separate APIs. Representation follows the backend, avoiding eager normalization or an extra metadata allocation. Readiness and Tokio retain `std::fs::Metadata` in `SystemFileMetadata`, exposing platform-specific extensions through `as_std()`. io_uring requires the `Statx` opcode at initialization and returns `StatxMetadata`, a transparent wrapper of Linux `statx` with direct access through `as_statx()`. It requests `STATX_BASIC_STATS | STATX_BTIME` using an empty pathname and `AT_EMPTY_PATH`, so the operation identifies the descriptor, not a pathname. Portable accessors consult the returned validity mask and interpret only requested fields. See [statx(2)](https://man7.org/linux/man-pages/man2/statx.2.html) and [io_uring_prep_statx(3)](https://man7.org/linux/man-pages/man3/io_uring_prep_statx.3.html). @@ -926,7 +976,7 @@ Implemented `file_read` and `file_write` use and advance the file's shared curso Allocation is distinct from truncation. An allocation interface needs to account for an offset, length, and supported modes. Platform-specific modes may require an extension rather than a universal flags type. See [io\_uring\_prep\_fallocate](https://man7.org/linux/man-pages/man3/io_uring_prep_fallocate.3.html). -Current `File` opens regular files and provides seeking, shared-cursor and positioned reads and writes, using explicit offsets that fit in `i64`. The internal io\_uring representation distinguishes a shared cursor from a supplied offset, so `file_read_at(..., u64::MAX, ...)` remains invalid rather than selecting stateful access. Each call can complete with a short transfer; read returns zero at EOF. Buffers and their allocations are returned on success, error, and cancellation. Separate opens start with independent cursors; aliases of the same resource share one. `FileOptions` omits append mode so writes respect their supplied offsets. Its Windows fallback uses `seek_read`/`seek_write`, which update the shared cursor according to [Windows FileExt](https://doc.rust-lang.org/std/os/windows/fs/trait.FileExt.html). Resolving this difference is required before promising uniform cursor-preserving semantics. +Current `FileScheduler` opens regular files and provides seeking, shared-cursor and positioned reads and writes, using explicit offsets that fit in `i64`. The internal io\_uring representation distinguishes a shared cursor from a supplied offset, so `file_read_at(..., u64::MAX, ...)` remains invalid rather than selecting stateful access. Each call can complete with a short transfer; read returns zero at EOF. Buffers and their allocations are returned on success, error, and cancellation. Separate opens start with independent cursors; aliases of the same resource share one. `FileOptions` omits append mode so writes respect their supplied offsets. Its Windows fallback uses `seek_read`/`seek_write`, which update the shared cursor according to [Windows FileExt](https://doc.rust-lang.org/std/os/windows/fs/trait.FileExt.html). Resolving this difference is required before promising uniform cursor-preserving semantics. ### Syscall proximity and messages @@ -1030,7 +1080,7 @@ Process creation/waiting and pipes remain useful optional extensions. Prepared l ### Executable interface experiment -[interface\_probe.rs](examples/interface_probe.rs) runs the same `copy`, `serve`, and `exchange` functions through the public `File` and `Socket` traits with native, Tokio, and in-memory implementations: +[interface\_probe.rs](examples/interface_probe.rs) runs the same `copy`, `serve`, and `exchange` functions through the public `FileScheduler` and `SocketScheduler` traits with native, Tokio, and in-memory implementations: ```sh cargo run -p socketry-executor --example interface_probe --features tokio @@ -1056,7 +1106,7 @@ The simulator has bounded in-memory file sizes and unbounded byte queues. It doe | Cancellation | `Cancellation`, `Cancelled`, `defer_cancel`, and cancellation-aware file/socket operations implemented. Application shutdown is cooperative. | | Signal notifications | `Notify`, `Signals`, and optional Unix registration guards implemented for native and Tokio; each guard owns its dispatch task. | | Runtime lifetime | Application waits are explicit. Runtime shutdown abandons futures; backend retention protects submitted I/O. Accepted file-close and socket-close/shutdown submissions survive abandonment and are drained by graceful shutdown. | -| Capabilities | Public `File`, `Socket`, `SocketOptions`, `SocketMessages`, `SocketRegistration`, `AddressResolution`, `Clock`, `Spawn`, and `Blocking`; associated resources and initial file/socket creation implemented. Operation-level traits, shared resource bases, and convenience aggregates are agreed follow-up work; the broad interfaces remain implemented today. | +| Capabilities | Public `FileScheduler`, `SocketScheduler`, `SocketOptions`, `SocketMessages`, `SocketRegistration`, `AddressResolution`, `Clock`, `Spawn`, and `Blocking`; associated resources and initial file/socket creation implemented. File operation traits share `FileOperation`; `FileScheduler` is a convenience aggregate. The equivalent `SocketOperation` split remains follow-up work. | | Blocking execution | Dedicated native/Tokio pools, configurable worker and admission limits, queued cancellation, and owning completion handles implemented. Started closures finish independently; graceful completion requires awaiting them. | | Linked operations | Prepared chains and `Linked` remain proposed; native linking, whole-chain blocking jobs, and readiness sequencing are not implemented. | | File operations | `file_open`, `file_seek`, `file_sync`, `file_truncate`, shared-cursor `file_read`/`file_write`, and explicit-offset `file_read_at`/`file_write_at` implemented with owned buffers. Optional `FileAllocate` implemented on Linux with `Extend` and `KeepSize`; optional `FileAdvice` supplies six hints with completion/submission forms. `file_close`/`file_close_submit` implemented with exclusive ownership and bounded cleanup. `file_stat` returns associated native-backed metadata; path-based metadata remains separate. | diff --git a/crates/executor/examples/file_advice.rs b/crates/executor/examples/file_advice.rs index 403ee7b..0a23cb6 100644 --- a/crates/executor/examples/file_advice.rs +++ b/crates/executor/examples/file_advice.rs @@ -6,7 +6,9 @@ //! On Linux, run `cargo run -p socketry-executor --example file_advice`. Add `--features tokio` for the adapter or `--features io-uring` for the native completion backend. Advice currently uses the cleanup pool on all three. #[cfg(all(target_os = "linux", any(feature = "native", feature = "tokio")))] -async fn advise( +async fn advise< + S: socketry_executor::FileAdvice + socketry_executor::FileOpen + socketry_executor::FileClose, +>( scheduler: &S, path: &std::path::Path, ) -> socketry_executor::OperationResult<()> { diff --git a/crates/executor/examples/file_close.rs b/crates/executor/examples/file_close.rs index 0ef0e5e..fa83c2f 100644 --- a/crates/executor/examples/file_close.rs +++ b/crates/executor/examples/file_close.rs @@ -6,13 +6,18 @@ //! Run with `cargo run -p socketry-executor --example file_close`. Add `--features tokio` to also exercise the Tokio adapter, or `--features io-uring` on Linux for native completion I/O. #[cfg(any(feature = "native", feature = "tokio"))] -use socketry_executor::{Cancellation, File, FileOptions, OperationResult, yield_now}; +use socketry_executor::{ + Cancellation, FileClose, FileOpen, FileOptions, FileRead, OperationResult, yield_now, +}; use std::io; #[cfg(any(feature = "native", feature = "tokio"))] use std::path::Path; #[cfg(any(feature = "native", feature = "tokio"))] -async fn read_then_close(scheduler: &S, path: &Path) -> OperationResult { +async fn read_then_close( + scheduler: &S, + path: &Path, +) -> OperationResult { let file = scheduler .file_open(path, FileOptions::READ, &Cancellation::never()) .await?; diff --git a/crates/executor/examples/file_stat.rs b/crates/executor/examples/file_stat.rs index bf4b01c..3ad2844 100644 --- a/crates/executor/examples/file_stat.rs +++ b/crates/executor/examples/file_stat.rs @@ -6,11 +6,16 @@ //! Run with `cargo run -p socketry-executor --example file_stat`. Add `--features tokio` for the Tokio adapter, or `--features io-uring` on Linux for native `statx` completion. #[cfg(any(feature = "native", feature = "tokio"))] -use socketry_executor::{Cancellation, File, FileMetadata, FileOptions, OperationResult}; +use socketry_executor::{ + Cancellation, FileClose, FileMetadata, FileOpen, FileOptions, FileStat, OperationResult, +}; use std::io; #[cfg(any(feature = "native", feature = "tokio"))] -async fn inspect(scheduler: &S, path: &std::path::Path) -> OperationResult<()> { +async fn inspect( + scheduler: &S, + path: &std::path::Path, +) -> OperationResult<()> { // `never` is an explicit choice for this short example. A real application can pass its shared shutdown cancellation instead. let cancellation = Cancellation::never(); let file = scheduler diff --git a/crates/executor/examples/interface_probe.rs b/crates/executor/examples/interface_probe.rs index 31145f0..3c49c7f 100644 --- a/crates/executor/examples/interface_probe.rs +++ b/crates/executor/examples/interface_probe.rs @@ -1,15 +1,16 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -//! The same generic application using the public File, Socket, and AddressResolution capabilities. +//! The same generic application using the public FileScheduler, SocketScheduler, and AddressResolution capabilities. //! Run with `--features tokio` for native, Tokio, and simulated I/O. //! With `--no-default-features`, only the in-memory implementation runs. #[path = "interface_probe/simulated.rs"] mod simulated; use socketry_executor::{ - AddressHints, AddressResolution, AddressService, Cancellation, File, FileOptions, IoBuf, - OperationResult, Socket, SocketDomain, SocketProtocol, SocketType, + AddressHints, AddressResolution, AddressService, Cancellation, FileOpen, FileOptions, + FileReadAt, FileScheduler, FileWriteAt, IoBuf, OperationResult, SocketDomain, SocketProtocol, + SocketScheduler, SocketType, }; use std::{io, path::Path}; static NEVER: Cancellation = Cancellation::never(); @@ -18,7 +19,7 @@ const PAYLOAD: &[u8] = b"Explicit schedulers keep protocol code portable.\n"; const BUFFER_SIZE: usize = 16; /// Application code only sees the backend's associated resources. -async fn copy( +async fn copy( scheduler: &S, source: &Path, destination: &Path, @@ -62,7 +63,7 @@ async fn copy( } } -async fn send_all( +async fn send_all( scheduler: &S, socket: &S::Socket, mut buffer: Vec, @@ -84,7 +85,7 @@ async fn send_all( } /// Serve one connection until peer EOF, echoing each received prefix. -async fn serve(scheduler: &S, listener: &S::Socket) -> OperationResult { +async fn serve(scheduler: &S, listener: &S::Socket) -> OperationResult { let (socket, _) = scheduler.socket_accept(listener, &NEVER).await?; let mut total = 0; loop { @@ -101,7 +102,7 @@ async fn serve(scheduler: &S, listener: &S::Socket) -> OperationResul } } -async fn exchange(scheduler: &S) -> OperationResult<()> { +async fn exchange(scheduler: &S) -> OperationResult<()> { let address = "127.0.0.1:0".parse().unwrap(); let listener = scheduler .socket_open( @@ -166,7 +167,7 @@ async fn exchange(scheduler: &S) -> OperationResu Ok(()) } -async fn exercise( +async fn exercise( scheduler: &S, source: &Path, destination: &Path, diff --git a/crates/executor/examples/interface_probe/simulated.rs b/crates/executor/examples/interface_probe/simulated.rs index 77570e8..6192c5a 100644 --- a/crates/executor/examples/interface_probe/simulated.rs +++ b/crates/executor/examples/interface_probe/simulated.rs @@ -3,9 +3,13 @@ //! In-memory files and duplex byte streams. No OS resources or native runtime. //! This is an interface probe, not a model of TCP scheduling or durability. -use super::{Cancellation, File, FileOptions, OperationResult, Socket}; +use super::{Cancellation, FileOptions, OperationResult, SocketScheduler}; use event_listener::Event; use socketry_executor::{AllocationMode, BufferResult, FileAllocate, SyncMode}; +use socketry_executor::{ + FileClose, FileOpen, FileOperation, FileRead, FileReadAt, FileSeek, FileStat, FileSync, + FileTruncate, FileWrite, FileWriteAt, +}; use socketry_executor::{Interest, IoBuf, IoBufMut, SocketDomain, SocketProtocol, SocketType}; use std::collections::{HashMap, VecDeque}; use std::io::{self, SeekFrom}; @@ -320,10 +324,11 @@ impl socketry_executor::FileAdvice for Scheduler { } } -impl File for Scheduler { +impl FileOperation for Scheduler { type File = FileDescriptor; - type Metadata = FileMetadata; +} +impl FileClose for Scheduler { async fn file_close(&self, file: Self::File) -> io::Result<()> { drop(file); Ok(()) @@ -332,7 +337,9 @@ impl File for Scheduler { async fn file_close_submit(&self, file: Self::File) -> io::Result<()> { self.file_close(file).await } +} +impl FileTruncate for Scheduler { async fn file_truncate( &self, file: &FileDescriptor, @@ -353,6 +360,10 @@ impl File for Scheduler { file.bytes.lock().unwrap().resize(length as usize, 0); Ok(()) } +} + +impl FileStat for Scheduler { + type Metadata = FileMetadata; async fn file_stat( &self, @@ -364,7 +375,9 @@ impl File for Scheduler { size: file.bytes.lock().unwrap().len() as u64, }) } +} +impl FileSync for Scheduler { async fn file_sync( &self, _file: &Self::File, @@ -374,7 +387,9 @@ impl File for Scheduler { cancellation.check()?; Ok(()) } +} +impl FileSeek for Scheduler { async fn file_seek( &self, file: &FileDescriptor, @@ -394,7 +409,9 @@ impl File for Scheduler { *cursor = position as u64; Ok(*cursor) } +} +impl FileOpen for Scheduler { async fn file_open( &self, path: &Path, @@ -425,7 +442,9 @@ impl File for Scheduler { readable: options.read, }) } +} +impl FileRead for Scheduler { async fn file_read( &self, file: &FileDescriptor, @@ -439,7 +458,9 @@ impl File for Scheduler { } (result, buffer) } +} +impl FileReadAt for Scheduler { async fn file_read_at( &self, file: &FileDescriptor, @@ -449,6 +470,9 @@ impl File for Scheduler { ) -> BufferResult { self.read_file_at(file, buffer, offset, cancellation) } +} + +impl FileWrite for Scheduler { async fn file_write( &self, file: &FileDescriptor, @@ -462,7 +486,9 @@ impl File for Scheduler { } (result, buffer) } +} +impl FileWriteAt for Scheduler { async fn file_write_at( &self, file: &FileDescriptor, @@ -474,7 +500,7 @@ impl File for Scheduler { } } -impl Socket for Scheduler { +impl SocketScheduler for Scheduler { type Socket = SocketDescriptor; async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { drop(socket); diff --git a/crates/executor/examples/interface_probe/tests.rs b/crates/executor/examples/interface_probe/tests.rs index bd3589e..47a251f 100644 --- a/crates/executor/examples/interface_probe/tests.rs +++ b/crates/executor/examples/interface_probe/tests.rs @@ -3,7 +3,8 @@ use super::*; use futures_lite::future::block_on; -use socketry_executor::{IoBufMut, SocketDomain, SocketProtocol, SocketType, SyncMode}; +use socketry_executor::{FileOpen, FileOperation, FileReadAt, FileSeek, FileTruncate, FileWriteAt}; +use socketry_executor::{IoBufMut, SocketDomain, SocketProtocol, SocketType}; use std::future::Future; use std::pin::pin; use std::sync::{ @@ -64,18 +65,14 @@ mod file_allocate; mod file_close; #[test] -fn stateful_operations_forward_and_preserve_short_transfers() { - let scheduler = FailingFiles { - scheduler: simulated::Scheduler::new(2, 1), - failure: Failure::None, - }; - // Exercise forwarding through the wrapper with failure injection disabled. +fn simulated_files_preserve_operation_contracts() { + let scheduler = simulated::Scheduler::new(2, 1); block_on(stateful_file::check(&scheduler, Path::new("stateful"))); block_on(file_sync::check(&scheduler, Path::new("sync"))); block_on(file_stat::check(&scheduler, Path::new("stat"))); block_on(file_truncate::check(&scheduler, Path::new("truncate"))); block_on(file_close::check(&scheduler, Path::new("close"))); - assert_eq!(scheduler.scheduler.open_files(), 0); + assert_eq!(scheduler.open_files(), 0); } #[derive(Default)] @@ -88,7 +85,6 @@ impl Wake for WakeCount { } enum Failure { - None, Create, Read, Write, @@ -100,52 +96,11 @@ struct FailingFiles { failure: Failure, } -impl File for FailingFiles { - type File = ::File; - type Metadata = ::Metadata; - async fn file_close(&self, file: Self::File) -> io::Result<()> { - self.scheduler.file_close(file).await - } - async fn file_close_submit(&self, file: Self::File) -> io::Result<()> { - self.scheduler.file_close_submit(file).await - } - - async fn file_truncate( - &self, - file: &Self::File, - length: u64, - cancellation: &Cancellation, - ) -> OperationResult<()> { - self.scheduler - .file_truncate(file, length, cancellation) - .await - } - - async fn file_stat( - &self, - file: &Self::File, - cancellation: &Cancellation, - ) -> OperationResult { - self.scheduler.file_stat(file, cancellation).await - } - async fn file_sync( - &self, - file: &Self::File, - mode: SyncMode, - cancellation: &Cancellation, - ) -> OperationResult<()> { - self.scheduler.file_sync(file, mode, cancellation).await - } - - async fn file_seek( - &self, - file: &Self::File, - position: std::io::SeekFrom, - cancellation: &Cancellation, - ) -> OperationResult { - self.scheduler.file_seek(file, position, cancellation).await - } +impl FileOperation for FailingFiles { + type File = ::File; +} +impl FileOpen for FailingFiles { async fn file_open( &self, path: &Path, @@ -157,25 +112,9 @@ impl File for FailingFiles { } self.scheduler.file_open(path, mode, cancellation).await } +} - async fn file_read( - &self, - file: &Self::File, - buffer: B, - cancellation: &Cancellation, - ) -> socketry_executor::BufferResult { - self.scheduler.file_read(file, buffer, cancellation).await - } - - async fn file_write( - &self, - file: &Self::File, - buffer: B, - cancellation: &Cancellation, - ) -> socketry_executor::BufferResult { - self.scheduler.file_write(file, buffer, cancellation).await - } - +impl FileReadAt for FailingFiles { async fn file_read_at( &self, file: &Self::File, @@ -193,7 +132,9 @@ impl File for FailingFiles { .file_read_at(file, buffer, offset, cancellation) .await } +} +impl FileWriteAt for FailingFiles { async fn file_write_at( &self, file: &Self::File, diff --git a/crates/executor/examples/interface_probe/tests/address_resolution.rs b/crates/executor/examples/interface_probe/tests/address_resolution.rs index b890b22..bb037f3 100644 --- a/crates/executor/examples/interface_probe/tests/address_resolution.rs +++ b/crates/executor/examples/interface_probe/tests/address_resolution.rs @@ -154,7 +154,7 @@ pub async fn check(scheduler: &impl AddressResolution) { /// Exercise the boundary between resolution and associated socket resources through public traits only. pub async fn connection( - scheduler: &(impl AddressResolution + socketry_executor::Socket), + scheduler: &(impl AddressResolution + socketry_executor::SocketScheduler), name: &str, ) { use socketry_executor::SocketType; diff --git a/crates/executor/examples/interface_probe/tests/file_advice.rs b/crates/executor/examples/interface_probe/tests/file_advice.rs index ee0d248..1a679c9 100644 --- a/crates/executor/examples/interface_probe/tests/file_advice.rs +++ b/crates/executor/examples/interface_probe/tests/file_advice.rs @@ -2,12 +2,16 @@ // Copyright, 2026, by Samuel Williams. use socketry_executor::{AdviceMode, Cancellation, FileAdvice, FileOptions}; +use socketry_executor::{FileClose, FileOpen, FileRead, FileSeek, FileWrite}; use std::{ io::{self, SeekFrom}, path::Path, }; -pub async fn check(scheduler: &S, root: &Path) { +pub async fn check( + scheduler: &S, + root: &Path, +) { let never = Cancellation::never(); let path = root.join("advice"); let file = scheduler diff --git a/crates/executor/examples/interface_probe/tests/file_allocate.rs b/crates/executor/examples/interface_probe/tests/file_allocate.rs index a974b57..1259a6b 100644 --- a/crates/executor/examples/interface_probe/tests/file_allocate.rs +++ b/crates/executor/examples/interface_probe/tests/file_allocate.rs @@ -1,9 +1,8 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use socketry_executor::{ - AllocationMode, Cancellation, File, FileAllocate, FileOptions, OperationError, -}; +use socketry_executor::{AllocationMode, Cancellation, FileAllocate, FileOptions, OperationError}; +use socketry_executor::{FileOpen, FileRead, FileSeek, FileWrite}; use std::{ io::{self, SeekFrom}, path::Path, @@ -11,7 +10,7 @@ use std::{ static NEVER: Cancellation = Cancellation::never(); -async fn contents(scheduler: &S, path: &Path) -> Vec { +async fn contents(scheduler: &S, path: &Path) -> Vec { let reader = scheduler .file_open(path, FileOptions::READ, &NEVER) .await @@ -27,7 +26,10 @@ async fn contents(scheduler: &S, path: &Path) -> Vec { } } -pub async fn check(scheduler: &S, root: &Path) { +pub async fn check( + scheduler: &S, + root: &Path, +) { let path = root.join("allocate"); let file = scheduler .file_open(&path, FileOptions::CREATE, &NEVER) diff --git a/crates/executor/examples/interface_probe/tests/file_close.rs b/crates/executor/examples/interface_probe/tests/file_close.rs index 3175693..5cdadc4 100644 --- a/crates/executor/examples/interface_probe/tests/file_close.rs +++ b/crates/executor/examples/interface_probe/tests/file_close.rs @@ -1,10 +1,11 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use socketry_executor::{Cancellation, File, FileOptions, OperationError}; +use socketry_executor::{Cancellation, FileOptions, OperationError}; +use socketry_executor::{FileClose, FileOpen, FileRead, FileWrite}; use std::path::Path; -pub async fn check(scheduler: &S, root: &Path) { +pub async fn check(scheduler: &S, root: &Path) { let never = Cancellation::never(); let path = root.join("close"); let file = scheduler diff --git a/crates/executor/examples/interface_probe/tests/file_stat.rs b/crates/executor/examples/interface_probe/tests/file_stat.rs index 5329ea3..bcdf9b6 100644 --- a/crates/executor/examples/interface_probe/tests/file_stat.rs +++ b/crates/executor/examples/interface_probe/tests/file_stat.rs @@ -1,12 +1,16 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use socketry_executor::{Cancellation, File, FileMetadata, FileOptions, FileType, OperationError}; +use socketry_executor::{Cancellation, FileMetadata, FileOptions, FileType, OperationError}; +use socketry_executor::{FileClose, FileOpen, FileSeek, FileStat, FileTruncate}; use std::{io::SeekFrom, path::Path}; static NEVER: Cancellation = Cancellation::never(); -pub async fn check(scheduler: &S, root: &Path) { +pub async fn check( + scheduler: &S, + root: &Path, +) { let path = root.join("stat"); let file = scheduler .file_open(&path, FileOptions::CREATE, &NEVER) diff --git a/crates/executor/examples/interface_probe/tests/file_sync.rs b/crates/executor/examples/interface_probe/tests/file_sync.rs index 7b1401d..16fe576 100644 --- a/crates/executor/examples/interface_probe/tests/file_sync.rs +++ b/crates/executor/examples/interface_probe/tests/file_sync.rs @@ -1,12 +1,16 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use socketry_executor::{Cancellation, File, FileOptions, IoBuf, OperationError, SyncMode}; +use socketry_executor::{Cancellation, FileOptions, IoBuf, OperationError, SyncMode}; +use socketry_executor::{FileOpen, FileRead, FileSeek, FileSync, FileWrite}; use std::{io::SeekFrom, path::Path}; static NEVER: Cancellation = Cancellation::never(); -pub async fn check(scheduler: &S, root: &Path) { +pub async fn check( + scheduler: &S, + root: &Path, +) { let path = root.join("sync"); let file = scheduler .file_open( diff --git a/crates/executor/examples/interface_probe/tests/file_truncate.rs b/crates/executor/examples/interface_probe/tests/file_truncate.rs index 3a64f29..59db657 100644 --- a/crates/executor/examples/interface_probe/tests/file_truncate.rs +++ b/crates/executor/examples/interface_probe/tests/file_truncate.rs @@ -1,7 +1,8 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use socketry_executor::{Cancellation, File, FileOptions, IoBuf, OperationError, SyncMode}; +use socketry_executor::{Cancellation, FileOptions, IoBuf, OperationError, SyncMode}; +use socketry_executor::{FileOpen, FileRead, FileSeek, FileSync, FileTruncate, FileWrite}; use std::{ io::{self, SeekFrom}, path::Path, @@ -9,14 +10,14 @@ use std::{ static NEVER: Cancellation = Cancellation::never(); -async fn position(scheduler: &S, file: &S::File) -> u64 { +async fn position(scheduler: &S, file: &S::File) -> u64 { scheduler .file_seek(file, SeekFrom::Current(0), &NEVER) .await .unwrap() } -async fn contents(scheduler: &S, path: &Path) -> Vec { +async fn contents(scheduler: &S, path: &Path) -> Vec { let reader = scheduler .file_open(path, FileOptions::READ, &NEVER) .await @@ -33,7 +34,10 @@ async fn contents(scheduler: &S, path: &Path) -> Vec { bytes } -pub async fn check(scheduler: &S, root: &Path) { +pub async fn check( + scheduler: &S, + root: &Path, +) { let path = root.join("truncate"); let file = scheduler .file_open( diff --git a/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs b/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs index f194c47..40423c8 100644 --- a/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs +++ b/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs @@ -1,10 +1,10 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use socketry_executor::{Cancellation, OperationError, Socket, SocketDomain, SocketType}; +use socketry_executor::{Cancellation, OperationError, SocketDomain, SocketScheduler, SocketType}; use std::net::Shutdown; -async fn open(scheduler: &S) -> S::Socket { +async fn open(scheduler: &S) -> S::Socket { scheduler .socket_open( SocketDomain::Ipv4, @@ -16,7 +16,7 @@ async fn open(scheduler: &S) -> S::Socket { .unwrap() } -async fn pair(scheduler: &S) -> (S::Socket, S::Socket) { +async fn pair(scheduler: &S) -> (S::Socket, S::Socket) { let never = Cancellation::never(); let listener = open(scheduler).await; scheduler @@ -41,7 +41,7 @@ async fn pair(scheduler: &S) -> (S::Socket, S::Socket) { (client, server) } -pub async fn check(scheduler: &S) { +pub async fn check(scheduler: &S) { let never = Cancellation::never(); let (client, server) = pair(scheduler).await; // An unpolled shutdown must have no effect; the following write still succeeds. diff --git a/crates/executor/examples/interface_probe/tests/socket_messages.rs b/crates/executor/examples/interface_probe/tests/socket_messages.rs index f7dc960..64fe8e9 100644 --- a/crates/executor/examples/interface_probe/tests/socket_messages.rs +++ b/crates/executor/examples/interface_probe/tests/socket_messages.rs @@ -4,8 +4,8 @@ //! Shared message contracts for backends implementing the optional capability. use socketry_executor::{ - Cancellation, IoBuf, IoBufMut, OperationError, Socket, SocketDomain, SocketMessage, - SocketMessages, SocketRecvFlags, SocketSendFlags, SocketType, + Cancellation, IoBuf, IoBufMut, OperationError, SocketDomain, SocketMessage, SocketMessages, + SocketRecvFlags, SocketScheduler, SocketSendFlags, SocketType, }; use std::{ future::{Future, poll_fn}, @@ -15,7 +15,11 @@ use std::{ const NEVER: Cancellation = Cancellation::never(); -async fn bound(scheduler: &S, address: SocketAddr, kind: SocketType) -> S::Socket { +async fn bound( + scheduler: &S, + address: SocketAddr, + kind: SocketType, +) -> S::Socket { let socket = scheduler .socket_open(SocketDomain::from(address), kind, None, &NEVER) .await diff --git a/crates/executor/examples/interface_probe/tests/socket_options.rs b/crates/executor/examples/interface_probe/tests/socket_options.rs index 531692c..dc95066 100644 --- a/crates/executor/examples/interface_probe/tests/socket_options.rs +++ b/crates/executor/examples/interface_probe/tests/socket_options.rs @@ -2,8 +2,8 @@ // Copyright, 2026, by Samuel Williams. use socketry_executor::{ - Cancellation, OperationError, Socket, SocketDomain, SocketKeepalive, SocketOption, - SocketOptions, SocketReuseAddress, SocketType, TcpNoDelay, + Cancellation, OperationError, SocketDomain, SocketKeepalive, SocketOption, SocketOptions, + SocketReuseAddress, SocketScheduler, SocketType, TcpNoDelay, }; async fn round_trip(scheduler: &S, socket: &S::Socket, option: O) @@ -50,7 +50,7 @@ where ); } -async fn open(scheduler: &S) -> S::Socket { +async fn open(scheduler: &S) -> S::Socket { scheduler .socket_open( SocketDomain::Ipv4, diff --git a/crates/executor/examples/interface_probe/tests/stateful_file.rs b/crates/executor/examples/interface_probe/tests/stateful_file.rs index 97c76c6..6e91492 100644 --- a/crates/executor/examples/interface_probe/tests/stateful_file.rs +++ b/crates/executor/examples/interface_probe/tests/stateful_file.rs @@ -1,12 +1,13 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use socketry_executor::{Cancellation, File, FileOptions, IoBuf, OperationError}; +use socketry_executor::{Cancellation, FileOptions, IoBuf, OperationError}; +use socketry_executor::{FileOpen, FileRead, FileReadAt, FileSeek, FileWrite, FileWriteAt}; use std::{io::SeekFrom, path::Path}; static NEVER: Cancellation = Cancellation::never(); -async fn write_all(scheduler: &S, file: &S::File, bytes: &[u8]) { +async fn write_all(scheduler: &S, file: &S::File, bytes: &[u8]) { let mut buffer = bytes.to_vec(); let allocation = buffer.as_ptr(); let mut offset = 0; @@ -23,7 +24,7 @@ async fn write_all(scheduler: &S, file: &S::File, bytes: &[u8]) { } } -async fn read_all(scheduler: &S, file: &S::File) -> Vec { +async fn read_all(scheduler: &S, file: &S::File) -> Vec { let mut output = Vec::new(); let mut buffer = vec![255; 6].into_boxed_slice(); let allocation = buffer.as_ptr(); @@ -44,7 +45,10 @@ async fn read_all(scheduler: &S, file: &S::File) -> Vec { output } -pub async fn check(scheduler: &S, root: &Path) { +pub async fn check( + scheduler: &S, + root: &Path, +) { check_seek(scheduler, root).await; let path = root.join("stateful"); let file = scheduler @@ -189,7 +193,10 @@ pub async fn check(scheduler: &S, root: &Path) { } } -async fn check_seek(scheduler: &S, root: &Path) { +async fn check_seek( + scheduler: &S, + root: &Path, +) { let path = root.join("seek"); let file = scheduler .file_open( diff --git a/crates/executor/examples/portable_io.rs b/crates/executor/examples/portable_io.rs index 76c391c..dbbee4e 100644 --- a/crates/executor/examples/portable_io.rs +++ b/crates/executor/examples/portable_io.rs @@ -6,14 +6,14 @@ static NEVER: socketry_executor::Cancellation = socketry_executor::Cancellation::never(); #[cfg(any(feature = "native", feature = "tokio"))] -use socketry_executor::{Socket, SocketDomain, SocketProtocol, SocketType, Spawn}; +use socketry_executor::{SocketDomain, SocketProtocol, SocketScheduler, SocketType, Spawn}; #[cfg(any(feature = "native", feature = "tokio"))] use std::io; #[cfg(any(feature = "native", feature = "tokio"))] async fn exchange(scheduler: SchedulerType) -> io::Result where - SchedulerType: Socket + Spawn + Clone + 'static, + SchedulerType: SocketScheduler + Spawn + Clone + 'static, { let listener = scheduler .socket_open( diff --git a/crates/executor/examples/socket_shutdown.rs b/crates/executor/examples/socket_shutdown.rs index 1fbc372..928fa12 100644 --- a/crates/executor/examples/socket_shutdown.rs +++ b/crates/executor/examples/socket_shutdown.rs @@ -4,13 +4,13 @@ //! A small loopback request/response exchange framed by TCP half-close. Run with `cargo run -p socketry-executor --example socket_shutdown --features tokio` to exercise both native and Tokio backends. #[cfg(any(feature = "native", feature = "tokio"))] -use socketry_executor::{Cancellation, IoBuf, Socket, SocketDomain, SocketType}; +use socketry_executor::{Cancellation, IoBuf, SocketDomain, SocketScheduler, SocketType}; use std::io; #[cfg(any(feature = "native", feature = "tokio"))] use std::net::Shutdown; #[cfg(any(feature = "native", feature = "tokio"))] -async fn send_all( +async fn send_all( scheduler: &S, socket: &S::Socket, mut buffer: Vec, @@ -33,7 +33,7 @@ async fn send_all( } #[cfg(any(feature = "native", feature = "tokio"))] -async fn receive_all( +async fn receive_all( scheduler: &S, socket: &S::Socket, cancellation: &Cancellation, @@ -53,7 +53,7 @@ async fn receive_all( } #[cfg(any(feature = "native", feature = "tokio"))] -async fn exchange(scheduler: &S) -> io::Result<()> { +async fn exchange(scheduler: &S) -> io::Result<()> { // This example deliberately runs to completion. An application can pass its cooperative shutdown signal to reads and writes instead. let cancellation = Cancellation::never(); let listener = scheduler diff --git a/crates/executor/readme.md b/crates/executor/readme.md index eeb837d..71e7f59 100644 --- a/crates/executor/readme.md +++ b/crates/executor/readme.md @@ -187,27 +187,33 @@ Queues allocate backing storage as needed. Rescheduling reuses the existing task ## I/O and selectors -The public `scheduler` module contains portable `Socket`, `File`, and `Clock` traits, the Socketry implementation in `socketry.rs`, the optional Tokio adapter in `tokio.rs`, and native implementations under `selector/`. +File capabilities are split by operation: `FileOperation` selects the associated resource type, and `FileOpen`, `FileClose`, `FileStat`, `FileTruncate`, `FileSync`, `FileSeek`, `FileRead`, `FileWrite`, `FileReadAt`, and `FileWriteAt` supply the core operations. Optional `FileAllocate` and `FileAdvice` depend only on `FileOperation`. `FileStat` separately selects its metadata output. The `FileScheduler` aggregate is implemented automatically for schedulers providing every core file capability; consumers can instead require only what they use, such as `FileRead + FileWrite`. + +When migrating an implementation, move `type File` from the former `impl File` into `impl FileOperation`, move `type Metadata` and `file_stat` into `impl FileStat`, and move each remaining method into its operation trait implementation. Do not implement `FileScheduler` explicitly. Fully qualified paths become `::File`, `::Metadata`, and operation paths such as `FileRead::file_read`. For method calls on concrete schedulers, import the relevant operation traits; importing `FileScheduler` alone is insufficient. Generic functions bounded by `S: FileScheduler` still see every core method. Bounds on `FileAdvice` or `FileAllocate` now require explicit additional capabilities when the function also opens, reads, or closes files. + +The broad file and socket traits are named `FileScheduler` and `SocketScheduler`, replacing `File` and `Socket`. Update aggregate bounds and imports accordingly. Socket implementations use `impl SocketScheduler` and `::Socket`; the operation-level socket split remains follow-up work, so this trait still declares its methods directly. Individual operation traits and resource names are unchanged. + +The public `scheduler` module contains portable `SocketScheduler`, `FileScheduler`, and `Clock` traits, the Socketry implementation in `socketry.rs`, the optional Tokio adapter in `tokio.rs`, and native implementations under `selector/`. - `native` (default) supplies nonblocking IPv4/IPv6 stream and datagram sockets through async-io's process-wide reactor: epoll on Linux, kqueue on Apple/BSD, and IOCP/AFD socket readiness on Windows. The three platform modules expose the shared implementation; they do not duplicate its registration machinery. - `io-uring` selects a dedicated Linux completion selector for socket and file reads/writes. Connection setup, accept, readiness and timers use async-io. On other supported platforms this feature leaves the platform default intact. - `tokio` adds an adapter using a supplied `tokio::runtime::Handle`. It uses Tokio `AsyncFd` on Unix. Windows uses async-io's process-wide reactor for generic socket readiness because Tokio has no matching generic socket wrapper; this fallback is not driven by Tokio's I/O driver. The adapter does not construct or drive a runtime. Enable that runtime's I/O and time facilities for the operations that use it. - With default features disabled, the executor and public contracts still build. Enabling only `tokio` omits the native blocking and io\_uring backends; async-io remains a dependency for Tokio's generic Windows socket-readiness fallback. -`Scheduler` and `SchedulerHandle` implement the traits. `file_open` returns `File::File`; `socket_open` returns the backend's single associated socket resource, `Socket::Socket`. Call `socket_bind` and then `socket_listen(socket, backlog, cancellation)` to listen, or `socket_connect(socket, address, cancellation)` to connect an opened socket. `socket_accept` returns another resource of the same type. Native and Tokio use non-`Clone` `SocketDescriptor` values. Importing existing standard-library TCP resources uses the separate optional `SocketRegistration` trait. Returned operation futures are Send. Registrations remain with their original reactor as tasks migrate; they are not re-created per poll. Tokio resources passed to an adapter for another runtime return InvalidInput. +`Scheduler` and `SchedulerHandle` implement the traits. `file_open` returns `FileOperation::File`; `socket_open` returns the backend's single associated socket resource, `SocketScheduler::Socket`. Call `socket_bind` and then `socket_listen(socket, backlog, cancellation)` to listen, or `socket_connect(socket, address, cancellation)` to connect an opened socket. `socket_accept` returns another resource of the same type. Native and Tokio use non-`Clone` `SocketDescriptor` values. Importing existing standard-library TCP resources uses the separate optional `SocketRegistration` trait. Returned operation futures are Send. Registrations remain with their original reactor as tasks migrate; they are not re-created per poll. Tokio resources passed to an adapter for another runtime return InvalidInput. -The initial API selects IPv4 or IPv6, stream or datagram, and an optional TCP or UDP protocol. Connected datagram reads and writes preserve message boundaries. The higher-level `Socket` trait deliberately does not provide protocol buffering: line, frame, and message semantics belong in protocol adapters built above it. Generic static dispatch is the default, with dynamic dispatch available to consumers that need runtime selection or type erasure. +The initial API selects IPv4 or IPv6, stream or datagram, and an optional TCP or UDP protocol. Connected datagram reads and writes preserve message boundaries. The higher-level `SocketScheduler` trait deliberately does not provide protocol buffering: line, frame, and message semantics belong in protocol adapters built above it. Generic static dispatch is the default, with dynamic dispatch available to consumers that need runtime selection or type erasure. Read/write operations take an owned `IoBuf` (`IoBufMut` for reads) and return `BufferResult = (OperationResult, B)`, including errors and cancellation. `OperationError` distinguishes `Cancelled` and `Io`. `Vec` and `Box<[u8]>` are supported. `buffer.slice(range)` creates an owned `BufferSlice` without copying; `into_inner()` recovers its original owner for reuse. Read buffers must have an initialized length (`vec![0; capacity]`); capacity alone supplies no writable bytes. Buffer length is unchanged; only the first returned byte count contains new data. Reads and writes can be partial. Each read/write call is one operation, not a read-exact/write-all convenience method. -`File::file_read` and `file_write` borrow `&Self::File` and advance its shared cursor by the transferred byte count. `file_read_at` and `file_write_at` take an explicit offset instead. Await cursor operations sequentially when ordering matters; concurrent calls have no portable ordering or non-overlap guarantee. Dropping an admitted operation can still advance the cursor. Each call can transfer fewer bytes than requested, and a read returns zero at EOF. `file_open(path, FileOptions, cancellation)` creates the associated resource. Current native/Tokio backends internally retain an `Arc`; generic callers do not require that representation. The readiness and Tokio implementations use blocking pools; io\_uring uses current-position SQEs for stateful access. There is no scheduler-side cursor or extra per-file locking. Use ordinary files opened without append mode, and offsets fitting i64. Unix positioned operations leave the shared cursor unchanged; the Windows blocking fallback updates it according to std's seek\_read/seek\_write semantics. +`FileRead::file_read` and `file_write` borrow `&Self::File` and advance its shared cursor by the transferred byte count. `file_read_at` and `file_write_at` take an explicit offset instead. Await cursor operations sequentially when ordering matters; concurrent calls have no portable ordering or non-overlap guarantee. Dropping an admitted operation can still advance the cursor. Each call can transfer fewer bytes than requested, and a read returns zero at EOF. `file_open(path, FileOptions, cancellation)` creates the associated resource. Current native/Tokio backends internally retain an `Arc`; generic callers do not require that representation. The readiness and Tokio implementations use blocking pools; io\_uring uses current-position SQEs for stateful access. There is no scheduler-side cursor or extra per-file locking. Use ordinary files opened without append mode, and offsets fitting i64. Unix positioned operations leave the shared cursor unchanged; the Windows blocking fallback updates it according to std's seek\_read/seek\_write semantics. -`file_stat(file, cancellation)` queries an open file without changing its cursor. It returns owned `File::Metadata`; import `FileMetadata` to use its portable accessors. Fields may be unavailable, and timestamps may be unrepresentable, so accessors return `Option`. Results can reflect concurrent modifications and are not atomic snapshots. Await preceding operations when ordering matters. Querying requires no write permission, and the result remains usable after the file is closed. +`file_stat(file, cancellation)` queries an open file without changing its cursor. It returns owned `FileStat::Metadata`; import `FileMetadata` to use its portable accessors. Fields may be unavailable, and timestamps may be unrepresentable, so accessors return `Option`. Results can reflect concurrent modifications and are not atomic snapshots. Await preceding operations when ordering matters. Querying requires no write permission, and the result remains usable after the file is closed. ```rust -use socketry_executor::{Cancellation, File, FileMetadata, OperationResult}; +use socketry_executor::{Cancellation, FileStat, FileMetadata, OperationResult}; -async fn size(scheduler: &S, file: &S::File, cancellation: &Cancellation) -> OperationResult> { +async fn size(scheduler: &S, file: &S::File, cancellation: &Cancellation) -> OperationResult> { let metadata = scheduler.file_stat(file, cancellation).await?; Ok(metadata.size()) } @@ -222,9 +228,9 @@ Readiness and Tokio retain `std::fs::Metadata` in `SystemFileMetadata`; `as_std( Truncation uses native io\_uring requests when supported by the running kernel, with the blocking file fallback otherwise. Readiness and Tokio use their existing file-operation pools. Cancellation is checked before dispatch and again by blocking workers before execution; started blocking calls preserve their result. Ring cancellation is best effort and waits for the original completion. Dropping a waiter does not undo a resize or release a file still in use by the backend. ```rust,no_run -use socketry_executor::{Cancellation, File, OperationResult}; +use socketry_executor::{Cancellation, FileTruncate, OperationResult}; -async fn resize( +async fn resize( scheduler: &S, file: &S::File, length: u64, @@ -269,9 +275,9 @@ async fn reserve( io\_uring uses native `Fsync` requests; readiness and Tokio use their existing blocking file pools. Cancellation before dispatch prevents submission, and blocking workers check again before starting. Once submitted, cancellation is best effort; a successful completion wins concurrent cancellation. Dropping a waiter does not undo the operation or release a file still in use by the backend. ```rust,no_run -use socketry_executor::{Cancellation, File, OperationResult, SyncMode}; +use socketry_executor::{Cancellation, FileSync, OperationResult, SyncMode}; -async fn synchronize( +async fn synchronize( scheduler: &S, file: &S::File, cancellation: &Cancellation, @@ -283,9 +289,9 @@ async fn synchronize( Repeated calls to `file_read` continue from the position reached by the previous call. For example, this helper returns one chunk, which can be shorter than 4096 bytes: ```rust -use socketry_executor::{Cancellation, File, OperationResult}; +use socketry_executor::{Cancellation, FileRead, OperationResult}; -async fn read_chunk( +async fn read_chunk( scheduler: &S, file: &S::File, cancellation: &Cancellation, @@ -299,10 +305,10 @@ async fn read_chunk( To rewind an existing file and read from its beginning: ```rust -use socketry_executor::{Cancellation, File, OperationResult}; +use socketry_executor::{Cancellation, FileRead, FileSeek, OperationResult}; use std::io::SeekFrom; -async fn read_header( +async fn read_header( scheduler: &S, file: &S::File, cancellation: &Cancellation, @@ -318,9 +324,9 @@ async fn read_header( ### Explicit file close -`File::file_close(file).await` consumes the handle, waits for bounded admission and OS completion, and reports close errors. `file_close_submit(file).await` waits only for admission; accepted work finishes even if its waiter is dropped, and graceful scheduler shutdown drains it. Neither form takes a cancellation token. Close does not synchronize data and does not retry an OS close error. `_submit` execution errors are discarded in the current backends. +`FileClose::file_close(file).await` consumes the handle, waits for bounded admission and OS completion, and reports close errors. `file_close_submit(file).await` waits only for admission; accepted work finishes even if its waiter is dropped, and graceful scheduler shutdown drains it. Neither form takes a cancellation token. Close does not synchronize data and does not retry an OS close error. `_submit` execution errors are discarded in the current backends. -Native and Tokio resources are owned, non-`Clone` `FileDescriptor` values with private backend retention. Existing concrete callers using `Arc` must instead move an OS file into `FileDescriptor::from(file)` or use `file_open`. `AsRef` permits synchronous platform access; generic consumers continue to use `S::File`. External `File` implementations must provide both close methods. +Native and Tokio resources are owned, non-`Clone` `FileDescriptor` values with private backend retention. Existing concrete callers using `Arc` must instead move an OS file into `FileDescriptor::from(file)` or use `file_open`. `AsRef` permits synchronous platform access; generic consumers continue to use `S::File`. `FileClose` implementations provide both close methods. Finish outstanding operations before explicit close. Request cancellation, keep awaiting the operation, then close; dropping its future alone does not establish that the kernel or blocking worker has finished. Closing a handle still retained by an abandoned operation panics. Ordinary `Drop` is a fallback that releases ownership and discards close errors without joining outstanding operations. @@ -346,11 +352,11 @@ The [commented half-close example](examples/socket_shutdown.rs) demonstrates a r cargo run -p socketry-executor --example socket_shutdown --features tokio ``` -External `Socket` implementations must provide `socket_shutdown`, `socket_shutdown_submit`, `socket_close`, and `socket_close_submit`. +External `SocketScheduler` implementations must provide `socket_shutdown`, `socket_shutdown_submit`, `socket_close`, and `socket_close_submit`. ### Typed socket options -Require `SocketOptions` for each option your generic code needs. `SocketReuseAddress`, `SocketKeepalive`, and `TcpNoDelay` are typed identifiers with boolean values, implemented by native and Tokio schedulers/handles and the native selectors. This is an extension of `Socket`; existing implementations need not support every option. +Require `SocketOptions` for each option your generic code needs. `SocketReuseAddress`, `SocketKeepalive`, and `TcpNoDelay` are typed identifiers with boolean values, implemented by native and Tokio schedulers/handles and the native selectors. This is an extension of `SocketScheduler`; existing implementations need not support every option. ```rust,no_run use socketry_executor::{Cancellation, OperationResult, SocketOptions, TcpNoDelay}; @@ -407,7 +413,7 @@ cargo test -p socketry-executor --example interface_probe --features tokio cargo run -p socketry-executor --example interface_probe --no-default-features ``` -It uses the exported `File` and `Socket` traits, associated resources, cancellation, and owned buffer views. Its in-memory implementation supplies the same capabilities without importing OS descriptors. See the [design findings](design.md#executable-interface-experiment) for results and remaining contracts. +It uses the exported `FileScheduler` and `SocketScheduler` traits, associated resources, cancellation, and owned buffer views. Its in-memory implementation supplies the same capabilities without importing OS descriptors. See the [design findings](design.md#executable-interface-experiment) for results and remaining contracts. ## Preserved prototype diff --git a/crates/executor/src/file_metadata.rs b/crates/executor/src/file_metadata.rs index d6dd9cc..733cf33 100644 --- a/crates/executor/src/file_metadata.rs +++ b/crates/executor/src/file_metadata.rs @@ -4,7 +4,7 @@ use crate::FileType; use std::time::SystemTime; -/// Portable accessors for owned file status returned by [`crate::File::file_stat`]. Each backend chooses its representation, so native results need no eager field conversion and simulations need no OS resources. +/// Portable accessors for owned file status returned by [`crate::FileStat::file_stat`]. Each backend chooses its representation, so native results need no eager field conversion and simulations need no OS resources. /// /// `None` means a field was not supplied or cannot be represented. Fields can reflect different instants during concurrent modification; metadata is an observation, not an atomic snapshot or synchronization operation. Native fields remain available through the concrete result type. pub trait FileMetadata: Send + Sync + 'static { diff --git a/crates/executor/src/lib.rs b/crates/executor/src/lib.rs index 372f9c3..5c9e37c 100644 --- a/crates/executor/src/lib.rs +++ b/crates/executor/src/lib.rs @@ -77,8 +77,10 @@ pub use io_buf_mut::IoBufMut; pub use notify::Notify; pub use operation_error::{OperationError, OperationResult}; pub use scheduler::{ - AddressResolution, BufferResult, Clock, File, FileAdvice, FileAllocate, Interest, Scheduler, - SchedulerHandle, Signals, Socket, SocketMessages, SocketOptions, SocketRegistration, + AddressResolution, BufferResult, Clock, FileAdvice, FileAllocate, FileClose, FileOpen, + FileOperation, FileRead, FileReadAt, FileScheduler, FileSeek, FileStat, FileSync, FileTruncate, + FileWrite, FileWriteAt, Interest, Scheduler, SchedulerHandle, Signals, SocketMessages, + SocketOptions, SocketRegistration, SocketScheduler, }; #[cfg(all(unix, feature = "signals"))] pub use signal_hook::consts::signal; diff --git a/crates/executor/src/scheduler.rs b/crates/executor/src/scheduler.rs index e398648..117e6e8 100644 --- a/crates/executor/src/scheduler.rs +++ b/crates/executor/src/scheduler.rs @@ -9,7 +9,7 @@ pub mod socketry; #[cfg(any(feature = "native", feature = "tokio"))] mod cleanup; #[cfg(any(feature = "native", feature = "tokio"))] -mod file_close; +mod file_close_io; #[cfg(any(feature = "native", feature = "tokio"))] mod file_io; @@ -40,8 +40,8 @@ pub use socket_registration::SocketRegistration; mod interest; pub use interest::Interest; -mod socket; -pub use socket::Socket; +mod socket_scheduler; +pub use socket_scheduler::SocketScheduler; mod socket_options; pub use socket_options::SocketOptions; @@ -49,8 +49,41 @@ pub use socket_options::SocketOptions; #[cfg(any(feature = "native", feature = "tokio"))] mod native_socket_option; -mod file; -pub use file::File; +mod file_scheduler; +pub use file_scheduler::FileScheduler; + +mod file_operation; +pub use file_operation::FileOperation; + +mod file_open; +pub use file_open::FileOpen; + +mod file_close; +pub use file_close::FileClose; + +mod file_stat; +pub use file_stat::FileStat; + +mod file_truncate; +pub use file_truncate::FileTruncate; + +mod file_sync; +pub use file_sync::FileSync; + +mod file_seek; +pub use file_seek::FileSeek; + +mod file_read; +pub use file_read::FileRead; + +mod file_write; +pub use file_write::FileWrite; + +mod file_read_at; +pub use file_read_at::FileReadAt; + +mod file_write_at; +pub use file_write_at::FileWriteAt; mod file_advice; pub use file_advice::FileAdvice; diff --git a/crates/executor/src/scheduler/address_resolution.rs b/crates/executor/src/scheduler/address_resolution.rs index fcc57ea..b477247 100644 --- a/crates/executor/src/scheduler/address_resolution.rs +++ b/crates/executor/src/scheduler/address_resolution.rs @@ -10,8 +10,8 @@ pub(crate) mod system; /// Optional IP endpoint resolution, independent of socket creation and connection. Implementations may use a system resolver, an asynchronous resolver, or a deterministic table. /// /// ```compile_fail -/// use socketry_executor::{AddressResolution, AddressHints, AddressService, Cancellation, Socket}; -/// async fn resolve(scheduler: &S) { +/// use socketry_executor::{AddressResolution, AddressHints, AddressService, Cancellation, SocketScheduler}; +/// async fn resolve(scheduler: &S) { /// scheduler.address_resolve(Some("localhost"), AddressService::Port(80), AddressHints::default(), &Cancellation::never()).await; /// } /// ``` diff --git a/crates/executor/src/scheduler/file.rs b/crates/executor/src/scheduler/file.rs deleted file mode 100644 index a3b45c0..0000000 --- a/crates/executor/src/scheduler/file.rs +++ /dev/null @@ -1,115 +0,0 @@ -// Released under the MIT License. -// Copyright, 2026, by Samuel Williams. - -use crate::{ - BufferResult, Cancellation, FileMetadata, FileOptions, IoBuf, IoBufMut, OperationResult, - SyncMode, -}; -use std::{future::Future, io::SeekFrom, path::Path}; - -/// Regular-file creation, metadata, synchronization, shared-cursor operations, and explicit-offset operations through the chosen scheduler. The backend retains resources used by accepted operations even when the waiting future is destroyed. -/// -/// `file_read` and `file_write` use and advance the resource's shared cursor by the transferred byte count. Await operations sequentially when ordering matters: concurrent operations have no portable ordering or non-overlap guarantee. Dropping a submitted future does not roll back transferred bytes or cursor changes. -/// -/// Explicit offsets must fit in `i64`. On Unix, positioned operations leave the shared cursor unchanged; the current Windows fallback uses `std`'s `seek_read`/`seek_write` and changes it. Cancellation before execution prevents I/O. Once a blocking call starts, completion can win cancellation; buffers are returned only after access ends. -pub trait File: Send + Sync { - type File: Send + Sync; - type Metadata: FileMetadata; - - fn file_open( - &self, - path: &Path, - options: FileOptions, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Consume an exclusively owned handle, wait for bounded admission and OS close completion, and report admission or close errors. This future is lazy and takes no cancellation token. Close does not synchronize file contents and is never retried after an error. - /// - /// Finish all operations using the file first. Requesting cancellation or dropping an operation future alone does not establish that backend access has ended. Once admitted, close runs even if this future is dropped. Before admission, dropping the future releases the handle through ordinary destruction. - /// - /// # Panics - /// Backends may panic if an abandoned operation still retains the file. Close never interrupts or joins other operations. - fn file_close(&self, file: Self::File) -> impl Future> + Send; - - /// Consume an exclusively owned handle and wait only for bounded admission. The scheduler then owns completion even if this future is dropped. Errors report admission failure, not OS close failure; await `file_close` when execution errors matter. - /// - /// The ownership, laziness, and outstanding-operation preconditions are the same as [`File::file_close`]. Graceful scheduler shutdown drains accepted close jobs. - fn file_close_submit( - &self, - file: Self::File, - ) -> impl Future> + Send; - - /// Query the already-open resource and return owned metadata without moving its cursor. The result is an observation, not an atomic snapshot: await preceding modifications when ordering matters. Path lookup and symlink-following policy belong to resource creation, not this operation. - /// - /// The future is lazy. Cancellation before dispatch prevents submission; blocking workers check again before execution. Started blocking calls preserve their actual result. Ring cancellation is best effort and waits for original completion. The backend retains the file until access ends, even if the waiting future is dropped. - fn file_stat( - &self, - file: &Self::File, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Set the logical file length, discarding bytes when shrinking and filling an extension with zeros. The file must be open for writing. Lengths must fit in `i64`; filesystem limits and other OS errors are reported. The shared cursor is unchanged, even when it ends up past EOF. - /// - /// The future is lazy. Cancellation before dispatch prevents submission; blocking workers check again before execution. Once submitted, cancellation is best effort and successful completion wins a concurrent request. The backend retains the file until access ends, and dropping a waiter cannot undo a completed resize. - /// - /// Await reads, writes, seeks, and truncation sequentially when ordering matters. This operation does not guarantee disk-space reservation or durability; use `file_sync` after completion when synchronization is needed. - fn file_truncate( - &self, - file: &Self::File, - length: u64, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Synchronize file contents and selected metadata, waiting for OS completion and reporting errors. This leaves the shared cursor unchanged. Await preceding writes first; this call does not order concurrent operations or flush application buffers. - /// - /// The future is lazy. Cancellation before dispatch prevents submission; blocking workers check again before execution. Once submitted, cancellation is best effort, and a successful completion wins a concurrent cancellation request. Dropping the future cannot undo synchronization; the backend retains the file until access ends. - /// - /// Durability follows the platform, filesystem, and device guarantees. `SyncMode::Data` may perform full synchronization. Synchronizing a file does not synchronize its parent directory entry. - fn file_sync( - &self, - file: &Self::File, - mode: SyncMode, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Reposition the shared cursor and return its resulting byte offset. `SeekFrom::Current(0)` queries the position; seeking past EOF does not extend the file. Absolute offsets must fit in `i64`; invalid positions and OS limits return errors. - /// - /// Await cursor operations sequentially when ordering matters. A seek followed by a read or write is not atomic with respect to other users of the resource. Cancellation before execution prevents seeking; once started, the actual result is preserved, and dropping the future does not undo the seek. - fn file_seek( - &self, - file: &Self::File, - position: SeekFrom, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Read once from the shared cursor, returning a possibly short count and the original buffer. - fn file_read( - &self, - file: &Self::File, - buffer: B, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Write once at the shared cursor, returning a possibly short count and the original buffer. This does not guarantee durability. - fn file_write( - &self, - file: &Self::File, - buffer: B, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - fn file_read_at( - &self, - file: &Self::File, - buffer: B, - offset: u64, - cancellation: &Cancellation, - ) -> impl Future> + Send; - fn file_write_at( - &self, - file: &Self::File, - buffer: B, - offset: u64, - cancellation: &Cancellation, - ) -> impl Future> + Send; -} diff --git a/crates/executor/src/scheduler/file_advice.rs b/crates/executor/src/scheduler/file_advice.rs index 18e21c2..e89f19c 100644 --- a/crates/executor/src/scheduler/file_advice.rs +++ b/crates/executor/src/scheduler/file_advice.rs @@ -1,14 +1,32 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use super::File; +use super::FileOperation; use crate::AdviceMode; use std::{future::Future, io}; /// Optional file advice capability, following Linux `posix_fadvise` semantics. /// /// Native and Tokio implementations currently provide this capability on Linux. Other targets omit the implementation so consumers can require it at compile time. The OS may ignore advice; success promises neither prefetch completion nor eviction. Filesystem-specific errors are preserved. -pub trait FileAdvice: File { +/// +/// An advice-only adapter does not need to forward the core file operations: +/// +/// ``` +/// use socketry_executor::{AdviceMode, FileAdvice, FileOperation}; +/// struct AdviceOnly(S); +/// impl FileOperation for AdviceOnly { +/// type File = S::File; +/// } +/// impl FileAdvice for AdviceOnly { +/// async fn file_advise(&self, file: &Self::File, offset: u64, length: u64, advice: AdviceMode) -> std::io::Result<()> { +/// self.0.file_advise(file, offset, length, advice).await +/// } +/// async fn file_advise_submit(&self, file: &Self::File, offset: u64, length: u64, advice: AdviceMode) -> std::io::Result<()> { +/// self.0.file_advise_submit(file, offset, length, advice).await +/// } +/// } +/// ``` +pub trait FileAdvice: FileOperation { /// Apply an access or cache hint and await OS completion, reporting admission and execution errors. A zero length means from `offset` through EOF; otherwise the range is `[offset, offset + length)`. The offset and range end must fit in `i64`, or admission fails with `InvalidInput`. /// /// Access-policy hints may affect the whole open file description rather than only the supplied range. diff --git a/crates/executor/src/scheduler/file_allocate.rs b/crates/executor/src/scheduler/file_allocate.rs index 88935a6..9a6f063 100644 --- a/crates/executor/src/scheduler/file_allocate.rs +++ b/crates/executor/src/scheduler/file_allocate.rs @@ -1,14 +1,29 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use super::File; +use super::FileOperation; use crate::{AllocationMode, Cancellation, OperationResult}; use std::future::Future; /// Optional file-space reservation capability, with Linux `fallocate` semantics for ordinary allocation and `KEEP_SIZE`. /// /// Native and Tokio implementations currently provide this capability on Linux. Other targets omit the implementation, allowing generic consumers to require support at compile time. Filesystem and per-file support still vary at runtime; their errors are preserved. Allocation is not emulated by resizing or writing zeros. -pub trait FileAllocate: File { +/// +/// An allocation-only adapter does not need to forward the core file operations: +/// +/// ``` +/// use socketry_executor::{AllocationMode, Cancellation, FileAllocate, FileOperation, OperationResult}; +/// struct AllocationOnly(S); +/// impl FileOperation for AllocationOnly { +/// type File = S::File; +/// } +/// impl FileAllocate for AllocationOnly { +/// async fn file_allocate(&self, file: &Self::File, offset: u64, length: u64, mode: AllocationMode, cancellation: &Cancellation) -> OperationResult<()> { +/// self.0.file_allocate(file, offset, length, mode, cancellation).await +/// } +/// } +/// ``` +pub trait FileAllocate: FileOperation { /// Reserve backing space for `[offset, offset + length)` on a writable file. `Extend` grows the logical length when necessary; `KeepSize` preserves it. Both modes preserve existing contents and the shared cursor. Subsequent writes in the reserved range have the underlying filesystem's allocation guarantee against running out of space. /// /// Length must be positive and the range end must fit in `i64`; invalid ranges return `InvalidInput`. Filesystem limits and errors, including unsupported allocation, are reported. This does not unshare copy-on-write extents, guarantee durability, or order concurrent file operations. Await related operations sequentially and use `file_sync` for synchronization. diff --git a/crates/executor/src/scheduler/file_close.rs b/crates/executor/src/scheduler/file_close.rs index 09b4476..3f8f792 100644 --- a/crates/executor/src/scheduler/file_close.rs +++ b/crates/executor/src/scheduler/file_close.rs @@ -1,23 +1,24 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use super::cleanup::{self, Completion, wait}; -use crate::{FileDescriptor, blocking_pool::BlockingPool}; -use std::io; +use super::FileOperation; +use std::future::Future; -pub(crate) async fn close(pool: &BlockingPool, file: FileDescriptor) -> io::Result<()> { - wait(submit(pool, file).await?).await -} - -pub(crate) async fn submit_only(pool: &BlockingPool, file: FileDescriptor) -> io::Result<()> { - submit(pool, file).await.map(drop) -} +/// Close exclusively owned file resources with completion or admission-only semantics. +pub trait FileClose: FileOperation { + /// Consume an exclusively owned handle, wait for bounded admission and OS close completion, and report admission or close errors. This future is lazy and takes no cancellation token. Close does not synchronize file contents and is never retried after an error. + /// + /// Finish all operations using the file first. Requesting cancellation or dropping an operation future alone does not establish that backend access has ended. Once admitted, close runs even if this future is dropped. Before admission, dropping the future releases the handle through ordinary destruction. + /// + /// # Panics + /// Backends may panic if an abandoned operation still retains the file. Close never interrupts or joins other operations. + fn file_close(&self, file: Self::File) -> impl Future> + Send; -async fn submit(pool: &BlockingPool, file: FileDescriptor) -> io::Result { - // Establish exclusivity before admission. Never wait for or interrupt other uses of this handle. - let file = file.into_file(); - cleanup::submit(pool, move || super::file_io::close(file)).await + /// Consume an exclusively owned handle and wait only for bounded admission. The scheduler then owns completion even if this future is dropped. Errors report admission failure, not OS close failure; await `file_close` when execution errors matter. + /// + /// The ownership, laziness, and outstanding-operation preconditions are the same as [`FileClose::file_close`]. Graceful scheduler shutdown drains accepted close jobs. + fn file_close_submit( + &self, + file: Self::File, + ) -> impl Future> + Send; } - -#[cfg(test)] -mod tests; diff --git a/crates/executor/src/scheduler/file_close_io.rs b/crates/executor/src/scheduler/file_close_io.rs new file mode 100644 index 0000000..09b4476 --- /dev/null +++ b/crates/executor/src/scheduler/file_close_io.rs @@ -0,0 +1,23 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::cleanup::{self, Completion, wait}; +use crate::{FileDescriptor, blocking_pool::BlockingPool}; +use std::io; + +pub(crate) async fn close(pool: &BlockingPool, file: FileDescriptor) -> io::Result<()> { + wait(submit(pool, file).await?).await +} + +pub(crate) async fn submit_only(pool: &BlockingPool, file: FileDescriptor) -> io::Result<()> { + submit(pool, file).await.map(drop) +} + +async fn submit(pool: &BlockingPool, file: FileDescriptor) -> io::Result { + // Establish exclusivity before admission. Never wait for or interrupt other uses of this handle. + let file = file.into_file(); + cleanup::submit(pool, move || super::file_io::close(file)).await +} + +#[cfg(test)] +mod tests; diff --git a/crates/executor/src/scheduler/file_close/tests.rs b/crates/executor/src/scheduler/file_close_io/tests.rs similarity index 100% rename from crates/executor/src/scheduler/file_close/tests.rs rename to crates/executor/src/scheduler/file_close_io/tests.rs diff --git a/crates/executor/src/scheduler/file_open.rs b/crates/executor/src/scheduler/file_open.rs new file mode 100644 index 0000000..981a415 --- /dev/null +++ b/crates/executor/src/scheduler/file_open.rs @@ -0,0 +1,16 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::FileOperation; +use crate::{Cancellation, FileOptions, OperationResult}; +use std::{future::Future, path::Path}; + +/// Create file resources through the chosen scheduler. +pub trait FileOpen: FileOperation { + fn file_open( + &self, + path: &Path, + options: FileOptions, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/file_operation.rs b/crates/executor/src/scheduler/file_operation.rs new file mode 100644 index 0000000..9d04b48 --- /dev/null +++ b/crates/executor/src/scheduler/file_operation.rs @@ -0,0 +1,21 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +/// Shared resource type for file operations provided by a scheduler. +/// +/// Implement this trait on the scheduler, not the descriptor. All file operation traits use this one associated type, allowing generic consumers to combine capabilities without resource-type equality bounds. Implementations choose their own representation; no OS descriptor or shared-ownership wrapper is required by the interface. +/// +/// A submitted operation must retain every resource and buffer it can still access, even if its waiting future is dropped. Applications finish outstanding work before explicitly closing a resource; close never cancels or joins other operations. +/// +/// Selecting a resource type does not supply any operations: +/// +/// ```compile_fail,E0599 +/// use socketry_executor::{Cancellation, FileOperation, FileRead}; +/// async fn read(scheduler: &S, file: &S::File) { +/// scheduler.file_read(file, vec![0; 16], &Cancellation::never()).await; +/// } +/// ``` +pub trait FileOperation: Send + Sync { + /// The file resource accepted by this scheduler's file operations. + type File: Send + Sync; +} diff --git a/crates/executor/src/scheduler/file_read.rs b/crates/executor/src/scheduler/file_read.rs new file mode 100644 index 0000000..312fda8 --- /dev/null +++ b/crates/executor/src/scheduler/file_read.rs @@ -0,0 +1,37 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::FileOperation; +use crate::{BufferResult, Cancellation, IoBufMut}; +use std::future::Future; + +/// Read from a file resource's shared cursor. Successful reads advance it by the transferred byte count. Await cursor operations sequentially when ordering matters: concurrent operations have no portable ordering or non-overlap guarantee. Dropping a submitted future cannot roll back transferred bytes or cursor changes. +/// +/// Cancellation before execution prevents I/O. Once a blocking call starts, completion can win cancellation; the backend retains the resource and buffer until access ends, including after the waiting future is dropped. Buffers are returned on both success and failure. +/// +/// A read-only consumer needs no creation, writing, metadata, or close capability: +/// +/// ``` +/// use socketry_executor::{BufferResult, Cancellation, FileRead}; +/// async fn read_chunk(scheduler: &S, file: &S::File) -> BufferResult { +/// scheduler.file_read(file, vec![0; 16], &Cancellation::never()).await +/// } +/// ``` +/// +/// Writing requires an explicit additional bound, even with the trait in scope: +/// +/// ```compile_fail,E0599 +/// use socketry_executor::{Cancellation, FileRead, FileWrite}; +/// async fn write(scheduler: &S, file: &S::File) { +/// scheduler.file_write(file, vec![1], &Cancellation::never()).await; +/// } +/// ``` +pub trait FileRead: FileOperation { + /// Read once from the shared cursor, returning a possibly short count and the original buffer. + fn file_read( + &self, + file: &Self::File, + buffer: B, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/file_read_at.rs b/crates/executor/src/scheduler/file_read_at.rs new file mode 100644 index 0000000..c86a27f --- /dev/null +++ b/crates/executor/src/scheduler/file_read_at.rs @@ -0,0 +1,19 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::FileOperation; +use crate::{BufferResult, Cancellation, IoBufMut}; +use std::future::Future; + +/// Read at an explicit byte offset, which must fit in `i64`. Unix implementations leave the shared cursor unchanged; the current Windows fallback uses `std`'s `seek_read` and changes it. +/// +/// Cancellation before execution prevents I/O. Once a blocking call starts, completion can win cancellation; the backend retains the resource and buffer until access ends, including after the waiting future is dropped. Buffers are returned on both success and failure. +pub trait FileReadAt: FileOperation { + fn file_read_at( + &self, + file: &Self::File, + buffer: B, + offset: u64, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/file_scheduler.rs b/crates/executor/src/scheduler/file_scheduler.rs new file mode 100644 index 0000000..b1f7866 --- /dev/null +++ b/crates/executor/src/scheduler/file_scheduler.rs @@ -0,0 +1,56 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::{ + FileClose, FileOpen, FileRead, FileReadAt, FileSeek, FileStat, FileSync, FileTruncate, + FileWrite, FileWriteAt, +}; + +/// Convenience aggregate for the core file operations provided by a scheduler. +/// +/// This trait is implemented automatically for schedulers providing every core operation. Consumers can instead require individual capabilities such as `FileRead + FileWrite`; optional `FileAllocate` and `FileAdvice` remain outside this aggregate. +/// +/// Implement `FileOperation` to select the resource type, `FileStat` to select its metadata representation, and each operation trait to provide its methods. Import the operation traits for method calls on concrete scheduler values; a generic `S: FileScheduler` bound includes all core methods. +/// +/// Optional platform capabilities are not implied by this aggregate: +/// +/// ```compile_fail,E0599 +/// use socketry_executor::{AdviceMode, FileScheduler, FileAdvice}; +/// async fn advise(scheduler: &S, file: &S::File) { +/// scheduler.file_advise(file, 0, 0, AdviceMode::Normal).await; +/// } +/// ``` +/// +/// ```compile_fail,E0599 +/// use socketry_executor::{AllocationMode, Cancellation, FileScheduler, FileAllocate}; +/// async fn allocate(scheduler: &S, file: &S::File) { +/// scheduler.file_allocate(file, 0, 1, AllocationMode::Extend, &Cancellation::never()).await; +/// } +/// ``` +pub trait FileScheduler: + FileOpen + + FileClose + + FileStat + + FileTruncate + + FileSync + + FileSeek + + FileRead + + FileWrite + + FileReadAt + + FileWriteAt +{ +} + +impl FileScheduler for S where + S: FileOpen + + FileClose + + FileStat + + FileTruncate + + FileSync + + FileSeek + + FileRead + + FileWrite + + FileReadAt + + FileWriteAt +{ +} diff --git a/crates/executor/src/scheduler/file_seek.rs b/crates/executor/src/scheduler/file_seek.rs new file mode 100644 index 0000000..f5f189e --- /dev/null +++ b/crates/executor/src/scheduler/file_seek.rs @@ -0,0 +1,19 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::FileOperation; +use crate::{Cancellation, OperationResult}; +use std::{future::Future, io::SeekFrom}; + +/// Inspect or reposition a file resource's shared cursor. +pub trait FileSeek: FileOperation { + /// Reposition the shared cursor and return its resulting byte offset. `SeekFrom::Current(0)` queries the position; seeking past EOF does not extend the file. Absolute offsets must fit in `i64`; invalid positions and OS limits return errors. + /// + /// Await cursor operations sequentially when ordering matters. A seek followed by a read or write is not atomic with respect to other users of the resource. Cancellation before execution prevents seeking; once started, the actual result is preserved, and dropping the future does not undo the seek. + fn file_seek( + &self, + file: &Self::File, + position: SeekFrom, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/file_stat.rs b/crates/executor/src/scheduler/file_stat.rs new file mode 100644 index 0000000..e167548 --- /dev/null +++ b/crates/executor/src/scheduler/file_stat.rs @@ -0,0 +1,21 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::FileOperation; +use crate::{Cancellation, FileMetadata, OperationResult}; +use std::future::Future; + +/// Query owned metadata for an already-open file resource. +pub trait FileStat: FileOperation { + /// Owned metadata representation chosen by this implementation. + type Metadata: FileMetadata; + + /// Query the already-open resource and return owned metadata without moving its cursor. The result is an observation, not an atomic snapshot: await preceding modifications when ordering matters. Path lookup and symlink-following policy belong to resource creation, not this operation. + /// + /// The future is lazy. Cancellation before dispatch prevents submission; blocking workers check again before execution. Started blocking calls preserve their actual result. Ring cancellation is best effort and waits for original completion. The backend retains the file until access ends, even if the waiting future is dropped. + fn file_stat( + &self, + file: &Self::File, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/file_sync.rs b/crates/executor/src/scheduler/file_sync.rs new file mode 100644 index 0000000..b18d6b6 --- /dev/null +++ b/crates/executor/src/scheduler/file_sync.rs @@ -0,0 +1,21 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::FileOperation; +use crate::{Cancellation, OperationResult, SyncMode}; +use std::future::Future; + +/// Synchronize file contents and metadata through the chosen scheduler. +pub trait FileSync: FileOperation { + /// Synchronize file contents and selected metadata, waiting for OS completion and reporting errors. This leaves the shared cursor unchanged. Await preceding writes first; this call does not order concurrent operations or flush application buffers. + /// + /// The future is lazy. Cancellation before dispatch prevents submission; blocking workers check again before execution. Once submitted, cancellation is best effort, and a successful completion wins a concurrent cancellation request. Dropping the future cannot undo synchronization; the backend retains the file until access ends. + /// + /// Durability follows the platform, filesystem, and device guarantees. `SyncMode::Data` may perform full synchronization. Synchronizing a file does not synchronize its parent directory entry. + fn file_sync( + &self, + file: &Self::File, + mode: SyncMode, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/file_truncate.rs b/crates/executor/src/scheduler/file_truncate.rs new file mode 100644 index 0000000..32e408c --- /dev/null +++ b/crates/executor/src/scheduler/file_truncate.rs @@ -0,0 +1,21 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::FileOperation; +use crate::{Cancellation, OperationResult}; +use std::future::Future; + +/// Change the logical length of an already-open file resource. +pub trait FileTruncate: FileOperation { + /// Set the logical file length, discarding bytes when shrinking and filling an extension with zeros. The file must be open for writing. Lengths must fit in `i64`; filesystem limits and other OS errors are reported. The shared cursor is unchanged, even when it ends up past EOF. + /// + /// The future is lazy. Cancellation before dispatch prevents submission; blocking workers check again before execution. Once submitted, cancellation is best effort and successful completion wins a concurrent request. The backend retains the file until access ends, and dropping a waiter cannot undo a completed resize. + /// + /// Await reads, writes, seeks, and truncation sequentially when ordering matters. This operation does not guarantee disk-space reservation or durability; use `file_sync` after completion when synchronization is needed. + fn file_truncate( + &self, + file: &Self::File, + length: u64, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/file_write.rs b/crates/executor/src/scheduler/file_write.rs new file mode 100644 index 0000000..71fe3d4 --- /dev/null +++ b/crates/executor/src/scheduler/file_write.rs @@ -0,0 +1,19 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::FileOperation; +use crate::{BufferResult, Cancellation, IoBuf}; +use std::future::Future; + +/// Write at a file resource's shared cursor. Successful writes advance it by the transferred byte count. Await cursor operations sequentially when ordering matters: concurrent operations have no portable ordering or non-overlap guarantee. Dropping a submitted future cannot roll back transferred bytes or cursor changes. +/// +/// Cancellation before execution prevents I/O. Once a blocking call starts, completion can win cancellation; the backend retains the resource and buffer until access ends, including after the waiting future is dropped. Buffers are returned on both success and failure. +pub trait FileWrite: FileOperation { + /// Write once at the shared cursor, returning a possibly short count and the original buffer. This does not guarantee durability. + fn file_write( + &self, + file: &Self::File, + buffer: B, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/file_write_at.rs b/crates/executor/src/scheduler/file_write_at.rs new file mode 100644 index 0000000..b4a3255 --- /dev/null +++ b/crates/executor/src/scheduler/file_write_at.rs @@ -0,0 +1,19 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::FileOperation; +use crate::{BufferResult, Cancellation, IoBuf}; +use std::future::Future; + +/// Write at an explicit byte offset, which must fit in `i64`. Unix implementations leave the shared cursor unchanged; the current Windows fallback uses `std`'s `seek_write` and changes it. +/// +/// Cancellation before execution prevents I/O. Once a blocking call starts, completion can win cancellation; the backend retains the resource and buffer until access ends, including after the waiting future is dropped. Buffers are returned on both success and failure. +pub trait FileWriteAt: FileOperation { + fn file_write_at( + &self, + file: &Self::File, + buffer: B, + offset: u64, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/selector/io_uring.rs b/crates/executor/src/scheduler/selector/io_uring.rs index c45d980..f4c4fae 100644 --- a/crates/executor/src/scheduler/selector/io_uring.rs +++ b/crates/executor/src/scheduler/selector/io_uring.rs @@ -15,9 +15,13 @@ //! //! Allocation and truncation use native requests when their opcodes are available, otherwise the blocking file fallback preserves the same public contract. File advice uses bounded cleanup workers so accepted jobs survive ring shutdown; native advice SQEs remain future work. use super::readiness::{self, SocketDescriptor}; -use crate::scheduler::{BufferResult, Clock, File, Interest, Socket as SocketOperations}; +use crate::scheduler::{BufferResult, Clock, Interest, SocketScheduler}; use crate::{AllocationMode, FileAllocate, FileDescriptor, SyncMode}; use crate::{Cancellation, FileOptions, IoBuf, IoBufMut, OperationResult, SocketRegistration}; +use crate::{ + FileClose, FileOpen, FileRead, FileReadAt, FileSeek, FileStat, FileSync, FileTruncate, + FileWrite, FileWriteAt, +}; use event_listener::{Event as CompletionEvent, Listener as _}; use futures_channel::oneshot; use io_uring::{IoUring, opcode, squeue, types}; @@ -706,7 +710,7 @@ impl SocketRegistration for Selector { readiness::Selector.register_listener(listener) } } -impl SocketOperations for Selector { +impl SocketScheduler for Selector { type Socket = SocketDescriptor; async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { @@ -945,19 +949,23 @@ impl crate::FileAdvice for Selector { } } -impl File for Selector { +impl crate::FileOperation for Selector { type File = FileDescriptor; - type Metadata = crate::StatxMetadata; +} + +impl FileClose for Selector { async fn file_close(&self, file: Self::File) -> io::Result<()> { self.check_open()?; - crate::scheduler::file_close::close(&self.controller.cleanup, file).await + crate::scheduler::file_close_io::close(&self.controller.cleanup, file).await } async fn file_close_submit(&self, file: Self::File) -> io::Result<()> { self.check_open()?; - crate::scheduler::file_close::submit_only(&self.controller.cleanup, file).await + crate::scheduler::file_close_io::submit_only(&self.controller.cleanup, file).await } +} +impl FileTruncate for Selector { async fn file_truncate( &self, file: &Self::File, @@ -976,6 +984,10 @@ impl File for Selector { .await } } +} + +impl FileStat for Selector { + type Metadata = crate::StatxMetadata; async fn file_stat( &self, @@ -984,7 +996,9 @@ impl File for Selector { ) -> OperationResult { self.stat(file, cancellation).await } +} +impl FileSync for Selector { async fn file_sync( &self, file: &Self::File, @@ -994,7 +1008,9 @@ impl File for Selector { self.file_operation(file, FileOperation::Sync(mode), cancellation) .await } +} +impl FileSeek for Selector { async fn file_seek( &self, file: &Self::File, @@ -1007,7 +1023,9 @@ impl File for Selector { .file_seek(file, position, cancellation) .await } +} +impl FileOpen for Selector { async fn file_open( &self, path: &Path, @@ -1019,6 +1037,9 @@ impl File for Selector { .file_open(path, options, cancellation) .await } +} + +impl FileRead for Selector { async fn file_read( &self, file: &Self::File, @@ -1036,6 +1057,9 @@ impl File for Selector { .await; (operation_result(result, cancellation), buffer) } +} + +impl FileWrite for Selector { async fn file_write( &self, file: &Self::File, @@ -1053,6 +1077,9 @@ impl File for Selector { .await; (operation_result(result, cancellation), buffer) } +} + +impl FileReadAt for Selector { async fn file_read_at( &self, file: &Self::File, @@ -1071,6 +1098,9 @@ impl File for Selector { .await; (operation_result(result, cancellation), buffer) } +} + +impl FileWriteAt for Selector { async fn file_write_at( &self, file: &Self::File, diff --git a/crates/executor/src/scheduler/selector/readiness.rs b/crates/executor/src/scheduler/selector/readiness.rs index 2bedb05..758a2c1 100644 --- a/crates/executor/src/scheduler/selector/readiness.rs +++ b/crates/executor/src/scheduler/selector/readiness.rs @@ -9,10 +9,14 @@ //! coroutine stacks and may be polled on different workers. use crate::scheduler::file_io::{read_at, write_at}; use crate::scheduler::operation::cancellable; -use crate::scheduler::{BufferResult, Clock, File, Interest, Socket as SocketOperations}; +use crate::scheduler::{BufferResult, Clock, Interest, SocketScheduler}; #[cfg(target_os = "linux")] use crate::{AllocationMode, FileAllocate}; use crate::{Cancellation, FileOptions, IoBuf, IoBufMut, OperationResult, SocketRegistration}; +use crate::{ + FileClose, FileOpen, FileOperation, FileRead, FileReadAt, FileSeek, FileStat, FileSync, + FileTruncate, FileWrite, FileWriteAt, +}; use crate::{FileDescriptor, SyncMode}; use crate::{SocketMessage, SocketMessages, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; use std::io::{self, SeekFrom}; @@ -91,7 +95,7 @@ impl SocketRegistration for Selector { } } -impl SocketOperations for Selector { +impl SocketScheduler for Selector { type Socket = SocketDescriptor; async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { @@ -355,22 +359,26 @@ impl crate::FileAdvice for Selector { } } -impl File for Selector { +impl FileOperation for Selector { type File = FileDescriptor; - type Metadata = crate::SystemFileMetadata; +} + +impl FileClose for Selector { async fn file_close(&self, file: Self::File) -> io::Result<()> { - crate::scheduler::file_close::close(crate::scheduler::cleanup::standalone_pool(), file) + crate::scheduler::file_close_io::close(crate::scheduler::cleanup::standalone_pool(), file) .await } async fn file_close_submit(&self, file: Self::File) -> io::Result<()> { - crate::scheduler::file_close::submit_only( + crate::scheduler::file_close_io::submit_only( crate::scheduler::cleanup::standalone_pool(), file, ) .await } +} +impl FileTruncate for Selector { async fn file_truncate( &self, file: &Self::File, @@ -387,6 +395,10 @@ impl File for Selector { }) .await } +} + +impl FileStat for Selector { + type Metadata = crate::SystemFileMetadata; async fn file_stat( &self, @@ -403,6 +415,9 @@ impl File for Selector { }) .await } +} + +impl FileSync for Selector { async fn file_sync( &self, file: &Self::File, @@ -419,7 +434,9 @@ impl File for Selector { }) .await } +} +impl FileSeek for Selector { async fn file_seek( &self, file: &Self::File, @@ -436,7 +453,9 @@ impl File for Selector { }) .await } +} +impl FileOpen for Selector { async fn file_open( &self, path: &Path, @@ -453,6 +472,9 @@ impl File for Selector { }) .await } +} + +impl FileRead for Selector { async fn file_read( &self, file: &Self::File, @@ -465,6 +487,9 @@ impl File for Selector { }) .await } +} + +impl FileWrite for Selector { async fn file_write( &self, file: &Self::File, @@ -477,6 +502,9 @@ impl File for Selector { }) .await } +} + +impl FileReadAt for Selector { async fn file_read_at( &self, file: &Self::File, @@ -490,6 +518,9 @@ impl File for Selector { }) .await } +} + +impl FileWriteAt for Selector { async fn file_write_at( &self, file: &Self::File, diff --git a/crates/executor/src/scheduler/socket_messages.rs b/crates/executor/src/scheduler/socket_messages.rs index 95ac093..1c1919d 100644 --- a/crates/executor/src/scheduler/socket_messages.rs +++ b/crates/executor/src/scheduler/socket_messages.rs @@ -2,12 +2,12 @@ // Copyright, 2026, by Samuel Williams. use crate::{ - Cancellation, IoBuf, IoBufMut, OperationResult, Socket, SocketMessage, SocketRecvFlags, - SocketRecvResult, SocketSendFlags, + Cancellation, IoBuf, IoBufMut, OperationResult, SocketMessage, SocketRecvFlags, + SocketRecvResult, SocketScheduler, SocketSendFlags, }; use std::future::Future; -/// Scatter/gather message I/O with IP addresses. Generic consumers require this capability separately from `Socket`. +/// Scatter/gather message I/O with IP addresses. Generic consumers require this capability separately from `SocketScheduler`. /// /// Operations are lazy, check cancellation before I/O, and return the original buffer owners on every result. Cancellation can race successful progress; success wins. Completion backends retain payload and native metadata until the original operation's terminal completion, including after waiter abandonment. Cancel and await outstanding operations before explicit close. /// @@ -16,12 +16,12 @@ use std::future::Future; /// This capability carries payload and IP addresses only. It cannot send ancillary data and supplies no receive control storage. Unix reports `MSG_CTRUNC` through the result; the Windows payload/address fallback cannot observe control metadata. Ancillary data requires a separate capability rather than silent emulation. Unsupported flags are not part of this portable interface. /// A socket backend must explicitly implement message support: /// ```compile_fail -/// use socketry_executor::{Cancellation, Socket, SocketMessages, SocketRecvFlags}; -/// async fn receive(scheduler: &S, socket: &S::Socket) { +/// use socketry_executor::{Cancellation, SocketScheduler, SocketMessages, SocketRecvFlags}; +/// async fn receive(scheduler: &S, socket: &S::Socket) { /// scheduler.socket_recvmsg(socket, vec![vec![0; 1024]], SocketRecvFlags::default(), &Cancellation::never()).await; /// } /// ``` -pub trait SocketMessages: Socket { +pub trait SocketMessages: SocketScheduler { fn socket_sendmsg( &self, socket: &Self::Socket, diff --git a/crates/executor/src/scheduler/socket_options.rs b/crates/executor/src/scheduler/socket_options.rs index f095fd2..0aff066 100644 --- a/crates/executor/src/scheduler/socket_options.rs +++ b/crates/executor/src/scheduler/socket_options.rs @@ -1,7 +1,7 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use crate::{Cancellation, OperationResult, Socket, SocketOption}; +use crate::{Cancellation, OperationResult, SocketOption, SocketScheduler}; use std::future::Future; /// Typed get/set capability for one socket option. Generic consumers require only the options they use, such as `S: SocketOptions`. @@ -19,12 +19,12 @@ use std::future::Future; /// ``` /// Merely supporting sockets does not imply support for every option: /// ```compile_fail -/// use socketry_executor::{Cancellation, Socket, SocketOptions, TcpNoDelay}; -/// async fn configure(scheduler: &S, socket: &S::Socket) { +/// use socketry_executor::{Cancellation, SocketScheduler, SocketOptions, TcpNoDelay}; +/// async fn configure(scheduler: &S, socket: &S::Socket) { /// scheduler.socket_set_option(socket, TcpNoDelay, true, &Cancellation::never()).await; /// } /// ``` -pub trait SocketOptions: Socket { +pub trait SocketOptions: SocketScheduler { /// Query the option's current value, preserving OS errors. fn socket_get_option( &self, diff --git a/crates/executor/src/scheduler/socket_registration.rs b/crates/executor/src/scheduler/socket_registration.rs index 01273a2..4a70e65 100644 --- a/crates/executor/src/scheduler/socket_registration.rs +++ b/crates/executor/src/scheduler/socket_registration.rs @@ -1,7 +1,7 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use super::Socket; +use super::SocketScheduler; use std::{ io, net::{TcpListener, TcpStream}, @@ -10,7 +10,7 @@ use std::{ /// Optional interoperability with existing standard-library TCP resources. Streams and listeners become the same backend socket descriptor. /// Register once; the backend sets the nonblocking mode it needs. Do not change /// that mode through another handle. Simulated schedulers need not implement it. -pub trait SocketRegistration: Socket { +pub trait SocketRegistration: SocketScheduler { fn register_socket(&self, socket: TcpStream) -> io::Result; fn register_listener(&self, listener: TcpListener) -> io::Result; } diff --git a/crates/executor/src/scheduler/socket.rs b/crates/executor/src/scheduler/socket_scheduler.rs similarity index 99% rename from crates/executor/src/scheduler/socket.rs rename to crates/executor/src/scheduler/socket_scheduler.rs index 323e696..a719249 100644 --- a/crates/executor/src/scheduler/socket.rs +++ b/crates/executor/src/scheduler/socket_scheduler.rs @@ -16,7 +16,7 @@ use std::{ /// Creation, binding, listening, and connecting are separate operations. All operation futures are lazy. Pre-cancellation prevents a syscall; successful progress wins a cancellation race. Cancelling or abandoning a pending connect does not roll back kernel connection state: discard that socket before trying another connection. Completion backends retain owned buffers until the original operation completes. /// /// File and socket resources remain distinct. There is no common descriptor trait, protocol buffering, or automatic close coordination. Message/address I/O belongs to the optional `SocketMessages` capability. Non-IP families remain an extension. -pub trait Socket: Send + Sync { +pub trait SocketScheduler: Send + Sync { type Socket: Send + Sync; /// Consume and close an exclusively owned socket, awaiting bounded admission and OS completion. Finish all other operations first: close never cancels or joins them, and retained backend use may panic. Dropping an unpolled close releases the owned descriptor through its ordinary destructor. diff --git a/crates/executor/src/scheduler/socketry/operations.rs b/crates/executor/src/scheduler/socketry/operations.rs index 02ae23b..0187c0e 100644 --- a/crates/executor/src/scheduler/socketry/operations.rs +++ b/crates/executor/src/scheduler/socketry/operations.rs @@ -5,10 +5,14 @@ use super::{Scheduler, SchedulerHandle}; use crate::SyncMode; use crate::scheduler::selector::DefaultSelector; -use crate::scheduler::{BufferResult, Clock, File, Interest, Socket}; +use crate::scheduler::{BufferResult, Clock, Interest, SocketScheduler}; #[cfg(target_os = "linux")] use crate::{AllocationMode, FileAllocate}; use crate::{Cancellation, FileOptions, IoBuf, IoBufMut, OperationResult, SocketRegistration}; +use crate::{ + FileClose, FileOpen, FileOperation, FileRead, FileReadAt, FileSeek, FileStat, FileSync, + FileTruncate, FileWrite, FileWriteAt, +}; use crate::{SocketMessage, SocketMessages, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; use std::io::{self, SeekFrom}; use std::net::{SocketAddr, TcpListener, TcpStream}; @@ -115,8 +119,8 @@ impl SocketRegistration for SchedulerHandle { self.selector()?.register_listener(listener) } } -impl Socket for SchedulerHandle { - type Socket = ::Socket; +impl SocketScheduler for SchedulerHandle { + type Socket = ::Socket; async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { check_open( @@ -312,16 +316,18 @@ impl crate::FileAdvice for SchedulerHandle { } } -impl File for SchedulerHandle { - type File = ::File; - type Metadata = ::Metadata; +impl FileOperation for SchedulerHandle { + type File = ::File; +} + +impl FileClose for SchedulerHandle { async fn file_close(&self, file: Self::File) -> io::Result<()> { check_open( self.shared .closed .load(std::sync::atomic::Ordering::Acquire), )?; - crate::scheduler::file_close::close(&self.shared.cleanup, file).await + crate::scheduler::file_close_io::close(&self.shared.cleanup, file).await } async fn file_close_submit(&self, file: Self::File) -> io::Result<()> { @@ -330,9 +336,11 @@ impl File for SchedulerHandle { .closed .load(std::sync::atomic::Ordering::Acquire), )?; - crate::scheduler::file_close::submit_only(&self.shared.cleanup, file).await + crate::scheduler::file_close_io::submit_only(&self.shared.cleanup, file).await } +} +impl FileTruncate for SchedulerHandle { async fn file_truncate( &self, file: &Self::File, @@ -343,6 +351,10 @@ impl File for SchedulerHandle { .file_truncate(file, length, cancellation) .await } +} + +impl FileStat for SchedulerHandle { + type Metadata = ::Metadata; async fn file_stat( &self, @@ -351,6 +363,9 @@ impl File for SchedulerHandle { ) -> OperationResult { self.selector()?.file_stat(file, cancellation).await } +} + +impl FileSync for SchedulerHandle { async fn file_sync( &self, file: &Self::File, @@ -359,7 +374,9 @@ impl File for SchedulerHandle { ) -> OperationResult<()> { self.selector()?.file_sync(file, mode, cancellation).await } +} +impl FileSeek for SchedulerHandle { async fn file_seek( &self, file: &Self::File, @@ -370,7 +387,9 @@ impl File for SchedulerHandle { .file_seek(file, position, cancellation) .await } +} +impl FileOpen for SchedulerHandle { async fn file_open( &self, path: &Path, @@ -381,6 +400,9 @@ impl File for SchedulerHandle { .file_open(path, options, cancellation) .await } +} + +impl FileRead for SchedulerHandle { async fn file_read( &self, file: &Self::File, @@ -392,6 +414,9 @@ impl File for SchedulerHandle { Err(error) => (Err(error.into()), buffer), } } +} + +impl FileWrite for SchedulerHandle { async fn file_write( &self, file: &Self::File, @@ -403,6 +428,9 @@ impl File for SchedulerHandle { Err(error) => (Err(error.into()), buffer), } } +} + +impl FileReadAt for SchedulerHandle { async fn file_read_at( &self, file: &Self::File, @@ -419,6 +447,9 @@ impl File for SchedulerHandle { Err(error) => (Err(error.into()), buffer), } } +} + +impl FileWriteAt for SchedulerHandle { async fn file_write_at( &self, file: &Self::File, @@ -485,8 +516,8 @@ impl SocketRegistration for Scheduler { self.handle.register_listener(listener) } } -impl Socket for Scheduler { - type Socket = ::Socket; +impl SocketScheduler for Scheduler { + type Socket = ::Socket; async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { self.handle.socket_close(socket).await @@ -630,9 +661,11 @@ impl crate::FileAdvice for Scheduler { } } -impl File for Scheduler { - type File = ::File; - type Metadata = ::Metadata; +impl FileOperation for Scheduler { + type File = ::File; +} + +impl FileClose for Scheduler { async fn file_close(&self, file: Self::File) -> io::Result<()> { self.handle.file_close(file).await } @@ -640,7 +673,9 @@ impl File for Scheduler { async fn file_close_submit(&self, file: Self::File) -> io::Result<()> { self.handle.file_close_submit(file).await } +} +impl FileTruncate for Scheduler { async fn file_truncate( &self, file: &Self::File, @@ -649,6 +684,10 @@ impl File for Scheduler { ) -> OperationResult<()> { self.handle.file_truncate(file, length, cancellation).await } +} + +impl FileStat for Scheduler { + type Metadata = ::Metadata; async fn file_stat( &self, @@ -657,6 +696,9 @@ impl File for Scheduler { ) -> OperationResult { self.handle.file_stat(file, cancellation).await } +} + +impl FileSync for Scheduler { async fn file_sync( &self, file: &Self::File, @@ -665,7 +707,9 @@ impl File for Scheduler { ) -> OperationResult<()> { self.handle.file_sync(file, mode, cancellation).await } +} +impl FileSeek for Scheduler { async fn file_seek( &self, file: &Self::File, @@ -674,7 +718,9 @@ impl File for Scheduler { ) -> OperationResult { self.handle.file_seek(file, position, cancellation).await } +} +impl FileOpen for Scheduler { async fn file_open( &self, path: &Path, @@ -683,6 +729,9 @@ impl File for Scheduler { ) -> OperationResult { self.handle.file_open(path, options, cancellation).await } +} + +impl FileRead for Scheduler { async fn file_read( &self, file: &Self::File, @@ -691,6 +740,9 @@ impl File for Scheduler { ) -> BufferResult { self.handle.file_read(file, buffer, cancellation).await } +} + +impl FileWrite for Scheduler { async fn file_write( &self, file: &Self::File, @@ -699,6 +751,9 @@ impl File for Scheduler { ) -> BufferResult { self.handle.file_write(file, buffer, cancellation).await } +} + +impl FileReadAt for Scheduler { async fn file_read_at( &self, file: &Self::File, @@ -710,6 +765,9 @@ impl File for Scheduler { .file_read_at(file, buffer, offset, cancellation) .await } +} + +impl FileWriteAt for Scheduler { async fn file_write_at( &self, file: &Self::File, diff --git a/crates/executor/src/scheduler/socketry/operations/tests.rs b/crates/executor/src/scheduler/socketry/operations/tests.rs index 7e6103b..6b14173 100644 --- a/crates/executor/src/scheduler/socketry/operations/tests.rs +++ b/crates/executor/src/scheduler/socketry/operations/tests.rs @@ -1,11 +1,12 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. +use crate::{FileReadAt, FileWriteAt}; static NEVER: crate::Cancellation = crate::Cancellation::never(); use super::{DefaultSelector, Scheduler, check_initialized_open, check_open}; use crate::SocketRegistration; -use crate::scheduler::{Clock, File, Interest, Socket}; +use crate::scheduler::{Clock, Interest, SocketScheduler}; use crate::{OperationResult, SocketDomain, SocketProtocol, SocketType}; use std::fs::OpenOptions; use std::io; @@ -27,7 +28,10 @@ fn socket_pair() -> (TcpStream, TcpStream) { (client, server) } -async fn connect(scheduler: &S, address: SocketAddr) -> OperationResult { +async fn connect( + scheduler: &S, + address: SocketAddr, +) -> OperationResult { let socket = scheduler .socket_open( SocketDomain::from(address), diff --git a/crates/executor/src/scheduler/socketry/tests.rs b/crates/executor/src/scheduler/socketry/tests.rs index c65b66b..456ad15 100644 --- a/crates/executor/src/scheduler/socketry/tests.rs +++ b/crates/executor/src/scheduler/socketry/tests.rs @@ -2,6 +2,8 @@ // Copyright, 2026, by Samuel Williams. use super::Scheduler; +#[cfg(feature = "native")] +use crate::FileClose; use crate::{Spawn, SpawnError, Task, TaskError}; use std::sync::Arc; @@ -14,7 +16,7 @@ fn ready_task(value: usize) -> ReadyTask { #[cfg(feature = "native")] #[test] fn shutdown_drains_accepted_closes_after_their_waiters_are_dropped() { - use crate::{BlockingOptions, File}; + use crate::BlockingOptions; use futures_lite::future::{block_on, poll_once}; use std::{sync::mpsc, time::Duration}; let timeout = Duration::from_secs(10); @@ -61,7 +63,7 @@ fn shutdown_drains_accepted_closes_after_their_waiters_are_dropped() { #[cfg(feature = "native")] #[test] fn cleanup_completion_can_destroy_scheduler_without_waiting_on_its_own_worker() { - use crate::{BlockingOptions, File}; + use crate::BlockingOptions; use futures_lite::future::block_on; use std::future::Future; use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/crates/executor/src/scheduler/tokio.rs b/crates/executor/src/scheduler/tokio.rs index 2c4d57f..0157e83 100644 --- a/crates/executor/src/scheduler/tokio.rs +++ b/crates/executor/src/scheduler/tokio.rs @@ -11,7 +11,7 @@ mod signals; use super::operation::cancellable; -use super::{BufferResult, Clock, File, Interest, Socket as SocketOperations}; +use super::{BufferResult, Clock, Interest, SocketScheduler}; use crate::blocking_pool::BlockingPool; #[cfg(target_os = "linux")] use crate::{AllocationMode, FileAllocate}; @@ -19,6 +19,10 @@ use crate::{ Blocking, BlockingError, BlockingOptions, BlockingTask, Cancellation, FileOptions, IoBuf, IoBufMut, OperationResult, SocketRegistration, Spawn, SpawnError, TaskError, }; +use crate::{ + FileClose, FileOpen, FileOperation, FileRead, FileReadAt, FileSeek, FileStat, FileSync, + FileTruncate, FileWrite, FileWriteAt, +}; use crate::{FileDescriptor, SyncMode}; use crate::{SocketMessage, SocketMessages, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; use ::tokio::runtime::Handle; @@ -564,7 +568,7 @@ impl SocketRegistration for SchedulerHandle { SocketDescriptor::new(&self.shared.runtime, listener.into()) } } -impl SocketOperations for SchedulerHandle { +impl SocketScheduler for SchedulerHandle { type Socket = SocketDescriptor; async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { @@ -853,19 +857,23 @@ impl crate::FileAdvice for SchedulerHandle { } } -impl File for SchedulerHandle { +impl FileOperation for SchedulerHandle { type File = FileDescriptor; - type Metadata = crate::SystemFileMetadata; +} + +impl FileClose for SchedulerHandle { async fn file_close(&self, file: Self::File) -> io::Result<()> { self.shared.check_open()?; - super::file_close::close(&self.shared.cleanup, file).await + super::file_close_io::close(&self.shared.cleanup, file).await } async fn file_close_submit(&self, file: Self::File) -> io::Result<()> { self.shared.check_open()?; - super::file_close::submit_only(&self.shared.cleanup, file).await + super::file_close_io::submit_only(&self.shared.cleanup, file).await } +} +impl FileTruncate for SchedulerHandle { async fn file_truncate( &self, file: &Self::File, @@ -885,6 +893,10 @@ impl File for SchedulerHandle { .await, ) } +} + +impl FileStat for SchedulerHandle { + type Metadata = crate::SystemFileMetadata; async fn file_stat( &self, @@ -904,6 +916,9 @@ impl File for SchedulerHandle { .await, ) } +} + +impl FileSync for SchedulerHandle { async fn file_sync( &self, file: &Self::File, @@ -923,7 +938,9 @@ impl File for SchedulerHandle { .await, ) } +} +impl FileSeek for SchedulerHandle { async fn file_seek( &self, file: &Self::File, @@ -943,7 +960,9 @@ impl File for SchedulerHandle { .await, ) } +} +impl FileOpen for SchedulerHandle { async fn file_open( &self, path: &Path, @@ -965,6 +984,9 @@ impl File for SchedulerHandle { .await, ) } +} + +impl FileRead for SchedulerHandle { async fn file_read( &self, file: &Self::File, @@ -977,6 +999,9 @@ impl File for SchedulerHandle { }) .await } +} + +impl FileWrite for SchedulerHandle { async fn file_write( &self, file: &Self::File, @@ -989,6 +1014,9 @@ impl File for SchedulerHandle { }) .await } +} + +impl FileReadAt for SchedulerHandle { async fn file_read_at( &self, file: &Self::File, @@ -1002,6 +1030,9 @@ impl File for SchedulerHandle { }) .await } +} + +impl FileWriteAt for SchedulerHandle { async fn file_write_at( &self, file: &Self::File, @@ -1148,8 +1179,8 @@ impl SocketRegistration for Scheduler { self.handle.register_listener(listener) } } -impl SocketOperations for Scheduler { - type Socket = ::Socket; +impl SocketScheduler for Scheduler { + type Socket = ::Socket; async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { self.handle.socket_close(socket).await @@ -1293,9 +1324,11 @@ impl crate::FileAdvice for Scheduler { } } -impl File for Scheduler { - type File = ::File; - type Metadata = ::Metadata; +impl FileOperation for Scheduler { + type File = ::File; +} + +impl FileClose for Scheduler { async fn file_close(&self, file: Self::File) -> io::Result<()> { self.handle.file_close(file).await } @@ -1303,7 +1336,9 @@ impl File for Scheduler { async fn file_close_submit(&self, file: Self::File) -> io::Result<()> { self.handle.file_close_submit(file).await } +} +impl FileTruncate for Scheduler { async fn file_truncate( &self, file: &Self::File, @@ -1312,6 +1347,10 @@ impl File for Scheduler { ) -> OperationResult<()> { self.handle.file_truncate(file, length, cancellation).await } +} + +impl FileStat for Scheduler { + type Metadata = ::Metadata; async fn file_stat( &self, @@ -1320,6 +1359,9 @@ impl File for Scheduler { ) -> OperationResult { self.handle.file_stat(file, cancellation).await } +} + +impl FileSync for Scheduler { async fn file_sync( &self, file: &Self::File, @@ -1328,7 +1370,9 @@ impl File for Scheduler { ) -> OperationResult<()> { self.handle.file_sync(file, mode, cancellation).await } +} +impl FileSeek for Scheduler { async fn file_seek( &self, file: &Self::File, @@ -1337,7 +1381,9 @@ impl File for Scheduler { ) -> OperationResult { self.handle.file_seek(file, position, cancellation).await } +} +impl FileOpen for Scheduler { async fn file_open( &self, path: &Path, @@ -1346,6 +1392,9 @@ impl File for Scheduler { ) -> OperationResult { self.handle.file_open(path, options, cancellation).await } +} + +impl FileRead for Scheduler { async fn file_read( &self, file: &Self::File, @@ -1354,6 +1403,9 @@ impl File for Scheduler { ) -> BufferResult { self.handle.file_read(file, buffer, cancellation).await } +} + +impl FileWrite for Scheduler { async fn file_write( &self, file: &Self::File, @@ -1362,6 +1414,9 @@ impl File for Scheduler { ) -> BufferResult { self.handle.file_write(file, buffer, cancellation).await } +} + +impl FileReadAt for Scheduler { async fn file_read_at( &self, file: &Self::File, @@ -1373,6 +1428,9 @@ impl File for Scheduler { .file_read_at(file, buffer, offset, cancellation) .await } +} + +impl FileWriteAt for Scheduler { async fn file_write_at( &self, file: &Self::File, diff --git a/crates/executor/src/socket_option.rs b/crates/executor/src/socket_option.rs index 1d31568..6acb408 100644 --- a/crates/executor/src/socket_option.rs +++ b/crates/executor/src/socket_option.rs @@ -3,7 +3,7 @@ /// A typed socket-option identifier. This describes its value, not how an operating system represents or accesses it. /// -/// Backends implement `SocketOptions` separately for each supported option. Additional option types can be defined without changing the core `Socket` trait or introducing a runtime option enum. +/// Backends implement `SocketOptions` separately for each supported option. Additional option types can be defined without changing the core `SocketScheduler` trait or introducing a runtime option enum. pub trait SocketOption: Send { type Value: Send; } diff --git a/crates/executor/src/sync_mode.rs b/crates/executor/src/sync_mode.rs index 041a1f9..3ff7d23 100644 --- a/crates/executor/src/sync_mode.rs +++ b/crates/executor/src/sync_mode.rs @@ -1,7 +1,7 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -/// The file data and metadata requested by `File::file_sync`. A backend may provide stronger synchronization than requested. +/// The file data and metadata requested by `FileSync::file_sync`. A backend may provide stronger synchronization than requested. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SyncMode { /// Synchronize file contents and metadata, like `std::fs::File::sync_all`. diff --git a/crates/executor/tests/contracts.rs b/crates/executor/tests/contracts.rs index 9d6dc74..95f5f54 100644 --- a/crates/executor/tests/contracts.rs +++ b/crates/executor/tests/contracts.rs @@ -2,8 +2,8 @@ // Copyright, 2026, by Samuel Williams. use socketry_executor::{ - Cancellation, File, FileOptions, Interest, IoBuf, OperationError, OperationResult, Socket, - SocketDomain, SocketProtocol, SocketType, Spawn, + Cancellation, FileOptions, FileScheduler, Interest, IoBuf, OperationError, OperationResult, + SocketDomain, SocketProtocol, SocketScheduler, SocketType, Spawn, }; use std::{ future::{Future, poll_fn}, @@ -42,8 +42,8 @@ mod socket_options; async fn resources(scheduler: &S, root: &Path) where - S: File - + Socket + S: FileScheduler + + SocketScheduler + socketry_executor::SocketOptions + socketry_executor::SocketOptions + socketry_executor::SocketOptions, diff --git a/crates/executor/tests/file_capabilities.rs b/crates/executor/tests/file_capabilities.rs new file mode 100644 index 0000000..6272dfe --- /dev/null +++ b/crates/executor/tests/file_capabilities.rs @@ -0,0 +1,68 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use futures_lite::future::block_on; +use socketry_executor::{ + BufferResult, Cancellation, FileOperation, FileRead, IoBufMut, OperationError, +}; +use std::sync::Mutex; + +// This implementation has no OS resources and supplies no open, write, close, or metadata operations. +struct ReadOnly; + +impl FileOperation for ReadOnly { + type File = Mutex<&'static [u8]>; +} + +impl FileRead for ReadOnly { + async fn file_read( + &self, + file: &Self::File, + mut buffer: B, + cancellation: &Cancellation, + ) -> BufferResult { + if let Err(error) = cancellation.check() { + return (Err(error.into()), buffer); + } + let mut remaining = file.lock().unwrap(); + let count = remaining.len().min(buffer.as_mut().len()); + buffer.as_mut()[..count].copy_from_slice(&remaining[..count]); + *remaining = &remaining[count..]; + (Ok(count), buffer) + } +} + +async fn read_chunk( + scheduler: &S, + file: &S::File, + buffer: Vec, + cancellation: &Cancellation, +) -> BufferResult { + scheduler.file_read(file, buffer, cancellation).await +} + +#[test] +fn read_only_implementation_satisfies_a_read_only_consumer() { + let file = Mutex::new(b"abc".as_slice()); + let buffer = vec![0; 2]; + let allocation = buffer.as_ptr(); + let never = Cancellation::never(); + let (result, buffer) = block_on(read_chunk(&ReadOnly, &file, buffer, &never)); + assert_eq!(result.unwrap(), 2); + assert_eq!(buffer, b"ab"); + assert_eq!(buffer.as_ptr(), allocation); + + let cancelled = Cancellation::new(); + cancelled.cancel(); + let (result, buffer) = block_on(read_chunk(&ReadOnly, &file, buffer, &cancelled)); + assert!(matches!(result, Err(OperationError::Cancelled(_)))); + assert_eq!(buffer, b"ab"); + assert_eq!(buffer.as_ptr(), allocation); + + let (result, buffer) = block_on(read_chunk(&ReadOnly, &file, buffer, &never)); + assert_eq!(result.unwrap(), 1); + assert_eq!(buffer, b"cb"); + let (result, buffer) = block_on(read_chunk(&ReadOnly, &file, buffer, &never)); + assert_eq!(result.unwrap(), 0); + assert_eq!(buffer.as_ptr(), allocation); +} diff --git a/crates/executor/tests/io.rs b/crates/executor/tests/io.rs index 85b662f..bc378d8 100644 --- a/crates/executor/tests/io.rs +++ b/crates/executor/tests/io.rs @@ -8,7 +8,8 @@ mod support; static NEVER: socketry_executor::Cancellation = socketry_executor::Cancellation::never(); use socketry_executor::{ - Clock, File, Interest, OperationResult, Socket, SocketDomain, SocketProtocol, SocketType, Spawn, + Clock, FileScheduler, Interest, OperationResult, SocketDomain, SocketProtocol, SocketScheduler, + SocketType, Spawn, }; use socketry_executor::{SocketRegistration, SyncMode}; use std::fs::File as StdFile; @@ -52,7 +53,7 @@ fn socket_pair() -> (TcpStream, TcpStream) { (client, server) } -async fn connect( +async fn connect( scheduler: &S, address: SocketAddr, cancellation: &socketry_executor::Cancellation, @@ -71,7 +72,7 @@ async fn connect( Ok(socket) } -async fn socket_variants(scheduler: &S) { +async fn socket_variants(scheduler: &S) { let ipv4 = scheduler .socket_open(SocketDomain::Ipv4, SocketType::Stream, None, &NEVER) .await @@ -121,7 +122,7 @@ fn error_kind>( async fn round_trip(scheduler: SchedulerType) where - SchedulerType: Socket + SocketRegistration + Spawn + Clock + Clone + 'static, + SchedulerType: SocketScheduler + SocketRegistration + Spawn + Clock + Clone + 'static, { let (listener, address) = listening_socket(); let listener = scheduler.register_listener(listener).unwrap(); @@ -170,7 +171,7 @@ where async fn cancel_pending_read(scheduler: &SchedulerType) where - SchedulerType: Socket + SocketRegistration + Clock, + SchedulerType: SocketScheduler + SocketRegistration + Clock, { let (client, server) = socket_pair(); let socket = scheduler.register_socket(client).unwrap(); @@ -192,7 +193,7 @@ where async fn concurrent_datagram_reads(scheduler: SchedulerType) where - SchedulerType: Socket + Spawn + Clone + 'static, + SchedulerType: SocketScheduler + Spawn + Clone + 'static, { let socket = scheduler .socket_open( @@ -266,7 +267,7 @@ impl Drop for TemporaryFile { } } -async fn positioned_files(scheduler: &SchedulerType) { +async fn positioned_files(scheduler: &SchedulerType) { static NEXT_FILE: AtomicU64 = AtomicU64::new(0); let path = std::env::temp_dir().join(format!( "socketry-io-{}-{}", @@ -329,6 +330,10 @@ async fn positioned_files(scheduler: &SchedulerType) { #[cfg(feature = "native")] mod native { use super::*; + use socketry_executor::{ + FileClose, FileOpen, FileRead, FileReadAt, FileSeek, FileStat, FileSync, FileTruncate, + FileWrite, FileWriteAt, + }; use socketry_executor::{Scheduler, TaskError}; #[test] @@ -680,6 +685,10 @@ mod native { mod tokio_adapter { use super::*; use socketry_executor::scheduler::tokio::{Scheduler, SchedulerHandle}; + use socketry_executor::{ + FileClose, FileOpen, FileRead, FileReadAt, FileSeek, FileStat, FileSync, FileTruncate, + FileWrite, FileWriteAt, + }; use socketry_executor::{SpawnError, TaskError}; fn runtime() -> tokio::runtime::Runtime { @@ -1257,7 +1266,7 @@ fn ring_shutdown_racing_submissions_returns_owned_buffers() { } #[cfg(unix)] -async fn sync_errors>(scheduler: &S) { +async fn sync_errors>(scheduler: &S) { use std::os::{fd::OwnedFd, unix::net::UnixStream}; let (socket, _peer) = UnixStream::pair().unwrap(); let file = socketry_executor::FileDescriptor::from(StdFile::from(OwnedFd::from(socket))); diff --git a/crates/executor/tests/socket_lifecycle.rs b/crates/executor/tests/socket_lifecycle.rs index 58a9ca0..1013061 100644 --- a/crates/executor/tests/socket_lifecycle.rs +++ b/crates/executor/tests/socket_lifecycle.rs @@ -3,11 +3,11 @@ #![cfg(any(feature = "native", feature = "tokio"))] -use socketry_executor::{Cancellation, Socket, SocketDomain, SocketType}; +use socketry_executor::{Cancellation, SocketDomain, SocketScheduler, SocketType}; use std::{io, net::Shutdown}; -async fn sockets(scheduler: &S) -> [S::Socket; 3] { - async fn open(scheduler: &S) -> S::Socket { +async fn sockets(scheduler: &S) -> [S::Socket; 3] { + async fn open(scheduler: &S) -> S::Socket { scheduler .socket_open( SocketDomain::Ipv4, @@ -25,7 +25,7 @@ async fn sockets(scheduler: &S) -> [S::Socket; 3] { ] } -async fn rejected( +async fn rejected( scheduler: &S, [first, second, third]: [S::Socket; 3], expected: io::ErrorKind, diff --git a/crates/executor/tests/socket_messages.rs b/crates/executor/tests/socket_messages.rs index e2705a3..47365dd 100644 --- a/crates/executor/tests/socket_messages.rs +++ b/crates/executor/tests/socket_messages.rs @@ -7,7 +7,7 @@ mod contracts; mod support; use socketry_executor::{ - Cancellation, Socket, SocketDomain, SocketMessage, SocketMessages, SocketRecvFlags, + Cancellation, SocketDomain, SocketMessage, SocketMessages, SocketRecvFlags, SocketScheduler, SocketSendFlags, SocketType, }; use std::io; diff --git a/examples/echo/client.rs b/examples/echo/client.rs index c3d47fb..ef05b58 100644 --- a/examples/echo/client.rs +++ b/examples/echo/client.rs @@ -1,15 +1,15 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -// `Socket`, `AddressResolution`, and `IoBuf` are traits: importing them makes their methods available. +// `SocketScheduler`, `AddressResolution`, and `IoBuf` are traits: importing them makes their methods available. use socketry::{ AddressHints, AddressResolution, AddressService, Cancellation, IoBuf, OperationResult, - Scheduler, Socket, SocketDomain, SocketType, + Scheduler, SocketDomain, SocketScheduler, SocketType, }; use std::io; -// This function works with any scheduler implementing the `Socket` and `AddressResolution` traits. `&S` and `&Cancellation` borrow values owned by `main()`. The `Vec` is owned by this future and returned with the echoed bytes. -async fn echo( +// This function works with any scheduler implementing the `SocketScheduler` and `AddressResolution` traits. `&S` and `&Cancellation` borrow values owned by `main()`. The `Vec` is owned by this future and returned with the echoed bytes. +async fn echo( scheduler: &S, mut buffer: Vec, cancellation: &Cancellation, diff --git a/examples/echo/server.rs b/examples/echo/server.rs index 4180afe..3cef95c 100644 --- a/examples/echo/server.rs +++ b/examples/echo/server.rs @@ -3,11 +3,11 @@ // Bring these types and traits into scope. // -// Traits such as `Socket`, `Signals`, and `IoBuf` provide methods used below. Importing a trait makes its methods available on implementing types. +// Traits such as `SocketScheduler`, `Signals`, and `IoBuf` provide methods used below. Importing a trait makes its methods available on implementing types. #[cfg(unix)] use socketry::{ - Cancellation, IoBuf, OperationError, OperationResult, Scheduler, Signals, Socket, SocketDomain, - SocketProtocol, SocketType, signal::SIGINT, + Cancellation, IoBuf, OperationError, OperationResult, Scheduler, Signals, SocketDomain, + SocketProtocol, SocketScheduler, SocketType, signal::SIGINT, }; #[cfg(unix)] @@ -15,7 +15,7 @@ use std::io; // `async fn` returns a future. Its body runs when that future is polled. // -// `` means this function works with any scheduler type `S` implementing the `Socket` capability. +// `` means this function works with any scheduler type `S` implementing the `SocketScheduler` capability. // // `&S` borrows the scheduler instead of taking ownership. // @@ -28,7 +28,7 @@ use std::io; // - `Ok(())`: succeeded, with no meaningful return value. // - `Err(error)`: failed or was cancelled. #[cfg(unix)] -async fn echo( +async fn echo( scheduler: &S, socket: S::Socket, shutdown: &Cancellation, diff --git a/readme.md b/readme.md index f9d6d52..901bc7f 100644 --- a/readme.md +++ b/readme.md @@ -78,7 +78,7 @@ The optional `signals` feature adds Unix `Signals::signal_notify(&signals, targe ## Portable I/O and runtime selection -Generic code can accept `Socket`, `File`, `Clock`, and `Spawn` capabilities. Socketry and the optional Tokio adapter implement these contracts with concrete future and resource types. Import the traits to call their methods. +Generic code can accept `SocketScheduler`, `FileScheduler`, `Clock`, and `Spawn` capabilities. Socketry and the optional Tokio adapter implement these contracts with concrete future and resource types. File consumers can require individual operations such as `FileRead + FileWrite`; import those operation traits for method calls on concrete schedulers. `file_close` waits for OS completion; `file_close_submit` waits only for bounded admission. Both consume an owned file handle and require prior operations to have finished. The [commented file-close example](crates/executor/examples/file_close.rs) demonstrates requesting cancellation, awaiting completion, and then closing safely. @@ -86,20 +86,20 @@ Generic code can accept `Socket`, `File`, `Clock`, and `Spawn` capabilities. Soc `SocketOptions` adds typed, cancellable option get/set. Require `SocketReuseAddress`, `SocketKeepalive`, or `TcpNoDelay` as needed; the option selects its value type and backend capability. The [listener configuration example](crates/executor/examples/socket_options.rs) shows configuration before binding. -`File::file_stat` queries an open descriptor and returns owned, native-backed metadata through portable `FileMetadata` accessors. The result remains usable after close; the operation leaves the cursor unchanged. See the [commented metadata example](crates/executor/examples/file_stat.rs). +`FileStat::file_stat` queries an open descriptor and returns owned, native-backed metadata through portable `FileMetadata` accessors. The result remains usable after close; the operation leaves the cursor unchanged. See the [commented metadata example](crates/executor/examples/file_stat.rs). The optional `SocketMessages` capability provides `socket_sendmsg`/`socket_recvmsg` with owned scatter/gather buffers, IP addresses, portable flags, and receive truncation metadata. See the [commented UDP message example](crates/executor/examples/socket_messages.rs). Ancillary data remains a separate extension. `FileAllocate` adds optional range allocation with `AllocationMode::Extend` or `KeepSize`. Native and Tokio implementations currently provide it on Linux; unsupported targets omit the trait implementation. See the [executor I/O documentation](crates/executor/readme.md#io-and-selectors) for allocation and filesystem guarantees. -`File` is the scheduler capability trait; `File::File` is its associated resource type. Generic code can create and use files without importing OS resource types: +`FileScheduler` aggregates the core file capabilities; `FileOperation::File` supplies their shared associated resource type. Generic code can create and use files without importing OS resource types: ```rust -use socketry::{Cancellation, File, FileOptions, OperationResult}; +use socketry::{Cancellation, FileScheduler, FileOptions, OperationResult}; use std::path::Path; -async fn read_prefix(scheduler: &S, path: &Path, cancellation: &Cancellation) -> OperationResult> { +async fn read_prefix(scheduler: &S, path: &Path, cancellation: &Cancellation) -> OperationResult> { let file = scheduler.file_open(path, FileOptions::READ, cancellation).await?; let (result, mut buffer) = scheduler.file_read_at(&file, vec![0; 4096], 0, cancellation).await; buffer.truncate(result?); diff --git a/releases.md b/releases.md index 15db516..a0fe273 100644 --- a/releases.md +++ b/releases.md @@ -2,6 +2,8 @@ ## Unreleased +- Split file capabilities into `FileOperation` for the shared resource type and individual operation traits, with `FileScheduler` as a blanket convenience aggregate. Rename the broad `File` and `Socket` traits to `FileScheduler` and `SocketScheduler`. `FileStat` owns the metadata type; optional `FileAllocate` and `FileAdvice` no longer require all file operations. Implementors must migrate `impl File` to the individual traits and rename `impl Socket` to `impl SocketScheduler`; concrete file callers must import the operation traits. Fully qualified associated types move to `FileOperation::File`, `FileStat::Metadata`, and `SocketScheduler::Socket`. Update native, Tokio, simulated implementations, examples, and capability-focused contracts. + - Add optional Linux `FileAdvice` with six `AdviceMode` hints and completion/admission-only `file_advise` forms. Native, Tokio, and io_uring schedulers use bounded cleanup admission, retain descriptors through execution, and drain accepted jobs during graceful shutdown. Include simulator contracts and a commented example. - Add required `File::file_stat` and associated `File::Metadata: FileMetadata`, returning owned status with portable optional accessors. Readiness/Tokio retain standard metadata; io_uring requires `Statx` support and retains native output inside its request allocation. Add shared contracts, completion-lifetime tests, and a commented example. External `File` implementations must add the associated type and operation. diff --git a/src/lib.rs b/src/lib.rs index dd7ab33..e9abd5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,13 +8,14 @@ pub use socketry_executor as executor; pub use socketry_executor::{ AddressHints, AddressInfo, AddressResolution, AddressResolveError, AddressResolveErrorKind, AddressService, AdviceMode, AllocationMode, Blocking, BlockingError, BlockingOptions, - BlockingTask, BufferResult, BufferSlice, Cancellation, Cancelled, Clock, File, FileAdvice, - FileAllocate, FileDescriptor, FileMetadata, FileOptions, FileType, Interest, IoBuf, IoBufMut, - Notify, OperationError, OperationResult, Scheduler, SchedulerHandle, Signals, Socket, - SocketDomain, SocketKeepalive, SocketMessage, SocketMessages, SocketOption, SocketOptions, - SocketProtocol, SocketRecvFlags, SocketRecvResult, SocketRegistration, SocketReuseAddress, - SocketSendFlags, SocketType, Spawn, SpawnError, SyncMode, Task, TaskError, TaskHandle, - TcpNoDelay, defer_cancel, yield_now, + BlockingTask, BufferResult, BufferSlice, Cancellation, Cancelled, Clock, FileAdvice, + FileAllocate, FileClose, FileDescriptor, FileMetadata, FileOpen, FileOperation, FileOptions, + FileRead, FileReadAt, FileScheduler, FileSeek, FileStat, FileSync, FileTruncate, FileType, + FileWrite, FileWriteAt, Interest, IoBuf, IoBufMut, Notify, OperationError, OperationResult, + Scheduler, SchedulerHandle, Signals, SocketDomain, SocketKeepalive, SocketMessage, + SocketMessages, SocketOption, SocketOptions, SocketProtocol, SocketRecvFlags, SocketRecvResult, + SocketRegistration, SocketReuseAddress, SocketScheduler, SocketSendFlags, SocketType, Spawn, + SpawnError, SyncMode, Task, TaskError, TaskHandle, TcpNoDelay, defer_cancel, yield_now, }; #[cfg(all(unix, feature = "signals", any(feature = "native", feature = "tokio")))]