123 lines
3.4 KiB
Rust
123 lines
3.4 KiB
Rust
//! # Getting Started: Your First DAG
|
|
//!
|
|
//! This example introduces the fundamentals of building and executing a DAG with dagx.
|
|
//!
|
|
//! ## What You'll Learn
|
|
//! - How to define tasks using the `#[task]` macro
|
|
//! - How to create a DAG and add tasks
|
|
//! - How to wire dependencies between tasks
|
|
//! - How to execute the DAG and retrieve results
|
|
//!
|
|
//! ## Key Concepts
|
|
//! - **Task**: A unit of work with typed inputs and outputs
|
|
//! - **DAG (Directed Acyclic Graph)**: A workflow where tasks depend on other tasks
|
|
//! - **TaskHandle**: A typed reference to a task's output
|
|
//! - **Dependencies**: Relationships between tasks that determine execution order
|
|
//!
|
|
//! ## Running This Example
|
|
//! ```bash
|
|
//! cargo run --example basic
|
|
//! ```
|
|
//!
|
|
//! ## Expected Output
|
|
//! ```text
|
|
//! Running DAG...
|
|
//!
|
|
//! Computing value: 2
|
|
//! Computing value: 3
|
|
//! Sum: Computing 2 + 3
|
|
//!
|
|
//! Result: 5
|
|
//! ```
|
|
|
|
use dagx::{task, DagRunner};
|
|
|
|
|
|
// Step 1: Define a source task (no dependencies)
|
|
//
|
|
// Source tasks produce initial data for the DAG.
|
|
// They don't depend on any other tasks.
|
|
struct Value {
|
|
value: i32,
|
|
}
|
|
|
|
impl Value {
|
|
fn new(value: i32) -> Self {
|
|
Self { value }
|
|
}
|
|
}
|
|
|
|
// The #[task] macro converts your implementation into a full Task trait impl
|
|
#[task]
|
|
impl Value {
|
|
// Use `&mut self` to access the task's state
|
|
async fn run(&mut self) -> i32 {
|
|
println!("Computing value: {}", self.value);
|
|
self.value
|
|
}
|
|
}
|
|
|
|
// Step 2: Define a task with dependencies
|
|
//
|
|
// This task takes TWO inputs (a and b), both of type i32.
|
|
// The order of parameters matches the order in .depends_on()
|
|
struct Add {
|
|
label: String,
|
|
}
|
|
|
|
#[task]
|
|
impl Add {
|
|
// Parameters after `&mut self` are dependencies
|
|
// They're provided as references (&i32, not i32)
|
|
async fn run(&mut self, a: &i32, b: &i32) -> i32 {
|
|
println!("{}: Computing {} + {}", self.label, a, b);
|
|
a + b
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
// Step 3: Create a new DAG
|
|
let dag = DagRunner::new();
|
|
|
|
// Step 4: Add source tasks (tasks with no dependencies)
|
|
//
|
|
// add_task() returns a TaskHandle<T> where T is the task's output type
|
|
let x = dag.add_task(Value::new(2)).into(); // TaskHandle<i32>
|
|
let y = dag.add_task(Value::new(3)).into(); // TaskHandle<i32>
|
|
|
|
// Step 5: Add a task with dependencies
|
|
//
|
|
// The .depends_on() method wires up dependencies.
|
|
// Order matters! The first handle maps to the first parameter, etc.
|
|
let sum = dag
|
|
.add_task(Add {
|
|
label: "Sum".to_string(),
|
|
})
|
|
.depends_on((&x, &y)); // Pass handles as a tuple
|
|
// sum is now TaskHandle<i32>
|
|
|
|
// Step 6: Execute the DAG
|
|
//
|
|
// The run() method needs a "spawner" - a function that spawns futures.
|
|
// This enables runtime-agnostic execution.
|
|
//
|
|
// Execution order:
|
|
// 1. Tasks with no dependencies run first (x and y in parallel)
|
|
// 2. Once x and y complete, sum runs (it depends on both)
|
|
println!("Running DAG...\n");
|
|
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
|
.await
|
|
.unwrap();
|
|
|
|
// Step 7: Retrieve results
|
|
//
|
|
// Use .get() with a TaskHandle to retrieve the output.
|
|
// Results are not cloned, so you can only retrieve them once.
|
|
println!("\nResult: {}", dag.get(sum).unwrap());
|
|
|
|
// Note: You can also retrieve intermediate results
|
|
assert_eq!(dag.get(x).unwrap(), 2);
|
|
assert_eq!(dag.get(y).unwrap(), 3);
|
|
}
|