Skip to main content

melin_server_runtime/
connection_limit.rs

1//! The client connection cap, and the io_uring sizing derived from it.
2//!
3//! `--max-connections` bounds how many authenticated clients a node
4//! serves at once, and that bound is also what the kernel-TCP transport's
5//! two client-facing io_uring instances — the reader's and the response
6//! stage's — have to hold. The rings are sized from it once, at startup,
7//! rather than fixed for the largest deployment: the kernel charges ring
8//! memory against the locked-memory limit (`RLIMIT_MEMLOCK`), so a ring
9//! sized for thousands of connections costs a node that serves a handful
10//! the same budget as one that serves thousands.
11//!
12//! What has to fit, per drain of the reader's completion queue:
13//!
14//! - **Two SQEs per connection.** A connection's multishot RECV can
15//!   terminate and be re-armed, and the same drain can begin its teardown
16//!   (an `AsyncCancel`) — malformed frame or idle timeout.
17//! - **Housekeeping**: the eventfd READ re-arm, the tick `TIMEOUT`, the
18//!   shutdown `AsyncCancel`, and the legacy fallback's one-off
19//!   `ProvideBuffers` registration.
20//!
21//! This bounds the steady state, not every drain: a connection being torn
22//! down stays in the reader until its cancel completes, by which time the
23//! accept loop may already have admitted its replacement, so the reader
24//! can briefly track more connections than the cap. The reader hands
25//! queued SQEs to the kernel when the submission queue fills rather than
26//! relying on the bound, so that overshoot costs an extra submit, not a
27//! panic.
28//!
29//! The response stage submits at most one SEND per connection per flush,
30//! so the same ring rule covers it with room to spare.
31//!
32//! The provided-buffer pool holds two buffers per connection, so a
33//! connection can be refilled while its last chunk is still being parsed,
34//! and stays strictly smaller than the reader's ring (the legacy
35//! `ProvideBuffers` fallback re-provides one buffer per SQE).
36
37use std::fmt;
38use std::io;
39
40/// Largest ring io_uring creates (`IORING_MAX_ENTRIES`), which is also
41/// the largest provided-buffer ring the kernel registers.
42pub const MAX_RING_ENTRIES: u32 = 32768;
43
44/// Smallest ring the sizing hands out. A floor rather than the exact
45/// requirement: a ring of a few entries saves nothing worth having (the
46/// kernel charges ring memory in whole pages) and leaves no headroom for a
47/// housekeeping SQE added later.
48pub const MIN_RING_ENTRIES: u32 = 64;
49
50/// Smallest provided-buffer pool. Two buffers per connection is plenty
51/// for many connections, where one connection's burst is spread over the
52/// pool, and starves a node with a cap of one or two: a single client
53/// pipelining requests would exhaust it on every drain and pay a re-arm
54/// round trip for each. Half the ring floor, so the pool stays strictly
55/// below the ring.
56const MIN_PROVIDED_BUFFERS: u16 = 32;
57
58/// SQEs one connection can put on the reader's submission queue in one
59/// drain: a multishot RECV re-arm and a teardown `AsyncCancel`.
60const SQES_PER_CONNECTION: u64 = 2;
61
62/// SQEs the reader queues beyond the per-connection ones in one drain:
63/// the eventfd READ re-arm, the tick `TIMEOUT`, the shutdown
64/// `AsyncCancel`, and the legacy fallback's one-off `ProvideBuffers`
65/// registration.
66const HOUSEKEEPING_SQES: u64 = 4;
67
68/// Provided buffers per connection.
69const BUFFERS_PER_CONNECTION: u64 = 2;
70
71/// Largest `--max-connections` a node accepts.
72///
73/// The provided-buffer pool must stay strictly below the reader's ring,
74/// and both are powers of two, so the largest pool is half the largest
75/// ring; two buffers per connection then gives the cap. One more
76/// connection would round the pool up to the ring's own maximum.
77pub const MAX_SUPPORTED_CONNECTIONS: u64 = MAX_RING_ENTRIES as u64 / 2 / BUFFERS_PER_CONNECTION;
78
79/// io_uring sizes for the kernel-TCP transport, derived from the
80/// connection cap by [`RingSizing::for_max_connections`].
81///
82/// `u32` ring entries because that is io_uring's own type for them;
83/// `u16` buffers because the buf_ring ABI addresses a buffer by a 16-bit
84/// id (the kernel's maximum, 32768, fits).
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct RingSizing {
87    ring_entries: u32,
88    provided_buffers: u16,
89}
90
91impl RingSizing {
92    /// Size the rings for a node serving at most `max_connections`
93    /// clients.
94    ///
95    /// Provided buffers are the next power of two at or above two per
96    /// connection, with a small floor. Ring entries are the next power of
97    /// two at or above two SQEs per connection plus housekeeping, and
98    /// strictly above the pool, clamped to
99    /// [[`MIN_RING_ENTRIES`], [`MAX_RING_ENTRIES`]]. At the default cap
100    /// (1024) that is 4096 ring entries and 2048 buffers.
101    ///
102    /// Refuses `0` (formerly "unlimited": the rings need a bound to be
103    /// sized from) and anything above [`MAX_SUPPORTED_CONNECTIONS`].
104    pub fn for_max_connections(max_connections: u64) -> Result<Self, MaxConnectionsError> {
105        if max_connections == 0 {
106            return Err(MaxConnectionsError::Unlimited);
107        }
108        if max_connections > MAX_SUPPORTED_CONNECTIONS {
109            return Err(MaxConnectionsError::AboveCeiling(max_connections));
110        }
111        // No overflow: `max_connections` is at most the ceiling, so every
112        // product below is a few tens of thousands.
113        let sqes = SQES_PER_CONNECTION * max_connections + HOUSEKEEPING_SQES;
114        let buffers = (BUFFERS_PER_CONNECTION * max_connections)
115            .next_power_of_two()
116            .max(u64::from(MIN_PROVIDED_BUFFERS));
117        // The ring also has to stay strictly above the pool. Both are
118        // powers of two, so that is at least twice the pool — which only
119        // binds where two buffers per connection round up to the same
120        // power of two as the SQE count does.
121        let ring_entries = sqes
122            .next_power_of_two()
123            .max(2 * buffers)
124            .clamp(u64::from(MIN_RING_ENTRIES), u64::from(MAX_RING_ENTRIES));
125        let sizing = Self {
126            ring_entries: u32::try_from(ring_entries)
127                .expect("ring entries are clamped to MAX_RING_ENTRIES"),
128            provided_buffers: u16::try_from(buffers)
129                .expect("buffers stay below MAX_RING_ENTRIES at the connection ceiling"),
130        };
131        // Checked once at startup, so unconditional: the reader's
132        // `expect`s on SQ space and the buf_ring's ABI rest on these.
133        assert!(
134            u64::from(sizing.ring_entries) >= sqes,
135            "the ring holds a drain's worth of SQEs"
136        );
137        assert!(
138            sizing.provided_buffers.is_power_of_two(),
139            "the buf_ring ABI requires a power-of-two pool"
140        );
141        assert!(
142            u32::from(sizing.provided_buffers) < sizing.ring_entries,
143            "the provided-buffer pool stays strictly below the ring"
144        );
145        Ok(sizing)
146    }
147
148    /// Entries in each of the reader's and the response stage's
149    /// submission queues (the completion queue is twice that).
150    pub fn ring_entries(&self) -> u32 {
151        self.ring_entries
152    }
153
154    /// Buffers in the reader's provided-buffer pool.
155    pub fn provided_buffers(&self) -> u16 {
156        self.provided_buffers
157    }
158
159    /// A sizing that skips the invariants, for tests that need ring
160    /// creation to fail.
161    #[cfg(test)]
162    pub(crate) fn unchecked(ring_entries: u32, provided_buffers: u16) -> Self {
163        Self {
164            ring_entries,
165            provided_buffers,
166        }
167    }
168}
169
170/// A `--max-connections` the node cannot size its rings for.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum MaxConnectionsError {
173    /// `0`, which used to mean "unlimited".
174    Unlimited,
175    /// Above [`MAX_SUPPORTED_CONNECTIONS`].
176    AboveCeiling(u64),
177}
178
179impl fmt::Display for MaxConnectionsError {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        match self {
182            Self::Unlimited => write!(
183                f,
184                "--max-connections 0 (unlimited) is no longer supported: the io_uring rings \
185                 are sized from the connection cap. Set a cap between 1 and \
186                 {MAX_SUPPORTED_CONNECTIONS}"
187            ),
188            Self::AboveCeiling(n) => write!(
189                f,
190                "--max-connections {n} is above the largest supported value, \
191                 {MAX_SUPPORTED_CONNECTIONS}: the io_uring rings are sized from the \
192                 connection cap, and the kernel's largest ring bounds them"
193            ),
194        }
195    }
196}
197
198impl std::error::Error for MaxConnectionsError {}
199
200/// Whether a node holding `active` authenticated connections must refuse
201/// the next one. The one gate both transports apply.
202#[inline]
203pub(crate) fn connection_cap_reached(active: u64, max_connections: u64) -> bool {
204    active >= max_connections
205}
206
207/// The startup error for an io_uring instance a stage could not create.
208///
209/// `ENOMEM` from `io_uring_setup` is, in practice, the locked-memory
210/// limit: the kernel charges ring memory against `RLIMIT_MEMLOCK`, per
211/// user and across all of that user's processes, and systemd's default
212/// limit is a few megabytes. The message says so and names the fix.
213/// Other errors (io_uring disabled by sysctl, filtered by seccomp) are
214/// reported as they are, since the limit has nothing to do with them.
215pub(crate) fn ring_setup_error(stage: &str, entries: u32, err: io::Error) -> io::Error {
216    let kind = err.kind();
217    if err.raw_os_error() == Some(libc::ENOMEM) {
218        io::Error::new(
219            kind,
220            format!(
221                "{stage}: cannot create its io_uring instance ({entries} entries): {err}. \
222                 The kernel charges io_uring ring memory against the locked-memory limit \
223                 (RLIMIT_MEMLOCK), per user across all processes. Raise it for the node's \
224                 service (systemd: LimitMEMLOCK=infinity) or grant it CAP_IPC_LOCK, or \
225                 lower --max-connections"
226            ),
227        )
228    } else {
229        io::Error::new(
230            kind,
231            format!("{stage}: cannot create its io_uring instance ({entries} entries): {err}"),
232        )
233    }
234}
235
236/// Wait for a worker thread's startup report: `Ok` once it holds its
237/// io_uring instance, the creation error otherwise.
238///
239/// A worker that dies before reporting (a panic during setup) drops its
240/// sender, which ends the wait with an error rather than a hang.
241pub(crate) fn await_startup(
242    stage: &str,
243    report: &std::sync::mpsc::Receiver<io::Result<()>>,
244) -> io::Result<()> {
245    match report.recv() {
246        Ok(result) => result,
247        Err(std::sync::mpsc::RecvError) => Err(io::Error::other(format!(
248            "{stage}: thread exited before reporting whether it started"
249        ))),
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    fn sizing(max_connections: u64) -> RingSizing {
258        RingSizing::for_max_connections(max_connections).expect("a supported cap")
259    }
260
261    /// The default cap keeps the sizes the rings had when they were
262    /// fixed, so a production node's memory and behaviour do not move.
263    #[test]
264    fn the_default_cap_keeps_the_fixed_sizes() {
265        let s = sizing(1024);
266        assert_eq!(s.ring_entries(), 4096);
267        assert_eq!(s.provided_buffers(), 2048);
268    }
269
270    #[test]
271    fn sizes_round_up_to_powers_of_two() {
272        let s = sizing(64);
273        assert_eq!(s.ring_entries(), 256);
274        assert_eq!(s.provided_buffers(), 128);
275        let s = sizing(1025);
276        assert_eq!(s.ring_entries(), 8192);
277        assert_eq!(s.provided_buffers(), 4096);
278    }
279
280    /// Where two buffers per connection and the SQE count round up to the
281    /// same power of two, the ring doubles to stay above the pool.
282    #[test]
283    fn the_ring_stays_strictly_above_the_pool() {
284        // 2 × 100 + 4 = 204 and 2 × 100 = 200 both round to 256.
285        let s = sizing(100);
286        assert_eq!(s.provided_buffers(), 256);
287        assert_eq!(s.ring_entries(), 512);
288    }
289
290    #[test]
291    fn small_caps_take_the_floors() {
292        let s = sizing(1);
293        assert_eq!(s.ring_entries(), MIN_RING_ENTRIES);
294        assert_eq!(s.provided_buffers(), MIN_PROVIDED_BUFFERS);
295    }
296
297    #[test]
298    fn the_ceiling_is_the_largest_cap_that_sizes() {
299        assert_eq!(MAX_SUPPORTED_CONNECTIONS, 8192);
300        let s = sizing(MAX_SUPPORTED_CONNECTIONS);
301        assert_eq!(s.ring_entries(), MAX_RING_ENTRIES);
302        assert_eq!(s.provided_buffers(), 16384);
303        assert_eq!(
304            RingSizing::for_max_connections(MAX_SUPPORTED_CONNECTIONS + 1),
305            Err(MaxConnectionsError::AboveCeiling(
306                MAX_SUPPORTED_CONNECTIONS + 1
307            ))
308        );
309        assert_eq!(
310            RingSizing::for_max_connections(u64::MAX),
311            Err(MaxConnectionsError::AboveCeiling(u64::MAX))
312        );
313    }
314
315    #[test]
316    fn zero_is_refused_and_says_why() {
317        let err = RingSizing::for_max_connections(0).expect_err("0 is refused");
318        assert_eq!(err, MaxConnectionsError::Unlimited);
319        let msg = err.to_string();
320        assert!(msg.contains("unlimited"), "{msg}");
321        assert!(msg.contains("io_uring"), "{msg}");
322        assert!(
323            msg.contains(&MAX_SUPPORTED_CONNECTIONS.to_string()),
324            "{msg}"
325        );
326    }
327
328    #[test]
329    fn above_the_ceiling_names_it() {
330        let msg = MaxConnectionsError::AboveCeiling(9000).to_string();
331        assert!(msg.contains("9000"), "{msg}");
332        assert!(
333            msg.contains(&MAX_SUPPORTED_CONNECTIONS.to_string()),
334            "{msg}"
335        );
336    }
337
338    /// Every supported cap satisfies the invariants the reader relies on
339    /// (`for_max_connections` asserts them; this walks the whole range).
340    #[test]
341    fn every_supported_cap_holds_the_invariants() {
342        for n in 1..=MAX_SUPPORTED_CONNECTIONS {
343            let s = sizing(n);
344            assert!(s.ring_entries().is_power_of_two());
345            assert!(u64::from(s.ring_entries()) >= SQES_PER_CONNECTION * n + HOUSEKEEPING_SQES);
346            assert!(u64::from(s.provided_buffers()) >= BUFFERS_PER_CONNECTION * n);
347            assert!(u32::from(s.provided_buffers()) < s.ring_entries());
348            assert!(s.ring_entries() <= MAX_RING_ENTRIES);
349        }
350    }
351
352    #[test]
353    fn the_gate_admits_up_to_the_cap() {
354        assert!(!connection_cap_reached(0, 1));
355        assert!(connection_cap_reached(1, 1));
356        assert!(!connection_cap_reached(1023, 1024));
357        assert!(connection_cap_reached(1024, 1024));
358        assert!(!connection_cap_reached(
359            MAX_SUPPORTED_CONNECTIONS - 1,
360            MAX_SUPPORTED_CONNECTIONS
361        ));
362        assert!(connection_cap_reached(
363            MAX_SUPPORTED_CONNECTIONS,
364            MAX_SUPPORTED_CONNECTIONS
365        ));
366    }
367
368    #[test]
369    fn enomem_names_the_locked_memory_limit() {
370        let err = ring_setup_error(
371            "uring-reader",
372            4096,
373            io::Error::from_raw_os_error(libc::ENOMEM),
374        );
375        let msg = err.to_string();
376        assert!(msg.contains("RLIMIT_MEMLOCK"), "{msg}");
377        assert!(msg.contains("LimitMEMLOCK="), "{msg}");
378        assert!(msg.contains("uring-reader"), "{msg}");
379        assert_eq!(err.kind(), io::ErrorKind::OutOfMemory);
380    }
381
382    #[test]
383    fn other_setup_errors_do_not_blame_the_limit() {
384        let msg =
385            ring_setup_error("response", 64, io::Error::from_raw_os_error(libc::EPERM)).to_string();
386        assert!(!msg.contains("RLIMIT_MEMLOCK"), "{msg}");
387        assert!(msg.contains("response"), "{msg}");
388    }
389
390    #[test]
391    fn a_worker_that_dies_before_reporting_is_an_error() {
392        let (tx, rx) = std::sync::mpsc::sync_channel::<io::Result<()>>(1);
393        drop(tx);
394        let err = await_startup("response", &rx).expect_err("no report");
395        assert!(err.to_string().contains("response"));
396    }
397}