use std::sync::OnceLock;
use rustix::process::{getrlimit, setrlimit, Resource, Rlimit};
pub const PID1_NOFILE: u64 = 1 << 20;
pub const COMFORTABLE_NOFILE: u64 = 65_536;
pub const FDS_PER_OPEN_REPO: u64 = 3;
pub const RESERVED_FDS: u64 = 256;
pub const MIN_OPEN_REPOS: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Nofile {
pub soft: u64,
pub hard: u64,
pub was: u64,
}
impl Nofile {
pub fn raised(&self) -> bool {
self.soft > self.was
}
}
fn as_pair(limit: Rlimit) -> (u64, u64) {
(
limit.current.unwrap_or(u64::MAX),
limit.maximum.unwrap_or(u64::MAX),
)
}
fn raise(is_pid1: bool) -> Nofile {
let before = getrlimit(Resource::Nofile);
let (was, hard) = as_pair(before);
let want: &[(u64, u64)] = if is_pid1 {
&[(PID1_NOFILE, PID1_NOFILE), (hard, hard)]
} else {
&[(hard, hard)]
};
for &(soft, max) in want {
if soft <= was && max <= hard {
break;
}
let asked = Rlimit {
current: (soft != u64::MAX).then_some(soft),
maximum: (max != u64::MAX).then_some(max),
};
if setrlimit(Resource::Nofile, asked).is_ok() {
break;
}
}
let (soft, hard) = as_pair(getrlimit(Resource::Nofile));
Nofile { soft, hard, was }
}
pub fn nofile() -> Nofile {
static LIMIT: OnceLock<Nofile> = OnceLock::new();
*LIMIT.get_or_init(|| {
let limit = raise(appliance_pid1::net::is_init());
announce(&limit);
limit
})
}
fn announce(limit: &Nofile) {
if limit.soft < COMFORTABLE_NOFILE {
tracing::warn!(
target: "gunnar::limits",
event = "limits.nofile.low",
soft = limit.soft,
hard = limit.hard,
was = limit.was,
open_repos = open_repo_budget(limit.soft),
"fd limit {} is low (hard {}); the handle cache is bounded to fit but a busy box \
will re-open repositories — raise LimitNOFILE / ulimit -n",
limit.soft,
limit.hard
);
} else {
tracing::info!(
target: "gunnar::limits",
event = "limits.nofile",
soft = limit.soft,
hard = limit.hard,
was = limit.was,
raised = limit.raised(),
open_repos = open_repo_budget(limit.soft),
"fd limit {} (was {}); the handle cache keeps up to {} repositories open",
limit.soft,
limit.was,
open_repo_budget(limit.soft)
);
}
}
pub fn open_repo_budget(soft: u64) -> usize {
let fits = soft.saturating_sub(RESERVED_FDS) / FDS_PER_OPEN_REPO;
usize::try_from(fits)
.unwrap_or(usize::MAX)
.clamp(MIN_OPEN_REPOS, crate::stores::DEFAULT_OPEN_REPOS)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_budget_fits_under_the_kernel_default_and_caps_at_the_old_bound() {
assert_eq!(open_repo_budget(1024), 256);
assert!(open_repo_budget(1024) as u64 * FDS_PER_OPEN_REPO + RESERVED_FDS <= 1024);
assert_eq!(open_repo_budget(512), 85);
assert!(open_repo_budget(512) as u64 * FDS_PER_OPEN_REPO + RESERVED_FDS <= 512);
assert_eq!(open_repo_budget(2048), crate::stores::DEFAULT_OPEN_REPOS);
assert_eq!(open_repo_budget(65_536), crate::stores::DEFAULT_OPEN_REPOS);
assert_eq!(
open_repo_budget(u64::MAX),
crate::stores::DEFAULT_OPEN_REPOS
);
assert_eq!(open_repo_budget(256), MIN_OPEN_REPOS);
assert_eq!(open_repo_budget(0), MIN_OPEN_REPOS);
}
#[test]
fn a_non_pid1_process_ends_at_its_hard_limit_and_never_lower() {
let before = as_pair(getrlimit(Resource::Nofile));
let limit = raise(false);
assert_eq!(limit.was, before.0);
assert!(
limit.soft >= before.0,
"raise lowered the soft limit: {limit:?}"
);
assert_eq!(
limit.soft, limit.hard,
"an unprivileged raise stops at the hard limit"
);
assert_eq!(
limit.hard, before.1,
"an unprivileged raise must not move the hard limit"
);
assert_eq!(limit.raised(), limit.soft > before.0);
let again = raise(false);
assert_eq!((again.soft, again.hard), (limit.soft, limit.hard));
}
#[test]
fn nofile_is_one_answer_per_process() {
assert_eq!(nofile(), nofile());
assert!(nofile().soft >= MIN_OPEN_REPOS as u64);
}
}