// SPDX-License-Identifier: Apache-2.0 // Sealed with Magistamp. //! An example job execution framework. //! //! It includes: //! - A `Job` trait for defining asynchronous tasks. //! - A `JobRunner` struct for collecting and executing multiple jobs. //! - Concrete implementations of jobs (`FileJob`, `SleepJob`, `MathJob`). //! //! Jobs are run concurrently using Tokio, and results are gathered //! with proper error handling. use std::io::Write; use std::pin::Pin; use tokio::time::{sleep, Duration}; /// Run a predefined set of jobs asynchronously. /// /// This function creates a `JobRunner`, adds three different jobs, /// and executes them concurrently. Results are printed to stdout, /// and any errors are reported to stderr. pub async fn run_jobs() { let mut runner: JobRunner = JobRunner::new(); runner.add_job(FileJob::new()); runner.add_job(SleepJob::new()); runner.add_job(MathJob::new()); for result in runner.run_all().await.iter() { match &result.1 { Ok(msg) => { println!("{}: {}", result.0, msg); } Err(error) => { eprintln!("{}: {}", result.0, error); } } } } /// A Job will return this after running. The traits need to be Send /// because they maybe sent between threads. type JobResult = Result>; /// The Future task that Jobs will return that can later be run. /// Here we Pin a heap allocated future so that it can be referenced safely. /// The output is then set to the desired output of the function. /// This is the may way to do 'async dyn traits'. type PinnedFuture<'a> = Pin + Send + 'a>>; /// An async trait that can be used as a dyn trait. pub trait Job: Send + Sync { /// Retrieve the name of the Job. fn name(&self) -> &str; /// Run the Job. /// /// This function needs to be async. /// Here we Pin a heap allocated future so that it can be referenced safely. /// The output is then set to the desired output of the function. /// /// Inside the implementation all you need to do is: /// ```ignore /// Box::pin(async move /// { /// // Place your functions code here. /// }) /// ``` fn run<'a>(&self) -> PinnedFuture<'a> where Self: Sync + 'a; } /// A struct to hold and execute multiple jobs. pub struct JobRunner { jobs: Vec> } impl JobRunner { /// Create a new JobRunner with an empty job list. pub fn new() -> Self { JobRunner { jobs: Vec::new() } } /// Add a new job to the runner. pub fn add_job(&mut self, job: impl Job + 'static) { self.jobs.push(Box::new(job)); } /// Run all added jobs concurrently and collect their results. /// /// Uses Tokio to spawn concurrent tasks for each job. After /// completion, all jobs are cleared from the runner. pub async fn run_all(&mut self) -> Vec<(String, JobResult)> { let tasks: tokio::task::JoinSet<(String, JobResult)> = self.jobs .iter() .map(|j| { let name = j.name().to_string(); let fut = j.run(); async move { (name, fut.await) } }) .collect(); let result = tasks.join_all().await; self.jobs.clear(); result } } /// A dummy job simulating a file operation. pub struct FileJob {} impl FileJob { /// Create a new FileJob instance. fn new() -> Self { FileJob { } } } /// Implementation of the Job trait for FileJob. impl Job for FileJob { /// Retrieve the name of the Job. fn name(&self) -> &str { "File Job" } /// Run the File Job. fn run<'a>(&self) -> PinnedFuture<'a> where Self: Sync + 'a { Box::pin(async move { let mut write_target = std::env::temp_dir(); write_target.push("file_job"); write_target.set_extension("txt"); let mut file = std::fs::File::create(write_target) .map_err(|e| Box::new(e) as Box)?; file.write_all(b"Jason is an awesome programmer!!") .map_err(|e| Box::new(e) as Box)?; Ok(String::from("File written")) }) } } /// A dummy job simulating a sleep or delay operation. pub struct SleepJob {} impl SleepJob { /// Create a new SleepJob instance. fn new() -> Self { SleepJob { } } } /// Implementation of the Job trait for SleepJob. impl Job for SleepJob { /// Retrieve the name of the Job. fn name(&self) -> &str { "Sleep Job" } /// Run the Sleep Job. fn run<'a>(&self) -> PinnedFuture<'a> where Self: Sync + 'a { Box::pin(async move { sleep(Duration::from_millis(500)).await; Ok(String::from("Zzzzzzzzzzz")) }) } } /// A dummy job simulating a math operation. pub struct MathJob {} impl MathJob { /// Create a new MathJob instance. fn new() -> Self { MathJob { } } } /// Implementation of the Job trait for MathJob. impl Job for MathJob { /// Retrieve the name of the Job. fn name(&self) -> &str { "Math Job" } /// Run the Math Job. fn run<'a>(&self) -> PinnedFuture<'a> where Self: Sync + 'a { Box::pin(async move { let _ans = 10 * 4 + 2; Ok(String::from("Math stuff")) }) } } #[cfg(test)] mod tests { use super::*; use tokio::time::Instant; #[tokio::test] async fn test_file_job() { let job = FileJob::new(); assert_eq!(job.name(), "File Job"); let result = job.run().await; assert!(result.is_ok()); assert_eq!(result.unwrap(), "File written"); } #[tokio::test] async fn test_sleep_job() { let job = SleepJob::new(); assert_eq!(job.name(), "Sleep Job"); let start = Instant::now(); let result = job.run().await; let elapsed = start.elapsed(); assert!(result.is_ok()); assert_eq!(result.unwrap(), "Zzzzzzzzzzz"); assert!(elapsed >= Duration::from_millis(500), "Sleep duration too short"); } #[tokio::test] async fn test_math_job() { let job = MathJob::new(); assert_eq!(job.name(), "Math Job"); let result = job.run().await; assert!(result.is_ok()); assert_eq!(result.unwrap(), "Math stuff"); } #[tokio::test] async fn test_job_runner_executes_all_jobs() { let mut runner = JobRunner::new(); runner.add_job(FileJob::new()); runner.add_job(SleepJob::new()); runner.add_job(MathJob::new()); let mut results = runner.run_all().await; // Ensure jobs are cleared after run assert!(runner.jobs.is_empty()); // Sort results by name for consistent testing results.sort_by(|a, b| a.0.cmp(&b.0)); assert_eq!(results.len(), 3); assert_eq!(results[0].0, "File Job"); assert_eq!(results[0].1.as_ref().unwrap(), "File written"); assert_eq!(results[1].0, "Math Job"); assert_eq!(results[1].1.as_ref().unwrap(), "Math stuff"); assert_eq!(results[2].0, "Sleep Job"); assert_eq!(results[2].1.as_ref().unwrap(), "Zzzzzzzzzzz"); } #[tokio::test] async fn test_job_runner_with_no_jobs() { let mut runner = JobRunner::new(); let results = runner.run_all().await; assert!(results.is_empty()); } }