use crate::config::{Config, SchedulerPriority};
use eyre::{Context, Result};
use log::debug;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
const SCHEDULER_DIR: &str = "scheduler";
const LEASES_DIR: &str = "leases";
const INFLIGHT_DIR: &str = "inflight";
const POOL_LOCK: &str = "pool.lock";
const PRIORITY_WAIT_STAMP: &str = "priority-wait";
const LEDGER_FILE: &str = "memory.json";
const LEDGER_LOCK: &str = "memory.lock";
pub(crate) const SCHED_DIR_ENV: &str = "MBX_SCHED_DIR";
pub(crate) const SCHED_SLOTS_ENV: &str = "MBX_SCHED_SLOTS";
pub(crate) const SCHED_SLOT_BYTES_ENV: &str = "MBX_SCHED_SLOT_BYTES";
pub(crate) const SCHED_PRIORITY_ENV: &str = "MBX_SCHED_PRIORITY";
const SCHED_BUILD_ID_ENV: &str = "MBX_SCHED_BUILD_ID";
const SCHED_BUILD_SLOTS_ENV: &str = "MBX_SCHED_BUILD_SLOTS";
const LEASE_VERSION: u8 = 1;
const FLIGHT_VERSION: u8 = 1;
const MAX_FLIGHT_PAYLOAD: usize = 4 * 1024 * 1024;
const LEDGER_VERSION: u8 = 1;
const MAX_LEDGER_ENTRIES: usize = 1024;
const MAX_LINK_SAMPLES: usize = 16;
const MIN_LINK_SAMPLES: usize = 3;
const LINK_WEIGHT: u64 = 2;
const POLL_INITIAL: Duration = Duration::from_millis(2);
const POLL_MAX: Duration = Duration::from_millis(25);
const PRIORITY_WAIT_FRESHNESS: Duration = Duration::from_secs(2);
const PRIORITY_STAMP_INTERVAL: Duration = Duration::from_millis(500);
const SLOW_WAIT_NOTICE: Duration = Duration::from_secs(60);
const GATE_DEADLINE: Duration = Duration::from_secs(120);
static LEASE_NONCE: AtomicU64 = AtomicU64::new(0);
const LEASE_NAME_ATTEMPTS: usize = 8;
fn process_token() -> &'static str {
static TOKEN: OnceLock<String> = OnceLock::new();
TOKEN.get_or_init(|| crate::util::random_string(8))
}
pub(crate) struct Demand {
name: String,
links: bool,
}
impl Demand {
pub(crate) fn new(name: &str, links: bool) -> Self {
Self {
name: name.to_string(),
links,
}
}
}
pub(crate) struct Permit {
lock: Option<fslock::LockFile>,
path: PathBuf,
}
impl Drop for Permit {
fn drop(&mut self) {
drop(self.lock.take());
let _ = std::fs::remove_file(&self.path);
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Lease {
version: u8,
weight: u64,
priority: String,
#[serde(default)]
build_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct FlightRecord {
version: u8,
adapter: String,
invocation: String,
payload: String,
}
pub(crate) struct Flight {
lock: Option<fslock::LockFile>,
path: PathBuf,
adapter: &'static str,
invocation: String,
waited: bool,
inherited: Option<String>,
}
impl Flight {
pub(crate) fn waited(&self) -> bool {
self.waited
}
pub(crate) fn inherited(&self) -> Option<&str> {
self.inherited.as_deref()
}
pub(crate) fn leave(&self, payload: &str) {
if payload.len() > MAX_FLIGHT_PAYLOAD {
return;
}
let record = FlightRecord {
version: FLIGHT_VERSION,
adapter: self.adapter.into(),
invocation: self.invocation.clone(),
payload: payload.into(),
};
let written = serde_json::to_vec(&record)
.map_err(eyre::Report::from)
.and_then(|bytes| crate::util::write_atomic(&record_path(&self.path), &bytes));
if let Err(error) = written {
debug!("a flight prediction was not left behind: {error:#}");
}
}
}
fn record_path(lock_path: &Path) -> PathBuf {
let mut path = lock_path.as_os_str().to_owned();
path.push(".prediction");
PathBuf::from(path)
}
const FLIGHT_MAX_AGE: Duration = Duration::from_secs(30 * 24 * 60 * 60);
pub(crate) fn prune_flights(cache_dir: &Path) {
let dir = cache_dir.join(SCHEDULER_DIR).join(INFLIGHT_DIR);
let Ok(entries) = std::fs::read_dir(&dir) else {
return;
};
let stale = |path: &Path| {
std::fs::metadata(path)
.and_then(|metadata| metadata.modified())
.ok()
.and_then(|modified| modified.elapsed().ok())
.is_some_and(|age| age > FLIGHT_MAX_AGE)
};
for entry in entries.flatten() {
let path = entry.path();
if path
.extension()
.is_some_and(|extension| extension == "prediction")
{
if stale(&path) && !path.with_extension("").exists() {
let _ = std::fs::remove_file(&path);
}
continue;
}
let record = record_path(&path);
if !stale(&path) || (record.exists() && !stale(&record)) {
continue;
}
let Ok(mut lock) = fslock::LockFile::open(&path) else {
continue;
};
if lock.try_lock().is_ok_and(|taken| taken) {
let _ = std::fs::remove_file(&record);
drop(lock);
let _ = std::fs::remove_file(&path);
}
}
}
impl Drop for Flight {
fn drop(&mut self) {
drop(self.lock.take());
}
}
pub(crate) fn flight(adapter: &'static str, invocation: &str) -> Option<Flight> {
let pool = pool()?;
match flight_at(&pool.dir, adapter, invocation) {
Ok(flight) => Some(flight),
Err(error) => {
debug!("compiling outside a flight: {error:#}");
None
}
}
}
fn flight_at(pool_dir: &Path, adapter: &'static str, invocation: &str) -> Result<Flight> {
let dir = pool_dir.join(INFLIGHT_DIR);
std::fs::create_dir_all(&dir)
.wrap_err_with(|| format!("failed to create {}", dir.display()))?;
let path = dir.join(format!("{adapter}-{invocation}"));
let mut lock = fslock::LockFile::open(&path)?;
let waited = !lock.try_lock()?;
if waited {
lock.lock()?;
}
let inherited = std::fs::read(record_path(&path))
.ok()
.and_then(|bytes| serde_json::from_slice::<FlightRecord>(&bytes).ok())
.filter(|record| {
record.version == FLIGHT_VERSION
&& record.adapter == adapter
&& record.invocation == invocation
})
.map(|record| record.payload);
Ok(Flight {
lock: Some(lock),
path,
adapter,
invocation: invocation.to_string(),
waited,
inherited,
})
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct MemoryLedger {
version: u8,
crates: BTreeMap<String, u64>,
link_peaks: Vec<u64>,
}
pub(crate) struct Pool {
dir: PathBuf,
capacity: u64,
build: Option<(String, u64)>,
bytes_per_permit: u64,
priority: SchedulerPriority,
available_memory: fn() -> Option<u64>,
}
pub(crate) fn pool() -> Option<&'static Pool> {
static POOL: OnceLock<Option<Pool>> = OnceLock::new();
POOL.get_or_init(resolve_pool).as_ref()
}
fn resolve_pool() -> Option<Pool> {
match std::env::var_os(SCHED_DIR_ENV) {
Some(dir) if dir.is_empty() => None,
Some(dir) => {
let capacity = env_number(SCHED_SLOTS_ENV).unwrap_or_else(default_capacity);
let mut pool = Pool::new(
PathBuf::from(dir),
capacity,
env_number(SCHED_SLOT_BYTES_ENV).unwrap_or(0),
std::env::var(SCHED_PRIORITY_ENV)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or_default(),
);
pool.build = std::env::var(SCHED_BUILD_ID_ENV)
.ok()
.filter(|id| !id.is_empty())
.map(|id| {
let slots = env_number(SCHED_BUILD_SLOTS_ENV)
.unwrap_or(capacity)
.clamp(1, capacity.max(1));
(id, slots)
});
Some(pool)
}
None => match Config::load() {
Ok(config) => Pool::from_config(&config),
Err(error) => {
debug!("compile scheduling is off; configuration did not load: {error:#}");
None
}
},
}
}
fn env_number(name: &str) -> Option<u64> {
std::env::var(name).ok()?.parse().ok()
}
fn default_capacity() -> u64 {
std::thread::available_parallelism().map_or(1, |cpus| cpus.get() as u64)
}
pub(crate) fn session_environment_with_jobs(
config: &Config,
cargo_jobs: Option<u64>,
) -> Vec<(String, String)> {
let scheduler = &config.scheduler;
if !scheduler.enabled {
return vec![(SCHED_DIR_ENV.into(), String::new())];
}
let permits = scheduler.permits();
let build_permits = cargo_jobs.map_or(permits, |jobs| permits.min(jobs.max(1)));
vec![
(
SCHED_DIR_ENV.into(),
config
.cache_dir
.join(SCHEDULER_DIR)
.to_string_lossy()
.into_owned(),
),
(SCHED_SLOTS_ENV.into(), permits.to_string()),
(
SCHED_SLOT_BYTES_ENV.into(),
bytes_per_permit(scheduler.memory_bytes, permits).to_string(),
),
(
SCHED_PRIORITY_ENV.into(),
scheduler.priority.as_str().into(),
),
(SCHED_BUILD_ID_ENV.into(), crate::util::random_string(12)),
(SCHED_BUILD_SLOTS_ENV.into(), build_permits.to_string()),
]
}
fn bytes_per_permit(memory_bytes: Option<u64>, cpus: u64) -> u64 {
memory_bytes.map_or(0, |memory| memory / cpus.max(1))
}
pub(crate) fn record_compiler_memory(demand: &Demand, status: &ExitStatus) {
let Some(pool) = pool() else {
return;
};
pool.note_compilation(demand, status);
}
impl Pool {
fn new(
dir: PathBuf,
capacity: u64,
bytes_per_permit: u64,
priority: SchedulerPriority,
) -> Self {
Self {
dir,
capacity: capacity.max(1),
build: None,
bytes_per_permit,
priority,
available_memory: crate::util::memory_available_bytes,
}
}
fn from_config(config: &Config) -> Option<Self> {
let scheduler = &config.scheduler;
let permits = scheduler.permits();
scheduler.enabled.then(|| {
Self::new(
config.cache_dir.join(SCHEDULER_DIR),
permits,
bytes_per_permit(scheduler.memory_bytes, permits),
scheduler.priority,
)
})
}
pub(crate) fn admit(&self, demand: &Demand) -> Option<Permit> {
let (weight, predicted) = self.plan(demand);
let started = Instant::now();
let mut delay = POLL_INITIAL;
let mut stamped: Option<Instant> = None;
let mut complained = false;
loop {
let gate = predicted.filter(|_| started.elapsed() < GATE_DEADLINE);
match self.try_admit(weight, gate) {
Ok(Some(permit)) => return Some(permit),
Ok(None) => {}
Err(error) => {
debug!("compiling without a machine-wide permit: {error:#}");
return None;
}
}
if self.priority == SchedulerPriority::Normal
&& stamped.is_none_or(|last| last.elapsed() >= PRIORITY_STAMP_INTERVAL)
{
let _ = std::fs::write(self.dir.join(PRIORITY_WAIT_STAMP), b"");
stamped = Some(Instant::now());
}
if !complained && started.elapsed() >= SLOW_WAIT_NOTICE {
complained = true;
debug!(
"waiting for {weight} of {} machine-wide compile permits ({})",
self.capacity, demand.name
);
}
std::thread::sleep(jittered(delay));
delay = (delay * 2).min(POLL_MAX);
}
}
fn plan(&self, demand: &Demand) -> (u64, Option<u64>) {
let link_floor = demand.links.then_some(LINK_WEIGHT);
if self.bytes_per_permit == 0 {
return (link_floor.unwrap_or(1).min(self.capacity), None);
}
let ledger = read_ledger(&self.ledger_path());
let recorded = ledger
.crates
.get(&ledger_key(&demand.name, demand.links))
.copied();
let link_bytes = link_floor.map(|weight| {
let measured = (ledger.link_peaks.len() >= MIN_LINK_SAMPLES)
.then(|| ledger.link_peaks.iter().copied().max())
.flatten();
measured.unwrap_or_else(|| weight.saturating_mul(self.bytes_per_permit))
});
let predicted = recorded.or(link_bytes);
let weight = predicted
.map_or(1, |bytes| bytes.div_ceil(self.bytes_per_permit))
.clamp(1, self.capacity);
(weight, predicted)
}
fn try_admit(&self, weight: u64, predicted: Option<u64>) -> Result<Option<Permit>> {
let leases = self.dir.join(LEASES_DIR);
std::fs::create_dir_all(&leases)
.wrap_err_with(|| format!("failed to create {}", leases.display()))?;
let registrar_path = self.dir.join(POOL_LOCK);
let mut registrar = fslock::LockFile::open(®istrar_path)?;
registrar.lock()?;
let live = scan_leases(&leases)?;
let capacity = self.capacity - self.reserved();
let used: u64 = live.iter().map(|lease| lease.weight).sum();
if let Some((build_id, build_capacity)) = &self.build {
let build_used = live
.iter()
.filter(|lease| lease.build_id.as_ref() == Some(build_id))
.map(|lease| lease.weight)
.sum::<u64>();
if build_used > 0 && build_used.saturating_add(weight) > *build_capacity {
return Ok(None);
}
}
if used == 0 && weight > capacity {
return self.grant(&leases, weight).map(Some);
}
if used.saturating_add(weight) > capacity {
return Ok(None);
}
if used > 0
&& let Some(needed) = predicted
&& let Some(available) = (self.available_memory)()
&& available < needed
{
return Ok(None);
}
self.grant(&leases, weight).map(Some)
}
fn grant(&self, leases: &Path, weight: u64) -> Result<Permit> {
let lease = serde_json::to_vec(&Lease {
version: LEASE_VERSION,
weight,
priority: self.priority.as_str().into(),
build_id: self.build.as_ref().map(|(id, _)| id.clone()),
})?;
let mut last = None;
for _ in 0..LEASE_NAME_ATTEMPTS {
let path = leases.join(format!(
"{}-{}-{}",
std::process::id(),
process_token(),
LEASE_NONCE.fetch_add(1, Ordering::Relaxed)
));
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(mut file) => {
std::io::Write::write_all(&mut file, &lease)
.wrap_err_with(|| format!("failed to write {}", path.display()))?;
drop(file);
let mut lock = fslock::LockFile::open(&path)?;
if !lock.try_lock()? {
eyre::bail!("a freshly created lease was already locked");
}
return Ok(Permit {
lock: Some(lock),
path,
});
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
last = Some(error);
}
Err(error) => {
return Err(error)
.wrap_err_with(|| format!("failed to create {}", path.display()));
}
}
}
Err(last.expect("a name is only retried after a collision"))
.wrap_err("failed to name a lease that was not already taken")
}
fn reserved(&self) -> u64 {
if self.priority == SchedulerPriority::Low && self.priority_wait_is_fresh() {
(self.capacity / 4).max(1).min(self.capacity - 1)
} else {
0
}
}
fn priority_wait_is_fresh(&self) -> bool {
std::fs::metadata(self.dir.join(PRIORITY_WAIT_STAMP))
.and_then(|metadata| metadata.modified())
.ok()
.and_then(|modified| modified.elapsed().ok())
.is_some_and(|since| since < PRIORITY_WAIT_FRESHNESS)
}
fn note_compilation(&self, demand: &Demand, status: &ExitStatus) {
if self.bytes_per_permit == 0 {
return;
}
let Some(measured) = child_peak_rss_bytes() else {
return;
};
let Some(peak) = ledger_peak(
measured,
oom_killed(status),
demand.links,
status.success(),
self.bytes_per_permit,
) else {
return;
};
if let Err(error) =
self.record_peak(&ledger_key(&demand.name, demand.links), peak, demand.links)
{
debug!("compiler memory was not recorded: {error:#}");
}
}
fn ledger_path(&self) -> PathBuf {
self.dir.join(LEDGER_FILE)
}
fn record_peak(&self, name: &str, peak: u64, links: bool) -> Result<()> {
std::fs::create_dir_all(&self.dir)
.wrap_err_with(|| format!("failed to create {}", self.dir.display()))?;
let mut lock = fslock::LockFile::open(&self.dir.join(LEDGER_LOCK))?;
lock.lock()?;
let path = self.ledger_path();
let mut ledger = read_ledger(&path);
ledger.version = LEDGER_VERSION;
if links {
ledger.link_peaks.push(peak);
let surplus = ledger.link_peaks.len().saturating_sub(MAX_LINK_SAMPLES);
ledger.link_peaks.drain(..surplus);
}
let known = ledger.crates.get(name).is_some_and(|known| *known >= peak);
if known && !links {
return Ok(());
}
if !known {
ledger.crates.insert(name.to_string(), peak);
}
while ledger.crates.len() > MAX_LEDGER_ENTRIES {
let smallest = ledger
.crates
.iter()
.min_by_key(|(_, peak)| **peak)
.map(|(name, _)| name.clone())
.expect("an overfull ledger has entries");
ledger.crates.remove(&smallest);
}
let mut contents = serde_json::to_vec(&ledger)?;
contents.push(b'\n');
crate::util::write_atomic(&path, &contents)
}
}
fn ledger_key(name: &str, links: bool) -> String {
if links {
format!("{name} [link]")
} else {
name.to_string()
}
}
fn read_ledger(path: &Path) -> MemoryLedger {
std::fs::read(path)
.ok()
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
.unwrap_or_default()
}
fn scan_leases(leases: &Path) -> Result<Vec<Lease>> {
let mut live = Vec::new();
for entry in std::fs::read_dir(leases)? {
let path = entry?.path();
let mut lock = fslock::LockFile::open(&path)?;
if lock.try_lock()? {
drop(lock);
let _ = std::fs::remove_file(&path);
continue;
}
let lease = std::fs::read(&path)
.ok()
.and_then(|bytes| serde_json::from_slice::<Lease>(&bytes).ok())
.map_or_else(
|| Lease {
version: LEASE_VERSION,
weight: 1,
priority: SchedulerPriority::Normal.as_str().into(),
build_id: None,
},
|mut lease| {
lease.weight = lease.weight.max(1);
lease
},
);
live.push(lease);
}
Ok(live)
}
fn ledger_peak(
measured: u64,
oom_killed: bool,
links: bool,
succeeded: bool,
bytes_per_permit: u64,
) -> Option<u64> {
if oom_killed {
return Some(
measured
.saturating_mul(2)
.max(bytes_per_permit.saturating_add(1)),
);
}
if links && succeeded {
return Some(measured);
}
(measured > bytes_per_permit).then_some(measured)
}
#[cfg(unix)]
fn child_peak_rss_bytes() -> Option<u64> {
let usage = unsafe {
let mut usage = std::mem::zeroed::<libc::rusage>();
if libc::getrusage(libc::RUSAGE_CHILDREN, &mut usage) != 0 {
return None;
}
usage
};
let maxrss = u64::try_from(usage.ru_maxrss).ok()?;
let bytes = if cfg!(target_vendor = "apple") {
maxrss
} else {
maxrss.saturating_mul(1024)
};
Some(bytes).filter(|bytes| *bytes > 0)
}
#[cfg(not(unix))]
fn child_peak_rss_bytes() -> Option<u64> {
None
}
#[cfg(unix)]
fn oom_killed(status: &ExitStatus) -> bool {
use std::os::unix::process::ExitStatusExt as _;
status.signal() == Some(libc::SIGKILL)
}
#[cfg(not(unix))]
fn oom_killed(_status: &ExitStatus) -> bool {
false
}
fn jittered(delay: Duration) -> Duration {
use rand::RngExt as _;
delay + delay.mul_f64(rand::rng().random_range(0.0..0.5))
}
#[cfg(test)]
#[path = "scheduler_tests.rs"]
mod tests;