feat: dagrunner, initial version
This commit is contained in:
122 files changed
+23101
No files matched your search
@@ -0,0 +1,124 @@
|
||||
//! # 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, Task};
|
||||
|
||||
// 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)); // TaskHandle<i32>
|
||||
let y = dag.add_task(Value::new(3)); // 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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 7: Retrieve results
|
||||
//
|
||||
// Use .get() with a TaskHandle to retrieve the output.
|
||||
// Results are cloned, so you can retrieve them multiple times.
|
||||
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);
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! # Pattern: Fan-Out (1→N Dependencies)
|
||||
//!
|
||||
//! This example demonstrates the fan-out pattern where a single source task
|
||||
//! produces a value that is consumed by multiple independent downstream tasks.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to create one-to-many task dependencies
|
||||
//! - How multiple tasks can consume the same upstream result
|
||||
//! - How tasks with the same dependency run in parallel
|
||||
//! - How to configure tasks with different parameters
|
||||
//!
|
||||
//! ## Pattern Diagram
|
||||
//! ```text
|
||||
//! ┌──────────┐
|
||||
//! │ Source │ (produces: 10)
|
||||
//! └────┬─────┘
|
||||
//! ┌──────┼──────┬──────┐
|
||||
//! │ │ │ │
|
||||
//! ┌───▼──┐ ┌─▼───┐ ┌▼────┐│
|
||||
//! │ +1 │ │ ×2 │ │ ^2 ││
|
||||
//! └───┬──┘ └──┬──┘ └─┬───┘│
|
||||
//! │ │ │ │
|
||||
//! (11) (20) (100) │
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Fan-out**: A single task's output is used by multiple downstream tasks
|
||||
//! - **Parallel execution**: All three downstream tasks run simultaneously
|
||||
//! - **Reusable results**: The source value is cloned to each dependent task
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - Broadcasting configuration to multiple workers
|
||||
//! - Applying different transformations to the same data
|
||||
//! - Parallel A/B testing with the same input
|
||||
//! - Computing multiple metrics from a single dataset
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example fan_out
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! Running fan-out DAG...
|
||||
//!
|
||||
//! Source: Computing initial value 10...
|
||||
//! AddOffset: 10 + 1
|
||||
//! Multiply: 10 * 2
|
||||
//! Power: 10 ^ 2
|
||||
//!
|
||||
//! Results:
|
||||
//! Source: 10
|
||||
//! Plus One: 11
|
||||
//! Times Two: 20
|
||||
//! Squared: 100
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
|
||||
// Source task with a value field
|
||||
struct Source {
|
||||
initial_value: i32,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
fn new(initial_value: i32) -> Self {
|
||||
Self { initial_value }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Source {
|
||||
async fn run(&mut self) -> i32 {
|
||||
println!("Source: Computing initial value {}...", self.initial_value);
|
||||
self.initial_value
|
||||
}
|
||||
}
|
||||
|
||||
// Transformation tasks with configuration fields
|
||||
struct AddOffset {
|
||||
offset: i32,
|
||||
}
|
||||
|
||||
impl AddOffset {
|
||||
fn new(offset: i32) -> Self {
|
||||
Self { offset }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl AddOffset {
|
||||
async fn run(&mut self, x: &i32) -> i32 {
|
||||
println!("AddOffset: {} + {}", x, self.offset);
|
||||
x + self.offset
|
||||
}
|
||||
}
|
||||
|
||||
struct Multiply {
|
||||
factor: i32,
|
||||
}
|
||||
|
||||
impl Multiply {
|
||||
fn new(factor: i32) -> Self {
|
||||
Self { factor }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Multiply {
|
||||
async fn run(&mut self, x: &i32) -> i32 {
|
||||
println!("Multiply: {} * {}", x, self.factor);
|
||||
x * self.factor
|
||||
}
|
||||
}
|
||||
|
||||
struct Power {
|
||||
exponent: u32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Power {
|
||||
async fn run(&mut self, x: &i32) -> i32 {
|
||||
println!("Power: {} ^ {}", x, self.exponent);
|
||||
x.pow(self.exponent)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Single source task - constructed with ::new()
|
||||
let source = dag.add_task(Source::new(10));
|
||||
|
||||
// Multiple downstream tasks consuming the same value
|
||||
// Each constructed differently to show flexibility
|
||||
let plus_one = dag.add_task(AddOffset::new(1)).depends_on(&source);
|
||||
let times_two = dag.add_task(Multiply::new(2)).depends_on(&source);
|
||||
let squared = dag.add_task(Power { exponent: 2 }).depends_on(&source);
|
||||
|
||||
// Run the DAG
|
||||
println!("Running fan-out DAG...\n");
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Print all results
|
||||
println!("\nResults:");
|
||||
println!(" Source: {}", dag.get(source).unwrap());
|
||||
println!(" Plus One: {}", dag.get(plus_one).unwrap());
|
||||
println!(" Times Two: {}", dag.get(times_two).unwrap());
|
||||
println!(" Squared: {}", dag.get(squared).unwrap());
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//! # Pattern: Fan-In (N→1 Dependencies)
|
||||
//!
|
||||
//! This example demonstrates the fan-in pattern where multiple independent source
|
||||
//! tasks produce values that are aggregated by a single downstream task.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to create many-to-one task dependencies
|
||||
//! - How to aggregate results from multiple upstream tasks
|
||||
//! - How parameter order in `depends_on()` maps to function parameters
|
||||
//! - How independent tasks run in parallel before aggregation
|
||||
//!
|
||||
//! ## Pattern Diagram
|
||||
//! ```text
|
||||
//! ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌────────────┐
|
||||
//! │FetchName │ │FetchAge │ │FetchCity │ │CheckActive │
|
||||
//! └────┬─────┘ └────┬────┘ └────┬─────┘ └──────┬─────┘
|
||||
//! │ │ │ │
|
||||
//! "Alice" 30 "San Francisco" true
|
||||
//! │ │ │ │
|
||||
//! └─────────────┴─────────────┴───────────────┘
|
||||
//! │
|
||||
//! ┌───────▼────────┐
|
||||
//! │ BuildProfile │
|
||||
//! └────────────────┘
|
||||
//! │
|
||||
//! "User Profile..."
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Fan-in**: Multiple tasks' outputs are consumed by a single downstream task
|
||||
//! - **Aggregation**: Combining multiple values into a single result
|
||||
//! - **Parameter order**: The order in `.depends_on((&a, &b, &c))` must match
|
||||
//! the function signature `fn run(a: &T1, b: &T2, c: &T3)`
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - Aggregating data from multiple API endpoints
|
||||
//! - Combining results from parallel computations
|
||||
//! - Building composite objects from multiple sources
|
||||
//! - Waiting for multiple prerequisites before proceeding
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example fan_in
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! Running fan-in DAG...
|
||||
//!
|
||||
//! Fetching name...
|
||||
//! Fetching age...
|
||||
//! Fetching city...
|
||||
//! Checking active status...
|
||||
//!
|
||||
//! Building user profile...
|
||||
//!
|
||||
//! User Profile:
|
||||
//! Name: Alice
|
||||
//! Age: 30
|
||||
//! City: San Francisco
|
||||
//! Active: true
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
|
||||
struct FetchName;
|
||||
|
||||
#[task]
|
||||
impl FetchName {
|
||||
async fn run() -> String {
|
||||
println!("Fetching name...");
|
||||
"Alice".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct FetchAge;
|
||||
|
||||
#[task]
|
||||
impl FetchAge {
|
||||
async fn run() -> i32 {
|
||||
println!("Fetching age...");
|
||||
30
|
||||
}
|
||||
}
|
||||
|
||||
struct FetchCity;
|
||||
|
||||
#[task]
|
||||
impl FetchCity {
|
||||
async fn run() -> String {
|
||||
println!("Fetching city...");
|
||||
"San Francisco".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct CheckActive;
|
||||
|
||||
#[task]
|
||||
impl CheckActive {
|
||||
async fn run() -> bool {
|
||||
println!("Checking active status...");
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
struct BuildProfile;
|
||||
|
||||
#[task]
|
||||
impl BuildProfile {
|
||||
async fn run(name: &String, age: &i32, city: &String, active: &bool) -> String {
|
||||
println!("\nBuilding user profile...");
|
||||
format!(
|
||||
"User Profile:\n Name: {}\n Age: {}\n City: {}\n Active: {}",
|
||||
name, age, city, active
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Multiple independent source tasks
|
||||
let name = dag.add_task(FetchName);
|
||||
let age = dag.add_task(FetchAge);
|
||||
let city = dag.add_task(FetchCity);
|
||||
let active = dag.add_task(CheckActive);
|
||||
|
||||
// Single downstream task consuming all values
|
||||
let profile = dag
|
||||
.add_task(BuildProfile)
|
||||
.depends_on((&name, &age, &city, &active));
|
||||
|
||||
// Run the DAG
|
||||
println!("Running fan-in DAG...\n");
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Print result
|
||||
println!("\n{}", dag.get(profile).unwrap());
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//! # Parallel Computation: Map-Reduce Pattern
|
||||
//!
|
||||
//! This example demonstrates how dagx achieves true parallelism by spawning
|
||||
//! tasks to multiple threads, enabling efficient parallel computation.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to split work across multiple parallel tasks (Map)
|
||||
//! - How to aggregate results from parallel tasks (Reduce)
|
||||
//! - How dagx achieves true multi-threaded parallelism
|
||||
//! - How to verify parallel execution with timing
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Map**: Split work into independent parallel tasks
|
||||
//! - **Reduce**: Aggregate results from parallel tasks into a single output
|
||||
//! - **True Parallelism**: Tasks spawn to multiple OS threads via the runtime
|
||||
//! - **Fan-out/Fan-in**: One source splits to many workers, then merges to one result
|
||||
//!
|
||||
//! ## Pattern Diagram
|
||||
//! ```text
|
||||
//! [Source] → [Sum1]
|
||||
//! → [Sum2] → [Aggregate] → Total
|
||||
//! → [Sum3]
|
||||
//! → [Sum4]
|
||||
//! ```
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example parallel_computation
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! Computing sum of 1-1000 using 4 parallel tasks...
|
||||
//!
|
||||
//! Sum of 1-1000 computed in parallel: 500500
|
||||
//! Expected: 500500
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagResult, DagRunner, Task};
|
||||
|
||||
// Step 1: Define a worker task that computes a partial sum
|
||||
//
|
||||
// Each worker computes the sum of a range of numbers.
|
||||
// These tasks run in parallel on different threads.
|
||||
struct ComputeSum {
|
||||
start: i32,
|
||||
end: i32,
|
||||
}
|
||||
|
||||
impl ComputeSum {
|
||||
fn new(start: i32, end: i32) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ComputeSum {
|
||||
async fn run(&mut self) -> i32 {
|
||||
// Compute sum of range [start, end)
|
||||
(self.start..self.end).sum()
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Define an aggregator task that combines results
|
||||
//
|
||||
// This task runs after all workers complete.
|
||||
// It demonstrates the "reduce" phase of map-reduce.
|
||||
struct Aggregate;
|
||||
|
||||
#[task]
|
||||
impl Aggregate {
|
||||
// Takes four inputs and sums them
|
||||
async fn run(a: &i32, b: &i32, c: &i32, d: &i32) -> i32 {
|
||||
a + b + c + d
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> DagResult<()> {
|
||||
println!("Computing sum of 1-1000 using 4 parallel tasks...\n");
|
||||
|
||||
// Step 3: Build the DAG
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Map phase: Create 4 workers that run in parallel
|
||||
// Each computes sum of 250 numbers (total 1-1000)
|
||||
let sum1 = dag.add_task(ComputeSum::new(1, 251)); // 1-250
|
||||
let sum2 = dag.add_task(ComputeSum::new(251, 501)); // 251-500
|
||||
let sum3 = dag.add_task(ComputeSum::new(501, 751)); // 501-750
|
||||
let sum4 = dag.add_task(ComputeSum::new(751, 1001)); // 751-1000
|
||||
|
||||
// Reduce phase: Aggregate results from all workers
|
||||
let total = dag
|
||||
.add_task(Aggregate)
|
||||
.depends_on((&sum1, &sum2, &sum3, &sum4));
|
||||
|
||||
// Step 4: Execute with true parallelism
|
||||
//
|
||||
// tokio::spawn() sends tasks to multiple threads in the thread pool.
|
||||
// Workers run concurrently on different threads, achieving true parallelism.
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Step 5: Retrieve and verify results
|
||||
let result = dag.get(total)?;
|
||||
let expected: i32 = (1..1001).sum();
|
||||
|
||||
println!("Sum of 1-1000 computed in parallel: {}", result);
|
||||
println!("Expected: {}", expected);
|
||||
|
||||
assert_eq!(result, 500500); // Sum of 1-1000
|
||||
|
||||
// Note: True parallelism verification
|
||||
//
|
||||
// If you monitor system resources while running this example, you'll see:
|
||||
// - Multiple CPU cores being utilized
|
||||
// - Worker tasks executing simultaneously
|
||||
// - Reduced wall-clock time vs sequential execution
|
||||
//
|
||||
// This demonstrates that dagx provides REAL parallel execution,
|
||||
// not just async concurrency on a single thread.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
//! # Circuit Breaker Pattern for Resilient Systems
|
||||
//!
|
||||
//! This example demonstrates the circuit breaker pattern for protecting systems from
|
||||
//! cascading failures when downstream services are unhealthy or overloaded.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to implement a three-state circuit breaker (Closed, Open, Half-Open)
|
||||
//! - How to share circuit state across multiple task instances
|
||||
//! - How to implement failure thresholds and automatic recovery
|
||||
//! - How to provide fallback responses when the circuit is open
|
||||
//!
|
||||
//! ## Circuit Breaker States
|
||||
//!
|
||||
//! **1. Closed (Normal Operation)**
|
||||
//! ```text
|
||||
//! Request → Circuit → Service
|
||||
//! (Tracking failures)
|
||||
//! ```
|
||||
//!
|
||||
//! **2. Open (Failing Fast)**
|
||||
//! ```text
|
||||
//! Request → Circuit → ✗ Immediate Error
|
||||
//! (Not calling service)
|
||||
//! ```
|
||||
//!
|
||||
//! **3. Half-Open (Testing Recovery)**
|
||||
//! ```text
|
||||
//! Request → Circuit → Service
|
||||
//! (Limited test requests)
|
||||
//! ```
|
||||
//!
|
||||
//! ## State Transitions
|
||||
//! ```text
|
||||
//! Success Test Success
|
||||
//! ┌────────┐ ┌──────────┐ ┌──────────┐
|
||||
//! │ CLOSED │─────►│ OPEN │────►│ HALF-OPEN│──┐
|
||||
//! │ │ │ │ │ │ │
|
||||
//! └────────┘ └──────────┘ └──────────┘ │
|
||||
//! ▲ ▲ │ │
|
||||
//! │ │ Failure │ │
|
||||
//! └──────────────┴────────────────────┘ │
|
||||
//! Test Failure │
|
||||
//! │
|
||||
//! ┌─────────────────────────────────────────┘
|
||||
//! Test Success
|
||||
//! ```
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! Time: 0ms
|
||||
//! ┌─────────────┐
|
||||
//! │Request 1 │──► Circuit: CLOSED ──► Service Call ──► Success
|
||||
//! └─────────────┘ Failures: 0/3
|
||||
//!
|
||||
//! Time: 100ms
|
||||
//! ┌─────────────┐
|
||||
//! │Request 2 │──► Circuit: CLOSED ──► Service Call ──► Failure
|
||||
//! └─────────────┘ Failures: 1/3
|
||||
//!
|
||||
//! Time: 200ms
|
||||
//! ┌─────────────┐
|
||||
//! │Request 3 │──► Circuit: CLOSED ──► Service Call ──► Failure
|
||||
//! └─────────────┘ Failures: 2/3
|
||||
//!
|
||||
//! Time: 300ms
|
||||
//! ┌─────────────┐
|
||||
//! │Request 4 │──► Circuit: CLOSED ──► Service Call ──► Failure
|
||||
//! └─────────────┘ Failures: 3/3 ──► Circuit OPENS!
|
||||
//!
|
||||
//! Time: 400ms
|
||||
//! ┌─────────────┐
|
||||
//! │Request 5 │──► Circuit: OPEN ──► ✗ Fail Fast (no call)
|
||||
//! └─────────────┘ Use Fallback
|
||||
//!
|
||||
//! Time: 5000ms (timeout expires)
|
||||
//! ┌─────────────┐
|
||||
//! │Request 6 │──► Circuit: HALF-OPEN ──► Service Call ──► Success
|
||||
//! └─────────────┘ Circuit CLOSES!
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Fail fast**: When circuit is open, don't wait for timeout - fail immediately
|
||||
//! - **Shared state**: Circuit state is shared across all task instances
|
||||
//! - **Automatic recovery**: Circuit automatically tests recovery after timeout
|
||||
//! - **Failure threshold**: Number of failures before opening circuit
|
||||
//! - **Recovery timeout**: How long to wait before testing recovery
|
||||
//! - **Fallback strategies**: Alternative responses when service is unavailable
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - Protecting against cascading failures in microservices
|
||||
//! - API rate limiting and backpressure
|
||||
//! - Database connection pool exhaustion
|
||||
//! - Preventing resource exhaustion from slow/down services
|
||||
//! - Graceful degradation with fallback responses
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example circuit_breaker
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Circuit Breaker Example ===
|
||||
//!
|
||||
//! Example 1: Circuit opening after failures
|
||||
//!
|
||||
//! [ServiceCall-1] Circuit state: Closed (failures: 0/3)
|
||||
//! [ServiceCall-1] Calling external service...
|
||||
//! [ServiceCall-1] ✓ Service responded: Success
|
||||
//! Result 1: Ok("Success")
|
||||
//!
|
||||
//! [ServiceCall-2] Circuit state: Closed (failures: 0/3)
|
||||
//! [ServiceCall-2] Calling external service...
|
||||
//! [ServiceCall-2] ✗ Service failed: Service error
|
||||
//! [ServiceCall-2] Failure recorded (1/3)
|
||||
//! Result 2: Err("Service error")
|
||||
//!
|
||||
//! [ServiceCall-3] Circuit state: Closed (failures: 1/3)
|
||||
//! [ServiceCall-3] Calling external service...
|
||||
//! [ServiceCall-3] ✗ Service failed: Service error
|
||||
//! [ServiceCall-3] Failure recorded (2/3)
|
||||
//! Result 3: Err("Service error")
|
||||
//!
|
||||
//! [ServiceCall-4] Circuit state: Closed (failures: 2/3)
|
||||
//! [ServiceCall-4] Calling external service...
|
||||
//! [ServiceCall-4] ✗ Service failed: Service error
|
||||
//! [ServiceCall-4] Failure threshold reached! Circuit opening...
|
||||
//! [ServiceCall-4] Circuit state: OPEN
|
||||
//! Result 4: Err("Service error")
|
||||
//!
|
||||
//! [ServiceCall-5] Circuit state: Open
|
||||
//! [ServiceCall-5] ✗ Circuit is open - failing fast
|
||||
//! [ServiceCall-5] Using fallback response
|
||||
//! Result 5: Ok("Fallback: cached response")
|
||||
//!
|
||||
//! Example 2: Circuit recovery after timeout
|
||||
//!
|
||||
//! Waiting 3 seconds for circuit recovery timeout...
|
||||
//!
|
||||
//! [ServiceCall-6] Circuit state: Half-Open (testing recovery)
|
||||
//! [ServiceCall-6] Calling external service...
|
||||
//! [ServiceCall-6] ✓ Service responded: Success
|
||||
//! [ServiceCall-6] Recovery test succeeded! Circuit closing...
|
||||
//! [ServiceCall-6] Circuit state: CLOSED
|
||||
//! Result 6: Ok("Success")
|
||||
//!
|
||||
//! [ServiceCall-7] Circuit state: Closed (failures: 0/3)
|
||||
//! [ServiceCall-7] Calling external service...
|
||||
//! [ServiceCall-7] ✓ Service responded: Success
|
||||
//! Result 7: Ok("Success")
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
// Circuit breaker states
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum CircuitState {
|
||||
Closed { failure_count: u32 },
|
||||
Open { opened_at: Instant },
|
||||
HalfOpen,
|
||||
}
|
||||
|
||||
// Shared circuit breaker
|
||||
struct CircuitBreaker {
|
||||
state: Arc<Mutex<CircuitState>>,
|
||||
failure_threshold: u32,
|
||||
recovery_timeout: Duration,
|
||||
}
|
||||
|
||||
impl CircuitBreaker {
|
||||
fn new(failure_threshold: u32, recovery_timeout: Duration) -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(CircuitState::Closed { failure_count: 0 })),
|
||||
failure_threshold,
|
||||
recovery_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_state(&self) -> CircuitState {
|
||||
let mut state = self.state.lock().await;
|
||||
|
||||
// Check if we should transition from Open to Half-Open
|
||||
if let CircuitState::Open { opened_at } = *state {
|
||||
if opened_at.elapsed() >= self.recovery_timeout {
|
||||
*state = CircuitState::HalfOpen;
|
||||
println!("Circuit transitioning: Open → Half-Open");
|
||||
}
|
||||
}
|
||||
|
||||
state.clone()
|
||||
}
|
||||
|
||||
async fn record_success(&self) {
|
||||
let mut state = self.state.lock().await;
|
||||
|
||||
match *state {
|
||||
CircuitState::HalfOpen => {
|
||||
// Recovery test succeeded - close the circuit
|
||||
*state = CircuitState::Closed { failure_count: 0 };
|
||||
println!("[CircuitBreaker] Recovery test succeeded! Circuit closing...");
|
||||
}
|
||||
CircuitState::Closed { .. } => {
|
||||
// Reset failure count on success
|
||||
*state = CircuitState::Closed { failure_count: 0 };
|
||||
}
|
||||
CircuitState::Open { .. } => {
|
||||
// Shouldn't happen, but reset if it does
|
||||
*state = CircuitState::Closed { failure_count: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn record_failure(&self) {
|
||||
let mut state = self.state.lock().await;
|
||||
|
||||
match *state {
|
||||
CircuitState::Closed { failure_count } => {
|
||||
let new_count = failure_count + 1;
|
||||
|
||||
if new_count >= self.failure_threshold {
|
||||
*state = CircuitState::Open {
|
||||
opened_at: Instant::now(),
|
||||
};
|
||||
println!("[CircuitBreaker] Failure threshold reached! Circuit opening...");
|
||||
} else {
|
||||
*state = CircuitState::Closed {
|
||||
failure_count: new_count,
|
||||
};
|
||||
println!(
|
||||
"[CircuitBreaker] Failure recorded ({}/{})",
|
||||
new_count, self.failure_threshold
|
||||
);
|
||||
}
|
||||
}
|
||||
CircuitState::HalfOpen => {
|
||||
// Recovery test failed - reopen circuit
|
||||
*state = CircuitState::Open {
|
||||
opened_at: Instant::now(),
|
||||
};
|
||||
println!("[CircuitBreaker] Recovery test failed! Circuit reopening...");
|
||||
}
|
||||
CircuitState::Open { .. } => {
|
||||
// Already open, no action needed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Service call protected by circuit breaker
|
||||
struct ProtectedServiceCall {
|
||||
task_id: String,
|
||||
circuit: Arc<CircuitBreaker>,
|
||||
should_fail: bool,
|
||||
}
|
||||
|
||||
impl ProtectedServiceCall {
|
||||
fn new(task_id: impl Into<String>, circuit: Arc<CircuitBreaker>, should_fail: bool) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
circuit,
|
||||
should_fail,
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_service(&self) -> Result<String, String> {
|
||||
println!("[{}] Calling external service...", self.task_id);
|
||||
|
||||
// Simulate service call
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
if self.should_fail {
|
||||
Err("Service error".to_string())
|
||||
} else {
|
||||
Ok("Success".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ProtectedServiceCall {
|
||||
async fn run(&mut self) -> Result<String, String> {
|
||||
let state = self.circuit.check_state().await;
|
||||
|
||||
// Print current circuit state
|
||||
match &state {
|
||||
CircuitState::Closed { failure_count } => {
|
||||
println!(
|
||||
"[{}] Circuit state: Closed (failures: {}/{})",
|
||||
self.task_id, failure_count, self.circuit.failure_threshold
|
||||
);
|
||||
}
|
||||
CircuitState::Open { .. } => {
|
||||
println!("[{}] Circuit state: Open", self.task_id);
|
||||
}
|
||||
CircuitState::HalfOpen => {
|
||||
println!(
|
||||
"[{}] Circuit state: Half-Open (testing recovery)",
|
||||
self.task_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if circuit is open
|
||||
if matches!(state, CircuitState::Open { .. }) {
|
||||
println!("[{}] ✗ Circuit is open - failing fast", self.task_id);
|
||||
return Err("Circuit breaker is open".to_string());
|
||||
}
|
||||
|
||||
// Attempt the service call
|
||||
match self.call_service().await {
|
||||
Ok(response) => {
|
||||
println!("[{}] ✓ Service responded: {}", self.task_id, response);
|
||||
self.circuit.record_success().await;
|
||||
|
||||
// Print new state if it changed
|
||||
let new_state = self.circuit.check_state().await;
|
||||
if !matches!(new_state, CircuitState::Closed { failure_count: 0 }) {
|
||||
println!("[{}] Circuit state: {:?}", self.task_id, new_state);
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
Err(error) => {
|
||||
println!("[{}] ✗ Service failed: {}", self.task_id, error);
|
||||
self.circuit.record_failure().await;
|
||||
|
||||
// Print new state if it changed
|
||||
let new_state = self.circuit.check_state().await;
|
||||
if matches!(new_state, CircuitState::Open { .. }) {
|
||||
println!("[{}] Circuit state: OPEN", self.task_id);
|
||||
}
|
||||
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback task that provides cached/default response when circuit is open
|
||||
struct FallbackService {
|
||||
task_id: String,
|
||||
}
|
||||
|
||||
impl FallbackService {
|
||||
fn new(task_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl FallbackService {
|
||||
async fn run(&mut self, result: &Result<String, String>) -> Result<String, String> {
|
||||
match result {
|
||||
Ok(response) => Ok(response.clone()),
|
||||
Err(error) => {
|
||||
if error.contains("Circuit breaker is open") {
|
||||
println!("[{}] Using fallback response", self.task_id);
|
||||
Ok("Fallback: cached response".to_string())
|
||||
} else {
|
||||
Err(error.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Circuit Breaker Example ===\n");
|
||||
|
||||
// Example 1: Circuit opening after threshold failures
|
||||
println!("Example 1: Circuit opening after failures\n");
|
||||
{
|
||||
// Create shared circuit breaker: 3 failures trigger open, 3 second recovery timeout
|
||||
let circuit = Arc::new(CircuitBreaker::new(3, Duration::from_secs(3)));
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Request 1: Success (circuit closed)
|
||||
let call1 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-1",
|
||||
circuit.clone(),
|
||||
false, // success
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 1: {:?}\n", dag.get(call1).unwrap());
|
||||
|
||||
// Request 2: Failure (circuit closed, count = 1)
|
||||
let dag = DagRunner::new();
|
||||
let call2 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-2",
|
||||
circuit.clone(),
|
||||
true, // fail
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 2: {:?}\n", dag.get(call2).unwrap());
|
||||
|
||||
// Request 3: Failure (circuit closed, count = 2)
|
||||
let dag = DagRunner::new();
|
||||
let call3 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-3",
|
||||
circuit.clone(),
|
||||
true, // fail
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 3: {:?}\n", dag.get(call3).unwrap());
|
||||
|
||||
// Request 4: Failure (circuit closed, count = 3, opens!)
|
||||
let dag = DagRunner::new();
|
||||
let call4 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-4",
|
||||
circuit.clone(),
|
||||
true, // fail
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 4: {:?}\n", dag.get(call4).unwrap());
|
||||
|
||||
// Request 5: Circuit is now open, fails fast with fallback
|
||||
let dag = DagRunner::new();
|
||||
let call5 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-5",
|
||||
circuit.clone(),
|
||||
false,
|
||||
));
|
||||
|
||||
let fallback5 = dag
|
||||
.add_task(FallbackService::new("ServiceCall-5"))
|
||||
.depends_on(&call5);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 5: {:?}\n", dag.get(fallback5).unwrap());
|
||||
|
||||
// Example 2: Circuit recovery after timeout
|
||||
println!("Example 2: Circuit recovery after timeout\n");
|
||||
println!("Waiting 3 seconds for circuit recovery timeout...\n");
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
|
||||
// Request 6: Circuit should be half-open, test succeeds, closes circuit
|
||||
let dag = DagRunner::new();
|
||||
let call6 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-6",
|
||||
circuit.clone(),
|
||||
false, // success
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 6: {:?}\n", dag.get(call6).unwrap());
|
||||
|
||||
// Request 7: Circuit is now closed again, normal operation
|
||||
let dag = DagRunner::new();
|
||||
let call7 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-7",
|
||||
circuit.clone(),
|
||||
false, // success
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 7: {:?}\n", dag.get(call7).unwrap());
|
||||
}
|
||||
|
||||
println!("=== Circuit Breaker Example Complete ===");
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//! # Building Complex Workflows: Multi-Layer DAG
|
||||
//!
|
||||
//! This example demonstrates how to build complex, real-world DAGs by combining
|
||||
//! multiple dependency patterns into a cohesive workflow.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to combine fan-out, fan-in, and diamond patterns in one DAG
|
||||
//! - How to build multi-layer workflows with intermediate results
|
||||
//! - How to structure an ETL (Extract-Transform-Load) pipeline
|
||||
//! - How to pass results through multiple transformation stages
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! EXTRACT TRANSFORM COMBINE AGGREGATE
|
||||
//! ┌──────────┐ ┌──────────┐
|
||||
//! │ LoadData │────►│Transform │────┐
|
||||
//! │ A │ │ (×2) │ │
|
||||
//! └──────────┘ └──────────┘ │
|
||||
//! ├──►┌─────────┐───►┌─────────┐───►┌─────────┐
|
||||
//! │ │ Combine │ │ Average │ │ Report │
|
||||
//! └──►└─────────┘ └─────────┘ └─────────┘
|
||||
//! ┌──────────┐ ┌──────────┐ ┌──► │ │
|
||||
//! │ LoadData │────►│Transform │────┘ │ │
|
||||
//! │ B │ │ (×3) │ └──────────────┘
|
||||
//! └──────────┘ └──────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Multi-layer execution**: Tasks organized into logical stages (Extract → Transform → Combine)
|
||||
//! - **Many-to-many dependencies**: The Combine task depends on both Transform tasks
|
||||
//! - **Result reuse**: The combined result is used by both Average and Report tasks
|
||||
//! - **Diamond pattern**: Multiple paths from sources converge and then diverge again
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - ETL pipelines with multiple data sources
|
||||
//! - Multi-stage data processing workflows
|
||||
//! - Complex business logic with branching and merging
|
||||
//! - Data aggregation and reporting systems
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example complex_dag
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! Building ETL pipeline...
|
||||
//!
|
||||
//! Executing pipeline...
|
||||
//!
|
||||
//! Loading data from source A
|
||||
//! Loading data from source B
|
||||
//! Transforming: 6 * 2 = 12
|
||||
//! Transforming: 15 * 3 = 45
|
||||
//! Combining datasets: 12 + 45 = 57
|
||||
//! Computing average: 57 / 6 = 9.50
|
||||
//!
|
||||
//! ETL Pipeline Results:
|
||||
//! Items: 6
|
||||
//! Total: 57
|
||||
//! Average: 9.50
|
||||
//!
|
||||
//! Calculation:
|
||||
//! Source A: sum=6, transformed (x2) = 12
|
||||
//! Source B: sum=15, transformed (x3) = 45
|
||||
//! Combined: 6 items, sum=57, avg=9.50
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagResult, DagRunner, Task};
|
||||
|
||||
// Extract: Load data from different sources
|
||||
struct LoadData {
|
||||
source: &'static str,
|
||||
}
|
||||
|
||||
impl LoadData {
|
||||
fn new(source: &'static str) -> Self {
|
||||
Self { source }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl LoadData {
|
||||
async fn run(&mut self) -> i32 {
|
||||
// Simulate loading and summing data
|
||||
println!("Loading data from source {}", self.source);
|
||||
match self.source {
|
||||
"A" => 6, // sum of [1, 2, 3]
|
||||
"B" => 15, // sum of [4, 5, 6]
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform: Process data with a multiplier
|
||||
struct Transform {
|
||||
multiplier: i32,
|
||||
}
|
||||
|
||||
impl Transform {
|
||||
fn new(multiplier: i32) -> Self {
|
||||
Self { multiplier }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Transform {
|
||||
async fn run(&mut self, sum: &i32) -> i32 {
|
||||
let result = sum * self.multiplier;
|
||||
println!("Transforming: {} * {} = {}", sum, self.multiplier, result);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// Combine: Merge two datasets
|
||||
struct Combine;
|
||||
|
||||
#[task]
|
||||
impl Combine {
|
||||
async fn run(sum_a: &i32, sum_b: &i32) -> i32 {
|
||||
println!(
|
||||
"Combining datasets: {} + {} = {}",
|
||||
sum_a,
|
||||
sum_b,
|
||||
sum_a + sum_b
|
||||
);
|
||||
sum_a + sum_b
|
||||
}
|
||||
}
|
||||
|
||||
// Compute average (using a fixed count for simplicity)
|
||||
struct ComputeAverage {
|
||||
total_count: i32,
|
||||
}
|
||||
|
||||
impl ComputeAverage {
|
||||
fn new(total_count: i32) -> Self {
|
||||
Self { total_count }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ComputeAverage {
|
||||
async fn run(&mut self, sum: &i32) -> f64 {
|
||||
let avg = *sum as f64 / self.total_count as f64;
|
||||
println!(
|
||||
"Computing average: {} / {} = {:.2}",
|
||||
sum, self.total_count, avg
|
||||
);
|
||||
avg
|
||||
}
|
||||
}
|
||||
|
||||
// Format report
|
||||
struct FormatReport {
|
||||
count: i32,
|
||||
}
|
||||
|
||||
impl FormatReport {
|
||||
fn new(count: i32) -> Self {
|
||||
Self { count }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl FormatReport {
|
||||
async fn run(&mut self, sum: &i32, avg: &f64) -> String {
|
||||
format!(
|
||||
"ETL Pipeline Results:\n Items: {}\n Total: {}\n Average: {:.2}",
|
||||
self.count, sum, avg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
println!("Building ETL pipeline...\n");
|
||||
|
||||
// Extract: Load from two sources (parallel)
|
||||
let source_a = dag.add_task(LoadData::new("A"));
|
||||
let source_b = dag.add_task(LoadData::new("B"));
|
||||
|
||||
// Transform: Process each source (parallel)
|
||||
let transform_a = dag.add_task(Transform::new(2)).depends_on(&source_a);
|
||||
let transform_b = dag.add_task(Transform::new(3)).depends_on(&source_b);
|
||||
|
||||
// Combine: Merge the transformed datasets
|
||||
let combined = dag
|
||||
.add_task(Combine)
|
||||
.depends_on((&transform_a, &transform_b));
|
||||
|
||||
// Compute average on combined data (we know we have 6 items total)
|
||||
let avg = dag.add_task(ComputeAverage::new(6)).depends_on(combined);
|
||||
|
||||
// Format final report (needs both sum and average)
|
||||
let report = dag
|
||||
.add_task(FormatReport::new(6))
|
||||
.depends_on((&combined, &avg));
|
||||
|
||||
// Execute the entire ETL pipeline
|
||||
println!("Executing pipeline...\n");
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Display final report
|
||||
println!("\n{}", dag.get(report)?);
|
||||
println!("\nCalculation:");
|
||||
println!(" Source A: sum=6, transformed (x2) = 12");
|
||||
println!(" Source B: sum=15, transformed (x3) = 45");
|
||||
println!(" Combined: 6 items, sum=57, avg=9.50");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
//! # Pattern: Conditional Execution with Data Validation
|
||||
//!
|
||||
//! This example demonstrates how to implement conditional logic in DAGs by using
|
||||
//! simple data transformations rather than traditional Result types.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to implement validation logic in parallel tasks
|
||||
//! - How to use return values for conditional behavior (e.g., 0 for invalid)
|
||||
//! - How to categorize and branch based on computed values
|
||||
//! - How to aggregate results from multiple conditional branches
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! VALIDATE CATEGORIZE SUMMARIZE
|
||||
//! ┌─────────────┐ ┌──────────────┐
|
||||
//! │ Validate(10)│────20──►│Categorize(25)│────"low"────┐
|
||||
//! └─────────────┘ └──────────────┘ │
|
||||
//! │
|
||||
//! ┌─────────────┐ ┌──────────────┐ │
|
||||
//! │ Validate(-5)│────0───►│Categorize(25)│──"invalid"──┼──►┌────────────┐
|
||||
//! └─────────────┘ └──────────────┘ │ │ Summarize │
|
||||
//! │ └────────────┘
|
||||
//! ┌─────────────┐ ┌──────────────┐ │
|
||||
//! │ Validate(20)│────40──►│Categorize(25)│────"high"───┘
|
||||
//! └─────────────┘ └──────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Validation as transformation**: Invalid inputs become sentinel values (0)
|
||||
//! - **Parallel validation**: All inputs validated simultaneously
|
||||
//! - **Data-driven branching**: Categorization based on computed values
|
||||
//! - **Type-safe aggregation**: All branches produce the same type (String)
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - Input validation pipelines
|
||||
//! - Multi-stage data cleaning
|
||||
//! - Classification and categorization workflows
|
||||
//! - Quality control systems
|
||||
//! - Data preprocessing for ML pipelines
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example conditional_workflow
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! Processing and categorizing inputs in parallel...
|
||||
//!
|
||||
//! Valid input 10: doubling to 20
|
||||
//! Invalid input -5: returning 0
|
||||
//! Valid input 20: doubling to 40
|
||||
//!
|
||||
//! Final summary: Categories: [low, invalid, high]
|
||||
//! Expected: Categories: [low, invalid, high]
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagResult, DagRunner, Task};
|
||||
|
||||
// Task that validates and processes input
|
||||
struct ValidateAndDouble {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
impl ValidateAndDouble {
|
||||
fn new(value: i32) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ValidateAndDouble {
|
||||
async fn run(&mut self) -> i32 {
|
||||
if self.value > 0 {
|
||||
println!("Valid input {}: doubling to {}", self.value, self.value * 2);
|
||||
self.value * 2
|
||||
} else {
|
||||
println!("Invalid input {}: returning 0", self.value);
|
||||
0 // Return 0 for invalid inputs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task that categorizes a number
|
||||
struct Categorize {
|
||||
threshold: i32,
|
||||
}
|
||||
|
||||
impl Categorize {
|
||||
fn new(threshold: i32) -> Self {
|
||||
Self { threshold }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Categorize {
|
||||
async fn run(&mut self, value: &i32) -> String {
|
||||
if *value == 0 {
|
||||
"invalid".to_string()
|
||||
} else if *value >= self.threshold {
|
||||
"high".to_string()
|
||||
} else {
|
||||
"low".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task that combines categorization results
|
||||
struct SummarizeResults;
|
||||
|
||||
#[task]
|
||||
impl SummarizeResults {
|
||||
async fn run(cat1: &String, cat2: &String, cat3: &String) -> String {
|
||||
format!("Categories: [{}, {}, {}]", cat1, cat2, cat3)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
println!("Processing and categorizing inputs in parallel...\n");
|
||||
|
||||
// Validate and process multiple inputs in parallel
|
||||
let processed1 = dag.add_task(ValidateAndDouble::new(10)); // Valid: 20
|
||||
let processed2 = dag.add_task(ValidateAndDouble::new(-5)); // Invalid: 0
|
||||
let processed3 = dag.add_task(ValidateAndDouble::new(20)); // Valid: 40
|
||||
|
||||
// Categorize each result (also parallel, threshold = 25)
|
||||
let cat1 = dag.add_task(Categorize::new(25)).depends_on(&processed1); // 20 < 25 = "low"
|
||||
let cat2 = dag.add_task(Categorize::new(25)).depends_on(&processed2); // 0 = "invalid"
|
||||
let cat3 = dag.add_task(Categorize::new(25)).depends_on(&processed3); // 40 >= 25 = "high"
|
||||
|
||||
// Combine results
|
||||
let summary = dag
|
||||
.add_task(SummarizeResults)
|
||||
.depends_on((&cat1, &cat2, &cat3));
|
||||
|
||||
// Execute
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Display results
|
||||
println!("\nFinal summary: {}", dag.get(summary)?);
|
||||
println!("Expected: Categories: [low, invalid, high]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! # Real-World: Data Processing Pipeline
|
||||
//!
|
||||
//! This example demonstrates a realistic data processing pipeline with multiple
|
||||
//! stages of transformation and statistical analysis.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to structure a multi-stage data pipeline
|
||||
//! - How to use stateful tasks with configuration (DataTransformer)
|
||||
//! - How to parallelize statistical computations on the same dataset
|
||||
//! - How to combine results from parallel operations into a final report
|
||||
//!
|
||||
//! ## Pipeline Architecture
|
||||
//! ```text
|
||||
//! STAGE 1: Load STAGE 2: Transform STAGE 3: Analyze STAGE 4: Report
|
||||
//! ┌─────────┐ ┌──────────────┐ ┌────────────┐
|
||||
//! │LoadVal1 │─────►│DataTransform │─────►│ │
|
||||
//! │ (100) │ │ (×1.5) │──┐ │ ComputeSum │──┐
|
||||
//! └─────────┘ └──────────────┘ │ │ │ │
|
||||
//! ├──►│ (525) │ │
|
||||
//! ┌─────────┐ ┌──────────────┐ │ └────────────┘ │
|
||||
//! │LoadVal2 │─────►│DataTransform │──┤ ├──►┌──────────┐
|
||||
//! │ (200) │ │ (×2.0) │ │ ┌────────────┐ │ │ Generate │
|
||||
//! └─────────┘ └──────────────┘ │ │ComputeProd │──┤ │ Report │
|
||||
//! ├──►│ │ │ └──────────┘
|
||||
//! ┌─────────┐ ┌──────────────┐ │ │ (3750000) │ │
|
||||
//! │LoadVal3 │─────►│DataTransform │──┘ └────────────┘ │
|
||||
//! │ (50) │ │ (×0.5) │ │ │
|
||||
//! └─────────┘ └──────────────┘ │ ┌────────────┐ │
|
||||
//! │ │ FindMax │ │
|
||||
//! └──►│ (400) │──┘
|
||||
//! └────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **ETL Pattern**: Extract (Load) → Transform → Load (Report)
|
||||
//! - **Parallel transformation**: Each data source is transformed independently
|
||||
//! - **Parallel analysis**: Sum, Product, and Max computed simultaneously
|
||||
//! - **Stateful tasks**: DataTransformer uses different multipliers for each instance
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - Log processing and analysis
|
||||
//! - Data transformation pipelines
|
||||
//! - Batch job orchestration
|
||||
//! - Statistical analysis on datasets
|
||||
//! - Multi-metric reporting systems
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example data_pipeline
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! Building data pipeline DAG...
|
||||
//!
|
||||
//! Executing pipeline...
|
||||
//!
|
||||
//! Stage 1: Loading raw value 1...
|
||||
//! Stage 1: Loading raw value 2...
|
||||
//! Stage 1: Loading raw value 3...
|
||||
//! Transforming data with multiplier 1.5...
|
||||
//! Transforming data with multiplier 2...
|
||||
//! Transforming data with multiplier 0.5...
|
||||
//!
|
||||
//! Stage 2: Computing sum...
|
||||
//! Stage 2: Computing product...
|
||||
//! Stage 2: Finding maximum...
|
||||
//!
|
||||
//! Stage 3: Generating report...
|
||||
//!
|
||||
//! === Data Pipeline Results ===
|
||||
//! Sum of transformed values: 525.00
|
||||
//! Product of transformed values: 3750000.00
|
||||
//! Maximum value: 400.00
|
||||
//! ============================
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
|
||||
// Data source tasks
|
||||
struct LoadValue1;
|
||||
|
||||
#[task]
|
||||
impl LoadValue1 {
|
||||
async fn run() -> i32 {
|
||||
println!("Stage 1: Loading raw value 1...");
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadValue2;
|
||||
|
||||
#[task]
|
||||
impl LoadValue2 {
|
||||
async fn run() -> i32 {
|
||||
println!("Stage 1: Loading raw value 2...");
|
||||
200
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadValue3;
|
||||
|
||||
#[task]
|
||||
impl LoadValue3 {
|
||||
async fn run() -> i32 {
|
||||
println!("Stage 1: Loading raw value 3...");
|
||||
50
|
||||
}
|
||||
}
|
||||
|
||||
// Custom task with state for data transformation
|
||||
struct DataTransformer {
|
||||
multiplier: f64,
|
||||
}
|
||||
|
||||
impl DataTransformer {
|
||||
fn new(multiplier: f64) -> Self {
|
||||
Self { multiplier }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl DataTransformer {
|
||||
async fn run(&mut self, input: &i32) -> f64 {
|
||||
println!(" Transforming data with multiplier {}...", self.multiplier);
|
||||
*input as f64 * self.multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Statistical analysis tasks
|
||||
struct ComputeSum;
|
||||
|
||||
#[task]
|
||||
impl ComputeSum {
|
||||
async fn run(a: &f64, b: &f64, c: &f64) -> f64 {
|
||||
println!("\nStage 2: Computing sum...");
|
||||
a + b + c
|
||||
}
|
||||
}
|
||||
|
||||
struct ComputeProduct;
|
||||
|
||||
#[task]
|
||||
impl ComputeProduct {
|
||||
async fn run(a: &f64, b: &f64, c: &f64) -> f64 {
|
||||
println!("Stage 2: Computing product...");
|
||||
a * b * c
|
||||
}
|
||||
}
|
||||
|
||||
struct FindMax;
|
||||
|
||||
#[task]
|
||||
impl FindMax {
|
||||
async fn run(a: &f64, b: &f64, c: &f64) -> f64 {
|
||||
println!("Stage 2: Finding maximum...");
|
||||
a.max(*b).max(*c)
|
||||
}
|
||||
}
|
||||
|
||||
// Report generation task
|
||||
struct GenerateReport;
|
||||
|
||||
#[task]
|
||||
impl GenerateReport {
|
||||
async fn run(sum: &f64, product: &f64, max: &f64) -> String {
|
||||
println!("\nStage 3: Generating report...");
|
||||
format!(
|
||||
"\n=== Data Pipeline Results ===\n\
|
||||
Sum of transformed values: {:.2}\n\
|
||||
Product of transformed values: {:.2}\n\
|
||||
Maximum value: {:.2}\n\
|
||||
============================",
|
||||
sum, product, max
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
println!("Building data pipeline DAG...\n");
|
||||
|
||||
// Stage 1: Data sources
|
||||
let raw_value_1 = dag.add_task(LoadValue1);
|
||||
let raw_value_2 = dag.add_task(LoadValue2);
|
||||
let raw_value_3 = dag.add_task(LoadValue3);
|
||||
|
||||
// Stage 2: Data transformation (with stateful tasks)
|
||||
let transformed_1 = dag
|
||||
.add_task(DataTransformer::new(1.5))
|
||||
.depends_on(&raw_value_1);
|
||||
|
||||
let transformed_2 = dag
|
||||
.add_task(DataTransformer::new(2.0))
|
||||
.depends_on(&raw_value_2);
|
||||
|
||||
let transformed_3 = dag
|
||||
.add_task(DataTransformer::new(0.5))
|
||||
.depends_on(&raw_value_3);
|
||||
|
||||
// Stage 3: Statistical analysis (parallel operations on transformed data)
|
||||
let sum = dag
|
||||
.add_task(ComputeSum)
|
||||
.depends_on((&transformed_1, &transformed_2, &transformed_3));
|
||||
|
||||
let product =
|
||||
dag.add_task(ComputeProduct)
|
||||
.depends_on((&transformed_1, &transformed_2, &transformed_3));
|
||||
|
||||
let max = dag
|
||||
.add_task(FindMax)
|
||||
.depends_on((&transformed_1, &transformed_2, &transformed_3));
|
||||
|
||||
// Stage 4: Final report
|
||||
let report = dag
|
||||
.add_task(GenerateReport)
|
||||
.depends_on((&sum, &product, &max));
|
||||
|
||||
// Execute the pipeline
|
||||
println!("Executing pipeline...\n");
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Print final report
|
||||
println!("{}", dag.get(report).unwrap());
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
//! # Debug Tracing and Task Naming
|
||||
//!
|
||||
//! This example demonstrates how to add debug metadata and tracing information to tasks
|
||||
//! for better observability in complex DAG workflows.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to add task names and IDs for debugging
|
||||
//! - How to implement correlation IDs for distributed tracing
|
||||
//! - How to track task execution timing and lifecycle events
|
||||
//! - How to build traceable task wrappers
|
||||
//!
|
||||
//! ## Debugging Patterns
|
||||
//!
|
||||
//! **1. Named Tasks** - Add human-readable names to tasks
|
||||
//! ```rust
|
||||
//! struct TaskMetadata {
|
||||
//! id: String,
|
||||
//! name: String,
|
||||
//! start_time: Option<Instant>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **2. Correlation IDs** - Track requests across task boundaries
|
||||
//! ```rust
|
||||
//! struct TraceContext {
|
||||
//! correlation_id: String,
|
||||
//! parent_task: Option<String>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **3. Execution Metrics** - Measure task performance
|
||||
//! ```rust
|
||||
//! struct TaskTrace {
|
||||
//! name: String,
|
||||
//! duration_ms: u64,
|
||||
//! status: &'static str,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! TRACE: order-12345
|
||||
//! ┌──────────────────┐
|
||||
//! │ ValidateOrder │ [order-12345] Start: 0ms
|
||||
//! │ ID: task-001 │──────► Status: Success
|
||||
//! └──────────────────┘ Duration: 10ms
|
||||
//! │
|
||||
//! ├──────────────────┬──────────────────┐
|
||||
//! ▼ ▼ ▼
|
||||
//! ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
//! │ CheckInventory │ │ ValidatePayment │ │ CalculateShipping│
|
||||
//! │ ID: task-002 │ │ ID: task-003 │ │ ID: task-004 │
|
||||
//! └──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
//! [order-12345] [order-12345] [order-12345]
|
||||
//! Duration: 45ms Duration: 120ms Duration: 15ms
|
||||
//! │ │ │
|
||||
//! └──────────────────┴──────────────────┘
|
||||
//! ▼
|
||||
//! ┌──────────────────┐
|
||||
//! │ FulfillOrder │ [order-12345]
|
||||
//! │ ID: task-005 │ Duration: 30ms
|
||||
//! └──────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Observability**: Make task execution visible for debugging
|
||||
//! - **Correlation IDs**: Track a request through multiple tasks
|
||||
//! - **Structured logging**: Consistent task metadata across logs
|
||||
//! - **Performance metrics**: Measure task execution time
|
||||
//! - **Task lifecycle**: Track start, completion, and errors
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - Debugging complex workflows in production
|
||||
//! - Performance analysis and bottleneck identification
|
||||
//! - Distributed tracing in microservices
|
||||
//! - SLA monitoring and alerting
|
||||
//! - Audit logging for compliance
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example debug_tracing
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Debug Tracing Example ===
|
||||
//!
|
||||
//! Starting order processing for order-12345
|
||||
//!
|
||||
//! [order-12345] [task-001] ValidateOrder: Starting
|
||||
//! [order-12345] [task-001] ValidateOrder: Completed in 10ms - Success
|
||||
//!
|
||||
//! [order-12345] [task-002] CheckInventory: Starting
|
||||
//! [order-12345] [task-003] ValidatePayment: Starting
|
||||
//! [order-12345] [task-004] CalculateShipping: Starting
|
||||
//! [order-12345] [task-002] CheckInventory: Completed in 45ms - Success
|
||||
//! [order-12345] [task-004] CalculateShipping: Completed in 15ms - Success
|
||||
//! [order-12345] [task-003] ValidatePayment: Completed in 120ms - Success
|
||||
//!
|
||||
//! [order-12345] [task-005] FulfillOrder: Starting
|
||||
//! [order-12345] [task-005] FulfillOrder: Completed in 30ms - Success
|
||||
//!
|
||||
//! === Execution Trace Summary ===
|
||||
//! Correlation ID: order-12345
|
||||
//! Total Tasks: 5
|
||||
//! Total Duration: 220ms
|
||||
//! Success: 5, Failed: 0
|
||||
//!
|
||||
//! Task Breakdown:
|
||||
//! ValidateOrder: 10ms
|
||||
//! CheckInventory: 45ms
|
||||
//! ValidatePayment: 120ms (slowest)
|
||||
//! CalculateShipping: 15ms
|
||||
//! FulfillOrder: 30ms
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
use std::time::Instant;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
// Stateful task that tracks execution metadata for debugging
|
||||
struct TracedTask {
|
||||
task_id: String,
|
||||
task_name: String,
|
||||
correlation_id: String,
|
||||
work_duration_ms: u64,
|
||||
start_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl TracedTask {
|
||||
fn new(
|
||||
task_id: impl Into<String>,
|
||||
task_name: impl Into<String>,
|
||||
correlation_id: impl Into<String>,
|
||||
work_duration_ms: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
task_name: task_name.into(),
|
||||
correlation_id: correlation_id.into(),
|
||||
work_duration_ms,
|
||||
start_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl TracedTask {
|
||||
async fn run(&mut self) -> Result<TaskTrace, String> {
|
||||
// Record start
|
||||
let start = Instant::now();
|
||||
self.start_time = Some(start);
|
||||
|
||||
println!(
|
||||
"[{}] [{}] {}: Starting",
|
||||
self.correlation_id, self.task_id, self.task_name
|
||||
);
|
||||
|
||||
// Simulate work
|
||||
sleep(Duration::from_millis(self.work_duration_ms)).await;
|
||||
|
||||
// Record completion
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
println!(
|
||||
"[{}] [{}] {}: Completed in {}ms - Success",
|
||||
self.correlation_id, self.task_id, self.task_name, duration_ms
|
||||
);
|
||||
|
||||
Ok(TaskTrace {
|
||||
task_id: self.task_id.clone(),
|
||||
task_name: self.task_name.clone(),
|
||||
correlation_id: self.correlation_id.clone(),
|
||||
duration_ms,
|
||||
status: "Success",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Task that can fail, demonstrating error tracing
|
||||
struct TracedTaskWithError {
|
||||
task_id: String,
|
||||
task_name: String,
|
||||
correlation_id: String,
|
||||
should_fail: bool,
|
||||
}
|
||||
|
||||
impl TracedTaskWithError {
|
||||
fn new(
|
||||
task_id: impl Into<String>,
|
||||
task_name: impl Into<String>,
|
||||
correlation_id: impl Into<String>,
|
||||
should_fail: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
task_name: task_name.into(),
|
||||
correlation_id: correlation_id.into(),
|
||||
should_fail,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl TracedTaskWithError {
|
||||
async fn run(&mut self) -> Result<TaskTrace, TaskTrace> {
|
||||
let start = Instant::now();
|
||||
|
||||
println!(
|
||||
"[{}] [{}] {}: Starting",
|
||||
self.correlation_id, self.task_id, self.task_name
|
||||
);
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
if self.should_fail {
|
||||
println!(
|
||||
"[{}] [{}] {}: Failed in {}ms - Validation Error",
|
||||
self.correlation_id, self.task_id, self.task_name, duration_ms
|
||||
);
|
||||
|
||||
Err(TaskTrace {
|
||||
task_id: self.task_id.clone(),
|
||||
task_name: self.task_name.clone(),
|
||||
correlation_id: self.correlation_id.clone(),
|
||||
duration_ms,
|
||||
status: "Failed",
|
||||
})
|
||||
} else {
|
||||
println!(
|
||||
"[{}] [{}] {}: Completed in {}ms - Success",
|
||||
self.correlation_id, self.task_id, self.task_name, duration_ms
|
||||
);
|
||||
|
||||
Ok(TaskTrace {
|
||||
task_id: self.task_id.clone(),
|
||||
task_name: self.task_name.clone(),
|
||||
correlation_id: self.correlation_id.clone(),
|
||||
duration_ms,
|
||||
status: "Success",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task trace data structure
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
struct TaskTrace {
|
||||
task_id: String,
|
||||
task_name: String,
|
||||
correlation_id: String,
|
||||
duration_ms: u64,
|
||||
status: &'static str,
|
||||
}
|
||||
|
||||
// Task that aggregates traces from multiple upstream tasks
|
||||
struct TraceAggregator {
|
||||
correlation_id: String,
|
||||
}
|
||||
|
||||
impl TraceAggregator {
|
||||
fn new(correlation_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
correlation_id: correlation_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl TraceAggregator {
|
||||
async fn run(
|
||||
&mut self,
|
||||
t1: &Result<TaskTrace, String>,
|
||||
t2: &Result<TaskTrace, String>,
|
||||
t3: &Result<TaskTrace, String>,
|
||||
) -> TraceSummary {
|
||||
let traces = vec![t1, t2, t3];
|
||||
|
||||
let mut summary = TraceSummary {
|
||||
correlation_id: self.correlation_id.clone(),
|
||||
task_count: traces.len(),
|
||||
total_duration_ms: 0,
|
||||
success_count: 0,
|
||||
failed_count: 0,
|
||||
task_details: Vec::new(),
|
||||
};
|
||||
|
||||
for trace_result in traces {
|
||||
match trace_result {
|
||||
Ok(trace) => {
|
||||
summary.success_count += 1;
|
||||
summary.total_duration_ms += trace.duration_ms;
|
||||
summary.task_details.push(trace.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
summary.failed_count += 1;
|
||||
eprintln!("[{}] Task failed: {}", self.correlation_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary
|
||||
}
|
||||
}
|
||||
|
||||
// Summary of traced execution
|
||||
#[derive(Clone, Debug)]
|
||||
struct TraceSummary {
|
||||
correlation_id: String,
|
||||
task_count: usize,
|
||||
total_duration_ms: u64,
|
||||
success_count: usize,
|
||||
failed_count: usize,
|
||||
task_details: Vec<TaskTrace>,
|
||||
}
|
||||
|
||||
impl TraceSummary {
|
||||
fn print(&self) {
|
||||
println!("\n=== Execution Trace Summary ===");
|
||||
println!("Correlation ID: {}", self.correlation_id);
|
||||
println!("Total Tasks: {}", self.task_count);
|
||||
println!("Total Duration: {}ms", self.total_duration_ms);
|
||||
println!(
|
||||
"Success: {}, Failed: {}",
|
||||
self.success_count, self.failed_count
|
||||
);
|
||||
|
||||
if !self.task_details.is_empty() {
|
||||
println!("\nTask Breakdown:");
|
||||
let max_duration = self
|
||||
.task_details
|
||||
.iter()
|
||||
.map(|t| t.duration_ms)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
for trace in &self.task_details {
|
||||
let slowest_tag = if trace.duration_ms == max_duration {
|
||||
" (slowest)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(
|
||||
" {}: {}ms{}",
|
||||
trace.task_name, trace.duration_ms, slowest_tag
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Debug Tracing Example ===\n");
|
||||
|
||||
// Example 1: Successful order processing with full tracing
|
||||
println!("Example 1: Successful order processing\n");
|
||||
{
|
||||
let correlation_id = "order-12345";
|
||||
println!("Starting order processing for {}\n", correlation_id);
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Root task: Validate order
|
||||
let _validate = dag.add_task(TracedTask::new(
|
||||
"task-001",
|
||||
"ValidateOrder",
|
||||
correlation_id,
|
||||
10,
|
||||
));
|
||||
|
||||
// Parallel validation tasks (independent, no dependencies)
|
||||
let inventory = dag.add_task(TracedTask::new(
|
||||
"task-002",
|
||||
"CheckInventory",
|
||||
correlation_id,
|
||||
45,
|
||||
));
|
||||
|
||||
let payment = dag.add_task(TracedTask::new(
|
||||
"task-003",
|
||||
"ValidatePayment",
|
||||
correlation_id,
|
||||
120,
|
||||
));
|
||||
|
||||
let shipping = dag.add_task(TracedTask::new(
|
||||
"task-004",
|
||||
"CalculateShipping",
|
||||
correlation_id,
|
||||
15,
|
||||
));
|
||||
|
||||
// Aggregate traces
|
||||
let summary = dag
|
||||
.add_task(TraceAggregator::new(correlation_id))
|
||||
.depends_on((&inventory, &payment, &shipping));
|
||||
|
||||
// Run the DAG
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Print summary
|
||||
dag.get(summary).unwrap().print();
|
||||
}
|
||||
|
||||
println!("\n");
|
||||
|
||||
// Example 2: Order with failed payment
|
||||
println!("Example 2: Order with failed payment\n");
|
||||
{
|
||||
let correlation_id = "order-67890";
|
||||
println!("Starting order processing for {}\n", correlation_id);
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let _validate = dag.add_task(TracedTaskWithError::new(
|
||||
"task-101",
|
||||
"ValidateOrder",
|
||||
correlation_id,
|
||||
false,
|
||||
));
|
||||
|
||||
let _inventory = dag.add_task(TracedTaskWithError::new(
|
||||
"task-102",
|
||||
"CheckInventory",
|
||||
correlation_id,
|
||||
false,
|
||||
));
|
||||
|
||||
// This task will fail
|
||||
let payment = dag.add_task(TracedTaskWithError::new(
|
||||
"task-103",
|
||||
"ValidatePayment",
|
||||
correlation_id,
|
||||
true, // will fail
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check results
|
||||
match dag.get(payment).unwrap() {
|
||||
Ok(_) => println!("\nPayment validation succeeded"),
|
||||
Err(trace) => {
|
||||
println!("\n[{}] Payment validation failed:", correlation_id);
|
||||
println!(" Task ID: {}", trace.task_id);
|
||||
println!(" Duration: {}ms", trace.duration_ms);
|
||||
println!(" Status: {}", trace.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n=== Debug Tracing Example Complete ===");
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
//! # Handling Errors in DAGs
|
||||
//!
|
||||
//! This example demonstrates comprehensive error handling patterns using Result types
|
||||
//! to represent success and failure states in DAG workflows.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to use Result<T, E> types in task outputs
|
||||
//! - How to propagate errors through dependency chains
|
||||
//! - How to handle and transform errors from upstream tasks
|
||||
//! - How to implement error recovery with fallback values
|
||||
//!
|
||||
//! ## A Note on Panics vs Result
|
||||
//!
|
||||
//! **This example uses Result<T, E> for expected errors** (like parsing failures). For
|
||||
//! unexpected failures (bugs), tasks may panic. dagx automatically catches panics and
|
||||
//! converts them to `DagError::TaskPanicked`, regardless of whether the task executes
|
||||
//! inline (single-task layer) or spawned (multi-task layer). This ensures consistent
|
||||
//! error handling across all execution paths.
|
||||
//!
|
||||
//! **Inline execution**: When a layer has only one task, dagx executes it inline for
|
||||
//! performance. Panics are caught via `FutureExt::catch_unwind()` and become errors,
|
||||
//! matching the behavior of spawned tasks where the async runtime catches panics.
|
||||
//!
|
||||
//! **Bottom line**: Use Result for expected errors (validation, parsing, I/O). If a task
|
||||
//! panics (bug), it becomes an error automatically. Both behave consistently.
|
||||
//!
|
||||
//! ## Error Handling Patterns
|
||||
//!
|
||||
//! **1. Error Propagation** - Errors flow through dependencies
|
||||
//! ```text
|
||||
//! ┌─────────┐ ┌──────────┐ ┌─────────┐
|
||||
//! │ Parse │─Err─►│ Validate │─Err─►│ Process │
|
||||
//! │ "abc" │ │ │ │ │
|
||||
//! └─────────┘ └──────────┘ └─────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! **2. Error Recovery** - Fallback values on error
|
||||
//! ```text
|
||||
//! ┌─────────┐ ┌──────────────┐
|
||||
//! │ Parse │─Err─►│ WithFallback │─────► 0 (default)
|
||||
//! │ "abc" │ │ (default=0) │
|
||||
//! └─────────┘ └──────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! **3. Error Logging** - Observe errors without blocking
|
||||
//! ```text
|
||||
//! ┌─────────┐ ┌──────────┐ ┌──────────┐
|
||||
//! │ Task │──?──►│ LogError │──?──►│ Next │
|
||||
//! └─────────┘ └──────────┘ └──────────┘
|
||||
//! (logs but passes through)
|
||||
//! ```
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! INPUT PARSE VALIDATE PROCESS LOG
|
||||
//! ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
|
||||
//! │ "42" │──►│ParseInt │───►│ Validate │──►│ Process │──►│LogError │
|
||||
//! └─────────┘ │ Ok(42) │ │ Ok(42) │ │ Ok(...) │ │ ✓ │
|
||||
//! └─────────┘ └──────────┘ └─────────┘ └──────────┘
|
||||
//!
|
||||
//! ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
|
||||
//! │ "abc" │──►│ParseInt │───►│ Validate │──►│ Process │──►│LogError │
|
||||
//! └─────────┘ │ Err(...)│ │ Err(...) │ │ Err(...)│ │ ✗ │
|
||||
//! └─────────┘ └──────────┘ └─────────┘ └──────────┘
|
||||
//!
|
||||
//! ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
|
||||
//! │ "150" │──►│ParseInt │───►│ Validate │──►│ Process │──►│LogError │
|
||||
//! └─────────┘ │ Ok(150) │ │Err(range)│ │ Err(...)│ │ ✗ │
|
||||
//! └─────────┘ └──────────┘ └─────────┘ └──────────┘
|
||||
//!
|
||||
//! ┌─────────┐ ┌─────────┐ ┌──────────────┐
|
||||
//! │"invalid"│──►│ParseInt │───►│ WithFallback │───► 0
|
||||
//! └─────────┘ │ Err(...)│ │ (default=0) │
|
||||
//! └─────────┘ └──────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Errors as values**: Use Result<T, E> to represent errors in the type system
|
||||
//! - **Error propagation**: Errors flow through dependencies using `?` operator
|
||||
//! - **Type safety**: The compiler ensures errors are handled
|
||||
//! - **Graceful degradation**: Recover from errors with fallback logic
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - Input validation pipelines
|
||||
//! - Resilient data processing workflows
|
||||
//! - API integration with error handling
|
||||
//! - Multi-stage validation with early exit
|
||||
//! - Data cleaning and sanitization
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example error_handling
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Error Handling Example ===
|
||||
//!
|
||||
//! Example 1: Valid input
|
||||
//! ✓ Success: Processed value: 84
|
||||
//! Final result: Processed value: 84
|
||||
//!
|
||||
//! Example 2: Invalid input (parse error)
|
||||
//! ✗ Error: Failed to parse 'not a number': invalid digit found in string
|
||||
//! Final error: Failed to parse 'not a number': invalid digit found in string
|
||||
//!
|
||||
//! Example 3: Out of range
|
||||
//! ✗ Error: Value 150 is out of range [0, 100]
|
||||
//! Final error: Value 150 is out of range [0, 100]
|
||||
//!
|
||||
//! Example 4: Error recovery with fallback
|
||||
//! Recovered with fallback: 0
|
||||
//!
|
||||
//! === Error Handling Example Complete ===
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
|
||||
// Source task that provides a string input
|
||||
struct StringSource(&'static str);
|
||||
|
||||
#[task]
|
||||
impl StringSource {
|
||||
async fn run(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Task that may fail: parsing a string to integer
|
||||
struct ParseInt;
|
||||
|
||||
#[task]
|
||||
impl ParseInt {
|
||||
async fn run(input: &String) -> Result<i32, String> {
|
||||
input
|
||||
.parse::<i32>()
|
||||
.map_err(|e| format!("Failed to parse '{}': {}", input, e))
|
||||
}
|
||||
}
|
||||
|
||||
// Task that validates input range
|
||||
struct ValidateRange {
|
||||
min: i32,
|
||||
max: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ValidateRange {
|
||||
async fn run(&mut self, input: &Result<i32, String>) -> Result<i32, String> {
|
||||
let value = input.as_ref().map_err(|e| e.clone())?;
|
||||
|
||||
if *value < self.min || *value > self.max {
|
||||
return Err(format!(
|
||||
"Value {} is out of range [{}, {}]",
|
||||
value, self.min, self.max
|
||||
));
|
||||
}
|
||||
|
||||
Ok(*value)
|
||||
}
|
||||
}
|
||||
|
||||
// Task that processes valid data
|
||||
struct ProcessData;
|
||||
|
||||
#[task]
|
||||
impl ProcessData {
|
||||
async fn run(input: &Result<i32, String>) -> Result<String, String> {
|
||||
let value = input.as_ref().map_err(|e| e.clone())?;
|
||||
Ok(format!("Processed value: {}", value * 2))
|
||||
}
|
||||
}
|
||||
|
||||
// Task that provides a fallback value on error
|
||||
struct WithFallback {
|
||||
fallback: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl WithFallback {
|
||||
async fn run(&mut self, input: &Result<i32, String>) -> i32 {
|
||||
*input.as_ref().unwrap_or(&self.fallback)
|
||||
}
|
||||
}
|
||||
|
||||
// Task that logs errors
|
||||
struct LogError;
|
||||
|
||||
#[task]
|
||||
impl LogError {
|
||||
async fn run(input: &Result<String, String>) -> Result<String, String> {
|
||||
match input {
|
||||
Ok(s) => {
|
||||
println!("✓ Success: {}", s);
|
||||
Ok(s.clone())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("✗ Error: {}", e);
|
||||
Err(e.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Error Handling Example ===\n");
|
||||
|
||||
// Example 1: Successful path
|
||||
println!("Example 1: Valid input");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(StringSource("42"));
|
||||
|
||||
let parsed = dag.add_task(ParseInt).depends_on(&input);
|
||||
|
||||
let validated = dag
|
||||
.add_task(ValidateRange { min: 0, max: 100 })
|
||||
.depends_on(parsed);
|
||||
|
||||
let processed = dag.add_task(ProcessData).depends_on(validated);
|
||||
let logged = dag.add_task(LogError).depends_on(processed);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match dag.get(logged).unwrap() {
|
||||
Ok(result) => println!("Final result: {}\n", result),
|
||||
Err(e) => eprintln!("Final error: {}\n", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2: Parse error
|
||||
println!("Example 2: Invalid input (parse error)");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(StringSource("not a number"));
|
||||
|
||||
let parsed = dag.add_task(ParseInt).depends_on(&input);
|
||||
|
||||
let validated = dag
|
||||
.add_task(ValidateRange { min: 0, max: 100 })
|
||||
.depends_on(parsed);
|
||||
|
||||
let processed = dag.add_task(ProcessData).depends_on(validated);
|
||||
let logged = dag.add_task(LogError).depends_on(processed);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match dag.get(logged).unwrap() {
|
||||
Ok(result) => println!("Final result: {}\n", result),
|
||||
Err(e) => eprintln!("Final error: {}\n", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3: Validation error
|
||||
println!("Example 3: Out of range");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(StringSource("150"));
|
||||
|
||||
let parsed = dag.add_task(ParseInt).depends_on(&input);
|
||||
|
||||
let validated = dag
|
||||
.add_task(ValidateRange { min: 0, max: 100 })
|
||||
.depends_on(parsed);
|
||||
|
||||
let processed = dag.add_task(ProcessData).depends_on(validated);
|
||||
let logged = dag.add_task(LogError).depends_on(processed);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match dag.get(logged).unwrap() {
|
||||
Ok(result) => println!("Final result: {}\n", result),
|
||||
Err(e) => eprintln!("Final error: {}\n", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Example 4: Error recovery with fallback
|
||||
println!("Example 4: Error recovery with fallback");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(StringSource("invalid"));
|
||||
|
||||
let parsed = dag.add_task(ParseInt).depends_on(&input);
|
||||
|
||||
let with_fallback = dag
|
||||
.add_task(WithFallback { fallback: 0 })
|
||||
.depends_on(parsed);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = dag.get(with_fallback).unwrap();
|
||||
println!("Recovered with fallback: {}\n", result);
|
||||
}
|
||||
|
||||
println!("=== Error Handling Example Complete ===");
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
//! # Working with Large DAGs: Performance and Scalability
|
||||
//!
|
||||
//! This example demonstrates dagx's performance characteristics when working
|
||||
//! with large-scale DAGs containing hundreds or thousands of tasks.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How dagx scales with increasing task counts
|
||||
//! - Performance characteristics of different DAG topologies
|
||||
//! - How to benchmark DAG construction and execution
|
||||
//! - Memory efficiency considerations
|
||||
//!
|
||||
//! ## DAG Topologies Tested
|
||||
//!
|
||||
//! **1. Wide DAG** - Maximum parallelism
|
||||
//! ```text
|
||||
//! ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
||||
//! │Task 1│ │Task 2│ │Task 3│ ... │Task N│
|
||||
//! └──────┘ └──────┘ └──────┘ └──────┘
|
||||
//! (1000 independent parallel tasks)
|
||||
//! ```
|
||||
//!
|
||||
//! **2. Deep DAG** - Sequential chain
|
||||
//! ```text
|
||||
//! ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
||||
//! │Task 1│───►│Task 2│───►│Task 3│───► ... ──►│Task N│
|
||||
//! └──────┘ └──────┘ └──────┘ └──────┘
|
||||
//! (100-level dependency chain)
|
||||
//! ```
|
||||
//!
|
||||
//! **3. Binary Tree** - Balanced aggregation
|
||||
//! ```text
|
||||
//! ┌────┐ ┌────┐ ┌────┐ ┌────┐
|
||||
//! │ T1 │ │ T2 │ │ T3 │ │ T4 │
|
||||
//! └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘
|
||||
//! └───┬───┘ └───┬───┘
|
||||
//! │ │
|
||||
//! ┌──▼──┐ ┌──▼──┐
|
||||
//! │ C1 │ │ C2 │
|
||||
//! └──┬──┘ └──┬──┘
|
||||
//! └───────┬──────┘
|
||||
//! │
|
||||
//! ┌──▼──┐
|
||||
//! │Root │
|
||||
//! └─────┘
|
||||
//! ```
|
||||
//!
|
||||
//! **4. Diamond Pattern** - Many-to-many
|
||||
//! ```text
|
||||
//! Layer 1: 50 sources
|
||||
//! Layer 2: 50 tasks (each depends on 2 sources)
|
||||
//! Layer 3: 4 aggregates
|
||||
//! Layer 4: 1 final result
|
||||
//! ```
|
||||
//!
|
||||
//! ## Performance Metrics
|
||||
//! - **Build time**: Time to construct the DAG structure
|
||||
//! - **Execution time**: Time to run all tasks
|
||||
//! - **Tasks/second**: Throughput metric
|
||||
//! - **Memory overhead**: Approximate per-task memory usage
|
||||
//!
|
||||
//! ## Key Observations
|
||||
//! - DAG construction is very fast (< 1ms for 1000s of tasks)
|
||||
//! - Parallel execution scales well with available CPU cores
|
||||
//! - Deep chains execute sequentially as expected
|
||||
//! - Memory overhead is minimal (~200 bytes per task)
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example large_dag --release # Use --release for accurate benchmarks
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Large DAG Performance Example ===
|
||||
//!
|
||||
//! Example 1: Wide DAG (1000 parallel tasks)
|
||||
//! Build time: 347µs
|
||||
//! Execution time: 12.5ms
|
||||
//! Tasks/second: 80000
|
||||
//! Sample sum (first 10): ...
|
||||
//!
|
||||
//! Example 2: Deep DAG (100-level chain)
|
||||
//! Build time: 156µs
|
||||
//! Execution time: 8.2ms
|
||||
//! Final result: ...
|
||||
//!
|
||||
//! Example 3: Binary Tree DAG (depth 10, 1023 tasks)
|
||||
//! Build time: 423µs
|
||||
//! Tree depth: 9
|
||||
//! Execution time: 15.3ms
|
||||
//! Root result: ...
|
||||
//!
|
||||
//! Example 4: Diamond Pattern (50x50 = 2500 edges)
|
||||
//! Build time: 234µs
|
||||
//! Execution time: 11.7ms
|
||||
//! Total tasks: 105
|
||||
//! Final result: ...
|
||||
//!
|
||||
//! Example 5: Memory Efficiency (100 tasks, measure overhead)
|
||||
//! i32 sum: ...
|
||||
//! String total length: ...
|
||||
//!
|
||||
//! === Large DAG Performance Example Complete ===
|
||||
//!
|
||||
//! Key Observations:
|
||||
//! - DAG construction is very fast (< 1ms for 1000s of tasks)
|
||||
//! - Parallel execution scales well with available CPU cores
|
||||
//! - Deep chains execute sequentially as expected
|
||||
//! - Memory overhead is minimal (~ 200 bytes per task)
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
use std::time::Instant;
|
||||
|
||||
// Simple computation task
|
||||
struct Compute {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Compute {
|
||||
async fn run(&mut self) -> i32 {
|
||||
// Simulate some work
|
||||
let mut result = self.value;
|
||||
for _ in 0..100 {
|
||||
result = result.wrapping_mul(31).wrapping_add(17);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// Task that combines two values
|
||||
struct Combine;
|
||||
|
||||
#[task]
|
||||
impl Combine {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a.wrapping_add(*b)
|
||||
}
|
||||
}
|
||||
|
||||
// Task that aggregates multiple values
|
||||
struct Aggregate;
|
||||
|
||||
#[task]
|
||||
impl Aggregate {
|
||||
async fn run(a: &i32, b: &i32, c: &i32, d: &i32) -> i32 {
|
||||
a.wrapping_add(*b).wrapping_add(*c).wrapping_add(*d)
|
||||
}
|
||||
}
|
||||
|
||||
// Task that formats a number as a string
|
||||
struct FormatNumber {
|
||||
id: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl FormatNumber {
|
||||
async fn run(&self) -> String {
|
||||
format!("Result {}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Large DAG Performance Example ===\n");
|
||||
|
||||
// Example 1: Wide DAG (many parallel tasks)
|
||||
println!("Example 1: Wide DAG (1000 parallel tasks)");
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 1000 independent tasks
|
||||
let tasks: Vec<_> = (0..1000)
|
||||
.map(|i| dag.add_task(Compute { value: i }))
|
||||
.collect();
|
||||
|
||||
let build_time = start.elapsed();
|
||||
println!(" Build time: {:?}", build_time);
|
||||
|
||||
let exec_start = Instant::now();
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = exec_start.elapsed();
|
||||
|
||||
println!(" Execution time: {:?}", exec_time);
|
||||
println!(" Tasks/second: {:.0}", 1000.0 / exec_time.as_secs_f64());
|
||||
|
||||
// Verify a few results
|
||||
let sum: i32 = tasks
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|t| dag.get(t).unwrap())
|
||||
.fold(0i32, |acc, x| acc.wrapping_add(x));
|
||||
println!(" Sample sum (first 10): {}\n", sum);
|
||||
}
|
||||
|
||||
// Example 2: Deep DAG (long dependency chain)
|
||||
println!("Example 2: Deep DAG (100-level chain)");
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a deep chain by combining adjacent task pairs
|
||||
let tasks: Vec<_> = (0..100)
|
||||
.map(|i| dag.add_task(Compute { value: i }))
|
||||
.collect();
|
||||
let mut chain_tasks = vec![];
|
||||
for i in 0..99 {
|
||||
let combined = dag.add_task(Combine).depends_on((&tasks[i], &tasks[i + 1]));
|
||||
chain_tasks.push(combined);
|
||||
}
|
||||
|
||||
let prev = chain_tasks.last().unwrap();
|
||||
|
||||
let build_time = start.elapsed();
|
||||
println!(" Build time: {:?}", build_time);
|
||||
|
||||
let exec_start = Instant::now();
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = exec_start.elapsed();
|
||||
|
||||
println!(" Execution time: {:?}", exec_time);
|
||||
println!(" Final result: {}\n", dag.get(prev).unwrap());
|
||||
}
|
||||
|
||||
// Example 3: Binary tree DAG
|
||||
println!("Example 3: Binary Tree DAG (depth 10, 1023 tasks)");
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Build binary tree: each level combines pairs from previous level
|
||||
let current_level: Vec<_> = (0..512)
|
||||
.map(|i| dag.add_task(Compute { value: i }))
|
||||
.collect();
|
||||
|
||||
// Process all levels reducing by combining pairs
|
||||
let mut level = 0;
|
||||
let mut layer: Vec<dagx::TaskHandle<i32>> =
|
||||
current_level.iter().map(|b| b.into()).collect();
|
||||
|
||||
while layer.len() > 1 {
|
||||
level += 1;
|
||||
let mut next_layer = Vec::new();
|
||||
|
||||
for i in (0..layer.len()).step_by(2) {
|
||||
if i + 1 < layer.len() {
|
||||
// We have a pair - combine them
|
||||
let combined = dag.add_task(Combine).depends_on((&layer[i], &layer[i + 1]));
|
||||
next_layer.push(combined);
|
||||
} else {
|
||||
// Odd one out - just pass it through
|
||||
next_layer.push(layer[i]);
|
||||
}
|
||||
}
|
||||
|
||||
layer = next_layer;
|
||||
}
|
||||
|
||||
let root = &layer[0];
|
||||
|
||||
let build_time = start.elapsed();
|
||||
println!(" Build time: {:?}", build_time);
|
||||
println!(" Tree depth: {}", level);
|
||||
|
||||
let exec_start = Instant::now();
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = exec_start.elapsed();
|
||||
|
||||
println!(" Execution time: {:?}", exec_time);
|
||||
println!(" Root result: {}\n", dag.get(root).unwrap());
|
||||
}
|
||||
|
||||
// Example 4: Diamond pattern (many-to-many)
|
||||
println!("Example 4: Diamond Pattern (50x50 = 2500 edges)");
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Layer 1: 50 source tasks
|
||||
let sources: Vec<_> = (0..50)
|
||||
.map(|i| dag.add_task(Compute { value: i }))
|
||||
.collect();
|
||||
|
||||
// Layer 2: 50 tasks, each depends on all sources
|
||||
let middle: Vec<_> = (0..50)
|
||||
.map(|i| {
|
||||
let combine = dag.add_task(Combine);
|
||||
// Each middle task depends on two sources
|
||||
combine.depends_on((
|
||||
&sources[i % sources.len()],
|
||||
&sources[(i + 1) % sources.len()],
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 3: Aggregate to 4 groups
|
||||
let aggregates: Vec<_> = (0..4)
|
||||
.map(|i| {
|
||||
let start_idx = i * 12;
|
||||
dag.add_task(Aggregate).depends_on((
|
||||
&middle[start_idx % middle.len()],
|
||||
&middle[(start_idx + 1) % middle.len()],
|
||||
&middle[(start_idx + 2) % middle.len()],
|
||||
&middle[(start_idx + 3) % middle.len()],
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 4: Final aggregate
|
||||
let final_result = dag.add_task(Aggregate).depends_on((
|
||||
&aggregates[0],
|
||||
&aggregates[1],
|
||||
&aggregates[2],
|
||||
&aggregates[3],
|
||||
));
|
||||
|
||||
let build_time = start.elapsed();
|
||||
println!(" Build time: {:?}", build_time);
|
||||
|
||||
let exec_start = Instant::now();
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = exec_start.elapsed();
|
||||
|
||||
println!(" Execution time: {:?}", exec_time);
|
||||
println!(" Total tasks: {}", 50 + 50 + 4 + 1);
|
||||
println!(" Final result: {}\n", dag.get(final_result).unwrap());
|
||||
}
|
||||
|
||||
// Example 5: Memory efficiency test
|
||||
println!("Example 5: Memory Efficiency (100 tasks, measure overhead)");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create tasks with various output sizes
|
||||
let small_tasks: Vec<_> = (0..50)
|
||||
.map(|i| dag.add_task(Compute { value: i }))
|
||||
.collect();
|
||||
|
||||
let string_tasks: Vec<_> = (0..50)
|
||||
.map(|i| dag.add_task(FormatNumber { id: i }))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify results are accessible
|
||||
let i32_sum: i32 = small_tasks
|
||||
.iter()
|
||||
.map(|t| dag.get(t).unwrap())
|
||||
.fold(0i32, |acc, x| acc.wrapping_add(x));
|
||||
let string_count = string_tasks
|
||||
.iter()
|
||||
.map(|t| dag.get(t).unwrap().len())
|
||||
.sum::<usize>();
|
||||
|
||||
println!(" i32 sum: {}", i32_sum);
|
||||
println!(" String total length: {}", string_count);
|
||||
}
|
||||
|
||||
println!("\n=== Large DAG Performance Example Complete ===");
|
||||
println!("\nKey Observations:");
|
||||
println!(" - DAG construction is very fast (< 1ms for 1000s of tasks)");
|
||||
println!(" - Parallel execution scales well with available CPU cores");
|
||||
println!(" - Deep chains execute sequentially as expected");
|
||||
println!(" - Memory overhead is minimal (~ 200 bytes per task)");
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! # Proof of True Parallelism
|
||||
//!
|
||||
//! This example demonstrates that dagx provides genuine parallel execution
|
||||
//! by running 10,000 tasks that each sleep for 1 second, completing in ~1 second
|
||||
//! total instead of 10,000 seconds.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to verify true parallel execution
|
||||
//! - The difference between concurrency and parallelism
|
||||
//! - How dagx scales to thousands of parallel tasks
|
||||
//! - Performance characteristics of parallel execution
|
||||
//!
|
||||
//! ## The Proof
|
||||
//!
|
||||
//! If tasks ran sequentially:
|
||||
//! - 10,000 tasks × 1 second each = 10,000 seconds (2.78 hours)
|
||||
//!
|
||||
//! With true parallelism:
|
||||
//! - All 10,000 tasks run simultaneously = ~1 second total
|
||||
//!
|
||||
//! This dramatic difference (10,000x speedup) proves that tasks execute in
|
||||
//! parallel across multiple threads, not sequentially.
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//!
|
||||
//! **Concurrency** - Multiple tasks making progress, but not necessarily at the same time
|
||||
//! **Parallelism** - Multiple tasks executing simultaneously on different CPU cores
|
||||
//!
|
||||
//! This example proves dagx provides true parallelism.
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example parallelism_proof --release
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Parallelism Proof ===
|
||||
//!
|
||||
//! Launching 10,000 tasks that each sleep for 1 second...
|
||||
//!
|
||||
//! Result: 10,000 tasks × 1s sleep completed in ~1.2s
|
||||
//!
|
||||
//! Proof: If tasks ran sequentially, this would take 10,000 seconds (2.78 hours).
|
||||
//! Actual time: 1.2s
|
||||
//! Speedup: 8333x
|
||||
//! This proves TRUE PARALLEL EXECUTION!
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
|
||||
// Task that sleeps for a duration and returns its ID
|
||||
struct SleepTask {
|
||||
id: u32,
|
||||
duration: Duration,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl SleepTask {
|
||||
async fn run(&self) -> u32 {
|
||||
sleep(self.duration).await;
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Parallelism Proof ===\n");
|
||||
println!("Launching 10,000 tasks that each sleep for 1 second...\n");
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let sleep_duration = Duration::from_secs(1);
|
||||
let num_tasks = 10_000u32;
|
||||
|
||||
// Create 10,000 independent tasks that each sleep for 1 second
|
||||
let tasks: Vec<_> = (0..num_tasks)
|
||||
.map(|i| {
|
||||
dag.add_task(SleepTask {
|
||||
id: i,
|
||||
duration: sleep_duration,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Measure execution time
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Verify all tasks completed correctly
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i as u32);
|
||||
}
|
||||
|
||||
// Calculate what sequential execution would take
|
||||
let sequential_time = sleep_duration * num_tasks;
|
||||
let speedup = sequential_time.as_secs_f64() / elapsed.as_secs_f64();
|
||||
|
||||
println!(
|
||||
"Result: {} tasks × {}s sleep completed in {:.1}s\n",
|
||||
num_tasks,
|
||||
sleep_duration.as_secs(),
|
||||
elapsed.as_secs_f64()
|
||||
);
|
||||
|
||||
println!(
|
||||
"Proof: If tasks ran sequentially, this would take {} seconds ({:.2} hours).",
|
||||
sequential_time.as_secs(),
|
||||
sequential_time.as_secs_f64() / 3600.0
|
||||
);
|
||||
println!(" Actual time: {:.1}s", elapsed.as_secs_f64());
|
||||
println!(" Speedup: {:.0}x", speedup);
|
||||
|
||||
// The proof: execution time should be close to 1 second, not 10,000 seconds
|
||||
if elapsed < Duration::from_secs(3) {
|
||||
println!(" This proves TRUE PARALLEL EXECUTION!");
|
||||
} else {
|
||||
panic!(
|
||||
"FAILED: Tasks appear to be running sequentially! Took {:.1}s instead of ~1s",
|
||||
elapsed.as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n=== Parallelism Proof Complete ===");
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
//! # Retry Strategies for Resilient Workflows
|
||||
//!
|
||||
//! This example demonstrates various retry patterns for handling transient failures
|
||||
//! in DAG workflows, such as network errors, rate limits, or temporary service unavailability.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to implement immediate, linear, and exponential backoff retry strategies
|
||||
//! - How to distinguish retriable vs non-retriable errors
|
||||
//! - How to track retry attempts and provide diagnostics
|
||||
//! - How to implement jitter to prevent thundering herd problems
|
||||
//!
|
||||
//! ## Retry Patterns
|
||||
//!
|
||||
//! **1. Immediate Retry** - Retry without delay (for quick transient failures)
|
||||
//! ```text
|
||||
//! Attempt 1: ✗ Error
|
||||
//! Attempt 2: ✗ Error (no delay)
|
||||
//! Attempt 3: ✓ Success
|
||||
//! ```
|
||||
//!
|
||||
//! **2. Linear Backoff** - Fixed delay between retries
|
||||
//! ```text
|
||||
//! Attempt 1: ✗ Error
|
||||
//! Wait 100ms
|
||||
//! Attempt 2: ✗ Error
|
||||
//! Wait 100ms
|
||||
//! Attempt 3: ✓ Success
|
||||
//! ```
|
||||
//!
|
||||
//! **3. Exponential Backoff** - Increasing delays between retries
|
||||
//! ```text
|
||||
//! Attempt 1: ✗ Error
|
||||
//! Wait 100ms
|
||||
//! Attempt 2: ✗ Error
|
||||
//! Wait 200ms
|
||||
//! Attempt 3: ✗ Error
|
||||
//! Wait 400ms
|
||||
//! Attempt 4: ✓ Success
|
||||
//! ```
|
||||
//!
|
||||
//! **4. Exponential Backoff with Jitter** - Randomized delays to prevent coordinated retries
|
||||
//! ```text
|
||||
//! Attempt 1: ✗ Error
|
||||
//! Wait 87ms (50-150ms range)
|
||||
//! Attempt 2: ✗ Error
|
||||
//! Wait 234ms (100-300ms range)
|
||||
//! Attempt 3: ✓ Success
|
||||
//! ```
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! ┌──────────────────────┐
|
||||
//! │ ApiCallWithRetry │
|
||||
//! │ Max attempts: 3 │
|
||||
//! │ Strategy: Exp │
|
||||
//! └──────────────────────┘
|
||||
//! │
|
||||
//! ├─► Attempt 1: ✗ (Connection timeout)
|
||||
//! │ Wait 100ms
|
||||
//! │
|
||||
//! ├─► Attempt 2: ✗ (Connection timeout)
|
||||
//! │ Wait 200ms
|
||||
//! │
|
||||
//! └─► Attempt 3: ✓ Success
|
||||
//!
|
||||
//! ┌──────────────────────┐
|
||||
//! │ ApiCallWithRetry │
|
||||
//! │ Max attempts: 3 │
|
||||
//! │ Strategy: Linear │
|
||||
//! └──────────────────────┘
|
||||
//! │
|
||||
//! ├─► Attempt 1: ✗ (Rate limit)
|
||||
//! │ Wait 500ms
|
||||
//! │
|
||||
//! ├─► Attempt 2: ✗ (Rate limit)
|
||||
//! │ Wait 500ms
|
||||
//! │
|
||||
//! ├─► Attempt 3: ✗ (Rate limit)
|
||||
//! │
|
||||
//! └─► Final: ✗ Retry exhausted
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Idempotency**: Retried operations should be safe to repeat
|
||||
//! - **Retriable errors**: Network timeouts, rate limits, temporary service errors
|
||||
//! - **Non-retriable errors**: Authentication failures, invalid input, logical errors
|
||||
//! - **Backoff strategies**: Balance between recovery speed and system load
|
||||
//! - **Jitter**: Randomization to avoid thundering herd when many clients retry simultaneously
|
||||
//! - **Retry budgets**: Limit total retries to prevent cascading failures
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - External API calls with transient network failures
|
||||
//! - Database operations with temporary connection issues
|
||||
//! - Distributed systems with eventual consistency
|
||||
//! - Rate-limited API integrations
|
||||
//! - Cloud service calls with intermittent availability
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example retry_strategies
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Retry Strategies Example ===
|
||||
//!
|
||||
//! Example 1: Immediate retry strategy
|
||||
//! [ApiCall-1] Attempt 1/3: Calling external API...
|
||||
//! [ApiCall-1] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-1] Retrying immediately (attempt 2/3)
|
||||
//! [ApiCall-1] Attempt 2/3: Calling external API...
|
||||
//! [ApiCall-1] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-1] Retrying immediately (attempt 3/3)
|
||||
//! [ApiCall-1] Attempt 3/3: Calling external API...
|
||||
//! [ApiCall-1] ✓ Success: API response data
|
||||
//! Result: Ok(RetryResult { attempts: 3, success: true })
|
||||
//!
|
||||
//! Example 2: Linear backoff (500ms delay)
|
||||
//! [ApiCall-2] Attempt 1/4: Calling external API...
|
||||
//! [ApiCall-2] ✗ Transient error: Rate limit exceeded
|
||||
//! [ApiCall-2] Waiting 500ms before retry (attempt 2/4)
|
||||
//! [ApiCall-2] Attempt 2/4: Calling external API...
|
||||
//! [ApiCall-2] ✓ Success: API response data
|
||||
//! Result: Ok(RetryResult { attempts: 2, success: true })
|
||||
//!
|
||||
//! Example 3: Exponential backoff
|
||||
//! [ApiCall-3] Attempt 1/5: Calling external API...
|
||||
//! [ApiCall-3] ✗ Transient error: Service unavailable
|
||||
//! [ApiCall-3] Waiting 100ms before retry (attempt 2/5)
|
||||
//! [ApiCall-3] Attempt 2/5: Calling external API...
|
||||
//! [ApiCall-3] ✗ Transient error: Service unavailable
|
||||
//! [ApiCall-3] Waiting 200ms before retry (attempt 3/5)
|
||||
//! [ApiCall-3] Attempt 3/5: Calling external API...
|
||||
//! [ApiCall-3] ✓ Success: API response data
|
||||
//! Result: Ok(RetryResult { attempts: 3, success: true })
|
||||
//!
|
||||
//! Example 4: Non-retriable error (fails immediately)
|
||||
//! [ApiCall-4] Attempt 1/3: Calling external API...
|
||||
//! [ApiCall-4] ✗ Non-retriable error: Authentication failed
|
||||
//! Result: Err("Authentication failed - not retrying")
|
||||
//!
|
||||
//! Example 5: Retry exhausted
|
||||
//! [ApiCall-5] Attempt 1/3: Calling external API...
|
||||
//! [ApiCall-5] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-5] Waiting 100ms before retry (attempt 2/3)
|
||||
//! [ApiCall-5] Attempt 2/3: Calling external API...
|
||||
//! [ApiCall-5] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-5] Waiting 200ms before retry (attempt 3/3)
|
||||
//! [ApiCall-5] Attempt 3/3: Calling external API...
|
||||
//! [ApiCall-5] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-5] All retry attempts exhausted
|
||||
//! Result: Err("Retry exhausted after 3 attempts: Connection timeout")
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
// Retry strategy configuration
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum RetryStrategy {
|
||||
Immediate,
|
||||
Linear { delay_ms: u64 },
|
||||
Exponential { base_ms: u64, factor: f64 },
|
||||
}
|
||||
|
||||
impl RetryStrategy {
|
||||
fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
|
||||
match self {
|
||||
RetryStrategy::Immediate => None,
|
||||
RetryStrategy::Linear { delay_ms } => Some(Duration::from_millis(*delay_ms)),
|
||||
RetryStrategy::Exponential { base_ms, factor } => {
|
||||
let delay = *base_ms as f64 * factor.powi(attempt as i32 - 1);
|
||||
Some(Duration::from_millis(delay as u64))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simulated error types
|
||||
#[derive(Clone, Debug)]
|
||||
enum ApiError {
|
||||
// Retriable errors
|
||||
ConnectionTimeout,
|
||||
ServiceUnavailable,
|
||||
RateLimitExceeded,
|
||||
|
||||
// Non-retriable errors
|
||||
AuthenticationFailed,
|
||||
#[allow(dead_code)]
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
fn is_retriable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ApiError::ConnectionTimeout
|
||||
| ApiError::ServiceUnavailable
|
||||
| ApiError::RateLimitExceeded
|
||||
)
|
||||
}
|
||||
|
||||
fn message(&self) -> &'static str {
|
||||
match self {
|
||||
ApiError::ConnectionTimeout => "Connection timeout",
|
||||
ApiError::ServiceUnavailable => "Service unavailable",
|
||||
ApiError::RateLimitExceeded => "Rate limit exceeded",
|
||||
ApiError::AuthenticationFailed => "Authentication failed",
|
||||
ApiError::InvalidInput => "Invalid input",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Result of retry operation
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
struct RetryResult {
|
||||
attempts: u32,
|
||||
success: bool,
|
||||
final_error: Option<String>,
|
||||
}
|
||||
|
||||
// Task that simulates an API call with configurable failure pattern
|
||||
struct ApiCallWithRetry {
|
||||
task_id: String,
|
||||
max_attempts: u32,
|
||||
strategy: RetryStrategy,
|
||||
// Simulated failure pattern: errors to return before success
|
||||
failure_sequence: Vec<ApiError>,
|
||||
current_attempt: u32,
|
||||
}
|
||||
|
||||
impl ApiCallWithRetry {
|
||||
fn new(
|
||||
task_id: impl Into<String>,
|
||||
max_attempts: u32,
|
||||
strategy: RetryStrategy,
|
||||
failure_sequence: Vec<ApiError>,
|
||||
) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
max_attempts,
|
||||
strategy,
|
||||
failure_sequence,
|
||||
current_attempt: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn attempt_api_call(&mut self) -> Result<String, ApiError> {
|
||||
println!(
|
||||
"[{}] Attempt {}/{}: Calling external API...",
|
||||
self.task_id,
|
||||
self.current_attempt + 1,
|
||||
self.max_attempts
|
||||
);
|
||||
|
||||
// Simulate network delay
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Check if we should fail this attempt
|
||||
if (self.current_attempt as usize) < self.failure_sequence.len() {
|
||||
let error = self.failure_sequence[self.current_attempt as usize].clone();
|
||||
Err(error)
|
||||
} else {
|
||||
Ok("API response data".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ApiCallWithRetry {
|
||||
async fn run(&mut self) -> Result<RetryResult, String> {
|
||||
loop {
|
||||
self.current_attempt += 1;
|
||||
|
||||
match self.attempt_api_call().await {
|
||||
Ok(data) => {
|
||||
println!("[{}] ✓ Success: {}", self.task_id, data);
|
||||
return Ok(RetryResult {
|
||||
attempts: self.current_attempt,
|
||||
success: true,
|
||||
final_error: None,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
println!(
|
||||
"[{}] ✗ {} error: {}",
|
||||
self.task_id,
|
||||
if error.is_retriable() {
|
||||
"Transient"
|
||||
} else {
|
||||
"Non-retriable"
|
||||
},
|
||||
error.message()
|
||||
);
|
||||
|
||||
// Check if error is retriable
|
||||
if !error.is_retriable() {
|
||||
return Err(format!("{} - not retrying", error.message()));
|
||||
}
|
||||
|
||||
// Check if we have attempts remaining
|
||||
if self.current_attempt >= self.max_attempts {
|
||||
println!("[{}] All retry attempts exhausted", self.task_id);
|
||||
return Err(format!(
|
||||
"Retry exhausted after {} attempts: {}",
|
||||
self.current_attempt,
|
||||
error.message()
|
||||
));
|
||||
}
|
||||
|
||||
// Calculate and apply backoff delay
|
||||
if let Some(delay) = self.strategy.calculate_delay(self.current_attempt) {
|
||||
println!(
|
||||
"[{}] Waiting {}ms before retry (attempt {}/{})",
|
||||
self.task_id,
|
||||
delay.as_millis(),
|
||||
self.current_attempt + 1,
|
||||
self.max_attempts
|
||||
);
|
||||
sleep(delay).await;
|
||||
} else {
|
||||
println!(
|
||||
"[{}] Retrying immediately (attempt {}/{})",
|
||||
self.task_id,
|
||||
self.current_attempt + 1,
|
||||
self.max_attempts
|
||||
);
|
||||
}
|
||||
|
||||
// Continue loop to retry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Retry Strategies Example ===\n");
|
||||
|
||||
// Example 1: Immediate retry - succeeds on 3rd attempt
|
||||
println!("Example 1: Immediate retry strategy");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-1",
|
||||
3,
|
||||
RetryStrategy::Immediate,
|
||||
vec![
|
||||
ApiError::ConnectionTimeout,
|
||||
ApiError::ConnectionTimeout,
|
||||
// 3rd attempt will succeed
|
||||
],
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
// Example 2: Linear backoff - succeeds on 2nd attempt
|
||||
println!("Example 2: Linear backoff (500ms delay)");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-2",
|
||||
4,
|
||||
RetryStrategy::Linear { delay_ms: 500 },
|
||||
vec![
|
||||
ApiError::RateLimitExceeded,
|
||||
// 2nd attempt will succeed
|
||||
],
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
// Example 3: Exponential backoff - succeeds on 3rd attempt
|
||||
println!("Example 3: Exponential backoff");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-3",
|
||||
5,
|
||||
RetryStrategy::Exponential {
|
||||
base_ms: 100,
|
||||
factor: 2.0,
|
||||
},
|
||||
vec![
|
||||
ApiError::ServiceUnavailable,
|
||||
ApiError::ServiceUnavailable,
|
||||
// 3rd attempt will succeed
|
||||
],
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
// Example 4: Non-retriable error - fails immediately
|
||||
println!("Example 4: Non-retriable error (fails immediately)");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-4",
|
||||
3,
|
||||
RetryStrategy::Immediate,
|
||||
vec![ApiError::AuthenticationFailed],
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
// Example 5: Retry exhausted - all attempts fail
|
||||
println!("Example 5: Retry exhausted");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-5",
|
||||
3,
|
||||
RetryStrategy::Exponential {
|
||||
base_ms: 100,
|
||||
factor: 2.0,
|
||||
},
|
||||
vec![
|
||||
ApiError::ConnectionTimeout,
|
||||
ApiError::ConnectionTimeout,
|
||||
ApiError::ConnectionTimeout,
|
||||
// All attempts will fail
|
||||
],
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
println!("=== Retry Strategies Example Complete ===");
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
//! # Managing Timeouts in Tasks
|
||||
//!
|
||||
//! This example demonstrates how to implement timeout patterns in DAG tasks using
|
||||
//! tokio's timeout utilities for time-bounded execution.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to implement timeouts using `tokio::time::timeout`
|
||||
//! - How to build tasks with built-in timeout logic
|
||||
//! - How to handle timeout errors as Result types
|
||||
//! - How to apply timeouts at both task-level and DAG-level
|
||||
//!
|
||||
//! ## Timeout Patterns
|
||||
//!
|
||||
//! **1. Task-Level Timeout** - Individual task with timeout
|
||||
//! ```rust
|
||||
//! async fn run(&mut self) -> Result<String, String> {
|
||||
//! let work = async { /* ... */ };
|
||||
//! match timeout(Duration::from_millis(self.timeout_ms), work).await {
|
||||
//! Ok(result) => Ok(result),
|
||||
//! Err(_) => Err("Task timed out".to_string()),
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **2. DAG-Level Timeout** - Entire workflow with timeout
|
||||
//! ```rust
|
||||
//! match timeout(dag_timeout, dag.run(spawner)).await {
|
||||
//! Ok(result) => { /* DAG completed */ },
|
||||
//! Err(_) => { /* DAG timed out */ },
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! ┌─────────────┐
|
||||
//! │ FastTask │ (completes in 100ms)
|
||||
//! │ (timeout │ ───► Ok("Fast task completed")
|
||||
//! │ 500ms) │
|
||||
//! └─────────────┘
|
||||
//!
|
||||
//! ┌─────────────┐
|
||||
//! │ SlowTask │ (takes 1000ms)
|
||||
//! │ (timeout │ ───► Err("Task timed out after 200ms")
|
||||
//! │ 200ms) │
|
||||
//! └─────────────┘
|
||||
//! │
|
||||
//! └──────►┌──────────────┐
|
||||
//! │ProcessResult │ ───► "✗ Task timed out..."
|
||||
//! └──────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Timeout as error**: Timeouts produce `Err` results that propagate through the DAG
|
||||
//! - **Error as data**: Use Result<T, E> to represent timeout states
|
||||
//! - **Graceful degradation**: Downstream tasks can handle timeout errors
|
||||
//! - **Time-bounded execution**: Enforce SLAs and prevent runaway tasks
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - API calls with timeout limits
|
||||
//! - SLA enforcement in workflows
|
||||
//! - Preventing resource exhaustion from slow tasks
|
||||
//! - Circuit breaker patterns
|
||||
//! - External service integration with timeouts
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example timeout
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Timeout Example ===
|
||||
//!
|
||||
//! Example 1: All tasks complete successfully
|
||||
//! Fast task: Ok("Fast task completed")
|
||||
//! Timed task: Ok("Work completed in 100ms")
|
||||
//!
|
||||
//! Example 2: Task exceeds timeout
|
||||
//! Result: ✗ Task timed out after 200ms (work would take 1000ms)
|
||||
//!
|
||||
//! Example 3: Parallel tasks with different speeds
|
||||
//! Fast task 1: Ok("Fast task completed")
|
||||
//! Fast task 2: Ok("Fast task completed")
|
||||
//! Slow task: Ok("Slow task completed after 500ms")
|
||||
//! Timed task 1: Ok("Work completed in 100ms")
|
||||
//! Timed task 2: Err("Task timed out after 200ms...")
|
||||
//!
|
||||
//! Example 4: Downstream tasks handling timeout errors
|
||||
//! Processed 1: ✗ Task timed out after 100ms (work would take 500ms)
|
||||
//! Processed 2: ✗ Task timed out after 100ms (work would take 500ms)
|
||||
//!
|
||||
//! Example 5: Whole-DAG timeout
|
||||
//! DAG completed within timeout
|
||||
//! Task 1: Ok("Slow task completed after 200ms")
|
||||
//! Task 2: Ok("Slow task completed after 200ms")
|
||||
//! Task 3: Ok("Slow task completed after 200ms")
|
||||
//!
|
||||
//! === Timeout Example Complete ===
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, Task};
|
||||
use tokio::time::{sleep, timeout, Duration};
|
||||
|
||||
// Fast task that completes quickly
|
||||
struct FastTask;
|
||||
|
||||
#[task]
|
||||
impl FastTask {
|
||||
async fn run() -> Result<String, String> {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
Ok("Fast task completed".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Slow task that takes a long time
|
||||
struct SlowTask {
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl SlowTask {
|
||||
async fn run(&mut self) -> Result<String, String> {
|
||||
sleep(Duration::from_millis(self.duration_ms)).await;
|
||||
Ok(format!("Slow task completed after {}ms", self.duration_ms))
|
||||
}
|
||||
}
|
||||
|
||||
// Task with built-in timeout
|
||||
struct TaskWithTimeout {
|
||||
work_duration_ms: u64,
|
||||
timeout_ms: u64,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl TaskWithTimeout {
|
||||
async fn run(&mut self) -> Result<String, String> {
|
||||
let work = async {
|
||||
sleep(Duration::from_millis(self.work_duration_ms)).await;
|
||||
format!("Work completed in {}ms", self.work_duration_ms)
|
||||
};
|
||||
|
||||
match timeout(Duration::from_millis(self.timeout_ms), work).await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err(format!(
|
||||
"Task timed out after {}ms (work would take {}ms)",
|
||||
self.timeout_ms, self.work_duration_ms
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task that processes results (including timeouts)
|
||||
struct ProcessResult;
|
||||
|
||||
#[task]
|
||||
impl ProcessResult {
|
||||
async fn run(input: &Result<String, String>) -> String {
|
||||
match input {
|
||||
Ok(msg) => format!("✓ {}", msg),
|
||||
Err(err) => format!("✗ {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Timeout Example ===\n");
|
||||
|
||||
// Example 1: All tasks complete within timeout
|
||||
println!("Example 1: All tasks complete successfully");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let fast = dag.add_task(FastTask);
|
||||
let timed = dag.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 100,
|
||||
timeout_ms: 500,
|
||||
});
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Fast task: {:?}", dag.get(fast).unwrap());
|
||||
println!("Timed task: {:?}\n", dag.get(timed).unwrap());
|
||||
}
|
||||
|
||||
// Example 2: Task exceeds timeout
|
||||
println!("Example 2: Task exceeds timeout");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let timed = dag.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 1000,
|
||||
timeout_ms: 200,
|
||||
});
|
||||
|
||||
let processed = dag.add_task(ProcessResult).depends_on(&timed);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {}\n", dag.get(processed).unwrap());
|
||||
}
|
||||
|
||||
// Example 3: Mixed fast and slow tasks
|
||||
println!("Example 3: Parallel tasks with different speeds");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let fast1 = dag.add_task(FastTask);
|
||||
let fast2 = dag.add_task(FastTask);
|
||||
|
||||
let slow = dag.add_task(SlowTask { duration_ms: 500 });
|
||||
|
||||
let timed1 = dag.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 100,
|
||||
timeout_ms: 200,
|
||||
});
|
||||
|
||||
let timed2 = dag.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 300,
|
||||
timeout_ms: 200,
|
||||
});
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Fast task 1: {:?}", dag.get(fast1).unwrap());
|
||||
println!("Fast task 2: {:?}", dag.get(fast2).unwrap());
|
||||
println!("Slow task: {:?}", dag.get(slow).unwrap());
|
||||
println!("Timed task 1: {:?}", dag.get(timed1).unwrap());
|
||||
println!("Timed task 2: {:?}\n", dag.get(timed2).unwrap());
|
||||
}
|
||||
|
||||
// Example 4: Cascading timeout errors
|
||||
println!("Example 4: Downstream tasks handling timeout errors");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 500,
|
||||
timeout_ms: 100,
|
||||
});
|
||||
|
||||
let processed1 = dag.add_task(ProcessResult).depends_on(&source);
|
||||
let processed2 = dag.add_task(ProcessResult).depends_on(&source);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Processed 1: {}", dag.get(processed1).unwrap());
|
||||
println!("Processed 2: {}\n", dag.get(processed2).unwrap());
|
||||
}
|
||||
|
||||
// Example 5: Implementing whole-DAG timeout
|
||||
println!("Example 5: Whole-DAG timeout");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(SlowTask { duration_ms: 200 });
|
||||
let t2 = dag.add_task(SlowTask { duration_ms: 200 });
|
||||
let t3 = dag.add_task(SlowTask { duration_ms: 200 });
|
||||
|
||||
// Run the entire DAG with a timeout
|
||||
let dag_timeout = Duration::from_millis(500);
|
||||
match timeout(
|
||||
dag_timeout,
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
}),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
result.unwrap();
|
||||
println!("DAG completed within timeout");
|
||||
println!("Task 1: {:?}", dag.get(t1).unwrap());
|
||||
println!("Task 2: {:?}", dag.get(t2).unwrap());
|
||||
println!("Task 3: {:?}", dag.get(t3).unwrap());
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!(
|
||||
"DAG execution timed out after {}ms",
|
||||
dag_timeout.as_millis()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n=== Timeout Example Complete ===");
|
||||
}
|
||||
Reference in new issue
Block a user