use super::ExecutionContext;
use crate::adapter::rust::dag_scheduler::LockMode;
use crate::core::lock::FileLock;
use anyhow::Result;
use std::path::Path;
pub(crate) struct ImplicitJobGuard<'a>(pub(crate) &'a std::sync::atomic::AtomicBool);
impl Drop for ImplicitJobGuard<'_> {
fn drop(&mut self) {
self.0.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
pub(crate) type UnitJobReservation<'a> = (
Option<ImplicitJobGuard<'a>>,
Option<crate::core::jobserver::JobToken<'a>>,
);
pub(crate) enum UnitMissAdmission<'a> {
Restored,
Contested,
Compile {
lock: FileLock,
reservation: UnitJobReservation<'a>,
},
}
pub(crate) fn acquire_unit_job_token<'a>(
ctx: &ExecutionContext<'a>,
) -> Result<UnitJobReservation<'a>> {
crate::core::cache_phase::pause();
match (ctx.jobserver, ctx.is_first_job) {
(Some(js), Some(first_flag)) if js.has_implicit_token() => {
if first_flag
.compare_exchange(
true,
false,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.is_ok()
{
Ok((Some(ImplicitJobGuard(first_flag)), None))
} else {
Ok((None, js.acquire_token()?))
}
}
(Some(js), _) => Ok((None, js.acquire_token()?)),
_ => Ok((None, None)),
}
}
pub(crate) fn admit_unit_compilation<'a, R, A>(
lock_path: &Path,
lock_mode: LockMode,
eager_reservation: bool,
mut retained_job: Option<UnitJobReservation<'a>>,
try_restore_after_lock: R,
acquire_reservation: A,
) -> Result<UnitMissAdmission<'a>>
where
R: FnOnce() -> Result<bool>,
A: FnOnce() -> Result<UnitJobReservation<'a>>,
{
crate::core::cache_phase::pause();
if eager_reservation {
let reservation = match retained_job.take() {
Some(res) => res,
None => acquire_reservation()?,
};
crate::core::cache_phase::pause();
let lock = match lock_mode {
LockMode::TryLock => match FileLock::try_acquire_exclusive(lock_path)? {
Some(lock) => lock,
None => return Ok(UnitMissAdmission::Contested),
},
LockMode::Blocking => FileLock::acquire_exclusive(lock_path)?,
};
if try_restore_after_lock()? {
return Ok(UnitMissAdmission::Restored);
}
return Ok(UnitMissAdmission::Compile { lock, reservation });
}
let lock = if retained_job.is_some() {
match FileLock::try_acquire_exclusive(lock_path)? {
Some(lock) => lock,
None => {
drop(retained_job.take());
match lock_mode {
LockMode::TryLock => return Ok(UnitMissAdmission::Contested),
LockMode::Blocking => FileLock::acquire_exclusive(lock_path)?,
}
}
}
} else {
match lock_mode {
LockMode::TryLock => match FileLock::try_acquire_exclusive(lock_path)? {
Some(lock) => lock,
None => return Ok(UnitMissAdmission::Contested),
},
LockMode::Blocking => FileLock::acquire_exclusive(lock_path)?,
}
};
if retained_job.is_none() {
crate::core::cache_phase::resume();
}
if try_restore_after_lock()? {
return Ok(UnitMissAdmission::Restored);
}
crate::core::cache_phase::pause();
let reservation = match retained_job.take() {
Some(res) => res,
None => acquire_reservation()?,
};
Ok(UnitMissAdmission::Compile { lock, reservation })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::JobserverPool;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{mpsc, Mutex};
use std::time::Duration;
static TEST_LOCK: Mutex<()> = Mutex::new(());
#[test]
#[cfg(unix)]
fn contended_followers_reserve_zero_cpu_slots() {
let _serial = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let temp = tempfile::tempdir().unwrap();
let cache_root = temp.path().join("cache");
let lock_path = temp.path().join("unit.lock");
let pool = JobserverPool::create_managed(&cache_root, 2).unwrap();
JobserverPool::reset_tokens_acquired();
let held_unit_lock = FileLock::acquire_exclusive(&lock_path).unwrap();
let acquire_calls = AtomicUsize::new(0);
let try_res = admit_unit_compilation(
&lock_path,
LockMode::TryLock,
false,
None,
|| Ok(false),
|| {
acquire_calls.fetch_add(1, Ordering::SeqCst);
Ok((None, pool.acquire_token()?))
},
)
.unwrap();
let trylock_contested = matches!(try_res, UnitMissAdmission::Contested);
let (tx_started, rx_started) = mpsc::channel();
let follower_restored = std::thread::scope(|scope| {
let lock_path_ref = &lock_path;
let pool_ref = &pool;
let acquire_calls_ref = &acquire_calls;
let handle = scope.spawn(move || {
tx_started.send(()).unwrap();
let res = admit_unit_compilation(
lock_path_ref,
LockMode::Blocking,
false,
None,
|| Ok(true),
|| {
acquire_calls_ref.fetch_add(1, Ordering::SeqCst);
Ok((None, pool_ref.acquire_token()?))
},
)
.unwrap();
matches!(res, UnitMissAdmission::Restored)
});
rx_started.recv_timeout(Duration::from_secs(2)).unwrap();
std::thread::sleep(Duration::from_millis(40));
let tokens_while_blocked = JobserverPool::total_tokens_acquired();
drop(held_unit_lock);
let restored = handle.join().unwrap();
(tokens_while_blocked, restored)
});
assert!(trylock_contested, "TryLock follower must return Contested");
assert_eq!(
follower_restored.0, 0,
"contended Blocking follower must hold 0 CPU tokens while waiting on unit lock"
);
assert!(
follower_restored.1,
"Blocking follower must restore on second cache check"
);
assert_eq!(
acquire_calls.load(Ordering::SeqCst),
0,
"contended followers that hit after lock must never call acquire_reservation"
);
assert_eq!(
JobserverPool::total_tokens_acquired(),
0,
"contended followers must reserve 0 CPU tokens total"
);
}
#[test]
#[cfg(unix)]
fn retained_cpu_reservation_is_released_before_blocking_on_held_unit_lock() {
let _serial = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let temp = tempfile::tempdir().unwrap();
let cache_root = temp.path().join("cache");
let lock_path = temp.path().join("unit.lock");
let pool = JobserverPool::create_managed(&cache_root, 1).unwrap();
let held_unit_lock = FileLock::acquire_exclusive(&lock_path).unwrap();
let retained_try = pool.acquire_token().unwrap();
assert!(retained_try.is_some());
let try_outcome = admit_unit_compilation(
&lock_path,
LockMode::TryLock,
false,
Some((None, retained_try)),
|| Ok(false),
|| unreachable!("Contested TryLock must not acquire a new reservation"),
)
.unwrap();
assert!(matches!(try_outcome, UnitMissAdmission::Contested));
let retained_blocking = pool.acquire_token().unwrap();
assert!(retained_blocking.is_some());
let extra_acquires = AtomicUsize::new(0);
let (slot_freed_before_unlock, follower_restored) = std::thread::scope(|scope| {
let lock_path_ref = &lock_path;
let pool_ref = &pool;
let extra_acquires_ref = &extra_acquires;
let follower = scope.spawn(move || {
let res = admit_unit_compilation(
lock_path_ref,
LockMode::Blocking,
false,
Some((None, retained_blocking)),
|| Ok(true),
|| {
extra_acquires_ref.fetch_add(1, Ordering::SeqCst);
Ok((None, pool_ref.acquire_token()?))
},
)
.unwrap();
matches!(res, UnitMissAdmission::Restored)
});
let (tx_probe, rx_probe) = mpsc::channel();
let probe = scope.spawn(move || {
let tok = pool_ref.acquire_token().unwrap();
let got = tok.is_some();
drop(tok);
let _ = tx_probe.send(got);
});
let freed_while_unit_lock_held = rx_probe
.recv_timeout(Duration::from_millis(500))
.unwrap_or(false);
drop(held_unit_lock);
let restored = follower.join().unwrap();
probe.join().unwrap();
(freed_while_unit_lock_held, restored)
});
assert!(
slot_freed_before_unlock,
"retained CPU reservation must be released before blocking on a held unit lock"
);
assert!(
follower_restored,
"follower must complete via second restore after unit lock release"
);
assert_eq!(
extra_acquires.load(Ordering::SeqCst),
0,
"follower hitting second restore must not re-acquire a CPU slot"
);
}
#[test]
#[cfg(unix)]
fn uncontested_unit_lock_reuses_retained_reservation_without_second_acquire() {
let _serial = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let temp = tempfile::tempdir().unwrap();
let cache_root = temp.path().join("cache");
let lock_path = temp.path().join("unit.lock");
let pool = JobserverPool::create_managed(&cache_root, 1).unwrap();
JobserverPool::reset_tokens_acquired();
let retained = pool.acquire_token().unwrap();
assert_eq!(JobserverPool::total_tokens_acquired(), 1);
let extra_acquires = AtomicUsize::new(0);
let res = admit_unit_compilation(
&lock_path,
LockMode::Blocking,
false,
Some((None, retained)),
|| Ok(false),
|| {
extra_acquires.fetch_add(1, Ordering::SeqCst);
Ok((None, pool.acquire_token()?))
},
)
.unwrap();
assert!(matches!(res, UnitMissAdmission::Compile { .. }));
assert_eq!(extra_acquires.load(Ordering::SeqCst), 0);
assert_eq!(JobserverPool::total_tokens_acquired(), 1);
}
#[test]
#[cfg(unix)]
fn inherited_pool_preserves_eager_pre_lock_reservation() {
let _serial = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let temp = tempfile::tempdir().unwrap();
let lock_path = temp.path().join("unit.lock");
let first_flag = std::sync::atomic::AtomicBool::new(true);
let acquire_calls = AtomicUsize::new(0);
let held_unit_lock = FileLock::acquire_exclusive(&lock_path).unwrap();
let outcome = admit_unit_compilation(
&lock_path,
LockMode::TryLock,
true,
None,
|| Ok(false),
|| {
acquire_calls.fetch_add(1, Ordering::SeqCst);
assert!(
first_flag.swap(false, Ordering::SeqCst),
"eager inherited path must reserve before attempting unit lock"
);
Ok((Some(ImplicitJobGuard(&first_flag)), None))
},
)
.unwrap();
drop(held_unit_lock);
assert!(matches!(outcome, UnitMissAdmission::Contested));
assert_eq!(
acquire_calls.load(Ordering::SeqCst),
1,
"inherited pool must reserve before unit lock"
);
assert!(
first_flag.load(Ordering::SeqCst),
"Contested return must drop ImplicitJobGuard and restore first_flag"
);
}
}