use std::collections::VecDeque;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::hint::Hint;
use crate::sys::Instant;
use crate::task::TaskID;
use priority::Priority;
pub const DEFAULT_CAPACITY: usize = 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TaskState {
Live,
Completed,
Cancelled,
}
impl TaskState {
pub const fn name(self) -> &'static str {
match self {
TaskState::Live => "live",
TaskState::Completed => "completed",
TaskState::Cancelled => "cancelled",
}
}
}
#[derive(Clone, Debug)]
pub struct TaskEntry {
pub task_id: TaskID,
pub label: String,
pub priority: Priority,
pub hint: Hint,
pub spawned_at: Instant,
pub state: TaskState,
pub polls: u64,
pub last_poll: Option<Instant>,
}
struct Registry {
capacity: usize,
entries: VecDeque<TaskEntry>,
}
static REGISTRY: Mutex<Option<Registry>> = Mutex::new(None);
static DROPPED: AtomicU64 = AtomicU64::new(0);
static SPAWNED: AtomicU64 = AtomicU64::new(0);
static COMPLETED: AtomicU64 = AtomicU64::new(0);
static CANCELLED: AtomicU64 = AtomicU64::new(0);
fn configured_capacity() -> usize {
std::env::var("SOME_EXECUTOR_TASK_REGISTRY_CAPACITY")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|capacity| *capacity > 0)
.unwrap_or(DEFAULT_CAPACITY)
}
fn with<R>(f: impl FnOnce(&mut Registry) -> R) -> Option<R> {
let mut guard = REGISTRY.try_lock().ok()?;
let registry = guard.get_or_insert_with(|| Registry {
capacity: configured_capacity(),
entries: VecDeque::new(),
});
Some(f(registry))
}
pub(crate) fn record_spawn(task_id: TaskID, label: &str, priority: Priority, hint: Hint) {
SPAWNED.fetch_add(1, Ordering::Relaxed);
let recorded = with(|registry| {
while registry.entries.len() >= registry.capacity {
let victim = registry
.entries
.iter()
.position(|entry| entry.state != TaskState::Live)
.unwrap_or(0);
registry.entries.remove(victim);
DROPPED.fetch_add(1, Ordering::Relaxed);
}
registry.entries.push_back(TaskEntry {
task_id,
label: label.to_string(),
priority,
hint,
spawned_at: Instant::now(),
state: TaskState::Live,
polls: 0,
last_poll: None,
});
});
if recorded.is_none() {
DROPPED.fetch_add(1, Ordering::Relaxed);
}
}
pub(crate) fn record_poll(task_id: TaskID) {
let _ = with(|registry| {
if let Some(entry) = registry
.entries
.iter_mut()
.find(|entry| entry.task_id == task_id)
{
entry.polls += 1;
entry.last_poll = Some(Instant::now());
}
});
}
pub(crate) fn record_terminal(task_id: TaskID, state: TaskState) {
match state {
TaskState::Completed => COMPLETED.fetch_add(1, Ordering::Relaxed),
TaskState::Cancelled => CANCELLED.fetch_add(1, Ordering::Relaxed),
TaskState::Live => 0,
};
let _ = with(|registry| {
if let Some(entry) = registry
.entries
.iter_mut()
.find(|entry| entry.task_id == task_id)
{
entry.state = state;
}
});
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RegistryStats {
pub spawned: u64,
pub completed: u64,
pub cancelled: u64,
pub dropped: u64,
pub retained: usize,
pub capacity: usize,
}
pub fn stats() -> RegistryStats {
let (retained, capacity) = with(|registry| (registry.entries.len(), registry.capacity))
.unwrap_or((0, configured_capacity()));
RegistryStats {
spawned: SPAWNED.load(Ordering::Relaxed),
completed: COMPLETED.load(Ordering::Relaxed),
cancelled: CANCELLED.load(Ordering::Relaxed),
dropped: DROPPED.load(Ordering::Relaxed),
retained,
capacity,
}
}
pub fn entries() -> Option<Vec<TaskEntry>> {
with(|registry| registry.entries.iter().cloned().collect())
}
#[cfg(test)]
pub(crate) static TEST_SERIALIZE: Mutex<()> = Mutex::new(());
#[cfg(test)]
pub(crate) fn test_session(capacity: usize) -> std::sync::MutexGuard<'static, ()> {
let guard = TEST_SERIALIZE
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
reset_for_test(capacity);
guard
}
#[cfg(test)]
pub(crate) fn reset_for_test(capacity: usize) {
let mut guard = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
*guard = Some(Registry {
capacity,
entries: VecDeque::new(),
});
DROPPED.store(0, Ordering::Relaxed);
SPAWNED.store(0, Ordering::Relaxed);
COMPLETED.store(0, Ordering::Relaxed);
CANCELLED.store(0, Ordering::Relaxed);
}
#[cfg(test)]
mod tests {
use super::*;
use super::test_session as session;
fn spawn(id: u64, label: &str) {
record_spawn(
TaskID::from_u64(id),
label,
Priority::unit_test(),
Hint::Unknown,
);
}
#[test]
fn a_spawn_is_recorded_and_starts_live_and_unpolled() {
let _session = session(8);
spawn(1, "first");
let entries = entries().expect("registry available");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].state, TaskState::Live);
assert_eq!(entries[0].polls, 0);
assert!(
entries[0].last_poll.is_none(),
"never polled is distinguishable from polled long ago"
);
assert_eq!(stats().spawned, 1);
}
#[test]
fn polls_and_terminal_states_are_tracked() {
let _session = session(8);
spawn(1, "first");
record_poll(TaskID::from_u64(1));
record_poll(TaskID::from_u64(1));
record_terminal(TaskID::from_u64(1), TaskState::Completed);
let entries = entries().unwrap();
assert_eq!(entries[0].polls, 2);
assert!(entries[0].last_poll.is_some());
assert_eq!(entries[0].state, TaskState::Completed);
assert_eq!(stats().completed, 1);
assert_eq!(stats().cancelled, 0);
}
#[test]
fn overflow_forgets_finished_tasks_before_live_ones() {
let _session = session(2);
spawn(1, "live-and-old");
spawn(2, "finished");
record_terminal(TaskID::from_u64(2), TaskState::Completed);
spawn(3, "newcomer");
let entries = entries().unwrap();
let ids: Vec<u64> = entries.iter().map(|e| e.task_id.to_u64()).collect();
assert!(
ids.contains(&1),
"the live task should have survived: {ids:?}"
);
assert!(ids.contains(&3));
assert!(!ids.contains(&2), "the finished one should have gone first");
}
#[test]
fn overflow_of_all_live_tasks_drops_the_oldest_and_counts_it() {
let _session = session(2);
spawn(1, "oldest");
spawn(2, "middle");
spawn(3, "newest");
let entries = entries().unwrap();
let ids: Vec<u64> = entries.iter().map(|e| e.task_id.to_u64()).collect();
assert_eq!(ids, vec![2, 3]);
assert_eq!(stats().dropped, 1, "a drop must be counted, not implied");
assert_eq!(stats().retained, 2);
}
#[test]
fn counters_outlive_the_entries_they_counted() {
let _session = session(1);
for id in 1..=10 {
spawn(id, "churn");
}
let stats = stats();
assert_eq!(stats.spawned, 10);
assert_eq!(stats.retained, 1);
assert_eq!(stats.dropped, 9);
}
}