feat: dagrunner, initial version
This commit is contained in:
122 files changed
+23101
No files matched your search
@@ -0,0 +1,175 @@
|
||||
//! Tests for different error types and error handling
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagError, DagRunner};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_result_not_found_error() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn(|_: ()| async { 42 }));
|
||||
|
||||
// Try to get result before running
|
||||
let result = dag.get(&t1);
|
||||
assert!(matches!(result, Err(DagError::ResultNotFound { .. })));
|
||||
|
||||
// Run and verify it works after
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(&t1).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_nested_result_error_handling() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Task that returns a Result
|
||||
let task1 = dag.add_task(task_fn(|_: ()| async {
|
||||
Result::<i32, String>::Err("Internal error".to_string())
|
||||
}));
|
||||
|
||||
let task2 = dag.add_task(task_fn(|_: ()| async { Result::<i32, String>::Ok(42) }));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Both tasks complete successfully (they return Results)
|
||||
assert_eq!(dag.get(&task1).unwrap(), Err("Internal error".to_string()));
|
||||
assert_eq!(dag.get(&task2).unwrap(), Ok(42));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_option_none_not_error() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task = dag.add_task(task_fn(|_: ()| async { None::<i32> }));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// None is a valid result, not an error
|
||||
assert_eq!(dag.get(&task).unwrap(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_error_types() {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum CustomError {
|
||||
ValidationError(String),
|
||||
ProcessingError { code: i32, details: String },
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task1 = dag.add_task(task_fn(|_: ()| async {
|
||||
Result::<i32, CustomError>::Err(CustomError::ValidationError("Invalid input".to_string()))
|
||||
}));
|
||||
|
||||
let task2 = dag.add_task(task_fn(|_: ()| async {
|
||||
Result::<i32, CustomError>::Err(CustomError::ProcessingError {
|
||||
code: 500,
|
||||
details: "Processing failed".to_string(),
|
||||
})
|
||||
}));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check custom errors are preserved
|
||||
match dag.get(&task1).unwrap() {
|
||||
Err(CustomError::ValidationError(msg)) => assert_eq!(msg, "Invalid input"),
|
||||
_ => panic!("Wrong error type"),
|
||||
}
|
||||
|
||||
match dag.get(&task2).unwrap() {
|
||||
Err(CustomError::ProcessingError { code, .. }) => assert_eq!(code, 500),
|
||||
_ => panic!("Wrong error type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_in_tuple_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let ok_task = dag.add_task(task_fn(|_: ()| async { 10 }));
|
||||
let err_task = dag.add_task(task_fn(|_: ()| async {
|
||||
panic!("Error in dependency");
|
||||
}));
|
||||
|
||||
let dependent = dag
|
||||
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
|
||||
.depends_on((&ok_task, &err_task));
|
||||
|
||||
let result = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(dag.get(dependent).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_string_panic_vs_structured_panic() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let string_panic = dag.add_task(task_fn(|_: ()| async {
|
||||
panic!("String panic message");
|
||||
}));
|
||||
|
||||
let formatted_panic = dag.add_task(task_fn(|_: ()| async {
|
||||
panic!("Formatted panic: value={}, code={}", 42, "ERROR");
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
// Both should result in errors
|
||||
assert!(dag.get(&string_panic).is_err());
|
||||
assert!(dag.get(&formatted_panic).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_anyhow_like_error_chain() {
|
||||
// Test error chains with simple string-based errors
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task = dag.add_task(task_fn(|_: ()| async {
|
||||
// Simulate an error chain with nested Results
|
||||
let _inner: Result<i32, String> = Err("Inner error".to_string());
|
||||
let outer: Result<Result<i32, String>, String> = Err("Outer error".to_string());
|
||||
|
||||
outer
|
||||
}));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get the chained error
|
||||
let result = dag.get(&task).unwrap();
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(e) = result {
|
||||
assert_eq!(e, "Outer error");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! Error handling and recovery tests
|
||||
//!
|
||||
//! Tests for error scenarios and recovery patterns.
|
||||
|
||||
mod error_types;
|
||||
mod propagation;
|
||||
mod recovery;
|
||||
@@ -0,0 +1,395 @@
|
||||
//! Tests for error propagation through the DAG
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::DagRunner;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_linear_error_propagation() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn(|_: ()| async { 1 }));
|
||||
let t2 = dag
|
||||
.add_task(task_fn(|x: i32| async move {
|
||||
panic!("Error at t2 with input {}", x);
|
||||
}))
|
||||
.depends_on(&t1);
|
||||
let t3 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t2);
|
||||
let t4 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 2 }))
|
||||
.depends_on(t3);
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
// t1 succeeds
|
||||
assert_eq!(dag.get(&t1).unwrap(), 1);
|
||||
|
||||
// t2 fails
|
||||
assert!(dag.get(t2).is_err());
|
||||
|
||||
// t3 and t4 fail due to dependency
|
||||
assert!(dag.get(t3).is_err());
|
||||
assert!(dag.get(t4).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_error_propagation() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// source
|
||||
// / \
|
||||
// left right (fails)
|
||||
// \ /
|
||||
// sink
|
||||
|
||||
let source = dag.add_task(task_fn(|_: ()| async { 100 }));
|
||||
|
||||
let left = dag
|
||||
.add_task(task_fn(|x: i32| async move { x / 2 }))
|
||||
.depends_on(&source);
|
||||
|
||||
let right = dag
|
||||
.add_task(task_fn(|x: i32| async move {
|
||||
panic!("Right path fails with {}", x);
|
||||
}))
|
||||
.depends_on(&source);
|
||||
|
||||
let sink = dag
|
||||
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
|
||||
.depends_on((&left, &right));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(dag.get(&source).unwrap(), 100);
|
||||
assert_eq!(dag.get(left).unwrap(), 50);
|
||||
assert!(dag.get(right).is_err());
|
||||
assert!(dag.get(sink).is_err()); // Fails due to right dependency
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_stops_at_boundary() {
|
||||
let dag = DagRunner::new();
|
||||
let execution_tracker = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Branch with error
|
||||
let error_source = dag.add_task(task_fn({
|
||||
let tracker = execution_tracker.clone();
|
||||
move |_: ()| {
|
||||
let tracker = tracker.clone();
|
||||
async move {
|
||||
tracker.fetch_add(1, Ordering::SeqCst);
|
||||
panic!("Error source");
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let error_dependent = dag
|
||||
.add_task(task_fn({
|
||||
let tracker = execution_tracker.clone();
|
||||
move |_: i32| {
|
||||
let tracker = tracker.clone();
|
||||
async move {
|
||||
tracker.fetch_add(100, Ordering::SeqCst); // Should not execute
|
||||
42
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(&error_source);
|
||||
|
||||
// Independent branch
|
||||
let independent = dag.add_task(task_fn({
|
||||
let tracker = execution_tracker.clone();
|
||||
move |_: ()| {
|
||||
let tracker = tracker.clone();
|
||||
async move {
|
||||
tracker.fetch_add(10, Ordering::SeqCst);
|
||||
10
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
// Error branch: source runs but dependent doesn't
|
||||
assert!(dag.get(&error_source).is_err());
|
||||
assert!(dag.get(error_dependent).is_err());
|
||||
|
||||
// Independent branch runs
|
||||
assert_eq!(dag.get(&independent).unwrap(), 10);
|
||||
|
||||
// Check execution: 1 (error_source) + 10 (independent) = 11
|
||||
assert_eq!(execution_tracker.load(Ordering::SeqCst), 11);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_error_sources_convergence() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Multiple error sources
|
||||
let error1 = dag.add_task(task_fn(|_: ()| async {
|
||||
panic!("Error source 1");
|
||||
}));
|
||||
|
||||
let error2 = dag.add_task(task_fn(|_: ()| async {
|
||||
panic!("Error source 2");
|
||||
}));
|
||||
|
||||
let success = dag.add_task(task_fn(|_: ()| async { 42 }));
|
||||
|
||||
// Convergence point depends on all three
|
||||
let convergence = dag
|
||||
.add_task(task_fn(|(_, _, _): (i32, i32, i32)| async { 100 }))
|
||||
.depends_on((&error1, &error2, &success));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
// All error sources fail
|
||||
assert!(dag.get(&error1).is_err());
|
||||
assert!(dag.get(&error2).is_err());
|
||||
|
||||
// Success task works
|
||||
assert_eq!(dag.get(&success).unwrap(), 42);
|
||||
|
||||
// Convergence fails due to any error dependency
|
||||
assert!(dag.get(convergence).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_in_wide_fanout() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(task_fn(|_: ()| async {
|
||||
panic!("Source fails");
|
||||
}));
|
||||
|
||||
// Create wide fanout from failing source
|
||||
let dependents: Vec<_> = (0..20)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn(move |x: i32| async move { x + i }))
|
||||
.depends_on(&source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
// Source fails
|
||||
assert!(dag.get(&source).is_err());
|
||||
|
||||
// All dependents fail
|
||||
for dependent in &dependents {
|
||||
assert!(dag.get(dependent).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_selective_error_propagation() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Source that produces a Result
|
||||
let source = dag.add_task(task_fn(|_: ()| async {
|
||||
Result::<i32, String>::Err("Source error".to_string())
|
||||
}));
|
||||
|
||||
// Handler that processes the Result
|
||||
let handler = dag
|
||||
.add_task(task_fn(|result: Result<i32, String>| async move {
|
||||
match result {
|
||||
Ok(val) => val * 2,
|
||||
Err(_) => -1, // Default value on error
|
||||
}
|
||||
}))
|
||||
.depends_on(&source);
|
||||
|
||||
// Further processing
|
||||
let final_task = dag
|
||||
.add_task(task_fn(|val: i32| async move {
|
||||
if val < 0 {
|
||||
"Handled error case".to_string()
|
||||
} else {
|
||||
format!("Success: {}", val)
|
||||
}
|
||||
}))
|
||||
.depends_on(handler);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Source returns an Err variant (not a panic)
|
||||
assert_eq!(dag.get(&source).unwrap(), Err("Source error".to_string()));
|
||||
|
||||
// Handler processes it
|
||||
assert_eq!(dag.get(handler).unwrap(), -1);
|
||||
|
||||
// Final task handles the error case
|
||||
assert_eq!(dag.get(final_task).unwrap(), "Handled error case");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_propagation_timing() {
|
||||
let dag = DagRunner::new();
|
||||
let propagation_order = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
let t1 = dag.add_task(task_fn({
|
||||
let order = propagation_order.clone();
|
||||
move |_: ()| {
|
||||
let order = order.clone();
|
||||
async move {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
order.lock().push("t1_complete");
|
||||
1
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let t2 = dag
|
||||
.add_task(task_fn({
|
||||
let order = propagation_order.clone();
|
||||
move |x: i32| {
|
||||
let order = order.clone();
|
||||
async move {
|
||||
order.lock().push("t2_start");
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
panic!("t2 fails with {}", x);
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(&t1);
|
||||
|
||||
let _t3 = dag
|
||||
.add_task(task_fn({
|
||||
let order = propagation_order.clone();
|
||||
move |x: i32| {
|
||||
let order = order.clone();
|
||||
async move {
|
||||
order.lock().push("t3_should_not_run");
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(&t2);
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
let order = propagation_order.lock().clone();
|
||||
|
||||
// t1 completes, t2 starts and fails, t3 never runs
|
||||
assert!(order.contains(&"t1_complete"));
|
||||
assert!(order.contains(&"t2_start"));
|
||||
assert!(!order.contains(&"t3_should_not_run"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_partial_branch_failure_propagation() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// root
|
||||
// / | \
|
||||
// a1 b1 c1
|
||||
// | | |
|
||||
// a2 b2 c2
|
||||
// | (err) |
|
||||
// a3 b3 c3
|
||||
// \ | /
|
||||
// sink
|
||||
|
||||
let root = dag.add_task(task_fn(|_: ()| async { 100 }));
|
||||
|
||||
// Branch A
|
||||
let a1 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(&root);
|
||||
let a2 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 2 }))
|
||||
.depends_on(a1);
|
||||
let a3 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 3 }))
|
||||
.depends_on(a2);
|
||||
|
||||
// Branch B (with error) - use Result instead of panic
|
||||
let b1 = dag
|
||||
.add_task(task_fn(|x: i32| async move { Ok(x * 2) }))
|
||||
.depends_on(&root);
|
||||
let b2 = dag
|
||||
.add_task(task_fn(|_x: Result<i32, &str>| async move {
|
||||
Err("Branch B fails at b2")
|
||||
}))
|
||||
.depends_on(&b1);
|
||||
let b3 = dag
|
||||
.add_task(task_fn(
|
||||
|x: Result<i32, &str>| async move { x.map(|v| v * 3) },
|
||||
))
|
||||
.depends_on(&b2);
|
||||
|
||||
// Branch C
|
||||
let c1 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x - 1 }))
|
||||
.depends_on(&root);
|
||||
let c2 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x - 2 }))
|
||||
.depends_on(c1);
|
||||
let c3 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x - 3 }))
|
||||
.depends_on(c2);
|
||||
|
||||
// Sink depends on all branches - handle Result from B
|
||||
let sink = dag
|
||||
.add_task(task_fn(
|
||||
|(a, b, c): (i32, Result<i32, &str>, i32)| async move {
|
||||
match b {
|
||||
Ok(b_val) => Ok(a + b_val + c),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
},
|
||||
))
|
||||
.depends_on((&a3, &b3, &c3));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Branch A succeeds
|
||||
assert_eq!(dag.get(a3).unwrap(), 106);
|
||||
|
||||
// Branch B fails at b2
|
||||
assert_eq!(dag.get(b1).unwrap(), Ok(200));
|
||||
assert_eq!(dag.get(b2).unwrap(), Err("Branch B fails at b2"));
|
||||
assert_eq!(dag.get(b3).unwrap(), Err("Branch B fails at b2")); // Propagated error
|
||||
|
||||
// Branch C succeeds
|
||||
assert_eq!(dag.get(c3).unwrap(), 94);
|
||||
|
||||
// Sink fails due to branch B
|
||||
assert_eq!(dag.get(sink).unwrap(), Err("Branch B fails at b2"));
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
//! Tests for error recovery patterns
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fallback_on_error() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Primary task that fails
|
||||
let primary = dag.add_task(task_fn(|_: ()| async {
|
||||
panic!("Primary task failed");
|
||||
#[allow(unreachable_code)]
|
||||
42
|
||||
}));
|
||||
|
||||
// Fallback task
|
||||
let fallback = dag.add_task(task_fn(|_: ()| async { 100 }));
|
||||
|
||||
// Selector that uses fallback if primary fails
|
||||
let _selector = dag.add_task(task_fn(|_: ()| async {}));
|
||||
|
||||
// Run DAG
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
// Primary should fail
|
||||
assert!(dag.get(&primary).is_err());
|
||||
|
||||
// Fallback should succeed
|
||||
assert_eq!(dag.get(&fallback)?, 100);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_retry_pattern_simulation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let attempt_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Task that would fail on first attempts (simulated)
|
||||
let task = dag.add_task(task_fn({
|
||||
let counter = attempt_counter.clone();
|
||||
move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
let attempt = counter.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
// In real retry, this would be retried
|
||||
// Here we simulate by not panicking
|
||||
if attempt == 0 {
|
||||
// Would normally panic here
|
||||
return Err::<i32, &str>("Simulated failure");
|
||||
}
|
||||
|
||||
Ok(42)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Task completes with error on first "attempt"
|
||||
assert_eq!(dag.get(&task)?, Err("Simulated failure"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let failure_count = Arc::new(AtomicUsize::new(0));
|
||||
let circuit_open = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Tasks that check circuit breaker
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
let failures = failure_count.clone();
|
||||
let circuit = circuit_open.clone();
|
||||
dag.add_task(task_fn(move |_: ()| {
|
||||
let failures = failures.clone();
|
||||
let circuit = circuit.clone();
|
||||
async move {
|
||||
// Check if circuit is open
|
||||
if circuit.load(Ordering::SeqCst) {
|
||||
return Err("Circuit breaker open");
|
||||
}
|
||||
|
||||
// Simulate some failures
|
||||
if i < 3 {
|
||||
let count = failures.fetch_add(1, Ordering::SeqCst);
|
||||
if count >= 2 {
|
||||
circuit.store(true, Ordering::SeqCst);
|
||||
}
|
||||
return Err("Task failed");
|
||||
}
|
||||
|
||||
Ok(i)
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// First few tasks fail and open circuit
|
||||
for task in &tasks[0..3] {
|
||||
assert!(dag.get(task)?.is_err());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_partial_recovery_with_defaults() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Some tasks that might fail
|
||||
let _tasks: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn(move |_: ()| async move {
|
||||
if i % 3 == 0 {
|
||||
panic!("Task {} failed", i);
|
||||
}
|
||||
i * 10
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Aggregator that handles failures gracefully
|
||||
let aggregator = dag.add_task(task_fn(|_: ()| async {
|
||||
// In real scenario, would check task results and use defaults
|
||||
vec![0, 10, 20, 0, 40, 50, 0, 70, 80, 0] // 0 for failed tasks
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
let result = dag.get(&aggregator)?;
|
||||
assert_eq!(result.len(), 10);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let errors = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
// Tasks that may produce errors
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
let errors = errors.clone();
|
||||
dag.add_task(task_fn(move |_: ()| {
|
||||
let errors = errors.clone();
|
||||
async move {
|
||||
if i % 2 == 0 {
|
||||
errors.lock().push(format!("Error from task {}", i));
|
||||
return Err(format!("Task {} error", i));
|
||||
}
|
||||
Ok(i)
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Check accumulated errors
|
||||
let error_list = errors.lock().clone();
|
||||
assert_eq!(error_list.len(), 5); // Even numbered tasks
|
||||
|
||||
// Verify specific results
|
||||
assert!(dag.get(&tasks[0])?.is_err());
|
||||
assert_eq!(dag.get(&tasks[1])?, Ok(1));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_graceful_degradation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Critical task
|
||||
let critical = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] }));
|
||||
|
||||
// Optional enhancement that fails - returns Result
|
||||
let _enhancement = dag.add_task(task_fn(|_: ()| async {
|
||||
Err::<Vec<i32>, &str>("Enhancement failed")
|
||||
}));
|
||||
|
||||
// Final task that works with what's available
|
||||
let final_task = dag
|
||||
.add_task(task_fn(|base: Vec<i32>| async move {
|
||||
// Would combine with enhancement if available
|
||||
// Here just uses base
|
||||
base.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(&critical);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Critical path works
|
||||
assert_eq!(dag.get(final_task)?, 15);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_boundary_isolation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Group A: contains an error - use Result
|
||||
let a1 = dag.add_task(task_fn(|_: ()| async { 10 }));
|
||||
let a2 = dag
|
||||
.add_task(task_fn(|_x: i32| async move {
|
||||
Err::<i32, &str>("Group A error")
|
||||
}))
|
||||
.depends_on(&a1);
|
||||
|
||||
// Group B: should be isolated from Group A's error
|
||||
let b1 = dag.add_task(task_fn(|_: ()| async { 20 }));
|
||||
let b2 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 2 }))
|
||||
.depends_on(&b1);
|
||||
|
||||
// Group C: depends on B but not A
|
||||
let c1 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 5 }))
|
||||
.depends_on(b2);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Group A fails (returns Err)
|
||||
assert_eq!(dag.get(a2)?, Err("Group A error"));
|
||||
|
||||
// Groups B and C succeed
|
||||
assert_eq!(dag.get(b2)?, 40);
|
||||
assert_eq!(dag.get(c1)?, 45);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compensation_action() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let compensation_triggered = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Action that fails
|
||||
let action = dag.add_task(task_fn(|_: ()| async {
|
||||
// Perform some action that fails
|
||||
panic!("Action failed");
|
||||
}));
|
||||
|
||||
// Compensation task (runs independently)
|
||||
let compensation = dag.add_task(task_fn({
|
||||
let triggered = compensation_triggered.clone();
|
||||
move |_: ()| {
|
||||
let triggered = triggered.clone();
|
||||
async move {
|
||||
triggered.store(true, Ordering::SeqCst);
|
||||
"Compensation executed"
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await;
|
||||
|
||||
// Action failed
|
||||
assert!(dag.get(&action).is_err());
|
||||
|
||||
// Compensation ran successfully
|
||||
assert_eq!(dag.get(&compensation)?, "Compensation executed");
|
||||
assert!(compensation_triggered.load(Ordering::SeqCst));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user