use crate::outcome::{
PanicInfo, TaskId, TaskOutcome, TaskResult, TimedOutTask,
};
use crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender};
use log::trace;
use std::collections::HashMap;
use std::thread;
use std::time::Instant;
#[derive(Debug)]
pub struct DeadlineJoiner<T: Send + 'static> {
deadline: Instant,
sender: Sender<TaskResult<T>>,
receiver: Receiver<TaskResult<T>>,
pending: HashMap<TaskId, Instant>,
next_id: TaskId,
}
impl<T: Send + 'static> DeadlineJoiner<T> {
pub fn new(deadline: Instant) -> Self {
let (sender, receiver) = unbounded();
Self {
deadline,
sender,
receiver,
pending: HashMap::new(),
next_id: 0,
}
}
pub fn deadline(&self) -> Instant {
self.deadline
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
pub fn spawn<F>(&mut self, task: F) -> TaskId
where
F: FnOnce() -> T + Send + 'static,
{
let task_id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
let started_at = Instant::now();
self.pending.insert(task_id, started_at);
let sender = self.sender.clone();
trace!("spawning task {task_id}");
thread::spawn(move || {
let outcome =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(task))
.map(TaskOutcome::Completed)
.unwrap_or_else(|payload| {
TaskOutcome::Panicked(PanicInfo::from_payload(payload))
});
let result = TaskResult {
task_id,
started_at: Some(started_at),
finished_at: Instant::now(),
outcome,
};
if sender.send(result).is_err() {
trace!("receiver dropped before task {task_id} could report its status");
}
});
task_id
}
pub fn join_ready(mut self) -> DeadlineSummary<T> {
let mut completed = Vec::new();
let mut timed_out = Vec::new();
let deadline = self.deadline;
while !self.pending.is_empty() {
let now = Instant::now();
if now >= deadline {
trace!(
"deadline reached with {} tasks still pending",
self.pending.len()
);
break;
}
match self.receiver.recv_deadline(deadline) {
Ok(result) => {
trace!(
"task {} finished at {:?}",
result.task_id,
result.finished_at
);
self.pending.remove(&result.task_id);
if result.finished_at <= deadline {
completed.push(result);
} else {
timed_out.push(TimedOutTask {
task_id: result.task_id,
started_at: result.started_at,
});
}
}
Err(RecvTimeoutError::Timeout) => {
trace!(
"recv timed out with {} tasks still pending",
self.pending.len()
);
break;
}
Err(RecvTimeoutError::Disconnected) => {
trace!("all senders dropped; exiting join loop");
break;
}
}
}
for (task_id, started_at) in self.pending.into_iter() {
timed_out
.push(TimedOutTask { task_id, started_at: Some(started_at) });
}
DeadlineSummary { deadline, completed, timed_out }
}
}
#[derive(Debug)]
pub struct DeadlineSummary<T> {
pub deadline: Instant,
pub completed: Vec<TaskResult<T>>,
pub timed_out: Vec<TimedOutTask>,
}
impl<T> DeadlineSummary<T> {
pub fn completed_count(&self) -> usize {
self.completed.len()
}
pub fn timed_out_count(&self) -> usize {
self.timed_out.len()
}
pub fn total_tasks(&self) -> usize {
self.completed.len() + self.timed_out.len()
}
}