use std::fmt;
use std::io;
pub const MAX_RING_ENTRIES: u32 = 32768;
pub const MIN_RING_ENTRIES: u32 = 64;
const MIN_PROVIDED_BUFFERS: u16 = 32;
const SQES_PER_CONNECTION: u64 = 2;
const HOUSEKEEPING_SQES: u64 = 4;
const BUFFERS_PER_CONNECTION: u64 = 2;
pub const MAX_SUPPORTED_CONNECTIONS: u64 = MAX_RING_ENTRIES as u64 / 2 / BUFFERS_PER_CONNECTION;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RingSizing {
ring_entries: u32,
provided_buffers: u16,
}
impl RingSizing {
pub fn for_max_connections(max_connections: u64) -> Result<Self, MaxConnectionsError> {
if max_connections == 0 {
return Err(MaxConnectionsError::Unlimited);
}
if max_connections > MAX_SUPPORTED_CONNECTIONS {
return Err(MaxConnectionsError::AboveCeiling(max_connections));
}
let sqes = SQES_PER_CONNECTION * max_connections + HOUSEKEEPING_SQES;
let buffers = (BUFFERS_PER_CONNECTION * max_connections)
.next_power_of_two()
.max(u64::from(MIN_PROVIDED_BUFFERS));
let ring_entries = sqes
.next_power_of_two()
.max(2 * buffers)
.clamp(u64::from(MIN_RING_ENTRIES), u64::from(MAX_RING_ENTRIES));
let sizing = Self {
ring_entries: u32::try_from(ring_entries)
.expect("ring entries are clamped to MAX_RING_ENTRIES"),
provided_buffers: u16::try_from(buffers)
.expect("buffers stay below MAX_RING_ENTRIES at the connection ceiling"),
};
assert!(
u64::from(sizing.ring_entries) >= sqes,
"the ring holds a drain's worth of SQEs"
);
assert!(
sizing.provided_buffers.is_power_of_two(),
"the buf_ring ABI requires a power-of-two pool"
);
assert!(
u32::from(sizing.provided_buffers) < sizing.ring_entries,
"the provided-buffer pool stays strictly below the ring"
);
Ok(sizing)
}
pub fn ring_entries(&self) -> u32 {
self.ring_entries
}
pub fn provided_buffers(&self) -> u16 {
self.provided_buffers
}
#[cfg(test)]
pub(crate) fn unchecked(ring_entries: u32, provided_buffers: u16) -> Self {
Self {
ring_entries,
provided_buffers,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaxConnectionsError {
Unlimited,
AboveCeiling(u64),
}
impl fmt::Display for MaxConnectionsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unlimited => write!(
f,
"--max-connections 0 (unlimited) is no longer supported: the io_uring rings \
are sized from the connection cap. Set a cap between 1 and \
{MAX_SUPPORTED_CONNECTIONS}"
),
Self::AboveCeiling(n) => write!(
f,
"--max-connections {n} is above the largest supported value, \
{MAX_SUPPORTED_CONNECTIONS}: the io_uring rings are sized from the \
connection cap, and the kernel's largest ring bounds them"
),
}
}
}
impl std::error::Error for MaxConnectionsError {}
#[inline]
pub(crate) fn connection_cap_reached(active: u64, max_connections: u64) -> bool {
active >= max_connections
}
pub(crate) fn ring_setup_error(stage: &str, entries: u32, err: io::Error) -> io::Error {
let kind = err.kind();
if err.raw_os_error() == Some(libc::ENOMEM) {
io::Error::new(
kind,
format!(
"{stage}: cannot create its io_uring instance ({entries} entries): {err}. \
The kernel charges io_uring ring memory against the locked-memory limit \
(RLIMIT_MEMLOCK), per user across all processes. Raise it for the node's \
service (systemd: LimitMEMLOCK=infinity) or grant it CAP_IPC_LOCK, or \
lower --max-connections"
),
)
} else {
io::Error::new(
kind,
format!("{stage}: cannot create its io_uring instance ({entries} entries): {err}"),
)
}
}
pub(crate) fn await_startup(
stage: &str,
report: &std::sync::mpsc::Receiver<io::Result<()>>,
) -> io::Result<()> {
match report.recv() {
Ok(result) => result,
Err(std::sync::mpsc::RecvError) => Err(io::Error::other(format!(
"{stage}: thread exited before reporting whether it started"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sizing(max_connections: u64) -> RingSizing {
RingSizing::for_max_connections(max_connections).expect("a supported cap")
}
#[test]
fn the_default_cap_keeps_the_fixed_sizes() {
let s = sizing(1024);
assert_eq!(s.ring_entries(), 4096);
assert_eq!(s.provided_buffers(), 2048);
}
#[test]
fn sizes_round_up_to_powers_of_two() {
let s = sizing(64);
assert_eq!(s.ring_entries(), 256);
assert_eq!(s.provided_buffers(), 128);
let s = sizing(1025);
assert_eq!(s.ring_entries(), 8192);
assert_eq!(s.provided_buffers(), 4096);
}
#[test]
fn the_ring_stays_strictly_above_the_pool() {
let s = sizing(100);
assert_eq!(s.provided_buffers(), 256);
assert_eq!(s.ring_entries(), 512);
}
#[test]
fn small_caps_take_the_floors() {
let s = sizing(1);
assert_eq!(s.ring_entries(), MIN_RING_ENTRIES);
assert_eq!(s.provided_buffers(), MIN_PROVIDED_BUFFERS);
}
#[test]
fn the_ceiling_is_the_largest_cap_that_sizes() {
assert_eq!(MAX_SUPPORTED_CONNECTIONS, 8192);
let s = sizing(MAX_SUPPORTED_CONNECTIONS);
assert_eq!(s.ring_entries(), MAX_RING_ENTRIES);
assert_eq!(s.provided_buffers(), 16384);
assert_eq!(
RingSizing::for_max_connections(MAX_SUPPORTED_CONNECTIONS + 1),
Err(MaxConnectionsError::AboveCeiling(
MAX_SUPPORTED_CONNECTIONS + 1
))
);
assert_eq!(
RingSizing::for_max_connections(u64::MAX),
Err(MaxConnectionsError::AboveCeiling(u64::MAX))
);
}
#[test]
fn zero_is_refused_and_says_why() {
let err = RingSizing::for_max_connections(0).expect_err("0 is refused");
assert_eq!(err, MaxConnectionsError::Unlimited);
let msg = err.to_string();
assert!(msg.contains("unlimited"), "{msg}");
assert!(msg.contains("io_uring"), "{msg}");
assert!(
msg.contains(&MAX_SUPPORTED_CONNECTIONS.to_string()),
"{msg}"
);
}
#[test]
fn above_the_ceiling_names_it() {
let msg = MaxConnectionsError::AboveCeiling(9000).to_string();
assert!(msg.contains("9000"), "{msg}");
assert!(
msg.contains(&MAX_SUPPORTED_CONNECTIONS.to_string()),
"{msg}"
);
}
#[test]
fn every_supported_cap_holds_the_invariants() {
for n in 1..=MAX_SUPPORTED_CONNECTIONS {
let s = sizing(n);
assert!(s.ring_entries().is_power_of_two());
assert!(u64::from(s.ring_entries()) >= SQES_PER_CONNECTION * n + HOUSEKEEPING_SQES);
assert!(u64::from(s.provided_buffers()) >= BUFFERS_PER_CONNECTION * n);
assert!(u32::from(s.provided_buffers()) < s.ring_entries());
assert!(s.ring_entries() <= MAX_RING_ENTRIES);
}
}
#[test]
fn the_gate_admits_up_to_the_cap() {
assert!(!connection_cap_reached(0, 1));
assert!(connection_cap_reached(1, 1));
assert!(!connection_cap_reached(1023, 1024));
assert!(connection_cap_reached(1024, 1024));
assert!(!connection_cap_reached(
MAX_SUPPORTED_CONNECTIONS - 1,
MAX_SUPPORTED_CONNECTIONS
));
assert!(connection_cap_reached(
MAX_SUPPORTED_CONNECTIONS,
MAX_SUPPORTED_CONNECTIONS
));
}
#[test]
fn enomem_names_the_locked_memory_limit() {
let err = ring_setup_error(
"uring-reader",
4096,
io::Error::from_raw_os_error(libc::ENOMEM),
);
let msg = err.to_string();
assert!(msg.contains("RLIMIT_MEMLOCK"), "{msg}");
assert!(msg.contains("LimitMEMLOCK="), "{msg}");
assert!(msg.contains("uring-reader"), "{msg}");
assert_eq!(err.kind(), io::ErrorKind::OutOfMemory);
}
#[test]
fn other_setup_errors_do_not_blame_the_limit() {
let msg =
ring_setup_error("response", 64, io::Error::from_raw_os_error(libc::EPERM)).to_string();
assert!(!msg.contains("RLIMIT_MEMLOCK"), "{msg}");
assert!(msg.contains("response"), "{msg}");
}
#[test]
fn a_worker_that_dies_before_reporting_is_an_error() {
let (tx, rx) = std::sync::mpsc::sync_channel::<io::Result<()>>(1);
drop(tx);
let err = await_startup("response", &rx).expect_err("no report");
assert!(err.to_string().contains("response"));
}
}