use std::{future::Future, marker::PhantomData, pin::Pin, time::Duration};
use ::tokio::time::sleep;
use anyhow::Result;
use chrono::{DateTime, Utc};
#[cfg_attr(test, mockall::automock)]
pub trait Executor: Send + Sync {}
pub struct Orchestrator<EXEC: Executor> {
exec: EXEC,
delay: Duration,
}
impl<EXEC: Executor> Orchestrator<EXEC> {
pub fn new(exec: EXEC) -> Self {
Self {
exec,
delay: Duration::from_secs(5),
}
}
pub async fn orchestrate<TASK: Task<EXEC>>(
&self,
chain: &TaskChain<EXEC, TASK>,
) -> Result<TaskChainStatus> {
let mut status = self.orchestrate_once(chain).await?;
while !status.is_finished() {
sleep(self.delay).await;
status = self.orchestrate_once(chain).await?;
}
Ok(status)
}
pub fn orchestrate_once<'a, TASK: Task<EXEC>>(
&'a self,
chain: &'a TaskChain<EXEC, TASK>,
) -> Pin<Box<dyn Future<Output = Result<TaskChainStatus>> + 'a>> {
Box::pin(async {
let mut finished = 0;
let mut succeeded = 0;
for task in &chain.tasks {
let status = task.status(&self.exec).await?;
self.handle_status(&status, task, &mut finished, &mut succeeded)
.await?;
}
if finished == chain.tasks.len() {
if succeeded == finished {
if let Some(next) = &chain.next {
self.orchestrate_once(next.as_ref()).await
} else {
Ok(TaskChainStatus::Succeeded)
}
} else {
Ok(TaskChainStatus::Failed)
}
} else {
Ok(TaskChainStatus::Running)
}
})
}
pub fn with_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
fn handle_status<'a, TASK: Task<EXEC>>(
&'a self,
status: &'a TaskStatus,
task: &'a TASK,
finished: &'a mut usize,
succeeded: &'a mut usize,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
Box::pin(async move {
match status {
TaskStatus::Failed { .. } => {
if !task.is_deleted(&self.exec).await? {
task.delete(&self.exec).await?;
}
*finished += 1;
}
TaskStatus::Succeeded { .. } => {
if !task.is_deleted(&self.exec).await? {
task.delete(&self.exec).await?;
}
*finished += 1;
*succeeded += 1;
}
TaskStatus::Pending => {
let status = task.start(&self.exec).await?;
self.handle_status(&status, task, finished, succeeded)
.await?;
}
_ => {}
}
Ok(())
})
}
}
#[cfg_attr(test, mockall::automock)]
pub trait Task<EXEC: Executor> {
fn delete(&self, exec: &EXEC) -> impl Future<Output = Result<()>>;
fn is_deleted(&self, exec: &EXEC) -> impl Future<Output = Result<bool>>;
fn start(&self, exec: &EXEC) -> impl Future<Output = Result<TaskStatus>>;
fn status(&self, exec: &EXEC) -> impl Future<Output = Result<TaskStatus>>;
}
pub struct TaskChain<EXEC: Executor, TASK: Task<EXEC>> {
next: Option<Box<TaskChain<EXEC, TASK>>>,
tasks: Vec<TASK>,
_exec: PhantomData<EXEC>,
}
impl<EXEC: Executor, TASK: Task<EXEC>> TaskChain<EXEC, TASK> {
pub fn builder<TASKS: IntoIterator<Item = TASK>>(tasks: TASKS) -> TaskChainBuilder<EXEC, TASK> {
TaskChainBuilder::new(tasks)
}
}
pub struct TaskChainBuilder<EXEC: Executor, TASK: Task<EXEC>> {
parent: Option<Box<TaskChainBuilder<EXEC, TASK>>>,
tasks: Vec<TASK>,
_exec: PhantomData<EXEC>,
}
impl<EXEC: Executor, TASK: Task<EXEC>> TaskChainBuilder<EXEC, TASK> {
pub fn new<TASKS: IntoIterator<Item = TASK>>(tasks: TASKS) -> Self {
Self {
parent: None,
tasks: tasks.into_iter().collect(),
_exec: PhantomData,
}
}
pub fn build(self) -> TaskChain<EXEC, TASK> {
self.build_recursively(None)
}
pub fn then<TASKS: IntoIterator<Item = TASK>>(self, tasks: TASKS) -> Self {
Self {
parent: Some(Box::new(self)),
tasks: tasks.into_iter().collect(),
_exec: PhantomData,
}
}
fn build_recursively(self, next: Option<TaskChain<EXEC, TASK>>) -> TaskChain<EXEC, TASK> {
let chain = TaskChain {
next: next.map(Box::new),
tasks: self.tasks,
_exec: PhantomData,
};
if let Some(parent) = self.parent {
parent.build_recursively(Some(chain))
} else {
chain
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TaskChainStatus {
Failed,
Running,
Succeeded,
}
impl TaskChainStatus {
pub fn is_finished(&self) -> bool {
matches!(self, TaskChainStatus::Failed | TaskChainStatus::Succeeded)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TaskStatus {
Failed {
finished_at: DateTime<Utc>,
started_at: DateTime<Utc>,
},
Pending,
Provisioning,
Running { started_at: DateTime<Utc> },
Succeeded {
finished_at: DateTime<Utc>,
started_at: DateTime<Utc>,
},
}
pub mod tokio;
#[cfg(test)]
mod test {
use chrono::{Duration, Utc};
use super::{MockExecutor, MockTask, Orchestrator, TaskChain, TaskChainStatus, TaskStatus};
mod orchestrator {
use super::*;
mod orchestrate_once {
use super::*;
#[tokio::test]
async fn start_a_b() {
let mut a = MockTask::new();
a.expect_start()
.times(1)
.returning(move |_| Box::pin(async move { Ok(TaskStatus::Provisioning) }));
a.expect_status()
.times(1)
.returning(|_| Box::pin(async move { Ok(TaskStatus::Pending) }));
let mut b = MockTask::new();
b.expect_start()
.times(1)
.returning(move |_| Box::pin(async move { Ok(TaskStatus::Provisioning) }));
b.expect_status()
.times(1)
.returning(|_| Box::pin(async move { Ok(TaskStatus::Pending) }));
let c = MockTask::new();
let chain = TaskChain::builder([a, b]).then([c]).build();
let orch = Orchestrator::new(MockExecutor::new());
let status = orch
.orchestrate_once(&chain)
.await
.expect("failed to orchestrate");
assert_eq!(status, TaskChainStatus::Running);
}
#[tokio::test]
async fn a_b_succeeded_instantly() {
let mut a = MockTask::new();
a.expect_delete()
.times(1)
.returning(|_| Box::pin(async move { Ok(()) }));
a.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(false) }));
a.expect_start().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
a.expect_status()
.times(1)
.returning(|_| Box::pin(async move { Ok(TaskStatus::Pending) }));
let mut b = MockTask::new();
b.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(true) }));
b.expect_start().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
b.expect_status()
.times(1)
.returning(|_| Box::pin(async move { Ok(TaskStatus::Pending) }));
let mut c = MockTask::new();
c.expect_start()
.times(1)
.returning(move |_| Box::pin(async move { Ok(TaskStatus::Provisioning) }));
c.expect_status()
.times(1)
.returning(|_| Box::pin(async move { Ok(TaskStatus::Pending) }));
let chain = TaskChain::builder([a, b]).then([c]).build();
let orch = Orchestrator::new(MockExecutor::new());
let status = orch
.orchestrate_once(&chain)
.await
.expect("failed to orchestrate");
assert_eq!(status, TaskChainStatus::Running);
}
#[tokio::test]
async fn a_failed_instantly_b_succeeded() {
let mut a = MockTask::new();
a.expect_delete()
.times(1)
.returning(|_| Box::pin(async move { Ok(()) }));
a.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(false) }));
a.expect_start().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Failed {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
a.expect_status()
.times(1)
.returning(|_| Box::pin(async move { Ok(TaskStatus::Pending) }));
let mut b = MockTask::new();
b.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(false) }));
b.expect_status().times(1).returning(|_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
b.expect_delete()
.times(1)
.returning(|_| Box::pin(async { Ok(()) }));
let c = MockTask::new();
let chain = TaskChain::builder([a, b]).then([c]).build();
let orch = Orchestrator::new(MockExecutor::new());
let status = orch
.orchestrate_once(&chain)
.await
.expect("failed to orchestrate");
assert_eq!(status, TaskChainStatus::Failed);
}
#[tokio::test]
async fn a_still_running_start_b() {
let mut a = MockTask::new();
a.expect_status().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Running {
started_at: Utc::now(),
})
})
});
let mut b = MockTask::new();
b.expect_start()
.times(1)
.returning(move |_| Box::pin(async move { Ok(TaskStatus::Provisioning) }));
b.expect_status()
.times(1)
.returning(|_| Box::pin(async move { Ok(TaskStatus::Pending) }));
let c = MockTask::new();
let chain = TaskChain::builder([a, b]).then([c]).build();
let orch = Orchestrator::new(MockExecutor::new());
let status = orch
.orchestrate_once(&chain)
.await
.expect("failed to orchestrate");
assert_eq!(status, TaskChainStatus::Running);
}
#[tokio::test]
async fn a_still_running_b_succeeded() {
let mut a = MockTask::new();
a.expect_status().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Running {
started_at: Utc::now(),
})
})
});
let mut b = MockTask::new();
b.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(true) }));
b.expect_status().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
let c = MockTask::new();
let chain = TaskChain::builder([a, b]).then([c]).build();
let orch = Orchestrator::new(MockExecutor::new());
let status = orch
.orchestrate_once(&chain)
.await
.expect("failed to orchestrate");
assert_eq!(status, TaskChainStatus::Running);
}
#[tokio::test]
async fn start_c() {
let mut a = MockTask::new();
a.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(true) }));
a.expect_status().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
let mut b = MockTask::new();
b.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(true) }));
b.expect_status().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
let mut c = MockTask::new();
c.expect_start()
.times(1)
.returning(move |_| Box::pin(async move { Ok(TaskStatus::Provisioning) }));
c.expect_status()
.times(1)
.returning(|_| Box::pin(async move { Ok(TaskStatus::Pending) }));
let chain = TaskChain::builder([a, b]).then([c]).build();
let orch = Orchestrator::new(MockExecutor::new());
let status = orch
.orchestrate_once(&chain)
.await
.expect("failed to orchestrate");
assert_eq!(status, TaskChainStatus::Running);
}
#[tokio::test]
async fn a_failed() {
let mut a = MockTask::new();
a.expect_delete()
.times(1)
.returning(|_| Box::pin(async move { Ok(()) }));
a.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(false) }));
a.expect_status().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Failed {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
let mut b = MockTask::new();
b.expect_delete()
.times(1)
.returning(|_| Box::pin(async move { Ok(()) }));
b.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(false) }));
b.expect_status().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
let c = MockTask::new();
let chain = TaskChain::builder([a, b]).then([c]).build();
let orch = Orchestrator::new(MockExecutor::new());
let status = orch
.orchestrate_once(&chain)
.await
.expect("failed to orchestrate");
assert_eq!(status, TaskChainStatus::Failed);
}
#[tokio::test]
async fn c_succeeded() {
let mut a = MockTask::new();
a.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(true) }));
a.expect_status().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
let mut b = MockTask::new();
b.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(true) }));
b.expect_status().times(1).returning(move |_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
let mut c = MockTask::new();
c.expect_delete()
.times(1)
.returning(|_| Box::pin(async move { Ok(()) }));
c.expect_is_deleted()
.times(1)
.returning(|_| Box::pin(async { Ok(false) }));
c.expect_status().times(1).returning(|_| {
Box::pin(async move {
Ok(TaskStatus::Succeeded {
finished_at: Utc::now(),
started_at: Utc::now() - Duration::seconds(5),
})
})
});
let chain = TaskChain::builder([a, b]).then([c]).build();
let orch = Orchestrator::new(MockExecutor::new());
let status = orch
.orchestrate_once(&chain)
.await
.expect("failed to orchestrate");
assert_eq!(status, TaskChainStatus::Succeeded);
}
}
}
}