use crate::{
event::Event,
executor::{Executor, JobExecutor},
job::{Job, JobId, JobState},
queue::{EventTimeQueue, Queue},
task::{Task, TaskId, TaskStatistics, TaskStatus},
ControlChannel, Error, Result,
};
use std::{
collections::HashMap,
sync::Arc,
time::{Duration, SystemTime},
};
use tokio::{
select,
sync::{mpsc::Sender, RwLock},
task::yield_now,
};
use tracing::{debug, instrument, warn};
pub(crate) const DEFAULT_MAX_PARALLEL_JOBS: usize = 16;
const SCHEDULER_CONTROL_CHANNEL_SIZE: usize = 1024;
#[cfg(feature = "async-trait")]
#[allow(async_fn_in_trait)]
pub trait TaskScheduler {
async fn add(&self, task: Task) -> Result<TaskId>;
async fn cancel(&self, id: TaskId, opts: CancelOpts) -> Result<()>;
async fn status(&self, id: &TaskId) -> Result<TaskStatus>;
async fn statistics(&self, id: &TaskId) -> Result<TaskStatistics>;
async fn shutdown(self, opts: ShutdownOpts) -> Result<()>;
}
#[cfg(not(feature = "async-trait"))]
pub trait TaskScheduler {
fn add(&self, task: Task) -> impl std::future::Future<Output = Result<TaskId>> + Send;
fn cancel(
&self,
id: TaskId,
opts: CancelOpts,
) -> impl std::future::Future<Output = Result<()>> + Send;
fn status(&self, id: &TaskId) -> impl std::future::Future<Output = Result<TaskStatus>> + Send;
fn statistics(
&self,
id: &TaskId,
) -> impl std::future::Future<Output = Result<TaskStatistics>> + Send;
fn shutdown(self, opts: ShutdownOpts) -> impl std::future::Future<Output = Result<()>> + Send;
}
#[derive(Debug)]
pub struct Scheduler {
tasks: Arc<RwLock<HashMap<TaskId, Task>>>,
channel: Sender<ChangeStateEvent>,
handler: tokio::task::JoinHandle<Result<()>>,
}
#[derive(Debug, Clone)]
enum ChangeStateEvent {
Shutdown(ShutdownOpts),
EnqueueTask(Task),
DropTask(TaskId, CancelOpts),
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum WorkerType {
#[default]
CurrentRuntime,
CurrentThread,
MultiThread(RuntimeThreads),
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RuntimeThreads {
#[default]
CpuCores,
Limited(usize),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum WorkerParallelism {
Unlimited,
Limited(usize),
}
impl Default for WorkerParallelism {
fn default() -> Self {
Self::Limited(DEFAULT_MAX_PARALLEL_JOBS)
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CancelOpts {
#[default]
Ignore,
Kill,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ShutdownOpts {
IgnoreRunning,
CancelTasks(CancelOpts),
#[default]
WaitForFinish,
WaitFor(Duration),
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GarbageCollector {
#[default]
Disabled,
Immediate,
Periodic {
expire_after: Duration,
interval: Duration,
},
}
impl GarbageCollector {
pub fn disabled() -> Self {
Self::Disabled
}
pub fn immediate() -> Self {
Self::Immediate
}
pub fn periodic(expire_after: Duration, interval: Duration) -> Self {
Self::Periodic {
expire_after,
interval,
}
}
}
impl GarbageCollector {
#[instrument("collect garbage", skip_all, level = "debug")]
async fn collect_garbage(tasks: Arc<RwLock<HashMap<TaskId, Task>>>, expire_after: Duration) {
let mut tasks = tasks.write().await;
let expired_at = SystemTime::now().checked_sub(expire_after).unwrap();
let to_remove: Vec<TaskId> = tasks
.iter()
.filter(|(_id, task)| task.state.is_task_finished())
.filter(|(_id, task)| {
task.state
.last_finished_at()
.expect("finished task has no `finished_at` time set, looks like a BUG")
<= expired_at
})
.map(|(id, _task)| id.clone())
.collect();
to_remove.iter().for_each(|id| {
debug!(task_id = %id, "remove expired task");
tasks.remove(id);
});
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SchedulerBuilder {
worker_type: WorkerType,
parallelism: WorkerParallelism,
garbage_collector: GarbageCollector,
}
impl SchedulerBuilder {
pub fn new() -> Self {
Self {
worker_type: WorkerType::default(),
parallelism: WorkerParallelism::default(),
garbage_collector: GarbageCollector::default(),
}
}
pub fn worker_type(self, worker_type: WorkerType) -> Self {
Self {
worker_type,
..self
}
}
pub fn parallelism(self, parallelism: WorkerParallelism) -> Self {
Self {
parallelism,
..self
}
}
pub fn garbage_collector(self, garbage_collector: GarbageCollector) -> Self {
Self {
garbage_collector,
..self
}
}
pub fn build(self) -> Scheduler {
Scheduler::new(self.worker_type, self.parallelism, self.garbage_collector)
}
}
impl Scheduler {
pub fn new(
worker_type: WorkerType,
parallelism: WorkerParallelism,
garbage_collector: GarbageCollector,
) -> Self {
debug!(
?worker_type,
?parallelism,
?garbage_collector,
"construct new scheduler"
);
let channel = ControlChannel::<ChangeStateEvent>::new(SCHEDULER_CONTROL_CHANNEL_SIZE);
let tasks = Arc::new(RwLock::new(HashMap::new()));
Self {
tasks: tasks.clone(),
channel: channel.sender(),
handler: tokio::task::spawn(Scheduler::work(
worker_type,
parallelism,
tasks.clone(),
channel,
garbage_collector,
)),
}
}
#[instrument("scheduler loop", skip_all, level = "debug")]
async fn work(
worker_type: WorkerType,
parallelism: WorkerParallelism,
tasks: Arc<RwLock<HashMap<TaskId, Task>>>,
channel: ControlChannel<ChangeStateEvent>,
garbage_collector: GarbageCollector,
) -> Result<()> {
let queue = Queue::default();
let executor = Executor::new(worker_type, parallelism);
let mut jobs: HashMap<JobId, TaskId> = HashMap::new();
match garbage_collector {
GarbageCollector::Disabled => {}
GarbageCollector::Immediate => {}
GarbageCollector::Periodic {
expire_after,
interval,
} => {
let tasks = tasks.clone();
let task = Task::new(
crate::task::TaskSchedule::IntervalDelayed(interval, interval),
move |id| {
let tasks = tasks.clone();
Box::pin(async move {
debug!(job_id = %id, "collecting garbage");
GarbageCollector::collect_garbage(tasks, expire_after).await;
})
},
);
debug!(task = ?task, "schedule GC task");
channel
.send(ChangeStateEvent::EnqueueTask(task))
.await
.map_err(|_e| Error::SendingChangeStateEvent)?;
}
}
loop {
debug!("scheduler loop iteration");
select! {
biased;
event = channel.receive() => {
if let Some(event) = event {
debug!(event = ?event, "control event received");
match event {
ChangeStateEvent::Shutdown(opts) => {
queue.shutdown().await;
executor.shutdown(opts).await?;
tasks.write().await.clear();
return Ok(())
},
ChangeStateEvent::EnqueueTask(mut task) => {
let event_id = task.id.clone();
let task_id = task.id.clone();
let at = task.schedule.initial_run_time();
queue.insert(Event::new(event_id, at)).await?;
task.state.task_enqueued();
let mut tasks = tasks.write().await;
tasks.insert(task_id, task);
},
ChangeStateEvent::DropTask(id, opts) => {
let mut tasks = tasks.write().await;
let task = tasks.get(&id);
if let Some(task) = task {
let event_id = id.clone().into();
queue.pop(&event_id).await?;
match opts {
CancelOpts::Ignore => {},
CancelOpts::Kill => {
for job in task.state.jobs() {
executor.cancel(&job).await?;
}
},
}
tasks.remove(&id);
}
}
}
} else {
warn!("empty events channel");
}
},
event = queue.next() => {
if let Ok(event) = event {
debug!(event= ?event, "queue event received");
let mut tasks = tasks.write().await;
let task = tasks.get_mut(&event.id.into());
if let Some(task) = task {
let job_id = JobId::new(task.id());
let job = task.job.clone();
let job = Job::new(job_id, job, task.timeout);
let job_id = executor.enqueue(job).await?;
jobs.insert(job_id.clone(), task.id.clone());
task.state.job_scheduled(job_id);
let at = task.schedule.after_start_run_time();
if let Some(at) = at {
let event_id = task.id.clone();
queue.insert(Event::new(event_id, at)).await?;
task.state.task_enqueued();
}
}
} else {
warn!(event = ?event, "error from queue received, exiting");
return Err(event.err().unwrap())
}
},
job_id = executor.work() => {
if let Ok(job_id) = job_id {
debug!(job_id = %job_id, "executor event received");
let mut tasks = tasks.write().await;
let job_state = executor.state(&job_id).await?;
let task_id = jobs.get(&job_id);
if let Some(task_id) = task_id {
let is_finished = {
let task = tasks.get_mut(task_id);
if let Some(task) = task {
debug!(job_state = ?job_state, "job state changed");
let mut at = None;
match job_state {
JobState::Running => { task.state.job_started(job_id); },
JobState::Completed => {
task.state.job_completed(&job_id);
at = task.schedule.after_finish_run_time();
},
JobState::Canceled => {
task.state.job_canceled(&job_id);
at = task.schedule.after_finish_run_time();
},
JobState::Timeout => {
task.state.job_timeout(&job_id);
at = task.schedule.after_finish_run_time();
},
JobState::Error => {
task.state.job_error(&job_id);
at = task.schedule.after_finish_run_time();
},
JobState::Pending | JobState::Starting => {},
};
if let Some(at) = at {
let event_id = task.id.clone();
queue.insert(Event::new(event_id, at)).await?;
task.state.task_enqueued();
}
task.state.is_task_finished()
} else {
false
}
};
if is_finished && garbage_collector == GarbageCollector::Immediate {
debug!(task_id = %task_id, "remove finished task");
tasks.remove(task_id);
}
}
} else {
warn!(job_id = ?job_id, "error from executor received");
}
}
}
}
}
async fn send_event(&self, event: ChangeStateEvent) -> Result<()> {
self.channel
.send(event)
.await
.map_err(|_e| Error::SendingChangeStateEvent)
}
}
impl Default for Scheduler {
fn default() -> Self {
Self::new(
WorkerType::default(),
WorkerParallelism::default(),
GarbageCollector::default(),
)
}
}
impl TaskScheduler for Scheduler {
async fn add(&self, task: Task) -> Result<TaskId> {
debug!(task_id = %task.id(), "add task");
let id = task.id();
yield_now().await; if self.tasks.read().await.get(&id).is_some() {
Err(Error::DuplicatedTaskId(id))
} else {
self.send_event(ChangeStateEvent::EnqueueTask(task)).await?;
Ok(id)
}
}
async fn cancel(&self, id: TaskId, opts: CancelOpts) -> Result<()> {
debug!(task_id = %id, ?opts, "cancel task");
self.send_event(ChangeStateEvent::DropTask(id, opts)).await
}
async fn status(&self, id: &TaskId) -> Result<TaskStatus> {
debug!(task_id = %id, "task status requested");
let mut tasks = self.tasks.write().await;
let task = tasks.get_mut(id);
if let Some(task) = task {
let status = task.state.status();
if task.state.is_task_finished() {
debug!(task_id = %id, "remove finished task");
tasks.remove(id);
}
return Ok(status);
}
Err(Error::IncorrectTaskId(id.clone()))
}
async fn statistics(&self, id: &TaskId) -> Result<TaskStatistics> {
debug!(task_id = %id, "task statistics requested");
let tasks = self.tasks.read().await;
let task = tasks.get(id);
if let Some(task) = task {
Ok(task.statistics())
} else {
Err(Error::IncorrectTaskId(id.clone()))
}
}
async fn shutdown(self, opts: ShutdownOpts) -> Result<()> {
debug!(?opts, "shutdown requested");
self.send_event(ChangeStateEvent::Shutdown(opts.clone()))
.await?;
match opts {
ShutdownOpts::IgnoreRunning => Ok(()),
ShutdownOpts::CancelTasks(_) | ShutdownOpts::WaitForFinish => {
futures::join!(self.handler)
.0
.map_err(|_e| Error::IncompleteShutdown)?
}
ShutdownOpts::WaitFor(timeout) => {
debug!(?timeout, "wait for task completion");
select! {
res = self.handler => {
res.map_err(|_e| Error::IncompleteShutdown)?
},
_ = tokio::time::sleep(timeout) => {
Err(Error::IncompleteShutdown)
},
}
}
}
}
}
#[cfg(test)]
mod test {
use ntest::timeout;
use super::*;
use crate::task::{CronOpts, TaskSchedule};
use std::time::UNIX_EPOCH;
async fn basic_test_suite(
scheduler: Scheduler,
schedules: Vec<TaskSchedule>,
durations: &[Duration],
suite_duration: Duration,
) -> Result<(Vec<String>, Vec<String>)> {
assert_eq!(
schedules.len(),
durations.len(),
"schedulers and durations arrays size mismatched"
);
let logs = Arc::new(RwLock::new(Vec::<String>::new()));
let jobs = Arc::new(RwLock::new(Vec::<JobId>::new()));
for s in 0..schedules.len() {
let log = logs.clone();
let jobs = jobs.clone();
let task_duration = durations[s];
let task = Task::new(schedules[s].clone(), move |id| {
let log = log.clone();
let jobs = jobs.clone();
Box::pin(async move {
jobs.write().await.push(id.clone());
log.write().await.push(format!("{s},start,{id}"));
tokio::time::sleep(task_duration).await;
log.write().await.push(format!("{s},finish,{id}"));
})
});
scheduler.add(task).await?;
tokio::time::sleep(Duration::from_millis(1)).await;
}
tokio::time::sleep(suite_duration).await;
scheduler.shutdown(ShutdownOpts::WaitForFinish).await?;
let logs: Vec<String> = logs.read().await.iter().map(String::from).collect();
let jobs: Vec<String> = jobs.read().await.iter().map(|s| format!("{s}")).collect();
Ok((logs, jobs))
}
#[tokio::test]
#[timeout(10000)]
async fn once_1_worker() {
let schedules: Vec<TaskSchedule> =
Vec::from([TaskSchedule::Once, TaskSchedule::Once, TaskSchedule::Once]);
let durations = [
Duration::from_secs(3),
Duration::from_secs(3),
Duration::from_secs(1),
];
let scheduler = Scheduler::new(
WorkerType::CurrentRuntime,
WorkerParallelism::Limited(1),
GarbageCollector::default(),
);
let (logs, jobs) =
basic_test_suite(scheduler, schedules, &durations, Duration::from_secs(5))
.await
.unwrap();
assert_eq!(logs.len(), 4);
assert_eq!(jobs.len(), 2);
let mut expected = vec![];
for j in jobs.iter().enumerate() {
expected.push(format!("{},start,{}", j.0, j.1));
expected.push(format!("{},finish,{}", j.0, j.1));
}
assert_eq!(logs, expected);
}
#[tokio::test]
#[timeout(10000)]
async fn once_2_workers() {
let schedules: Vec<TaskSchedule> = Vec::from([
TaskSchedule::Once,
TaskSchedule::Once,
TaskSchedule::Once,
TaskSchedule::Once,
TaskSchedule::Once,
]);
let durations = [
Duration::from_millis(2950),
Duration::from_millis(3000),
Duration::from_millis(2950),
Duration::from_millis(3000),
Duration::from_millis(1),
];
let scheduler = Scheduler::new(
WorkerType::CurrentRuntime,
WorkerParallelism::Limited(2),
GarbageCollector::default(),
);
let (logs, jobs) =
basic_test_suite(scheduler, schedules, &durations, Duration::from_secs(5))
.await
.unwrap();
assert_eq!(logs.len(), 8);
assert_eq!(jobs.len(), 4);
let expected: Vec<String> = Vec::from([
format!("0,start,{}", jobs[0]),
format!("1,start,{}", jobs[1]),
format!("0,finish,{}", jobs[0]),
format!("2,start,{}", jobs[2]),
format!("1,finish,{}", jobs[1]),
format!("3,start,{}", jobs[3]),
format!("2,finish,{}", jobs[2]),
format!("3,finish,{}", jobs[3]),
]);
assert_eq!(logs, expected);
}
#[tokio::test]
#[timeout(10000)]
async fn once_unlimited_workers() {
let schedules: Vec<TaskSchedule> = Vec::from([
TaskSchedule::Once,
TaskSchedule::Once,
TaskSchedule::Once,
TaskSchedule::Once,
TaskSchedule::Once,
]);
let durations = [
Duration::from_millis(1000),
Duration::from_millis(1200),
Duration::from_millis(1300),
Duration::from_millis(1400),
Duration::from_millis(2000),
];
let scheduler = Scheduler::new(
WorkerType::CurrentRuntime,
WorkerParallelism::Unlimited,
GarbageCollector::default(),
);
let (logs, jobs) =
basic_test_suite(scheduler, schedules, &durations, Duration::from_secs(1))
.await
.unwrap();
assert_eq!(logs.len(), 10);
assert_eq!(jobs.len(), 5);
let expected: Vec<String> = Vec::from([
format!("0,start,{}", jobs[0]),
format!("1,start,{}", jobs[1]),
format!("2,start,{}", jobs[2]),
format!("3,start,{}", jobs[3]),
format!("4,start,{}", jobs[4]),
format!("0,finish,{}", jobs[0]),
format!("1,finish,{}", jobs[1]),
format!("2,finish,{}", jobs[2]),
format!("3,finish,{}", jobs[3]),
format!("4,finish,{}", jobs[4]),
]);
assert_eq!(logs, expected);
}
#[tokio::test]
#[timeout(20000)]
async fn cron() {
let schedules: Vec<TaskSchedule> = Vec::from([
TaskSchedule::Cron("*/2 * * * * *".try_into().unwrap(), CronOpts::default()),
TaskSchedule::Cron("*/5 * * * * *".try_into().unwrap(), CronOpts::default()),
]);
let durations = [Duration::from_millis(1200), Duration::from_millis(3500)];
let scheduler = SchedulerBuilder::new()
.parallelism(WorkerParallelism::Limited(2))
.build();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let wait_for = 10 - now % 10 - 1;
tokio::time::sleep(Duration::from_secs(wait_for)).await;
let (logs, jobs) =
basic_test_suite(scheduler, schedules, &durations, Duration::from_secs(7))
.await
.unwrap();
assert_eq!(logs.len(), 12);
assert_eq!(jobs.len(), 6);
let expected1: Vec<String> = Vec::from([
format!("0,start,{}", jobs[0]), format!("1,start,{}", jobs[1]), format!("0,finish,{}", jobs[0]), format!("0,start,{}", jobs[2]), format!("0,finish,{}", jobs[2]), format!("1,finish,{}", jobs[1]), format!("0,start,{}", jobs[3]), format!("1,start,{}", jobs[4]), format!("0,finish,{}", jobs[3]), format!("0,start,{}", jobs[5]), format!("0,finish,{}", jobs[5]), format!("1,finish,{}", jobs[4]), ]);
let expected2: Vec<String> = Vec::from([
format!("1,start,{}", jobs[0]), format!("0,start,{}", jobs[1]), format!("0,finish,{}", jobs[1]), format!("0,start,{}", jobs[2]), format!("0,finish,{}", jobs[2]), format!("1,finish,{}", jobs[0]), format!("0,start,{}", jobs[3]), format!("1,start,{}", jobs[4]), format!("0,finish,{}", jobs[3]), format!("0,start,{}", jobs[5]), format!("0,finish,{}", jobs[5]), format!("1,finish,{}", jobs[4]), ]);
let debug = format!(
"jobs={jobs:?}\nlogs={logs:?}\nexpected1={expected1:?}\nexpected2={expected2:?}"
);
assert!((logs == expected1) || (logs == expected2), "{debug}");
}
#[tokio::test]
#[timeout(20000)]
async fn cron_at_start() {
let schedules: Vec<TaskSchedule> = Vec::from([TaskSchedule::Cron(
"*/5 * * * * *".try_into().unwrap(),
CronOpts {
at_start: true,
concurrent: false,
},
)]);
let durations = [Duration::from_millis(1000)];
let scheduler = SchedulerBuilder::new()
.parallelism(WorkerParallelism::Unlimited)
.build();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let wait_for = 5 - now % 5 + 1;
tokio::time::sleep(Duration::from_secs(wait_for)).await;
let (logs, jobs) =
basic_test_suite(scheduler, schedules, &durations, Duration::from_secs(6))
.await
.unwrap();
assert_eq!(logs.len(), 4);
assert_eq!(jobs.len(), 2);
let expected: Vec<String> = Vec::from([
format!("0,start,{}", jobs[0]),
format!("0,finish,{}", jobs[0]),
format!("0,start,{}", jobs[1]),
format!("0,finish,{}", jobs[1]),
]);
let debug = format!("jobs={jobs:?}\nlogs={logs:?}\nexpected={expected:?}");
assert_eq!(logs, expected, "{debug}");
}
#[tokio::test]
#[timeout(20000)]
async fn cron_non_concurrent() {
let schedules: Vec<TaskSchedule> = Vec::from([TaskSchedule::Cron(
"*/5 * * * * *".try_into().unwrap(),
CronOpts {
at_start: true,
concurrent: false,
},
)]);
let durations = [Duration::from_millis(7000)];
let scheduler = Scheduler::new(
WorkerType::CurrentRuntime,
WorkerParallelism::Unlimited,
GarbageCollector::default(),
);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let wait_for = 5 - now % 5 + 1;
tokio::time::sleep(Duration::from_secs(wait_for)).await;
let (logs, jobs) =
basic_test_suite(scheduler, schedules, &durations, Duration::from_secs(6))
.await
.unwrap();
assert_eq!(logs.len(), 2);
assert_eq!(jobs.len(), 1);
let expected: Vec<String> = Vec::from([
format!("0,start,{}", jobs[0]),
format!("0,finish,{}", jobs[0]),
]);
let debug = format!("jobs={jobs:?}\nlogs={logs:?}\nexpected={expected:?}");
assert_eq!(logs, expected, "{debug}");
}
#[tokio::test]
#[timeout(20000)]
async fn cron_concurrent() {
tracing_subscriber::fmt::init();
let schedules: Vec<TaskSchedule> = Vec::from([TaskSchedule::Cron(
"*/3 * * * * *".try_into().unwrap(),
CronOpts {
at_start: true,
concurrent: true,
},
)]);
let durations = [Duration::from_millis(3900)];
let scheduler = Scheduler::new(
WorkerType::CurrentRuntime,
WorkerParallelism::Unlimited,
GarbageCollector::default(),
);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let wait_for = 3 - now % 3 + 1;
tokio::time::sleep(Duration::from_secs(wait_for)).await;
let (logs, jobs) = basic_test_suite(
scheduler,
schedules,
&durations,
Duration::from_millis(5000),
)
.await
.unwrap();
assert_eq!(logs.len(), 6);
assert_eq!(jobs.len(), 3);
let expected: Vec<String> = Vec::from([
format!("0,start,{}", jobs[0]),
format!("0,start,{}", jobs[1]),
format!("0,finish,{}", jobs[0]),
format!("0,start,{}", jobs[2]),
format!("0,finish,{}", jobs[1]),
format!("0,finish,{}", jobs[2]),
]);
let debug = format!("jobs={jobs:?}\nlogs={logs:?}\nexpected={expected:?}");
assert_eq!(logs, expected, "{debug}");
}
#[tokio::test]
#[timeout(10000)]
async fn once_delayed_4_workers() {
let schedules: Vec<TaskSchedule> = Vec::from([
TaskSchedule::Once,
TaskSchedule::OnceDelayed(Duration::from_millis(500)),
TaskSchedule::OnceDelayed(Duration::from_secs(1)),
TaskSchedule::OnceDelayed(Duration::from_millis(3300)),
]);
let durations = [
Duration::from_secs(3),
Duration::from_secs(3),
Duration::from_secs(1),
Duration::from_secs(1),
];
let scheduler = Scheduler::new(
WorkerType::CurrentRuntime,
WorkerParallelism::Limited(4),
GarbageCollector::default(),
);
let (logs, jobs) =
basic_test_suite(scheduler, schedules, &durations, Duration::from_secs(4))
.await
.unwrap();
assert_eq!(logs.len(), 8);
assert_eq!(jobs.len(), 4);
let expected: Vec<String> = Vec::from([
format!("0,start,{}", jobs[0]),
format!("1,start,{}", jobs[1]),
format!("2,start,{}", jobs[2]),
format!("2,finish,{}", jobs[2]),
format!("0,finish,{}", jobs[0]),
format!("3,start,{}", jobs[3]),
format!("1,finish,{}", jobs[1]),
format!("3,finish,{}", jobs[3]),
]);
assert_eq!(logs, expected);
}
#[tokio::test]
#[timeout(10000)]
async fn interval_4_workers() {
let schedules: Vec<TaskSchedule> = Vec::from([
TaskSchedule::Interval(Duration::from_secs(3)),
TaskSchedule::Interval(Duration::from_secs(1)),
TaskSchedule::IntervalDelayed(Duration::from_millis(2100), Duration::from_millis(2100)),
TaskSchedule::IntervalDelayed(Duration::from_millis(5900), Duration::from_millis(5900)),
]);
let durations = [
Duration::from_millis(900),
Duration::from_secs(2),
Duration::from_secs(5),
Duration::from_secs(1),
];
let scheduler = Scheduler::new(
WorkerType::CurrentRuntime,
WorkerParallelism::Limited(4),
GarbageCollector::default(),
);
let (logs, jobs) = basic_test_suite(
scheduler,
schedules,
&durations,
Duration::from_millis(7200),
)
.await
.unwrap();
assert_eq!(logs.len(), 14);
assert_eq!(jobs.len(), 7);
let expected: Vec<String> = Vec::from([
format!("0,start,{}", jobs[0]),
format!("1,start,{}", jobs[1]),
format!("0,finish,{}", jobs[0]),
format!("1,finish,{}", jobs[1]),
format!("2,start,{}", jobs[2]),
format!("1,start,{}", jobs[3]),
format!("0,start,{}", jobs[4]),
format!("0,finish,{}", jobs[4]),
format!("1,finish,{}", jobs[3]),
format!("3,start,{}", jobs[5]),
format!("1,start,{}", jobs[6]),
format!("3,finish,{}", jobs[5]),
format!("2,finish,{}", jobs[2]),
format!("1,finish,{}", jobs[6]),
]);
assert_eq!(logs, expected);
}
#[tokio::test]
#[timeout(10000)]
async fn garbage_collector_periodic() {
let scheduler = SchedulerBuilder::new()
.garbage_collector(GarbageCollector::periodic(
Duration::from_millis(2500),
Duration::from_millis(500),
))
.build();
let task_1 = Task::new(TaskSchedule::Interval(Duration::from_millis(100)), |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
})
});
let task_2 = Task::new(TaskSchedule::Once, |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(4)).await;
})
});
let task_3 = Task::new(TaskSchedule::Once, |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(8)).await;
})
});
let id_1 = scheduler.add(task_1).await.unwrap();
let id_2 = scheduler.add(task_2).await.unwrap();
let id_3 = scheduler.add(task_3).await.unwrap();
tokio::time::sleep(Duration::from_millis(3500)).await;
assert_eq!(scheduler.status(&id_1).await.unwrap(), TaskStatus::Running);
assert_eq!(scheduler.status(&id_2).await.unwrap(), TaskStatus::Running);
assert_eq!(scheduler.status(&id_3).await.unwrap(), TaskStatus::Running);
tokio::time::sleep(Duration::from_millis(4400)).await;
assert_eq!(scheduler.status(&id_1).await.unwrap(), TaskStatus::Running);
assert!(scheduler.status(&id_2).await.is_err());
assert_eq!(scheduler.status(&id_3).await.unwrap(), TaskStatus::Running);
tokio::time::sleep(Duration::from_millis(1000)).await;
assert_eq!(scheduler.status(&id_1).await.unwrap(), TaskStatus::Running);
assert!(scheduler.status(&id_2).await.is_err());
assert_eq!(scheduler.status(&id_3).await.unwrap(), TaskStatus::Finished);
scheduler
.shutdown(ShutdownOpts::CancelTasks(CancelOpts::Kill))
.await
.unwrap();
}
#[tokio::test]
#[timeout(5000)]
async fn garbage_collector_immediate() {
let scheduler = SchedulerBuilder::new()
.garbage_collector(GarbageCollector::immediate())
.build();
let task_1 = Task::new(TaskSchedule::Interval(Duration::from_millis(100)), |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
})
});
let task_2 = Task::new(TaskSchedule::Once, |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
})
});
let task_3 = Task::new(TaskSchedule::Once, |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(3)).await;
})
});
let task_4 = Task::new(TaskSchedule::Once, |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(4)).await;
})
})
.with_timeout(Duration::from_secs(3));
let id_1 = scheduler.add(task_1).await.unwrap();
let id_2 = scheduler.add(task_2).await.unwrap();
let id_3 = scheduler.add(task_3).await.unwrap();
let id_4 = scheduler.add(task_4).await.unwrap();
tokio::time::sleep(Duration::from_millis(2500)).await;
assert_eq!(scheduler.status(&id_1).await.unwrap(), TaskStatus::Running);
assert!(scheduler.status(&id_2).await.is_err());
assert_eq!(scheduler.status(&id_3).await.unwrap(), TaskStatus::Running);
assert_eq!(scheduler.status(&id_4).await.unwrap(), TaskStatus::Running);
tokio::time::sleep(Duration::from_millis(1000)).await;
assert_eq!(scheduler.status(&id_1).await.unwrap(), TaskStatus::Running);
assert!(scheduler.status(&id_2).await.is_err());
assert!(scheduler.status(&id_3).await.is_err());
assert!(scheduler.status(&id_4).await.is_err());
scheduler
.shutdown(ShutdownOpts::CancelTasks(CancelOpts::Kill))
.await
.unwrap();
}
#[tokio::test]
#[timeout(5000)]
async fn reject_duplicated_task() {
let scheduler = SchedulerBuilder::new()
.worker_type(WorkerType::CurrentThread)
.garbage_collector(GarbageCollector::disabled())
.build();
let task_1 = Task::new(TaskSchedule::Interval(Duration::from_millis(100)), |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
})
})
.with_id("TASK_ID");
let task_2 = task_1.clone();
let _id1 = scheduler.add(task_1).await.unwrap();
let id2 = scheduler.add(task_2).await;
tokio::time::sleep(Duration::from_secs(1)).await;
assert!(id2.is_err());
let err = id2.err().unwrap();
match err {
Error::DuplicatedTaskId(id) => {
assert_eq!(id, TaskId::from("TASK_ID"))
}
_ => unreachable!("Incorrect error type or TaskId"),
}
scheduler
.shutdown(ShutdownOpts::IgnoreRunning)
.await
.unwrap();
}
#[tokio::test]
#[timeout(5000)]
async fn shutdown_with_timeout() {
let scheduler = Scheduler::default();
let task = Task::new(TaskSchedule::Interval(Duration::from_millis(100)), |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
})
});
scheduler.add(task).await.unwrap();
yield_now().await;
scheduler
.shutdown(ShutdownOpts::WaitFor(Duration::from_secs(3)))
.await
.unwrap();
let scheduler = Scheduler::default();
let task = Task::new(TaskSchedule::Once, |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(10)).await;
})
});
scheduler.add(task).await.unwrap();
yield_now().await;
match scheduler
.shutdown(ShutdownOpts::WaitFor(Duration::from_secs(1)))
.await
{
Ok(_) => unreachable!("unexpected Ok result"),
Err(e) => match e {
Error::IncompleteShutdown => {}
_ => unreachable!("failed with unexpected error"),
},
}
}
#[tokio::test]
#[timeout(5000)]
async fn task_with_timeout() {
let scheduler = Scheduler::default();
let task1 = Task::new(TaskSchedule::Once, |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
})
})
.with_id("OK")
.with_timeout(Duration::from_secs(5));
let task2 = task1
.clone()
.with_id("FAILED")
.with_timeout(Duration::from_secs(1));
let task3 = Task::new(TaskSchedule::Interval(Duration::from_secs(1)), |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
})
})
.with_id("FAILING")
.with_timeout(Duration::from_secs(1));
let id1 = scheduler.add(task1).await.unwrap();
let id2 = scheduler.add(task2).await.unwrap();
let id3 = scheduler.add(task3).await.unwrap();
tokio::time::sleep(Duration::from_millis(800)).await;
assert_eq!(scheduler.status(&id1).await.unwrap(), TaskStatus::Running);
assert_eq!(scheduler.status(&id2).await.unwrap(), TaskStatus::Running);
assert_eq!(scheduler.status(&id3).await.unwrap(), TaskStatus::Running);
tokio::time::sleep(Duration::from_millis(800)).await;
assert_eq!(scheduler.status(&id1).await.unwrap(), TaskStatus::Running);
assert_eq!(scheduler.status(&id2).await.unwrap(), TaskStatus::Finished);
assert_eq!(scheduler.status(&id3).await.unwrap(), TaskStatus::Waiting);
tokio::time::sleep(Duration::from_millis(800)).await;
assert_eq!(scheduler.status(&id1).await.unwrap(), TaskStatus::Finished);
assert!(scheduler.status(&id2).await.is_err());
assert_eq!(scheduler.status(&id3).await.unwrap(), TaskStatus::Running);
tokio::time::sleep(Duration::from_millis(800)).await;
assert!(scheduler.status(&id1).await.is_err());
assert!(scheduler.status(&id2).await.is_err());
assert_eq!(scheduler.status(&id3).await.unwrap(), TaskStatus::Waiting);
let _ = scheduler
.shutdown(ShutdownOpts::CancelTasks(CancelOpts::Kill))
.await;
}
#[tokio::test]
#[timeout(5000)]
async fn task_cancellation() {
let scheduler = Scheduler::default();
let task1 = Task::new(TaskSchedule::Once, |_id| {
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(1)).await;
})
})
.with_id("OK");
let task2 = task1.clone().with_id("CANCELED KILLED");
let task3 = task1.clone().with_id("CANCELED IGNORED");
let id1 = scheduler.add(task1).await.unwrap();
let id2 = scheduler.add(task2).await.unwrap();
let id3 = scheduler.add(task3).await.unwrap();
tokio::time::sleep(Duration::from_millis(500)).await;
assert_eq!(
scheduler.statistics(&id1).await.unwrap(),
TaskStatistics {
running: 1,
..Default::default()
}
);
assert_eq!(
scheduler.statistics(&id2).await.unwrap(),
TaskStatistics {
running: 1,
..Default::default()
}
);
assert_eq!(
scheduler.statistics(&id3).await.unwrap(),
TaskStatistics {
running: 1,
..Default::default()
}
);
assert_eq!(scheduler.status(&id1).await.unwrap(), TaskStatus::Running);
assert_eq!(scheduler.status(&id2).await.unwrap(), TaskStatus::Running);
assert_eq!(scheduler.status(&id3).await.unwrap(), TaskStatus::Running);
scheduler
.cancel(id2.clone(), CancelOpts::Kill)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
scheduler.statistics(&id1).await.unwrap(),
TaskStatistics {
running: 1,
..Default::default()
}
);
assert!(scheduler.statistics(&id2).await.is_err());
assert_eq!(
scheduler.statistics(&id3).await.unwrap(),
TaskStatistics {
running: 1,
..Default::default()
}
);
assert_eq!(scheduler.status(&id1).await.unwrap(), TaskStatus::Running);
assert!(scheduler.status(&id2).await.is_err());
assert_eq!(scheduler.status(&id3).await.unwrap(), TaskStatus::Running);
scheduler
.cancel(id3.clone(), CancelOpts::Ignore)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
scheduler.statistics(&id1).await.unwrap(),
TaskStatistics {
running: 1,
..Default::default()
}
);
assert!(scheduler.statistics(&id3).await.is_err());
assert_eq!(scheduler.status(&id1).await.unwrap(), TaskStatus::Running);
assert!(scheduler.status(&id3).await.is_err());
tokio::time::sleep(Duration::from_millis(400)).await;
assert_eq!(
scheduler.statistics(&id1).await.unwrap(),
TaskStatistics {
completed: 1,
..Default::default()
}
);
assert_eq!(scheduler.status(&id1).await.unwrap(), TaskStatus::Finished);
assert!(scheduler.status(&id1).await.is_err());
let _ = scheduler
.shutdown(ShutdownOpts::CancelTasks(CancelOpts::Kill))
.await;
}
#[test]
fn default_garbage_collector_value_should_be_disabled() {
assert_eq!(GarbageCollector::disabled(), GarbageCollector::default());
assert_eq!(GarbageCollector::default(), GarbageCollector::Disabled);
}
#[test]
fn scheduler_builder_should_provide_default_values() {
let builder = SchedulerBuilder::new();
assert_eq!(builder.worker_type, WorkerType::CurrentRuntime);
assert_eq!(builder.garbage_collector, GarbageCollector::Disabled);
assert_eq!(builder.parallelism, WorkerParallelism::Limited(16));
}
#[test]
fn scheduler_builder_should_provide_custom_worker_type() {
let builder = SchedulerBuilder::new();
assert_eq!(builder.worker_type, WorkerType::CurrentRuntime);
assert_eq!(
builder
.clone()
.worker_type(WorkerType::CurrentThread)
.worker_type,
WorkerType::CurrentThread
);
assert_eq!(
builder
.clone()
.worker_type(WorkerType::MultiThread(RuntimeThreads::CpuCores))
.worker_type,
WorkerType::MultiThread(RuntimeThreads::CpuCores)
);
assert_eq!(
builder
.worker_type(WorkerType::MultiThread(RuntimeThreads::Limited(8)))
.worker_type,
WorkerType::MultiThread(RuntimeThreads::Limited(8))
);
}
#[test]
fn scheduler_builder_should_provide_custom_garbage_collector() {
let builder = SchedulerBuilder::new();
assert_eq!(builder.garbage_collector, GarbageCollector::Disabled);
assert_eq!(
builder
.clone()
.garbage_collector(GarbageCollector::Immediate)
.garbage_collector,
GarbageCollector::Immediate
);
assert_eq!(
builder
.clone()
.garbage_collector(GarbageCollector::immediate())
.garbage_collector,
GarbageCollector::Immediate
);
assert_eq!(
builder
.clone()
.garbage_collector(GarbageCollector::Periodic {
expire_after: Duration::from_secs(30),
interval: Duration::from_secs(10)
})
.garbage_collector,
GarbageCollector::Periodic {
expire_after: Duration::from_secs(30),
interval: Duration::from_secs(10)
}
);
assert_eq!(
builder
.clone()
.garbage_collector(GarbageCollector::periodic(
Duration::from_secs(120),
Duration::from_millis(1000)
))
.garbage_collector,
GarbageCollector::Periodic {
expire_after: Duration::from_secs(120),
interval: Duration::from_millis(1000)
}
);
}
#[test]
fn scheduler_builder_should_provide_custom_parallelism() {
let builder = SchedulerBuilder::new();
assert_eq!(builder.parallelism, WorkerParallelism::Limited(16));
assert_eq!(
builder
.clone()
.parallelism(WorkerParallelism::Limited(8))
.parallelism,
WorkerParallelism::Limited(8)
);
assert_eq!(
builder
.clone()
.parallelism(WorkerParallelism::Unlimited)
.parallelism,
WorkerParallelism::Unlimited
);
}
}