1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! Global concurrency limiter for lean-ctx processes.
//!
//! Prevents runaway CPU usage by limiting the number of concurrent lean-ctx
//! processes to `MAX_CONCURRENT`. Each process acquires a numbered lock slot
//! under `~/.lean-ctx/locks/`. If all slots are taken, the caller gets `None`.
use std::fs::File;
use std::path::PathBuf;
const MAX_CONCURRENT: usize = 4;
pub(crate) struct ProcessGuard {
_file: File,
path: PathBuf,
}
impl Drop for ProcessGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn lock_dir() -> Option<PathBuf> {
let dir = crate::core::data_dir::lean_ctx_data_dir()
.ok()?
.join("locks");
let _ = std::fs::create_dir_all(&dir);
Some(dir)
}
/// Try to acquire one of N concurrent process slots.
/// Returns `None` if all slots are occupied (= too many lean-ctx already running).
pub(crate) fn acquire() -> Option<ProcessGuard> {
let dir = lock_dir()?;
for slot in 0..MAX_CONCURRENT {
let path = dir.join(format!("slot-{slot}.lock"));
let Ok(file) = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(&path)
else {
continue;
};
if try_flock(&file) {
use std::io::Write;
let mut f = file;
let _ = f.write_all(format!("{}", std::process::id()).as_bytes());
return Some(ProcessGuard { _file: f, path });
}
}
None
}
/// Checks how many slots are currently held (best-effort).
pub(crate) fn active_count() -> usize {
let Some(dir) = lock_dir() else { return 0 };
let mut count = 0;
for slot in 0..MAX_CONCURRENT {
let path = dir.join(format!("slot-{slot}.lock"));
if let Ok(f) = std::fs::OpenOptions::new().read(true).open(&path)
&& !try_flock(&f)
{
count += 1;
}
}
count
}
#[cfg(unix)]
fn try_flock(file: &File) -> bool {
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
// SAFETY: `fd` is a valid, open descriptor owned by `file`, which outlives
// this call; `flock` performs no pointer dereference and reports errors via
// its return value.
let rc = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
rc == 0
}
#[cfg(not(unix))]
fn try_flock(_file: &File) -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
/// Restores `LEAN_CTX_DATA_DIR` to its previous value on drop (panic-safe).
struct EnvRestore(Option<String>);
impl Drop for EnvRestore {
fn drop(&mut self) {
match &self.0 {
Some(v) => crate::test_env::set_var("LEAN_CTX_DATA_DIR", v),
None => crate::test_env::remove_var("LEAN_CTX_DATA_DIR"),
}
}
}
/// Runs `body` against a private, empty lock directory.
///
/// `acquire()` and `active_count()` both resolve the lock dir from
/// `LEAN_CTX_DATA_DIR`. Serializing on `test_env_lock` stops a concurrent
/// test from repointing that variable between the two calls (which made
/// `active_count` inspect a different, empty dir and miss the held slot), and
/// the private temp dir keeps slots independent of any real lean-ctx process
/// (daemon/proxy) that might otherwise occupy them.
fn with_isolated_lock_dir(body: impl FnOnce()) {
let _env = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().expect("tempdir");
// Restore runs before `tmp` is removed and while the lock is still held.
let _restore = EnvRestore(std::env::var("LEAN_CTX_DATA_DIR").ok());
crate::test_env::set_var("LEAN_CTX_DATA_DIR", tmp.path());
body();
}
#[test]
fn acquire_and_release() {
with_isolated_lock_dir(|| {
let guard = acquire();
assert!(guard.is_some(), "should acquire first slot");
drop(guard);
});
}
#[cfg(unix)]
#[test]
fn active_count_reflects_held_slots() {
with_isolated_lock_dir(|| {
let g1 = acquire();
assert!(g1.is_some());
let count = active_count();
assert!(count >= 1, "at least one slot held, got {count}");
drop(g1);
});
}
}