use std::sync::Arc;
use rustlavel_core::{Json, Result};
use rustlavel_queue::{
Chain, Job, JobContext, JobRegistry, MemoryProgress, MemoryQueue, ProgressStore, Queue, QueueExt,
QueuedJob, Worker,
};
static ORDER: std::sync::Mutex<Vec<(String, String)>> = std::sync::Mutex::new(Vec::new());
fn order(run: &str) -> Vec<String> {
ORDER.lock().unwrap().iter().filter(|(r, _)| r == run).map(|(_, s)| s.clone()).collect()
}
struct Step {
run: String,
name: String,
fail: bool,
}
fn step(run: &str, name: &str, fail: bool) -> Step {
Step { run: run.into(), name: name.into(), fail }
}
impl Job for Step {
const NAME: &'static str = "step";
fn payload(&self) -> Json {
Json::object([
("run", Json::from(self.run.as_str())),
("name", Json::from(self.name.as_str())),
("fail", Json::from(self.fail)),
])
}
fn from_payload(payload: &Json) -> Result<Self> {
Ok(Step {
run: payload.get("run").and_then(Json::as_str).unwrap_or("?").to_string(),
name: payload.get("name").and_then(Json::as_str).unwrap_or("?").to_string(),
fail: payload.get("fail").and_then(Json::as_bool).unwrap_or(false),
})
}
fn tries(&self) -> u32 {
1
}
async fn handle(&self) -> Result<()> {
ORDER.lock().unwrap().push((self.run.clone(), self.name.clone()));
if self.fail {
return Err(rustlavel_core::Error::msg(format!("{} failed on purpose", self.name)));
}
Ok(())
}
}
struct Render;
impl Job for Render {
const NAME: &'static str = "render";
fn payload(&self) -> Json {
Json::Null
}
fn from_payload(_: &Json) -> Result<Self> {
Ok(Render)
}
async fn handle(&self) -> Result<()> {
Ok(())
}
async fn handle_with(&self, ctx: &JobContext) -> Result<()> {
ctx.progress(10, "probing").await;
ctx.progress(60, "rendering clip 3 of 5").await;
Ok(())
}
}
fn worker(queue: Arc<MemoryQueue>, progress: Arc<dyn ProgressStore>) -> Worker {
let mut registry = JobRegistry::new();
registry.register::<Step>().register::<Render>();
Worker::new(queue, Arc::new(registry)).reporting_to(progress)
}
async fn drain(worker: &Worker) {
while worker.run_once().await.unwrap().is_some() {}
}
#[tokio::test]
async fn a_chain_runs_its_steps_in_order_one_at_a_time() {
let queue = Arc::new(MemoryQueue::new());
let progress: Arc<dyn ProgressStore> = Arc::new(MemoryProgress::new());
let worker = worker(Arc::clone(&queue), Arc::clone(&progress));
queue
.dispatch_chain(
Chain::new(step("v7", "transcribe", false))
.then(step("v7", "detect", false))
.then(step("v7", "render", false))
.named("video-7"),
)
.await
.unwrap();
assert_eq!(queue.size("default").await.unwrap(), 1, "every step was pushed at once");
worker.run_once().await.unwrap();
assert_eq!(order("v7"), ["transcribe"]);
assert_eq!(queue.size("default").await.unwrap(), 1, "the next step was not pushed");
drain(&worker).await;
assert_eq!(order("v7"), ["transcribe", "detect", "render"]);
assert_eq!(queue.size("default").await.unwrap(), 0);
let whole = progress.get("chain:video-7").await.unwrap().expect("the chain has progress");
assert!(whole.finished);
assert_eq!(whole.percent, 100);
assert!(whole.failed.is_none());
}
#[tokio::test]
async fn a_failed_step_ends_the_chain_and_reports_the_failure() {
let queue = Arc::new(MemoryQueue::new());
let progress: Arc<dyn ProgressStore> = Arc::new(MemoryProgress::new());
let worker = worker(Arc::clone(&queue), Arc::clone(&progress));
queue
.dispatch_chain(
Chain::new(step("d", "a", false))
.then(step("d", "b", true))
.then(step("d", "c", false))
.named("doomed"),
)
.await
.unwrap();
drain(&worker).await;
assert_eq!(order("d"), ["a", "b"], "a step after the failure ran");
let whole = progress.get("chain:doomed").await.unwrap().expect("recorded");
assert!(whole.finished);
assert!(whole.failed.as_deref().unwrap_or("").contains("b failed"), "{whole:?}");
let failed = queue.failed_jobs().await.unwrap();
assert_eq!(failed.len(), 1);
let stored = &failed[0].payload;
assert!(stored.get("$then").is_some(), "the unrun steps were dropped: {stored}");
}
#[tokio::test]
async fn progress_is_readable_by_job_id_while_and_after_it_runs() {
let queue = Arc::new(MemoryQueue::new());
let progress: Arc<dyn ProgressStore> = Arc::new(MemoryProgress::new());
let worker = worker(Arc::clone(&queue), Arc::clone(&progress));
let id = queue.dispatch(&Render).await.unwrap();
worker.run_once().await.unwrap();
let done = progress.get(&id).await.unwrap().expect("the job reported");
assert_eq!(done.percent, 100);
assert!(done.finished);
assert_eq!(done.stage, "finished");
}
#[tokio::test]
async fn a_silent_job_is_still_marked_finished() {
let queue = Arc::new(MemoryQueue::new());
let progress: Arc<dyn ProgressStore> = Arc::new(MemoryProgress::new());
let worker = worker(Arc::clone(&queue), Arc::clone(&progress));
let id = queue.dispatch(&step("q", "quiet", false)).await.unwrap();
worker.run_once().await.unwrap();
assert!(progress.get(&id).await.unwrap().expect("recorded").finished);
}
#[test]
fn the_envelope_round_trips_and_leaves_a_plain_payload_alone() {
let plain = QueuedJob::new("render", Json::object([("video", Json::from(7))]));
assert_eq!(plain.stored_payload(), plain.payload, "a chainless job was wrapped");
let chained = Chain::new(step("e", "a", false))
.then(step("e", "b", false))
.then(step("e", "c", false))
.named("v")
.into_queued();
let stored = chained.stored_payload();
assert!(stored.get("$then").is_some());
let back = QueuedJob::new("step", Json::Null).with_stored_payload(stored);
assert_eq!(back.chain.as_deref(), Some("v"));
assert_eq!(back.then.len(), 2);
assert_eq!(back.payload, chained.payload);
assert_eq!(back.then[1].payload.get("name").and_then(Json::as_str), Some("c"));
let next = back.next_in_chain().expect("two steps remain");
assert_eq!(next.chain.as_deref(), Some("v"));
assert_eq!(next.then.len(), 1);
assert!(next.next_in_chain().unwrap().next_in_chain().is_none());
}