use tokio_util::sync::CancellationToken;
const NOFILE_CEILING: nix::libc::rlim_t = 4096;
const NOFILE_FALLBACKS: [nix::libc::rlim_t; 3] = [2048, 1024, 512];
fn nofile_candidates(
soft: nix::libc::rlim_t,
hard: nix::libc::rlim_t,
ceiling: nix::libc::rlim_t,
) -> Vec<nix::libc::rlim_t> {
let top = hard.min(ceiling);
let mut candidates = vec![top];
candidates.extend(NOFILE_FALLBACKS.iter().copied().filter(|&c| c < top));
candidates.retain(|&c| c > soft);
candidates
}
pub fn raise_nofile_limit() {
raise_nofile_limit_with(read_nofile_limits, write_nofile_limits, NOFILE_CEILING);
}
fn read_nofile_limits() -> nix::Result<(nix::libc::rlim_t, nix::libc::rlim_t)> {
nix::sys::resource::getrlimit(nix::sys::resource::Resource::RLIMIT_NOFILE)
}
fn write_nofile_limits(soft: nix::libc::rlim_t, hard: nix::libc::rlim_t) -> nix::Result<()> {
nix::sys::resource::setrlimit(nix::sys::resource::Resource::RLIMIT_NOFILE, soft, hard)
}
#[derive(Debug, PartialEq, Eq)]
enum NofileOutcome {
Unreadable,
Adequate,
Raised(nix::libc::rlim_t),
Refused,
}
fn raise_nofile_limit_with(
get: impl FnOnce() -> nix::Result<(nix::libc::rlim_t, nix::libc::rlim_t)>,
mut set: impl FnMut(nix::libc::rlim_t, nix::libc::rlim_t) -> nix::Result<()>,
ceiling: nix::libc::rlim_t,
) -> NofileOutcome {
let (soft, hard) = match get() {
Ok(limits) => limits,
Err(e) => {
tracing::warn!("could not read the open-file limit: {e}");
return NofileOutcome::Unreadable;
}
};
let candidates = nofile_candidates(soft, hard, ceiling);
if candidates.is_empty() {
tracing::debug!("open-file limit {soft} (hard {hard}) needs no raising; leaving it");
return NofileOutcome::Adequate;
}
for target in candidates {
match set(target, hard) {
Ok(()) => {
tracing::info!("raised the open-file limit from {soft} to {target}");
return NofileOutcome::Raised(target);
}
Err(e) => {
tracing::warn!("could not raise the open-file limit from {soft} to {target}: {e}");
}
}
}
NofileOutcome::Refused
}
pub fn install_signal_handlers(shutdown: CancellationToken) {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
tokio::spawn(async move {
let mut term = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(e) => {
tracing::warn!("failed to install SIGTERM handler: {e}");
return;
}
};
let mut interrupt = match signal(SignalKind::interrupt()) {
Ok(s) => s,
Err(e) => {
tracing::warn!("failed to install SIGINT handler: {e}");
return;
}
};
let mut hangup = match signal(SignalKind::hangup()) {
Ok(s) => s,
Err(e) => {
tracing::warn!("failed to install SIGHUP handler: {e}");
return;
}
};
tokio::select! {
_ = term.recv() => tracing::info!("received SIGTERM; shutting down"),
_ = interrupt.recv() => tracing::info!("received SIGINT; shutting down"),
_ = hangup.recv() => tracing::info!("received SIGHUP; shutting down"),
}
shutdown.cancel();
});
}
#[cfg(not(unix))]
{
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
tracing::info!("received Ctrl-C; shutting down");
shutdown.cancel();
}
});
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn candidates_lead_with_the_ceiling_then_fall_back_to_smaller_values() {
assert_eq!(
nofile_candidates(256, nix::libc::RLIM_INFINITY, 4096),
vec![4096, 2048, 1024, 512]
);
}
#[test]
fn candidates_never_exceed_the_hard_limit() {
assert_eq!(nofile_candidates(256, 1024, 4096), vec![1024, 512]);
assert_eq!(nofile_candidates(256, 300, 4096), vec![300]);
}
#[test]
fn candidates_never_lower_or_rewrite_an_adequate_limit() {
assert!(nofile_candidates(4096, nix::libc::RLIM_INFINITY, 4096).is_empty());
assert!(nofile_candidates(65536, nix::libc::RLIM_INFINITY, 4096).is_empty());
assert!(nofile_candidates(1024, 1024, 4096).is_empty());
}
#[test]
fn candidates_skip_values_at_or_below_the_current_soft_limit() {
assert_eq!(
nofile_candidates(1500, nix::libc::RLIM_INFINITY, 4096),
vec![4096, 2048]
);
}
fn raise(
limits: nix::Result<(nix::libc::rlim_t, nix::libc::rlim_t)>,
refuse: impl Fn(nix::libc::rlim_t) -> bool,
) -> (NofileOutcome, Vec<nix::libc::rlim_t>) {
let mut attempts = Vec::new();
let outcome = raise_nofile_limit_with(
|| limits,
|target, _hard| {
attempts.push(target);
if refuse(target) {
Err(nix::errno::Errno::EINVAL)
} else {
Ok(())
}
},
4096,
);
(outcome, attempts)
}
#[test]
fn an_unreadable_limit_is_left_alone() {
let (outcome, attempts) = raise(Err(nix::errno::Errno::EPERM), |_| false);
assert_eq!(outcome, NofileOutcome::Unreadable);
assert!(attempts.is_empty());
}
#[test]
fn an_adequate_limit_is_not_touched() {
let (outcome, attempts) = raise(Ok((8192, nix::libc::RLIM_INFINITY)), |_| false);
assert_eq!(outcome, NofileOutcome::Adequate);
assert!(attempts.is_empty());
}
#[test]
fn the_limit_is_raised_to_the_ceiling_when_the_host_allows_it() {
let (outcome, attempts) = raise(Ok((256, nix::libc::RLIM_INFINITY)), |_| false);
assert_eq!(outcome, NofileOutcome::Raised(4096));
assert_eq!(attempts, vec![4096], "no further attempt after a success");
}
#[test]
fn a_refused_target_falls_back_to_the_next_smaller_one() {
let (outcome, attempts) = raise(Ok((256, nix::libc::RLIM_INFINITY)), |t| t > 1024);
assert_eq!(outcome, NofileOutcome::Raised(1024));
assert_eq!(attempts, vec![4096, 2048, 1024]);
}
#[test]
fn a_host_that_refuses_every_target_leaves_the_limit_as_it_was() {
let (outcome, attempts) = raise(Ok((256, nix::libc::RLIM_INFINITY)), |_| true);
assert_eq!(outcome, NofileOutcome::Refused);
assert_eq!(attempts, vec![4096, 2048, 1024, 512]);
}
#[test]
fn the_real_limit_accessors_round_trip() {
let (soft, hard) = read_nofile_limits().unwrap();
write_nofile_limits(soft, hard).unwrap();
assert_eq!(read_nofile_limits().unwrap(), (soft, hard));
}
#[test]
fn raise_nofile_limit_leaves_the_soft_limit_no_lower_than_it_found_it() {
use nix::sys::resource::{getrlimit, Resource};
let (before, _) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
raise_nofile_limit();
let (after, hard) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
assert!(after >= before, "soft limit fell from {before} to {after}");
assert!(after <= hard);
}
}