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:
94
README.md
94
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<Box<dyn Future>>`)
|
||||
- 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<dyn Trait>`, 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<Box<dyn Future<Output = JobResult> + Send + 'a>>
|
||||
where Self: Sync + 'a;
|
||||
}
|
||||
```
|
||||
|
||||
#### `JobRunner` Struct
|
||||
|
||||
```rust
|
||||
pub struct JobRunner {
|
||||
jobs: Vec<Box<dyn Job>>
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
318
src/adv_async.rs
Normal file
318
src/adv_async.rs
Normal 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());
|
||||
}
|
||||
}
|
@ -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<Box<dyn core::future::Future<Output = Result<String, String>>
|
||||
+ 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<dyn std::error::Error>>
|
||||
/// # {
|
||||
/// # 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]
|
@ -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
|
||||
|
16
src/lib.rs
Normal file
16
src/lib.rs
Normal file
@ -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;
|
Reference in New Issue
Block a user