diff --git a/README.md b/README.md index 6951ab0..ee13714 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,18 @@ # Example -A lightweight collection of examples demonstrating essential Rust features, -without relying on external dependencies (except `tokio`). +This repository demonstrates modern, idiomatic Rust through two parts: +1. **Foundational Rust Examples** – covering core concepts like lifetimes, + error handling, async, and testing. +2. **Advanced Async Job Runner** – a trait-based, concurrent task execution + framework built using `tokio`. --- +## Part 1: Foundational Rust Examples + +A lightweight collection of examples demonstrating essential Rust features, +without relying on external dependencies (except `tokio`). ### Concepts Covered @@ -29,34 +36,105 @@ without relying on external dependencies (except `tokio`). Includes both synchronous and asynchronous unit tests under a `#[cfg(test)]` module. - --- +## Part 2: Advanced Async Job Runner + +An extensible, asynchronous job system using: + +- Trait objects +- Pinned futures (`Pin>`) +- Tokio’s `JoinSet` for concurrent execution +- Fully dynamic job management without macros + +### Why It Matters + +This system demonstrates: +- Manual implementation of async trait behavior (without `async_trait`) +- Advanced type management (`Box`, lifetimes, `Pin`) +- Idiomatic use of `tokio::task::JoinSet` for parallelism + +### Architecture + +#### `Job` Trait + +```rust +pub trait Job: Send + Sync { + fn name(&self) -> &str; + fn run<'a>(&self) -> Pin + Send + 'a>> + where Self: Sync + 'a; +} +``` + +#### `JobRunner` Struct + +```rust +pub struct JobRunner { + jobs: Vec> +} +``` + +Manages and runs jobs concurrently. + +#### Built-In Jobs + +| Job Type | Behavior | +|-----------|---------------------------| +| `FileJob` | (Planned) Reads from disk | +| `SleepJob`| Simulates a delay | +| `MathJob` | Performs a math task | + +### Usage + +```rust +let mut runner = JobRunner::new(); +runner.add_job(FileJob::new()); +runner.add_job(SleepJob::new()); +runner.add_job(MathJob::new()); + +for (name, result) in runner.run_all().await { + match result { + Ok(msg) => println!("{name}: {msg}"), + Err(err) => eprintln!("{name}: {err}"), + } +} +``` + +--- ## Testing & Extending +You can add unit or integration tests using mock jobs to simulate both success and failure. + +Ideas for extension: +- Make the Jobs actually do something +- Interjob communication using channels +- Create a QueueRunner that will run jobs in sequence +- Create a ParallelRunner that will run jobs concurrently (simple rename) +- Create a system that allows QueueRunners and ParallelRunners to be combined + --- - ## Requirements - Rust 1.76+ (for full language support) - [Tokio 1.37+](https://docs.rs/tokio) (for async runtime) - --- - ## Project Structure ```text src/ -├── info.rs # Crate information from Cargo. -├── main.rs # Foundational examples. +├── adv_async.rs # Advanced async JobRunner +├── basic.rs # Foundational examples +├── info.rs # Crate information from Cargo +├── lib.rs # Entry point for the library ``` + --- ## Copyright & License diff --git a/src/adv_async.rs b/src/adv_async.rs new file mode 100644 index 0000000..5331fe1 --- /dev/null +++ b/src/adv_async.rs @@ -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>; + +/// 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 { + 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()); + } +} diff --git a/src/main.rs b/src/basic.rs similarity index 73% rename from src/main.rs rename to src/basic.rs index 1a04e26..a92d961 100644 --- a/src/main.rs +++ b/src/basic.rs @@ -1,10 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Sealed with Magistamp. -//! # Skills Example -//! -//! This demonstrates several foundational Rust concepts in a simple, -//! self-contained example using only the tokio runtime and no other crates. +//! This demonstrates several foundational Rust concepts. //! //! It includes working examples of: //! @@ -21,42 +18,14 @@ //! - `run_tasks`: Spawns multiple asynchronous tasks and waits for them to //! complete. //! -//! The `main` function is marked with the `#[tokio::main]` attribute and serves -//! as the entry point, demonstrating how to run async code in a typical binary. -//! //! Unit tests are included under the `#[cfg(test)]` module to validate //! functionality and show best practices. use std::io::Read; -use std::pin::Pin; use tokio::time::{sleep, Duration}; -/// An async trait that can be used as a dyn trait. -pub trait Job: Send + Sync -{ - fn name(&self) -> &str; - - /// 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) - -> Pin> - + Send - + 'a>> - where Self: Sync + 'a; -} - /// Takes a list of strings and returns the longest one without /// copying the strings. @@ -66,7 +35,7 @@ pub trait Job: Send + Sync /// returned reference needs to live as long as the owning String does. /// /// ``` -/// # use example::longest; +/// # use example::basic::longest; /// let strings: [String; 5] = [String::from("Jason"), /// String::from("is"), /// String::from("an"), @@ -106,7 +75,7 @@ pub fn longest<'a>(strings: &'a [String]) -> Option<&'a str> /// ``` /// # fn try_main() -> Result<(), Box> /// # { -/// # use example::read_and_parse; +/// # use example::basic::read_and_parse; /// use std::io::{ErrorKind, Write}; /// /// let val: i32 = 12345; @@ -173,39 +142,12 @@ pub async fn run_tasks() -/// Where everything gets started. -/// -/// That's not really true. Rust sets up a -/// function that calls this one, which is similar to what tokio does when -/// you use the tokio::main attribute. However, this is the common starting -/// point for Rust binaries. -/// -/// If you wanted to handle the actual main on your own do something -/// like below. Be warned that you will need to parse the arguments passed -/// to your program yourself and it is a different starting function call -/// on certain platforms. -/// ```ignore -/// #[no_mangle] -/// #[inline(never)] -/// #[start] -/// pub extern "C" fn main(argc: i32, argv: *const *const u8) -> i32 -/// { -/// } -/// ``` -#[tokio::main] -async fn main() -{ - run_tasks().await; -} - - - #[cfg(test)] mod test { use std::io::Write; - use crate::{longest, read_and_parse, run_tasks}; + use crate::basic::{longest, read_and_parse, run_tasks}; #[test] diff --git a/src/info.rs b/src/info.rs index 0316847..fefc737 100644 --- a/src/info.rs +++ b/src/info.rs @@ -26,8 +26,8 @@ pub fn get_name() -> &'static str } -/// Returns the name of the program as defined by the CARGO_PKG_VERSION. This is -/// set at compile time and comes from the Cargo.toml file. +/// Returns the version of the program as defined by the CARGO_PKG_VERSION. +/// This is set at compile time and comes from the Cargo.toml file. /// /// If a value is not found, then it will return the not defined value. pub fn get_version() -> &'static str diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..00abaca --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Sealed with Magistamp. + +//! This repository demonstrates modern, idiomatic Rust through two parts: +//! +//! 1. **Foundational Rust Examples** – covering core concepts like lifetimes, +//! error handling, async, and testing. +//! +//! 2. **Advanced Async Job Runner** – a trait-based, concurrent task +//! execution framework built using +//! `tokio`. + + + +pub mod basic; +pub mod adv_async;