319 lines
6.9 KiB
Rust
319 lines
6.9 KiB
Rust
|
//! 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::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<String, Box<dyn std::error::Error + Send>>;
|
||
|
|
||
|
/// 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<Box<dyn core::future::Future<Output = JobResult> + 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<Box<dyn Job>>
|
||
|
}
|
||
|
|
||
|
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 {
|
||
|
Ok(String::from("Reading file"))
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/// 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 {
|
||
|
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(), "Reading file");
|
||
|
}
|
||
|
|
||
|
#[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(), "Reading file");
|
||
|
|
||
|
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());
|
||
|
}
|
||
|
}
|