Skip to content

Commit 950dd53

Browse files
committed
feat(build): Added dynamic resource limit handling
This commit adds handling to attempt to increase the max file descriptors if we detect that there is a risk of hitting the limit. If we cannot increase the max file descriptors, we fall back to coarse grain locking as that is better than a build crashing due to resource limits.
1 parent 758fa0e commit 950dd53

File tree

3 files changed

+156
-1
lines changed

3 files changed

+156
-1
lines changed

src/cargo/core/compiler/build_runner/mod.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ use crate::core::compiler::locking::LockingMode;
1010
use crate::core::compiler::{self, Unit, artifact};
1111
use crate::util::cache_lock::CacheLockMode;
1212
use crate::util::errors::CargoResult;
13+
use crate::util::rlimit;
1314
use annotate_snippets::{Level, Message};
1415
use anyhow::{Context as _, bail};
1516
use cargo_util::paths;
1617
use filetime::FileTime;
1718
use itertools::Itertools;
1819
use jobserver::Client;
20+
use tracing::{debug, warn};
1921

2022
use super::build_plan::BuildPlan;
2123
use super::custom_build::{self, BuildDeps, BuildScriptOutputs, BuildScripts};
@@ -117,7 +119,7 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
117119
};
118120

119121
let locking_mode = if bcx.gctx.cli_unstable().fine_grain_locking {
120-
LockingMode::Fine
122+
determine_locking_mode(&bcx)?
121123
} else {
122124
LockingMode::Coarse
123125
};
@@ -736,3 +738,49 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
736738
}
737739
}
738740
}
741+
742+
/// Determines if we have enough resources to safely use fine grain locking.
743+
/// This function will raises the number of max number file descriptors the current process
744+
/// can have open at once to make sure we are able to compile without running out of fds.
745+
///
746+
/// If we cannot acquire a safe max number of file descriptors, we fallback to coarse grain
747+
/// locking.
748+
pub fn determine_locking_mode(bcx: &BuildContext<'_, '_>) -> CargoResult<LockingMode> {
749+
let total_units = bcx.unit_graph.keys().len() as u64;
750+
751+
// This is a bit arbitrary but if we do not have at least 10 times the remaining file
752+
// descriptors than total build units there is a chance we could hit the limit.
753+
// This is a fairly conservative estimate to make sure we don't hit max fd errors.
754+
let safe_threshold = total_units * 10;
755+
756+
let Ok(mut limit) = rlimit::get_max_file_descriptors() else {
757+
return Ok(LockingMode::Coarse);
758+
};
759+
760+
if limit.soft_limit >= safe_threshold {
761+
// The limit is higher or equal to what we deemed safe, so
762+
// there is no need to raise the limit.
763+
return Ok(LockingMode::Fine);
764+
}
765+
766+
let display_fd_warning = || -> CargoResult<()> {
767+
bcx.gctx.shell().verbose(|shell| shell.warn("ulimit was to low to safely enable fine grain locking, falling back to coarse grain locking"))
768+
};
769+
770+
if limit.hard_limit < safe_threshold {
771+
// The max we could raise the limit to is still not enough to safely compile.
772+
display_fd_warning()?;
773+
return Ok(LockingMode::Coarse);
774+
}
775+
776+
limit.soft_limit = safe_threshold;
777+
778+
debug!("raising fd limit to {safe_threshold}");
779+
if let Err(err) = rlimit::set_max_file_descriptors(limit) {
780+
warn!("failed to raise max fds: {err}");
781+
display_fd_warning()?;
782+
return Ok(LockingMode::Coarse);
783+
}
784+
785+
return Ok(LockingMode::Fine);
786+
}

src/cargo/util/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ mod once;
6161
mod progress;
6262
mod queue;
6363
pub mod restricted_names;
64+
pub mod rlimit;
6465
pub mod rustc;
6566
mod semver_eval_ext;
6667
mod semver_ext;

src/cargo/util/rlimit.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#[cfg(unix)]
2+
use libc::{RLIMIT_NOFILE, getrlimit, rlimit, setrlimit};
3+
4+
use crate::CargoResult;
5+
6+
pub struct ResourceLimits {
7+
pub soft_limit: u64,
8+
pub hard_limit: u64,
9+
}
10+
11+
#[cfg(unix)]
12+
pub fn get_max_file_descriptors() -> CargoResult<ResourceLimits> {
13+
let mut rlim = rlimit {
14+
rlim_cur: 0,
15+
rlim_max: 0,
16+
};
17+
18+
let result = unsafe { getrlimit(RLIMIT_NOFILE, &mut rlim) };
19+
if result != 0 {
20+
return Err(anyhow::Error::from(std::io::Error::last_os_error())
21+
.context("Failed to get rlimit with error code"));
22+
}
23+
24+
return Ok(ResourceLimits {
25+
soft_limit: rlim.rlim_cur,
26+
hard_limit: rlim.rlim_max,
27+
});
28+
}
29+
30+
#[cfg(windows)]
31+
pub fn get_max_file_descriptors() -> CargoResult<ResourceLimits> {
32+
let soft_limit = windows::getmaxstdio() as u64;
33+
34+
return Ok(ResourceLimits {
35+
soft_limit,
36+
// Windows does not provide a way to get the hard limit so we return a estimated max.
37+
// This is likely less than max for some systems but should be supported by most and is
38+
// likely high enough that most projects do not run out of file descriptors.
39+
// See: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio?view=msvc-170#remarks
40+
hard_limit: 8192,
41+
});
42+
}
43+
44+
#[cfg(unix)]
45+
pub fn set_max_file_descriptors(limits: ResourceLimits) -> CargoResult<()> {
46+
let rlim = rlimit {
47+
rlim_cur: limits.soft_limit,
48+
rlim_max: limits.hard_limit,
49+
};
50+
let result = unsafe { setrlimit(RLIMIT_NOFILE, &rlim) };
51+
if result != 0 {
52+
return Err(anyhow::Error::from(std::io::Error::last_os_error())
53+
.context("Failed to set rlimit with error code"));
54+
}
55+
56+
return Ok(());
57+
}
58+
59+
#[cfg(windows)]
60+
pub fn set_max_file_descriptors(limits: ResourceLimits) -> CargoResult<()> {
61+
windows::setmaxstdio(limits.soft_limit as u32)?;
62+
return Ok(());
63+
}
64+
65+
#[cfg(windows)]
66+
mod windows {
67+
use std::io;
68+
use std::os::raw::c_int;
69+
70+
unsafe extern "C" {
71+
fn _setmaxstdio(new_max: c_int) -> c_int;
72+
fn _getmaxstdio() -> c_int;
73+
}
74+
75+
/// Sets a maximum for the number of simultaneously open files at the stream I/O level.
76+
///
77+
/// See <https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio?view=msvc-170>
78+
pub fn setmaxstdio(new_max: u32) -> io::Result<u32> {
79+
// A negative `new_max` will cause EINVAL.
80+
// A negative `ret` should never appear.
81+
// It is safe even if the return value is wrong.
82+
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
83+
unsafe {
84+
let ret = _setmaxstdio(new_max as c_int);
85+
if ret < 0 {
86+
return Err(io::Error::last_os_error());
87+
}
88+
Ok(ret as u32)
89+
}
90+
}
91+
92+
/// Returns the number of simultaneously open files permitted at the stream I/O level.
93+
///
94+
/// See <https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/getmaxstdio?view=msvc-170>
95+
#[must_use]
96+
pub fn getmaxstdio() -> u32 {
97+
// A negative `ret` should never appear.
98+
// It is safe even if the return value is wrong.
99+
#[allow(clippy::cast_sign_loss)]
100+
unsafe {
101+
let ret = _getmaxstdio();
102+
debug_assert!(ret >= 0);
103+
ret as u32
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)