Added some advanced async stuff.

Really it doesn't have channels so the only advanced thing was the
dynamic trait part. Pinning the futures is only real hard part to
this module. The rest does show trait implementations on structs though
which was something missing from the library before.

Also, the project was turned into a library and the foundational
examples were put into a basic module.
This commit is contained in:
2025-06-28 16:52:04 -04:00
parent d395741822
commit 4b16a23712
5 changed files with 426 additions and 72 deletions

318
src/adv_async.rs Normal file
View File

@ -0,0 +1,318 @@
//! 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());
}
}