Files
example/src/adv_async.rs
Myrddin Dundragon d16f8e333e Made the jobs do something.
Also, stamped the advanced async file with the license.
2025-06-28 18:00:11 -04:00

333 lines
7.4 KiB
Rust

// 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<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 {
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<dyn std::error::Error + Send>)?;
file.write_all(b"Jason is an awesome programmer!!")
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)?;
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());
}
}