vane_core/engine.rs
1//! The `AsyncEngine` abstraction — completion-based transport (`IO-01`, `IO-05`).
2//!
3//! Two backends implement the same interface:
4//!
5//! - [`io_uring backend`](uring) (`IO-01`..`03`): one ring per worker,
6//! `IORING_SETUP_SQPOLL` optional, fixed buffers (`IORING_REGISTER_BUFFERS`)
7//! so steady-state reads/writes never enter the kernel allocator.
8//! - [`mio backend`](mio_engine): edge-triggered epoll/kqueue emulation of
9//! the same completion semantics, selected automatically when io_uring is
10//! unavailable or administratively disabled.
11//!
12//! The worker drives either through identical CQE dispatch, which is what
13//! keeps the HTTP/L4 state machines engine-agnostic.
14
15pub mod mio_engine;
16#[cfg(all(target_os = "linux", feature = "io-uring"))]
17pub mod uring;
18
19use std::io;
20use std::net::SocketAddr;
21use std::time::Duration;
22
23use crate::buffer::BufferPool;
24use crate::token::Token;
25
26/// Slow-path warning; vane-core stays dependency-light by design.
27macro_rules! tracing_slow_warn {
28 ($($arg:tt)*) => {
29 eprintln!("[vane:warn] {}", format_args!($($arg)*))
30 };
31}
32
33/// Result of an engine operation that may complete inline.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Poll {
36 /// Completed immediately with the given byte/fd count.
37 Done(u32),
38 /// Queued; completion will arrive as a CQE with the same token.
39 Pending,
40}
41
42/// A completion event.
43#[derive(Debug)]
44pub struct Cqe {
45 /// Token submitted with the operation.
46 pub token: Token,
47 /// Kernel result: bytes transferred, new fd (accept), or 0.
48 pub result: io::Result<u32>,
49}
50
51/// Transport engine — owned and driven by a single worker thread.
52pub trait Engine {
53 /// Backend name for diagnostics (`io_uring` / `mio`).
54 fn kind(&self) -> &'static str;
55
56 /// Registers a nonblocking listening socket for accept readiness.
57 ///
58 /// # Errors
59 /// Backend registration failure.
60 fn add_listener(&mut self, fd: std::os::fd::RawFd, token: Token) -> io::Result<()>;
61
62 /// Registers a connected socket for read/write readiness.
63 ///
64 /// # Errors
65 /// Backend registration failure.
66 fn add_stream(&mut self, fd: std::os::fd::RawFd, token: Token) -> io::Result<()>;
67
68 /// Queues one read into pool `slot`. The completion yields bytes read;
69 /// `0` means EOF.
70 ///
71 /// # Errors
72 /// Submission failure (not `EAGAIN` — that becomes `Pending`).
73 fn read(&mut self, token: Token, fd: std::os::fd::RawFd, slot: u32) -> io::Result<Poll>;
74
75 /// Queues a write of `slot[0..len]`, resuming from a prior partial write
76 /// when the backend tracks one for this token.
77 ///
78 /// # Errors
79 /// Submission failure (not `EAGAIN`).
80 fn write(
81 &mut self,
82 token: Token,
83 fd: std::os::fd::RawFd,
84 slot: u32,
85 len: usize,
86 offset: usize,
87 ) -> io::Result<Poll>;
88
89 /// Starts a nonblocking `connect(2)`; completion is a CQE where
90 /// `result == Ok(0)` means established.
91 ///
92 /// # Errors
93 /// Socket creation or connect submission failure.
94 fn connect(
95 &mut self,
96 token: Token,
97 addr: std::net::SocketAddr,
98 ) -> io::Result<(std::os::fd::RawFd, Poll)>;
99
100 /// Starts a nonblocking UDS `connect(2)` to `path`.
101 ///
102 /// # Errors
103 /// Socket creation or connect submission failure.
104 fn connect_unix(
105 &mut self,
106 token: Token,
107 path: &std::path::Path,
108 ) -> io::Result<(std::os::fd::RawFd, Poll)>;
109
110 /// Attempts an accept on a registered listener; `Ok(Some(..))` completes
111 /// inline, `Ok(None)` waits for a CQE on the listener token.
112 ///
113 /// # Errors
114 /// Fatal (non-`EAGAIN`) accept error.
115 fn accept(
116 &mut self,
117 lfd: std::os::fd::RawFd,
118 ltoken: Token,
119 ) -> io::Result<Option<(std::os::fd::RawFd, SocketAddr)>>;
120
121 /// Starts a bidirectional zero-copy splice pump between two registered
122 /// streams (`IO-04`). Completions on either token report bytes moved;
123 /// `Ok(0)` signals EOF for that direction.
124 ///
125 /// # Errors
126 /// Submission failure.
127 fn splice_pump(&mut self, a: Token, afd: i32, b: Token, bfd: i32) -> io::Result<()>;
128
129 /// Removes a descriptor (before the worker closes it).
130 fn remove(&mut self, fd: std::os::fd::RawFd);
131
132 /// Drives the backend, filling `out` with completions. Blocks up to
133 /// `timeout` (or indefinitely when `None`).
134 ///
135 /// # Errors
136 /// Backend event-loop failure (unrecoverable; worker exits).
137 fn poll(&mut self, timeout: Option<Duration>, out: &mut Vec<Cqe>) -> io::Result<()>;
138
139 /// Pops addresses of accepted connections reported by accept CQEs.
140 fn take_accepted(&mut self, fd: std::os::fd::RawFd) -> Option<SocketAddr>;
141}
142
143/// Creates the best available engine for this system (`IO-05` fallback).
144///
145/// Prefers io_uring (when compiled in and permitted — `SQPOLL` needs a
146/// privileged or unbounded user); falls back to mio otherwise.
147/// # Errors
148/// Returns the platform error when no engine can be created (io_uring
149/// syscall probe fails and mio fallback setup fails).
150pub fn create_engine(
151 entries: u32,
152 buffers: Option<&BufferPool>,
153 sqpoll: bool,
154) -> io::Result<Box<dyn Engine>> {
155 #[cfg(all(target_os = "linux", feature = "io-uring"))]
156 {
157 match uring::UringEngine::new(entries, buffers, sqpoll) {
158 Ok(e) => return Ok(Box::new(e)),
159 Err(err) => {
160 tracing_slow_warn!("io_uring unavailable ({}), falling back to mio", err);
161 }
162 }
163 }
164 let _ = (entries, sqpoll);
165 let fallback = buffers.ok_or_else(|| {
166 io::Error::new(
167 io::ErrorKind::InvalidInput,
168 "mio fallback requires a buffer pool",
169 )
170 })?;
171 Ok(Box::new(mio_engine::MioEngine::new(fallback)?))
172}
173
174#[cfg(test)]
175mod create_tests {
176 use super::*;
177
178 #[test]
179 fn creates_engine_with_pool() {
180 let pool = BufferPool::new(64, crate::buffer::DEFAULT_BUF_SIZE).expect("pool");
181 let engine = create_engine(64, Some(&pool), false).expect("engine");
182 // The engine constructs and is a valid trait object.
183 let _dyn: Box<dyn Engine> = engine;
184 }
185
186 #[test]
187 fn mio_fallback_requires_pool() {
188 // Without a pool the mio fallback cannot be constructed; on hosts
189 // where io_uring is unavailable this is an error, where available
190 // the uring engine succeeds (assert only the invariant we own).
191 let res = create_engine(8, None, false);
192 if let Err(e) = res {
193 assert_eq!(
194 e.kind(),
195 io::ErrorKind::InvalidInput,
196 "fallback error must be InvalidInput"
197 );
198 }
199 }
200
201 #[test]
202 fn zero_entries_still_builds_mio() {
203 // entries only affect the uring path; mio ignores them.
204 let pool = BufferPool::new(64, crate::buffer::DEFAULT_BUF_SIZE).expect("pool");
205 // Env-dependent path choice — the invariant is "no panic".
206 let _ = create_engine(0, Some(&pool), false);
207 }
208}