use std::{sync::Arc, time::Duration, time::Instant};
use moirai_core::executor::CleanupConfig;
use super::directory::SweepWindow;
use super::registry::TaskRegistry;
use super::state::{Retirement, TASK_STATE_BLOCK_SIZE, TaskStateBlock};
pub(super) const SWEEP_WINDOW: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetentionPolicy {
pub max_age: Duration,
pub max_completed_tasks: usize,
}
impl RetentionPolicy {
#[must_use]
pub fn from_cleanup(config: &CleanupConfig) -> Option<Self> {
config.enable_automatic_cleanup.then_some(Self {
max_age: config.task_retention_duration,
max_completed_tasks: config.max_retained_tasks,
})
}
pub(super) fn max_resident_blocks(&self) -> usize {
self.max_completed_tasks
.div_ceil(TASK_STATE_BLOCK_SIZE)
.saturating_add(1)
}
}
#[derive(Clone, Copy)]
enum Verdict {
Keep,
Expired,
OverCap,
}
impl Verdict {
fn of(
block: &TaskStateBlock,
first_slot_unissued: bool,
cutoff: Option<Instant>,
forceable: bool,
) -> Self {
let expired = || {
cutoff.is_some_and(|cutoff| {
block.is_settled(first_slot_unissued, Retirement::CompletedBefore(cutoff))
})
};
if forceable && !block.is_settled(first_slot_unissued, Retirement::Forced) {
Self::Keep
} else if expired() {
Self::Expired
} else if forceable {
Self::OverCap
} else {
Self::Keep
}
}
}
impl TaskRegistry {
pub(super) fn sweep_step(&self) {
let Some(policy) = self.retention else {
return;
};
let cutoff = Instant::now().checked_sub(policy.max_age);
self.sweep_window(cutoff, policy.max_resident_blocks());
}
pub fn cleanup_completed(&self, older_than: Duration) -> usize {
let Some(cutoff) = Instant::now().checked_sub(older_than) else {
return 0;
};
let resident: Vec<_> = self
.blocks
.read()
.expect("task registry block directory is never poisoned")
.resident_indexed()
.map(|(index, block)| (index, Arc::clone(block)))
.collect();
let settled: Vec<usize> = resident
.iter()
.filter(|(index, block)| {
block.is_settled(*index == 0, Retirement::CompletedBefore(cutoff))
})
.map(|&(index, _)| index)
.collect();
let released = self
.blocks
.write()
.expect("task registry block directory is never poisoned")
.cleanup(&settled);
released.len()
}
fn sweep_window(&self, cutoff: Option<Instant>, resident_cap: usize) {
let SweepWindow { blocks, resident } = self
.blocks
.write()
.expect("task registry block directory is never poisoned")
.check_out::<SWEEP_WINDOW>();
let forceable = resident > resident_cap;
let judged = blocks.map(|slot| {
slot.map(|(index, block)| {
let verdict = Verdict::of(&block, index == 0, cutoff, forceable);
(index, block, verdict)
})
});
let mut released = [const { None }; SWEEP_WINDOW];
let mut directory = self
.blocks
.write()
.expect("task registry block directory is never poisoned");
for (slot, (index, _, verdict)) in released.iter_mut().zip(judged.iter().flatten()) {
let retire = match verdict {
Verdict::Expired => true,
Verdict::OverCap => directory.resident() > resident_cap,
Verdict::Keep => false,
};
if retire {
*slot = directory.retire(*index);
} else {
directory.requeue(*index);
}
}
drop(directory);
}
}