ergon_io/endpoint.rs
1//! What a `Driver` drives: connection ids, the sealed endpoint traits and the
2//! application callbacks. Platform-independent.
3
4use crate::error::Error;
5
6/// Identifies a connection within one `Driver`.
7///
8/// A slot index plus a generation: once a connection is closed its id never matches again, even when
9/// the slot is reused, so a stale id cannot reach a newer connection.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
11pub struct ConnId {
12 idx: u32,
13 generation: u32,
14}
15
16#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
17impl ConnId {
18 pub(crate) const fn new(idx: u32, generation: u32) -> Self {
19 Self { idx, generation }
20 }
21
22 pub(crate) const fn generation(self) -> u32 {
23 self.generation
24 }
25
26 /// The slot index (dense, reused after close; for per-connection arrays).
27 #[must_use]
28 pub const fn index(self) -> usize {
29 self.idx as usize
30 }
31
32 /// Test lever: an id from its parts, for stand-in drivers in tests. A
33 /// made-up id never reaches a connection: the driver checks the generation.
34 #[doc(hidden)]
35 #[must_use]
36 pub const fn from_parts(index: u32, generation: u32) -> Self {
37 Self::new(index, generation)
38 }
39}
40
41/// The driver-facing side of endpoints and dials. The module is private, so
42/// code outside this crate can neither implement these traits nor call their
43/// methods: only the driver moves bytes for a connection it owns.
44pub(crate) mod sealed {
45 use std::net::SocketAddr;
46 use std::os::fd::RawFd;
47 use std::sync::{Arc, OnceLock};
48
49 use super::ConnId;
50 use crate::RxMeta;
51 use crate::error::Result;
52
53 /// Socket, receive buffer and send queue of a connection.
54 pub trait Io {
55 /// Whether [`Io::expire`] must be called (request timeouts). The
56 /// driver's periodic deadline check is compiled out when `false`.
57 const DEADLINES: bool = false;
58 /// Fails the connection if something it waits for is overdue, and
59 /// runs its timers (WebSocket pings).
60 fn expire(&mut self, now_ns: i64) -> Result<()> {
61 let _ = now_ns;
62 Ok(())
63 }
64 /// The error to report when the connection ends with `error`: a
65 /// WebSocket whose close handshake finished reports the Close, an
66 /// HTTP connection with requests in flight which of them were sent.
67 fn close_reason(&mut self, error: crate::error::Error) -> crate::error::Error {
68 error
69 }
70 /// The kernel took `n` bytes from the front of [`Io::tx_queue`]
71 /// (`io_uring`: they were submitted, an upper bound). One add per
72 /// send on the HTTP connection; nothing elsewhere.
73 #[inline]
74 fn on_handed(&mut self, n: usize) {
75 let _ = n;
76 }
77 /// The socket.
78 fn fd(&self) -> RawFd;
79 /// The free part of the receive buffer. With `io_uring` the kernel
80 /// writes into it after the call returns, so it must stay at the same
81 /// address until `on_received`.
82 fn rx_space(&mut self) -> &mut [u8];
83 /// Bytes waiting to be sent; the driver drains from the front.
84 fn tx_queue(&mut self) -> &mut Vec<u8>;
85 /// Input received before the endpoint was added, not yet decoded.
86 fn has_buffered_input(&self) -> bool;
87 /// Records what the quick-ACK route check found, or the cell
88 /// ergon-io's warning thread fills in (once, when the driver takes
89 /// the connection, or at TCP connect under `require`).
90 fn set_route(&mut self, route: RouteSlot);
91 /// What the connection holds of its route check.
92 fn route_state(&self) -> &RouteSlot;
93 }
94
95 /// Decoding received bytes for handler `H`.
96 pub trait Deliver<H: ?Sized>: Io {
97 /// Accounts for `n` bytes written into `rx_space`, decodes and delivers
98 /// every complete message. `n == 0` decodes buffered input.
99 fn on_received(
100 &mut self,
101 id: ConnId,
102 n: usize,
103 meta: &RxMeta,
104 handler: &mut H,
105 ) -> Result<usize>;
106 }
107
108 /// What a connection holds of its route check: the outcome, or the cell
109 /// ergon-io's warning thread fills in. Reading it is one atomic load, and
110 /// nothing on the read or write path reads it.
111 #[derive(Debug, Clone)]
112 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
113 pub enum RouteSlot {
114 /// Decided where the connection was set up.
115 Ready(crate::route::RouteCheck),
116 /// Filled in by the warning thread.
117 Pending(Arc<OnceLock<crate::route::RouteCheck>>),
118 }
119
120 impl Default for RouteSlot {
121 fn default() -> Self {
122 Self::Ready(crate::route::RouteCheck::NotChecked)
123 }
124 }
125
126 /// What a connection reads while its lookup is queued.
127 static PENDING: crate::route::RouteCheck = crate::route::RouteCheck::Pending;
128
129 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
130 impl RouteSlot {
131 /// The outcome, or [`crate::route::RouteCheck::Pending`].
132 pub fn get(&self) -> &crate::route::RouteCheck {
133 match self {
134 Self::Ready(c) => c,
135 Self::Pending(cell) => cell.get().unwrap_or(&PENDING),
136 }
137 }
138
139 /// Whether the route was checked where the connection was set up (a
140 /// gated dial under `require`).
141 pub const fn decided(&self) -> bool {
142 !matches!(
143 self,
144 Self::Ready(crate::route::RouteCheck::NotChecked) | Self::Pending(_)
145 )
146 }
147 }
148
149 /// A connection being set up.
150 pub trait DialIo {
151 /// The connection it produces.
152 type Conn: Io;
153 /// Host to resolve.
154 fn host(&self) -> &str;
155 /// TCP port.
156 fn port(&self) -> u16;
157 /// A fixed address, if DNS should be skipped.
158 fn addr(&self) -> Option<SocketAddr>;
159 /// Starts the non-blocking TCP connect.
160 fn start(&mut self, addr: SocketAddr) -> Result<()>;
161 /// Advances as far as the socket allows; `Some` once ready.
162 fn step(&mut self) -> Result<Option<Self::Conn>>;
163 /// Stop once TCP is connected, before the TLS handshake, until
164 /// [`DialIo::route_checked`]: the driver checks the route there
165 /// under `quickack_check=require`, so a refused route costs no
166 /// handshake.
167 fn gate_route(&mut self);
168 /// The socket, while the dial waits at that point.
169 fn awaiting_route(&self) -> Option<RawFd>;
170 /// The route check's outcome: the dial goes on to TLS, and the
171 /// connection it produces carries it.
172 fn route_checked(&mut self, check: crate::route::RouteCheck);
173 }
174}
175
176/// A connection a driver can own: `WsConn`, `HttpConn` or `AnyConn`.
177///
178/// Sealed: with `io_uring` the kernel writes into the receive buffer after the
179/// submitting call returns, so only this crate's types, whose buffers never move
180/// while owned by a driver, can be endpoints.
181#[allow(private_bounds)] // sealed on purpose: the bound is unnameable outside the crate
182pub trait Endpoint: sealed::Io {}
183impl<T: sealed::Io> Endpoint for T {}
184
185/// An endpoint that can decode for application handler `H`.
186#[allow(private_bounds)]
187pub trait Receive<H: ?Sized>: Endpoint + sealed::Deliver<H> {}
188impl<H: ?Sized, T: sealed::Deliver<H>> Receive<H> for T {}
189
190/// A connection being set up without blocking (`WsDial`, `HttpDial`,
191/// `AnyDial`): DNS is the driver's job; the dial does TCP, TLS and any
192/// protocol upgrade.
193#[allow(private_bounds)]
194pub trait Dial: sealed::DialIo {}
195impl<T: sealed::DialIo> Dial for T {}
196
197/// Mutable access to a connection a `Driver` owns.
198///
199/// It offers every method of the connection but no way to replace, swap or
200/// move it out. With `io_uring` a receive is always armed into the
201/// connection's receive buffer, and every backend tracks the connection by its
202/// socket descriptor and slot, so the connection stays where it is until the
203/// driver closes it.
204///
205/// ```compile_fail,E0594
206/// fn replace(mut c: ergon_io::ConnMut<'_, ergon_io::WsConn>, other: ergon_io::WsConn) {
207/// *c = other;
208/// }
209/// ```
210///
211/// ```compile_fail,E0596
212/// fn swap(mut a: ergon_io::ConnMut<'_, ergon_io::WsConn>, mut b: ergon_io::ConnMut<'_, ergon_io::WsConn>) {
213/// std::mem::swap(&mut *a, &mut *b);
214/// }
215/// ```
216pub struct ConnMut<'a, E>(&'a mut E);
217
218impl<'a, E> ConnMut<'a, E> {
219 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
220 pub(crate) const fn new(conn: &'a mut E) -> Self {
221 Self(conn)
222 }
223
224 /// The connection itself, for this crate's forwarding methods only.
225 pub(crate) const fn inner(&mut self) -> &mut E {
226 self.0
227 }
228}
229
230impl<E> std::ops::Deref for ConnMut<'_, E> {
231 type Target = E;
232 fn deref(&self) -> &E {
233 self.0
234 }
235}
236
237/// Connection lifecycle callbacks.
238pub trait Handler {
239 /// A connection started with `Driver::connect` is ready.
240 fn on_open(&mut self, id: ConnId) {
241 let _ = id;
242 }
243 /// The driver closed `id` because of `error`: [`Error::Closed`] for a peer
244 /// close, [`Error::WsClose`] after a WebSocket close handshake. The
245 /// connection is gone when this is called.
246 fn on_closed(&mut self, id: ConnId, error: Error) {
247 let _ = (id, error);
248 }
249}
250
251/// Kernel-interface counters.
252#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
253pub struct DriverStats {
254 /// `poll` calls.
255 pub polls: u64,
256 /// `recvmsg` calls.
257 pub recvmsg_calls: u64,
258 /// `recvmsg` calls that returned `EAGAIN`.
259 pub recvmsg_eagain: u64,
260 /// `epoll_wait` / `epoll_pwait2` calls.
261 pub epoll_wait_calls: u64,
262 /// `send` syscalls (epoll backends; `io_uring` sends are submitted through
263 /// `io_uring_enter` or the `SQPOLL` thread and are not syscalls of their own).
264 pub send_calls: u64,
265 /// Sends started, one per batch of queued bytes, on every backend.
266 pub sends: u64,
267 /// `io_uring_enter` calls.
268 pub enter_calls: u64,
269 /// Receive completions with data.
270 pub completions: u64,
271 /// Messages delivered.
272 pub messages: u64,
273 /// Connections closed because of an error or peer close.
274 pub closed: u64,
275 /// Connections opened by `Driver::connect`.
276 pub connects: u64,
277 /// `Driver::connect` attempts that failed or timed out.
278 pub connect_failures: u64,
279 /// Longest single connection-setup step on the polling thread, in
280 /// nanoseconds (the TLS handshake's crypto lands here).
281 pub connect_step_max_ns: u64,
282 /// Quick-ACK route lookups the polling thread made itself: under
283 /// `quickack_check=require`, one per connection it set up
284 /// (`Driver::connect` at TCP connect, before the TLS handshake;
285 /// `Driver::add`). Under `warn` the lookup runs on ergon-io's warning
286 /// thread, and `Driver::connect_background` makes it on the helper
287 /// thread: neither is counted.
288 pub route_lookups: u64,
289 /// Polling-thread time spent on the route check, in nanoseconds: the
290 /// lookups above, or under `warn` reading the connection's addresses
291 /// and queuing the lookup (see [`crate::route`]).
292 pub route_check_ns: u64,
293 /// The longest single one of those, in nanoseconds.
294 pub route_check_max_ns: u64,
295}
296
297impl DriverStats {
298 /// Total syscalls made by the driver.
299 #[must_use]
300 pub const fn syscalls(&self) -> u64 {
301 self.recvmsg_calls + self.epoll_wait_calls + self.send_calls + self.enter_calls
302 }
303}