use std::fs::File;
use std::path::PathBuf;
use std::time::{Duration, Instant};
const ENV_MAX_BUILDS: &str = "SEMANTEX_MAX_CONCURRENT_BUILDS";
const RAM_GB_PER_BUILD: u64 = 16;
const FALLBACK_CONCURRENCY: usize = 1;
pub fn max_concurrent_builds() -> usize {
let ram_default = match crate::memory::system_ram_mb() {
Some(mb) => (((mb / 1024) / RAM_GB_PER_BUILD).max(1)) as usize,
None => FALLBACK_CONCURRENCY,
};
crate::config::env_usize(ENV_MAX_BUILDS, ram_default)
}
fn slots_dir() -> PathBuf {
crate::config::SemantexConfig::semantex_home().join("build-slots")
}
#[must_use = "dropping the BuildSlot immediately releases the slot"]
pub struct BuildSlot {
_file: File,
}
const POLL_INTERVAL: Duration = Duration::from_millis(250);
const MAX_WAIT: Duration = Duration::from_hours(1);
pub fn acquire(log_waiting: impl FnOnce()) -> Option<BuildSlot> {
acquire_in(&slots_dir(), log_waiting)
}
fn acquire_in(dir: &std::path::Path, log_waiting: impl FnOnce()) -> Option<BuildSlot> {
if std::fs::create_dir_all(dir).is_err() {
return None;
}
let mut files: Vec<File> = (0..max_concurrent_builds())
.filter_map(|i| File::create(dir.join(format!("slot-{i}.lock"))).ok())
.collect();
if files.is_empty() {
return None;
}
let mut log_waiting = Some(log_waiting);
let start = Instant::now();
loop {
if let Some(i) = files.iter().position(|f| f.try_lock().is_ok()) {
return Some(BuildSlot {
_file: files.swap_remove(i),
});
}
if let Some(f) = log_waiting.take() {
f();
}
if start.elapsed() > MAX_WAIT {
return None;
}
std::thread::sleep(POLL_INTERVAL);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_GUARD: Mutex<()> = Mutex::new(());
#[test]
fn env_override_is_honored() {
let _g = ENV_GUARD.lock().unwrap();
unsafe { std::env::set_var(ENV_MAX_BUILDS, "3") };
assert_eq!(max_concurrent_builds(), 3);
unsafe { std::env::set_var(ENV_MAX_BUILDS, "0") };
assert!(max_concurrent_builds() >= 1, "must stay ≥ 1");
unsafe { std::env::remove_var(ENV_MAX_BUILDS) };
}
#[test]
fn default_scales_with_ram_and_is_at_least_one() {
let _g = ENV_GUARD.lock().unwrap();
unsafe { std::env::remove_var(ENV_MAX_BUILDS) };
assert!(max_concurrent_builds() >= 1);
}
#[test]
fn slot_is_held_until_dropped_then_reusable() {
let _g = ENV_GUARD.lock().unwrap();
unsafe { std::env::set_var(ENV_MAX_BUILDS, "1") };
let dir = tempfile::tempdir().unwrap();
let slot_path = dir.path().join("slot-0.lock");
let first = acquire_in(dir.path(), || {});
assert!(first.is_some(), "first acquire should get the only slot");
let probe = File::create(&slot_path).unwrap();
assert!(probe.try_lock().is_err(), "slot must be locked while held");
drop(probe);
drop(first);
let probe2 = File::create(&slot_path).unwrap();
let deadline = Instant::now() + Duration::from_secs(2);
let freed = loop {
if probe2.try_lock().is_ok() {
break true;
}
if Instant::now() > deadline {
break false;
}
std::thread::sleep(Duration::from_millis(20));
};
assert!(freed, "slot should be free after the guard is dropped");
unsafe { std::env::remove_var(ENV_MAX_BUILDS) };
}
}