Skip to content

Commit 91a1fae

Browse files
drichardsonclaude
andauthored
Add PREK_MAX_CONCURRENCY environment variable (#1697)
## Summary - Add `PREK_MAX_CONCURRENCY` environment variable to cap the maximum number of concurrent hooks - Value is floored at 1; values above CPU count are allowed (useful for I/O-bound hooks) - Invalid values show a user-visible warning and fall back to CPU count - `PREK_NO_CONCURRENCY` takes precedence when both are set ## Motivation When `ulimit -n` is low, concurrent hook execution can exhaust file descriptors. This provides an environment variable to limit concurrency. For #1696 ## Test plan - [x] `mise run lint` — no warnings - [x] `mise run test` — all tests pass - [x] Unit tests for `resolve_concurrency`: valid value, above CPU count, zero floors to 1, invalid string, empty string, unset, no_concurrency, no_concurrency overrides max Co-authored-by: Claude <noreply@anthropic.com>
1 parent f7b0c00 commit 91a1fae

File tree

3 files changed

+71
-7
lines changed

3 files changed

+71
-7
lines changed

crates/prek-consts/src/env_vars.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ impl EnvVars {
2222
pub const PREK_SKIP: &'static str = "PREK_SKIP";
2323
pub const PREK_ALLOW_NO_CONFIG: &'static str = "PREK_ALLOW_NO_CONFIG";
2424
pub const PREK_NO_CONCURRENCY: &'static str = "PREK_NO_CONCURRENCY";
25+
pub const PREK_MAX_CONCURRENCY: &'static str = "PREK_MAX_CONCURRENCY";
2526
pub const PREK_NO_FAST_PATH: &'static str = "PREK_NO_FAST_PATH";
2627
pub const PREK_UV_SOURCE: &'static str = "PREK_UV_SOURCE";
2728
pub const PREK_NATIVE_TLS: &'static str = "PREK_NATIVE_TLS";

crates/prek/src/run.rs

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use rustc_hash::FxHashMap;
1010
use tracing::trace;
1111

1212
use crate::hook::Hook;
13+
use crate::warn_user;
1314

1415
pub(crate) static USE_COLOR: LazyLock<bool> =
1516
LazyLock::new(|| match anstream::Stderr::choice(&std::io::stderr()) {
@@ -19,14 +20,34 @@ pub(crate) static USE_COLOR: LazyLock<bool> =
1920
ColorChoice::Auto => unreachable!(),
2021
});
2122

22-
pub(crate) static CONCURRENCY: LazyLock<usize> = LazyLock::new(|| {
23-
if EnvVars::is_set(EnvVars::PREK_NO_CONCURRENCY) {
24-
1
25-
} else {
26-
std::thread::available_parallelism()
27-
.map(std::num::NonZero::get)
28-
.unwrap_or(1)
23+
fn resolve_concurrency(no_concurrency: bool, max_concurrency: Option<&str>, cpu: usize) -> usize {
24+
if no_concurrency {
25+
return 1;
26+
}
27+
28+
if let Some(v) = max_concurrency {
29+
if let Ok(cap) = v.parse::<usize>() {
30+
return cap.max(1);
31+
}
32+
warn_user!(
33+
"Invalid value for {}: {v:?}, using default ({cpu})",
34+
EnvVars::PREK_MAX_CONCURRENCY,
35+
);
2936
}
37+
38+
cpu
39+
}
40+
41+
pub(crate) static CONCURRENCY: LazyLock<usize> = LazyLock::new(|| {
42+
let cpu = std::thread::available_parallelism()
43+
.map(std::num::NonZero::get)
44+
.unwrap_or(1);
45+
46+
resolve_concurrency(
47+
EnvVars::is_set(EnvVars::PREK_NO_CONCURRENCY),
48+
EnvVars::var(EnvVars::PREK_MAX_CONCURRENCY).ok().as_deref(),
49+
cpu,
50+
)
3051
});
3152

3253
fn target_concurrency(serial: bool) -> usize {
@@ -352,6 +373,46 @@ mod tests {
352373
assert_eq!(total_files, 100);
353374
}
354375

376+
#[test]
377+
fn test_resolve_concurrency_defaults_to_cpu() {
378+
assert_eq!(resolve_concurrency(false, None, 16), 16);
379+
}
380+
381+
#[test]
382+
fn test_resolve_concurrency_max_caps_value() {
383+
assert_eq!(resolve_concurrency(false, Some("4"), 16), 4);
384+
}
385+
386+
#[test]
387+
fn test_resolve_concurrency_max_above_cpu() {
388+
assert_eq!(resolve_concurrency(false, Some("32"), 8), 32);
389+
}
390+
391+
#[test]
392+
fn test_resolve_concurrency_max_zero_floors_to_one() {
393+
assert_eq!(resolve_concurrency(false, Some("0"), 16), 1);
394+
}
395+
396+
#[test]
397+
fn test_resolve_concurrency_max_invalid_falls_back() {
398+
assert_eq!(resolve_concurrency(false, Some("abc"), 16), 16);
399+
}
400+
401+
#[test]
402+
fn test_resolve_concurrency_max_empty_falls_back() {
403+
assert_eq!(resolve_concurrency(false, Some(""), 16), 16);
404+
}
405+
406+
#[test]
407+
fn test_resolve_concurrency_no_concurrency() {
408+
assert_eq!(resolve_concurrency(true, None, 16), 1);
409+
}
410+
411+
#[test]
412+
fn test_resolve_concurrency_no_concurrency_overrides_max() {
413+
assert_eq!(resolve_concurrency(true, Some("8"), 16), 1);
414+
}
415+
355416
#[test]
356417
fn test_partitions_respects_cli_length_limit() {
357418
// Create files that will exceed CLI length limit

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,8 @@ prek supports the following environment variables:
13321332

13331333
- `PREK_NO_CONCURRENCY` — Disable parallelism for installs and runs (If set, force concurrency to 1).
13341334

1335+
- `PREK_MAX_CONCURRENCY` — Set the maximum number of concurrent hooks (minimum 1). Defaults to the number of CPU cores when unset. Ignored when `PREK_NO_CONCURRENCY` is set. If you encounter "Too many open files" errors, lowering this value or raising the file descriptor limit with `ulimit -n` can help.
1336+
13351337
- `PREK_NO_FAST_PATH` — Disable Rust-native built-in hooks; always use the original hook implementation. See [Built-in Fast Hooks](builtin.md) for details.
13361338

13371339
- `PREK_UV_SOURCE` — Control how uv (Python package installer) is installed. Options:

0 commit comments

Comments
 (0)