Skip to main content

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}