use std::time::{Duration, Instant};
use std::path::PathBuf;
use super::admission::{Decision, decide, decide_unleased};
use super::census::Sampler;
use super::config::BuildLeaseConfig;
use super::orphan::Leftover;
use super::slots::{HolderRecord, SlotDir, SlotGuard};
use crate::core::builders::BuildersConfig;
#[derive(Debug)]
#[non_exhaustive]
pub enum Outcome {
Leased {
guard: SlotGuard,
decision: Decision,
},
TimedOut {
decision: Decision,
holders: Vec<HolderRecord>,
},
Unleased {
fault: LeaseFault,
decision: Decision,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct LeaseFault {
pub store: PathBuf,
pub error: String,
pub repair: String,
}
impl LeaseFault {
#[must_use]
pub fn new(store: PathBuf, error: impl Into<String>, repair: impl Into<String>) -> Self {
Self {
store,
error: error.into(),
repair: repair.into(),
}
}
#[must_use]
pub fn render(&self) -> String {
format!(
"{}: {}. Repair: {}",
self.store.display(),
self.error,
self.repair
)
}
}
pub type SlotBusy<'a> = &'a dyn Fn(u32) -> bool;
#[derive(Clone)]
#[non_exhaustive]
pub struct AcquireParams<'a> {
pub builders: &'a BuildersConfig,
pub lease: &'a BuildLeaseConfig,
pub ceiling: u32,
pub wait: Duration,
pub poll: Duration,
pub checkout: &'a str,
pub slot_busy: Option<SlotBusy<'a>>,
}
impl std::fmt::Debug for AcquireParams<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AcquireParams")
.field("ceiling", &self.ceiling)
.field("wait", &self.wait)
.field("poll", &self.poll)
.field("checkout", &self.checkout)
.field("slot_busy", &self.slot_busy.is_some())
.finish_non_exhaustive()
}
}
impl<'a> AcquireParams<'a> {
#[must_use]
pub fn new(
builders: &'a BuildersConfig,
lease: &'a BuildLeaseConfig,
ceiling: u32,
wait: Duration,
checkout: &'a str,
) -> Self {
Self {
builders,
lease,
ceiling,
wait,
poll: Duration::from_secs(2),
checkout,
slot_busy: None,
}
}
#[must_use]
pub fn with_slot_busy(mut self, busy: SlotBusy<'a>) -> Self {
self.slot_busy = Some(busy);
self
}
#[must_use]
pub fn with_poll(mut self, poll: Duration) -> Self {
self.poll = poll;
self
}
}
pub fn acquire(
slots: &SlotDir,
params: &AcquireParams<'_>,
sampler: &mut dyn Sampler,
on_wait: &mut dyn FnMut(&Decision, &[HolderRecord]),
) -> Outcome {
let deadline = Instant::now() + params.wait;
loop {
let admission = match slots.lock_admission(deadline) {
Ok(Some(lock)) => lock,
Ok(None) => return timed_out(slots, params, sampler),
Err(err) => {
let path = slots.path().join("admission.lock");
let fault = LeaseFault::new(
path.clone(),
err.to_string(),
format!(
"remove {} (`rm -rf` it); the next lease recreates it as a file",
path.display()
),
);
return acquire_unleased(params, deadline, fault, sampler, on_wait);
}
};
let holders = slots.holders();
let held = u32::try_from(holders.len()).unwrap_or(u32::MAX);
let readings = sampler.sample(&holders);
let decision = decide(
params.builders,
params.lease,
params.ceiling,
held,
&readings,
);
if decision.admit {
match take_a_slot(slots, params, held) {
Ok(Some(guard)) => {
drop(admission);
slots.remember_checkout(guard.slot(), params.checkout);
return Outcome::Leased { guard, decision };
}
Ok(None) => {}
Err(error) => {
drop(admission);
let fault = LeaseFault::new(
slots.path().to_path_buf(),
error,
format!(
"remove the broken slot-K.lock entries in {0} and make {0} \
writable by you (`chmod 700`)",
slots.path().display()
),
);
return acquire_unleased(params, deadline, fault, sampler, on_wait);
}
}
}
drop(admission);
on_wait(&decision, &holders);
if Instant::now() + params.poll >= deadline {
return Outcome::TimedOut { decision, holders };
}
std::thread::sleep(params.poll);
}
}
fn timed_out(slots: &SlotDir, params: &AcquireParams<'_>, sampler: &mut dyn Sampler) -> Outcome {
let holders = slots.holders();
let readings = sampler.sample(&holders);
let held = u32::try_from(holders.len()).unwrap_or(u32::MAX);
let decision = decide(
params.builders,
params.lease,
params.ceiling,
held,
&readings,
);
Outcome::TimedOut { decision, holders }
}
pub fn acquire_unleased(
params: &AcquireParams<'_>,
deadline: Instant,
fault: LeaseFault,
sampler: &mut dyn Sampler,
on_wait: &mut dyn FnMut(&Decision, &[HolderRecord]),
) -> Outcome {
loop {
let readings = sampler.sample_unleased();
let mut decision = decide_unleased(params.lease, params.ceiling, &readings);
decision.degraded.insert(
0,
format!(
"no build lease can be taken (UNKNOWN lease state) — {}",
fault.render()
),
);
if decision.admit {
return Outcome::Unleased { fault, decision };
}
on_wait(&decision, &[]);
if Instant::now() + params.poll >= deadline {
return Outcome::TimedOut {
decision,
holders: Vec::new(),
};
}
std::thread::sleep(params.poll);
}
}
fn take_a_slot(
slots: &SlotDir,
params: &AcquireParams<'_>,
held: u32,
) -> Result<Option<SlotGuard>, String> {
let broken = u32::try_from(slots.broken().len()).unwrap_or(u32::MAX);
let limit = params
.ceiling
.saturating_add(held)
.saturating_add(broken)
.max(1);
let mut errors = Vec::new();
let mut any_held = false;
for slot in slots.preference_order(limit, params.checkout) {
match slots.try_acquire(slot) {
Ok(Some(guard)) => {
match guard.leftover() {
Leftover::Clear => {}
Leftover::Running(_) => {
guard.release_keeping_record();
any_held = true;
continue;
}
Leftover::Unknown(err) => {
guard.release_keeping_record();
errors.push(format!("slot-{slot}.lock: {err}"));
continue;
}
}
if params.slot_busy.is_some_and(|busy| busy(slot)) {
drop(guard);
any_held = true;
continue;
}
return Ok(Some(guard));
}
Ok(None) => any_held = true,
Err(err) => errors.push(format!("slot-{slot}.lock: {err}")),
}
}
if !any_held && !errors.is_empty() {
return Err(format!(
"no slot file in {} could be locked ({})",
slots.path().display(),
errors.join("; ")
));
}
Ok(None)
}
#[cfg(test)]
#[path = "acquire_tests.rs"]
mod tests;