feat: dagrunner, initial version

This commit is contained in:
swaits committed 2025-10-07 16:31:03 -07:00
commit 7412287ee4
122 files changed
+23101

No files matched your search

+124
View File
@@ -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);
}
+155
View File
@@ -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());
}
+144
View File
@@ -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());
}
+126
View File
@@ -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(())
}
+507
View File
@@ -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 ===");
}
+217
View File
@@ -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(())
}
+150
View File
@@ -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(())
}
+231
View File
@@ -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());
}
+464
View File
@@ -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 ===");
}
+317
View File
@@ -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 ===");
}
+391
View File
@@ -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)");
}
+131
View File
@@ -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 ===");
}
+470
View File
@@ -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 ===");
}
+303
View File
@@ -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 ===");
}