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}