Merge pull request #1 from TechnoPorg/push-mmkvnrvwpuzm

Replace channel mechanism of dependency passing with a shared registry
This commit is contained in:
swaits authored and GitHub committed 2026-01-27 23:23:53 -07:00
commit 8f6472ea22
96 files changed
+1275 -2243

No files matched your search

+11 -3
View File
@@ -22,16 +22,24 @@ default = []
tracing = ["dep:tracing"] tracing = ["dep:tracing"]
[lints.rust] [lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin)', 'cfg(tarpaulin_include)'] } unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(tarpaulin)',
'cfg(tarpaulin_include)',
] }
[dependencies] [dependencies]
futures = "0.3" futures = { version = "0.3", default-features = false, features = ["std"] }
parking_lot = "0.12" parking_lot = "0.12"
dagx-macros = { version = "0.3.1", path = "./dagx-macros" } dagx-macros = { version = "0.3.1", path = "./dagx-macros" }
tracing = { version = "0.1", optional = true } tracing = { version = "0.1", optional = true }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] } tokio = { version = "1", features = [
"macros",
"rt-multi-thread",
"time",
"sync",
] }
async-std = { version = "1", features = ["attributes"] } async-std = { version = "1", features = ["attributes"] }
smol = "2" smol = "2"
criterion = "0.7" criterion = "0.7"
+10 -6
View File
@@ -38,7 +38,7 @@ See [how it works](docs/CYCLE_PREVENTION.md).
```rust ```rust
let sum = dag.add_task(Add).depends_on((&x, &y)); let sum = dag.add_task(Add).depends_on((&x, &y));
dag.run(|fut| tokio::spawn(fut)).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
``` ```
That's it. No trait boilerplate, no manual channels, no node IDs. That's it. No trait boilerplate, no manual channels, no node IDs.
@@ -90,7 +90,7 @@ async fn main() {
let sum = dag.add_task(Add).depends_on((&x, &y)); let sum = dag.add_task(Add).depends_on((&x, &y));
// Execute with true parallelism // Execute with true parallelism
dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
// Retrieve results // Retrieve results
assert_eq!(dag.get(sum).unwrap(), 5); assert_eq!(dag.get(sum).unwrap(), 5);
@@ -104,7 +104,6 @@ dagx provides true parallel execution with sub-microsecond overhead per task.
**Why is dagx so fast?** **Why is dagx so fast?**
- **Inline fast-path**: Sequential chains execute inline without spawning (8.9-129x faster) - **Inline fast-path**: Sequential chains execute inline without spawning (8.9-129x faster)
- **Primitives as scheduler**: No custom scheduler — channels coordinate execution
- **Adaptive execution**: Inline for sequential work, true parallelism for concurrent work - **Adaptive execution**: Inline for sequential work, true parallelism for concurrent work
- **Zero-cost abstractions**: Generics and monomorphization eliminate overhead - **Zero-cost abstractions**: Generics and monomorphization eliminate overhead
@@ -223,13 +222,18 @@ dagx works with any async runtime. Provide a spawner function to `run()`:
```rust ```rust
// With Tokio // With Tokio
dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); // The join handle result can be unwrapped because dagx catches panics internally
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
// With async-std // With async-std
dag.run(|fut| { async_std::task::spawn(fut); }).await.unwrap(); dag.run(|fut| async_std::task::spawn(fut)).await.unwrap();
// With smol // With smol
dag.run(|fut| { smol::spawn(fut).detach(); }).await.unwrap(); dag.run(|fut| smol::spawn(fut)).await.unwrap();
// Single-threaded on the invoking runtime
// Faster for situations with many computationally light tasks
dag.run(|fut| fut).await.unwrap()
``` ```
## Tutorials & Examples ## Tutorials & Examples
+10 -15
View File
@@ -2,6 +2,7 @@
use criterion::Criterion; use criterion::Criterion;
use dagx::{task_fn, DagRunner, Task}; use dagx::{task_fn, DagRunner, Task};
use futures::FutureExt;
pub fn bench_dag_execution(c: &mut Criterion) { pub fn bench_dag_execution(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -13,11 +14,9 @@ pub fn bench_dag_execution(c: &mut Criterion) {
for i in 0..10 { for i in 0..10 {
dag.add_task(task_fn(move |_: ()| async move { i })); dag.add_task(task_fn(move |_: ()| async move { i }));
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
@@ -29,11 +28,9 @@ pub fn bench_dag_execution(c: &mut Criterion) {
for i in 0..100 { for i in 0..100 {
dag.add_task(task_fn(move |_: ()| async move { i })); dag.add_task(task_fn(move |_: ()| async move { i }));
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
@@ -111,11 +108,9 @@ pub fn bench_dag_execution(c: &mut Criterion) {
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(t18); .depends_on(t18);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
+4 -5
View File
@@ -2,6 +2,7 @@
use criterion::{BenchmarkId, Criterion}; use criterion::{BenchmarkId, Criterion};
use dagx::{task_fn, DagRunner}; use dagx::{task_fn, DagRunner};
use futures::FutureExt;
pub fn bench_dag_scaling(c: &mut Criterion) { pub fn bench_dag_scaling(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -18,11 +19,9 @@ pub fn bench_dag_scaling(c: &mut Criterion) {
for i in 0..size { for i in 0..size {
dag.add_task(task_fn(move |_: ()| async move { i })); dag.add_task(task_fn(move |_: ()| async move { i }));
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
+4 -5
View File
@@ -3,6 +3,7 @@
//! Long sequential chain: 100 tasks in sequence //! Long sequential chain: 100 tasks in sequence
use criterion::Criterion; use criterion::Criterion;
use futures::FutureExt;
pub fn bench_deep_chain(c: &mut Criterion) { pub fn bench_deep_chain(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -27,11 +28,9 @@ pub fn bench_deep_chain(c: &mut Criterion) {
.depends_on(prev); .depends_on(prev);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
dag.get(prev).unwrap() dag.get(prev).unwrap()
}) })
}); });
+4 -5
View File
@@ -3,6 +3,7 @@
//! Classic diamond: A → B,C → D //! Classic diamond: A → B,C → D
use criterion::Criterion; use criterion::Criterion;
use futures::FutureExt;
pub fn bench_diamond_pattern(c: &mut Criterion) { pub fn bench_diamond_pattern(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -27,11 +28,9 @@ pub fn bench_diamond_pattern(c: &mut Criterion) {
.add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y }))
.depends_on((&b, &c)); .depends_on((&b, &c));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
dag.get(d).unwrap() dag.get(d).unwrap()
}) })
}); });
+4 -5
View File
@@ -3,6 +3,7 @@
//! Extract → Transform → Validate → Load pattern //! Extract → Transform → Validate → Load pattern
use criterion::Criterion; use criterion::Criterion;
use futures::FutureExt;
pub fn bench_etl_pipeline(c: &mut Criterion) { pub fn bench_etl_pipeline(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -66,11 +67,9 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
})) }))
.depends_on(validate); .depends_on(validate);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
dag.get(load).unwrap() dag.get(load).unwrap()
}) })
}); });
+4 -5
View File
@@ -3,6 +3,7 @@
//! 10,000 independent tasks to test scaling //! 10,000 independent tasks to test scaling
use criterion::Criterion; use criterion::Criterion;
use futures::FutureExt;
pub fn bench_large_scale(c: &mut Criterion) { pub fn bench_large_scale(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -21,11 +22,9 @@ pub fn bench_large_scale(c: &mut Criterion) {
dag.add_task(task_fn(move |_: ()| async move { i * 2 })); dag.add_task(task_fn(move |_: ()| async move { i * 2 }));
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
+4 -5
View File
@@ -3,6 +3,7 @@
//! Simple chain of tasks: A → B → C → D → E //! Simple chain of tasks: A → B → C → D → E
use criterion::Criterion; use criterion::Criterion;
use futures::FutureExt;
pub fn bench_linear_pipeline(c: &mut Criterion) { pub fn bench_linear_pipeline(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -30,11 +31,9 @@ pub fn bench_linear_pipeline(c: &mut Criterion) {
.add_task(task_fn(|x: i32| async move { x * 3 })) .add_task(task_fn(|x: i32| async move { x * 3 }))
.depends_on(d); .depends_on(d);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
dag.get(e).unwrap() dag.get(e).unwrap()
}) })
}); });
+4 -5
View File
@@ -3,6 +3,7 @@
//! One source task feeding 100 dependent tasks //! One source task feeding 100 dependent tasks
use criterion::Criterion; use criterion::Criterion;
use futures::FutureExt;
pub fn bench_wide_fanout(c: &mut Criterion) { pub fn bench_wide_fanout(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -23,11 +24,9 @@ pub fn bench_wide_fanout(c: &mut Criterion) {
.depends_on(&source); .depends_on(&source);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
+7 -10
View File
@@ -2,6 +2,7 @@
use criterion::Criterion; use criterion::Criterion;
use dagx::{task_fn, DagRunner}; use dagx::{task_fn, DagRunner};
use futures::FutureExt;
pub fn bench_10k_breakdown(c: &mut Criterion) { pub fn bench_10k_breakdown(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -26,11 +27,9 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
for i in 0..10_000 { for i in 0..10_000 {
dag.add_task(task_fn(move |_: ()| async move { i })); dag.add_task(task_fn(move |_: ()| async move { i }));
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
@@ -45,11 +44,9 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
b.iter(|| { b.iter(|| {
rt.block_on(async { rt.block_on(async {
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
+4 -5
View File
@@ -2,6 +2,7 @@
use criterion::Criterion; use criterion::Criterion;
use dagx::{task_fn, DagRunner}; use dagx::{task_fn, DagRunner};
use futures::FutureExt;
pub fn bench_diamond(c: &mut Criterion) { pub fn bench_diamond(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -25,11 +26,9 @@ pub fn bench_diamond(c: &mut Criterion) {
.depends_on((&left, &right)); .depends_on((&left, &right));
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
+10 -15
View File
@@ -2,6 +2,7 @@
use criterion::Criterion; use criterion::Criterion;
use dagx::{task_fn, DagRunner}; use dagx::{task_fn, DagRunner};
use futures::FutureExt;
pub fn bench_fanout(c: &mut Criterion) { pub fn bench_fanout(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -18,11 +19,9 @@ pub fn bench_fanout(c: &mut Criterion) {
.depends_on(&source); .depends_on(&source);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
@@ -45,11 +44,9 @@ pub fn bench_fanout(c: &mut Criterion) {
.depends_on(&source); .depends_on(&source);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
@@ -72,11 +69,9 @@ pub fn bench_fanout(c: &mut Criterion) {
.depends_on(&source); .depends_on(&source);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
+4 -5
View File
@@ -2,6 +2,7 @@
use criterion::{BenchmarkId, Criterion}; use criterion::{BenchmarkId, Criterion};
use dagx::{task_fn, DagRunner}; use dagx::{task_fn, DagRunner};
use futures::FutureExt;
use std::collections::HashMap; use std::collections::HashMap;
use std::hint::black_box; use std::hint::black_box;
@@ -51,11 +52,9 @@ pub fn bench_config_broadcast(c: &mut Criterion) {
.depends_on(&config); .depends_on(&config);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}, },
+2 -3
View File
@@ -2,6 +2,7 @@
use criterion::{BenchmarkId, Criterion}; use criterion::{BenchmarkId, Criterion};
use dagx::{task_fn, DagRunner}; use dagx::{task_fn, DagRunner};
use futures::FutureExt;
pub fn bench_etl_pipeline(c: &mut Criterion) { pub fn bench_etl_pipeline(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -90,9 +91,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
)) ))
.depends_on((&validate, &scores, &names)); .depends_on((&validate, &scores, &names));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut);
})
.await .await
.unwrap(); .unwrap();
}) })
+4 -5
View File
@@ -2,6 +2,7 @@
use criterion::{BenchmarkId, Criterion}; use criterion::{BenchmarkId, Criterion};
use dagx::{task_fn, DagRunner}; use dagx::{task_fn, DagRunner};
use futures::FutureExt;
use std::hint::black_box; use std::hint::black_box;
pub fn bench_memory_efficiency(c: &mut Criterion) { pub fn bench_memory_efficiency(c: &mut Criterion) {
@@ -38,11 +39,9 @@ pub fn bench_memory_efficiency(c: &mut Criterion) {
.depends_on(&source); .depends_on(&source);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}, },
+4 -5
View File
@@ -2,6 +2,7 @@
use criterion::Criterion; use criterion::Criterion;
use dagx::{task_fn, DagRunner}; use dagx::{task_fn, DagRunner};
use futures::FutureExt;
pub fn bench_mixed_patterns(c: &mut Criterion) { pub fn bench_mixed_patterns(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
@@ -50,11 +51,9 @@ pub fn bench_mixed_patterns(c: &mut Criterion) {
.depends_on(summary); .depends_on(summary);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
}) })
}); });
}); });
+18 -37
View File
@@ -335,7 +335,7 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
quote! { quote! {
fn extract_and_run( fn extract_and_run(
self, self,
_receivers: Vec<Box<dyn std::any::Any + Send>>, _dependencies: Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send { ) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send {
async move { async move {
let input = (); let input = ();
@@ -350,24 +350,20 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
quote! { quote! {
fn extract_and_run( fn extract_and_run(
self, self,
mut receivers: Vec<Box<dyn std::any::Any + Send>>, mut dependencies: Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send { ) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send {
async move { async move {
use futures::channel::oneshot;
use std::sync::Arc; use std::sync::Arc;
if receivers.len() != 1 { if dependencies.len() != 1 {
return Err(format!("Expected 1 dependency, got {}", receivers.len())); return Err(format!("Expected 1 dependency, got {}", dependencies.len()));
} }
let rx = *receivers.pop() let arc_value = dependencies.pop()
.unwrap() .unwrap()
.downcast::<oneshot::Receiver<Arc<#param_type>>>() .downcast::<#param_type>()
.map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<#param_type>()))?; .map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<#param_type>()))?;
let arc_value = rx.await
.map_err(|_| "Channel closed before receiving value".to_string())?;
let input = (*arc_value).clone(); let input = (*arc_value).clone();
Ok(self.run(input).await) Ok(self.run(input).await)
} }
@@ -380,51 +376,36 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
let param_types: Vec<_> = params.iter().map(|(_, ty)| ty).collect(); let param_types: Vec<_> = params.iter().map(|(_, ty)| ty).collect();
let indices: Vec<_> = (0..param_count).collect(); let indices: Vec<_> = (0..param_count).collect();
// Generate unique receiver variable names
let rx_vars: Vec<_> = (0..param_count)
.map(|i| syn::Ident::new(&format!("rx_{}", i), proc_macro2::Span::call_site()))
.collect();
// Create syn::Index for tuple field access (avoids the suffix warning) // Create syn::Index for tuple field access (avoids the suffix warning)
let syn_indices: Vec<_> = (0..param_count).map(syn::Index::from).collect(); let syn_indices: Vec<_> = (0..param_count).map(syn::Index::from).collect();
quote! { quote! {
fn extract_and_run( fn extract_and_run(
self, self,
receivers: Vec<Box<dyn std::any::Any + Send>>, dependencies: Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send { ) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send {
async move { async move {
use futures::channel::oneshot;
use std::sync::Arc; use std::sync::Arc;
let expected_count = #param_count; let expected_count = #param_count;
if receivers.len() != expected_count { if dependencies.len() != expected_count {
return Err(format!("Expected {} dependencies, got {}", expected_count, receivers.len())); return Err(format!("Expected {} dependencies, got {}", expected_count, dependencies.len()));
} }
let mut iter = receivers.into_iter(); let mut iter = dependencies.into_iter();
// Extract each receiver // Extract each parameter
#( let arc_results = (#(
let #rx_vars = *iter.next() iter.next()
.ok_or_else(|| format!("Missing receiver at index {}", #indices))? .ok_or_else(|| format!("Missing parameter at index {}", #indices))?
.downcast::<oneshot::Receiver<Arc<#param_types>>>() .downcast::<#param_types>()
.map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>", .map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>",
#indices, std::any::type_name::<#param_types>()))?; #indices, std::any::type_name::<#param_types>()))?,
)* )*);
// Await all channels concurrently
let arc_results = futures::join!(
#(
async move {
#rx_vars.await.map_err(|_| format!("Channel {} closed", #indices))
}
),*
);
// Clone inner values and build tuple // Clone inner values and build tuple
let input = (#( let input = (#(
(*arc_results.#syn_indices?).clone() (*arc_results.#syn_indices).clone()
),*); ),*);
Ok(self.run(input).await) Ok(self.run(input).await)
+2 -2
View File
@@ -19,7 +19,7 @@ let dag = DagRunner::new();
let x = dag.add_task(Value(2)); let x = dag.add_task(Value(2));
let y = dag.add_task(Value(3)); let y = dag.add_task(Value(3));
let sum = dag.add_task(Add).depends_on((&x, &y)); let sum = dag.add_task(Add).depends_on((&x, &y));
dag.run(|fut| tokio::spawn(fut)).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
``` ```
**dagrs** (Complex: traits + channels + IDs + manual wiring): **dagrs** (Complex: traits + channels + IDs + manual wiring):
@@ -227,7 +227,7 @@ impl Task<String, Bytes> for MyTask {
let mut dag = Dag::default(); let mut dag = Dag::default();
dag.add_vertex("one", || async move { /* task */ }); dag.add_vertex("one", || async move { /* task */ });
dag.add_edge("one", "two"); dag.add_edge("one", "two");
dag.run().await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
``` ```
### tasksitter (Dynamic Workflows) ### tasksitter (Dynamic Workflows)
+1 -1
View File
@@ -13,7 +13,7 @@ let a = dag.add_task(TaskA);
let b = dag.add_task(TaskB); let b = dag.add_task(TaskB);
a.depends_on(b); a.depends_on(b);
b.depends_on(a); // Compiles fine... b.depends_on(a); // Compiles fine...
dag.run().await?; // ❌ Error: "Cycle detected!" (at runtime) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
``` ```
**dagx prevents this at compile time:** **dagx prevents this at compile time:**
+23 -26
View File
@@ -8,28 +8,23 @@ Traditional DAG executors contain substantial scheduling code—algorithms that
dagx eliminates this entirely: dagx eliminates this entirely:
1. **Wire up tasks with primitives**: During DAG construction, tasks are connected using channels and organized into topological layers based on their dependencies 1. **Wire up tasks with primitives**: During DAG construction, tasks are organized into topological layers based on their dependencies
2. **Start everything simultaneously**: When you call `run()`, all tasks spawn at once—there's no scheduler deciding when each task should start 2. **Start everything simultaneously**: When you call `run()`, all tasks spawn at once—there's no scheduler deciding when each task should start
3. **Let the primitives handle coordination**: Channels naturally enforce execution order—tasks wait on their input channels until upstream tasks send data. No custom orchestration needed. 3. **Let the primitives handle coordination**: Layers naturally enforce execution order—tasks wait on their dependencies until . No custom orchestration needed.
4. **Runtime joins on completion**: The runtime simply spawns all tasks and waits for the completion channel to close. That's it. 4. **Runtime joins on completion**: The runtime simply spawns all tasks and waits for them to finish. That's it.
## The Implementation ## The Implementation
Under the hood, dagx uses: Under the hood, dagx uses:
- **Oneshot channels**: Each edge in the DAG gets a `futures::channel::oneshot` - producer sends once, consumer receives once
- **Ownership model**: Tasks take ownership (`self`) and are consumed during execution - no Mutex needed for task state - **Ownership model**: Tasks take ownership (`self`) and are consumed during execution - no Mutex needed for task state
- **Direct data flow**: Outputs flow from producer to consumer via channels, never stored in shared memory during execution - **Direct data flow**: Outputs flow from producer(s) to consumer(s) through a shared hash map
- **Fast-path optimization**: Single-task layers execute inline without spawning overhead - **Fast-path optimization**: Single-task layers execute inline without spawning overhead
The core execution logic in `run()` is remarkably simple: The core execution logic in `run()` is remarkably simple:
```rust ```rust
// Create oneshot channel for each edge let outputs = HashMap::new();
for (producer, consumer) in edges {
let (tx, rx) = oneshot::channel();
// Wire tx to producer, rx to consumer
}
// Execute layer by layer // Execute layer by layer
for layer in layers { for layer in layers {
@@ -40,25 +35,27 @@ for layer in layers {
// Convert panic to error to match spawned task behavior // Convert panic to error to match spawned task behavior
} else { } else {
// Parallel path: Spawn all tasks in layer // Parallel path: Spawn all tasks in layer
for task in layer { let tasks = layers
spawner(async { .map(async {
let input = await_on_input_channels(); // Blocks until deps complete let input = get_inputs(map); // Deps are guaranteed to be in the map
let output = task.run(input); // Consumes task let output = task.run(input); // Consumes task
send_to_output_channels(output); // Sends to dependents output
}); })
.collect::<FuturesUnordered<_>>(); // Run all tasks in parallel
while let Some(out) = tasks.next().await {
outputs.insert(out.task_id, out.output); // Make result available for dependents
} }
// Wait for layer completion
} }
// Wait for layer completion
} }
// Tasks coordinate themselves via channels - no scheduler needed // FuturesUnordered coordinates task execution - no custom scheduler needed
``` ```
No state machines. No task queues. No wake-up logic. No Mutex locks. Just channels doing what channels do, with an inline fast-path for sequential execution.
## Inline Execution Fast-Path ## Inline Execution Fast-Path
**Performance optimization for sequential workloads**: When a layer contains only a single task (common in deep chains and linear pipelines), dagx executes it inline rather than spawning it. This eliminates spawning overhead, context switching, and channel creation, resulting in 10-100x performance improvements for sequential patterns. **Performance optimization for sequential workloads**: When a layer contains only a single task (common in deep chains and linear pipelines), dagx executes it inline rather than spawning it. This eliminates spawning overhead and context switching, resulting in a 10-100x performance improvements for sequential patterns.
**Panic handling guarantee**: To maintain behavioral consistency between inline and spawned execution, panics in inline tasks are caught using `FutureExt::catch_unwind()` and converted to errors. This matches the behavior of all major async runtimes (Tokio, async-std, smol, embassy-rs), which catch panics in spawned tasks and convert them to `JoinError` or equivalent. **Panic handling guarantee**: To maintain behavioral consistency between inline and spawned execution, panics in inline tasks are caught using `FutureExt::catch_unwind()` and converted to errors. This matches the behavior of all major async runtimes (Tokio, async-std, smol, embassy-rs), which catch panics in spawned tasks and convert them to `JoinError` or equivalent.
@@ -72,19 +69,19 @@ This ensures your code behaves the same whether a task runs inline or spawned, m
## Benefits ## Benefits
**Simplicity**: The runtime is straightforward: create channels, spawn tasks, let them coordinate via awaiting. No complex scheduler code to maintain, debug, or optimize. **Simplicity**: The runtime is straightforward: spawn tasks, coordinate via compile-time dependency guarantees. No complex scheduler code to maintain, debug, or optimize.
**Reliability**: Built on battle-tested primitives (oneshot channels, async/await) from Rust's standard library and the futures crate. These have been used in production by thousands of projects and are orders of magnitude more reliable than custom scheduling logic. **Reliability**: Built on battle-tested primitives from Rust's standard library and the futures crate. These have been used in production by thousands of projects and are orders of magnitude more reliable than custom scheduling logic.
**Bug resistance**: Fewer moving parts means fewer places for bugs to hide. The type system enforces correct wiring at compile time. Channels handle synchronization. The ownership model prevents data races. What's left to break? **Bug resistance**: Fewer moving parts means fewer places for bugs to hide. The type system enforces correct wiring at compile time. The ownership model prevents data races. What's left to break?
**Performance**: Near zero-overhead. No Mutex locks during execution. Arc reference counting for efficient fanout (atomic operations, not locks). Oneshot channels are often lock-free. Tasks start as soon as their dependencies complete - maximum parallelism. **Performance**: Near zero-overhead. No Mutex locks during execution. Arc reference counting for efficient fanout (atomic operations, not locks). Tasks start as soon as their dependencies complete - maximum parallelism.
**Auditability**: Want to verify correctness? Check the channel wiring, verify tasks await their inputs, done. No need to trace through complex state machine transitions or wake-up cascades. **Auditability**: Want to verify correctness? Check the dependency wiring, verify tasks await their inputs, done. No need to trace through complex state machine transitions or wake-up cascades.
## The Insight ## The Insight
The key insight is that **dependencies ARE the schedule**. If task B depends on task A's output, a channel naturally enforces that B waits for A. The dependency graph already encodes all the scheduling information—we just need to wire up channels to match it. The key insight is that **dependencies ARE the schedule**. If task B depends on task A's output, the topological sort naturally enforces that B waits for A. The dependency graph already encodes all the scheduling information—we just need to wire outputs to match it.
This is dagx's core philosophy: leverage the type system for correctness, use primitives for coordination, and let the compiler optimize everything else away. This is dagx's core philosophy: leverage the type system for correctness, use primitives for coordination, and let the compiler optimize everything else away.
+4 -5
View File
@@ -31,6 +31,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
// Step 1: Define a source task (no dependencies) // Step 1: Define a source task (no dependencies)
// //
@@ -105,11 +106,9 @@ async fn main() {
// 1. Tasks with no dependencies run first (x and y in parallel) // 1. Tasks with no dependencies run first (x and y in parallel)
// 2. Once x and y complete, sum runs (it depends on both) // 2. Once x and y complete, sum runs (it depends on both)
println!("Running DAG...\n"); println!("Running DAG...\n");
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Step 7: Retrieve results // Step 7: Retrieve results
// //
+4 -5
View File
@@ -56,6 +56,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
// Source task with a value field // Source task with a value field
struct Source { struct Source {
@@ -140,11 +141,9 @@ async fn main() {
// Run the DAG // Run the DAG
println!("Running fan-out DAG...\n"); println!("Running fan-out DAG...\n");
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Print all results // Print all results
println!("\nResults:"); println!("\nResults:");
+4 -5
View File
@@ -62,6 +62,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
struct FetchName; struct FetchName;
@@ -133,11 +134,9 @@ async fn main() {
// Run the DAG // Run the DAG
println!("Running fan-in DAG...\n"); println!("Running fan-in DAG...\n");
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Print result // Print result
println!("\n{}", dag.get(profile).unwrap()); println!("\n{}", dag.get(profile).unwrap());
+2 -4
View File
@@ -53,6 +53,7 @@
//! ``` //! ```
use dagx::{task, DagResult, DagRunner, Task}; use dagx::{task, DagResult, DagRunner, Task};
use futures::FutureExt;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::time::sleep; use tokio::time::sleep;
@@ -126,10 +127,7 @@ async fn main() -> DagResult<()> {
.add_task(Aggregate) .add_task(Aggregate)
.depends_on((&sum1, &sum2, &sum3, &sum4)); .depends_on((&sum1, &sum2, &sum3, &sum4));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let result = dag.get(total)?; let result = dag.get(total)?;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
+22 -35
View File
@@ -151,6 +151,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -389,11 +390,9 @@ async fn main() {
false, // success false, // success
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result 1: {:?}\n", dag.get(call1).unwrap()); println!("Result 1: {:?}\n", dag.get(call1).unwrap());
@@ -405,11 +404,9 @@ async fn main() {
true, // fail true, // fail
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result 2: {:?}\n", dag.get(call2).unwrap()); println!("Result 2: {:?}\n", dag.get(call2).unwrap());
@@ -421,11 +418,9 @@ async fn main() {
true, // fail true, // fail
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result 3: {:?}\n", dag.get(call3).unwrap()); println!("Result 3: {:?}\n", dag.get(call3).unwrap());
@@ -437,11 +432,9 @@ async fn main() {
true, // fail true, // fail
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result 4: {:?}\n", dag.get(call4).unwrap()); println!("Result 4: {:?}\n", dag.get(call4).unwrap());
@@ -457,11 +450,9 @@ async fn main() {
.add_task(FallbackService::new("ServiceCall-5")) .add_task(FallbackService::new("ServiceCall-5"))
.depends_on(&call5); .depends_on(&call5);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result 5: {:?}\n", dag.get(fallback5).unwrap()); println!("Result 5: {:?}\n", dag.get(fallback5).unwrap());
@@ -478,11 +469,9 @@ async fn main() {
false, // success false, // success
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result 6: {:?}\n", dag.get(call6).unwrap()); println!("Result 6: {:?}\n", dag.get(call6).unwrap());
@@ -494,11 +483,9 @@ async fn main() {
false, // success false, // success
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result 7: {:?}\n", dag.get(call7).unwrap()); println!("Result 7: {:?}\n", dag.get(call7).unwrap());
} }
+2 -4
View File
@@ -67,6 +67,7 @@
//! ``` //! ```
use dagx::{task, DagResult, DagRunner, Task}; use dagx::{task, DagResult, DagRunner, Task};
use futures::FutureExt;
// Extract: Load data from different sources // Extract: Load data from different sources
struct LoadData { struct LoadData {
@@ -201,10 +202,7 @@ async fn main() -> DagResult<()> {
// Execute the entire ETL pipeline // Execute the entire ETL pipeline
println!("Executing pipeline...\n"); println!("Executing pipeline...\n");
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Display final report // Display final report
println!("\n{}", dag.get(report)?); println!("\n{}", dag.get(report)?);
+2 -4
View File
@@ -56,6 +56,7 @@
//! ``` //! ```
use dagx::{task, DagResult, DagRunner, Task}; use dagx::{task, DagResult, DagRunner, Task};
use futures::FutureExt;
// Task that validates and processes input // Task that validates and processes input
struct ValidateAndDouble { struct ValidateAndDouble {
@@ -137,10 +138,7 @@ async fn main() -> DagResult<()> {
.depends_on((&cat1, &cat2, &cat3)); .depends_on((&cat1, &cat2, &cat3));
// Execute // Execute
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Display results // Display results
println!("\nFinal summary: {}", dag.get(summary)?); println!("\nFinal summary: {}", dag.get(summary)?);
+4 -5
View File
@@ -37,6 +37,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
// Custom type - just derive Clone! // Custom type - just derive Clone!
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -107,11 +108,9 @@ async fn main() {
let validation = dag.add_task(ValidateUser).depends_on(&user_task); let validation = dag.add_task(ValidateUser).depends_on(&user_task);
// Execute the DAG // Execute the DAG
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Print results // Print results
println!("Greeting: {}", dag.get(greeting).unwrap()); println!("Greeting: {}", dag.get(greeting).unwrap());
+4 -5
View File
@@ -76,6 +76,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
// Data source tasks // Data source tasks
struct LoadValue1; struct LoadValue1;
@@ -220,11 +221,9 @@ async fn main() {
// Execute the pipeline // Execute the pipeline
println!("Executing pipeline...\n"); println!("Executing pipeline...\n");
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Print final report // Print final report
println!("{}", dag.get(report).unwrap()); println!("{}", dag.get(report).unwrap());
+7 -10
View File
@@ -115,6 +115,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
use std::time::Instant; use std::time::Instant;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
@@ -400,11 +401,9 @@ async fn main() {
.depends_on((&inventory, &payment, &shipping)); .depends_on((&inventory, &payment, &shipping));
// Run the DAG // Run the DAG
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Print summary // Print summary
dag.get(summary).unwrap().print(); dag.get(summary).unwrap().print();
@@ -442,11 +441,9 @@ async fn main() {
true, // will fail true, // will fail
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Check results // Check results
match dag.get(payment).unwrap() { match dag.get(payment).unwrap() {
+13 -20
View File
@@ -115,6 +115,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
// Source task that provides a string input // Source task that provides a string input
struct StringSource(&'static str); struct StringSource(&'static str);
@@ -222,11 +223,9 @@ async fn main() {
let processed = dag.add_task(ProcessData).depends_on(validated); let processed = dag.add_task(ProcessData).depends_on(validated);
let logged = dag.add_task(LogError).depends_on(processed); let logged = dag.add_task(LogError).depends_on(processed);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
match dag.get(logged).unwrap() { match dag.get(logged).unwrap() {
Ok(result) => println!("Final result: {}\n", result), Ok(result) => println!("Final result: {}\n", result),
@@ -250,11 +249,9 @@ async fn main() {
let processed = dag.add_task(ProcessData).depends_on(validated); let processed = dag.add_task(ProcessData).depends_on(validated);
let logged = dag.add_task(LogError).depends_on(processed); let logged = dag.add_task(LogError).depends_on(processed);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
match dag.get(logged).unwrap() { match dag.get(logged).unwrap() {
Ok(result) => println!("Final result: {}\n", result), Ok(result) => println!("Final result: {}\n", result),
@@ -278,11 +275,9 @@ async fn main() {
let processed = dag.add_task(ProcessData).depends_on(validated); let processed = dag.add_task(ProcessData).depends_on(validated);
let logged = dag.add_task(LogError).depends_on(processed); let logged = dag.add_task(LogError).depends_on(processed);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
match dag.get(logged).unwrap() { match dag.get(logged).unwrap() {
Ok(result) => println!("Final result: {}\n", result), Ok(result) => println!("Final result: {}\n", result),
@@ -303,11 +298,9 @@ async fn main() {
.add_task(WithFallback { fallback: 0 }) .add_task(WithFallback { fallback: 0 })
.depends_on(parsed); .depends_on(parsed);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let result = dag.get(with_fallback).unwrap(); let result = dag.get(with_fallback).unwrap();
println!("Recovered with fallback: {}\n", result); println!("Recovered with fallback: {}\n", result);
+16 -25
View File
@@ -110,6 +110,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
use std::time::Instant; use std::time::Instant;
// Simple computation task // Simple computation task
@@ -181,11 +182,9 @@ async fn main() {
println!(" Build time: {:?}", build_time); println!(" Build time: {:?}", build_time);
let exec_start = Instant::now(); let exec_start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let exec_time = exec_start.elapsed(); let exec_time = exec_start.elapsed();
println!(" Execution time: {:?}", exec_time); println!(" Execution time: {:?}", exec_time);
@@ -223,11 +222,9 @@ async fn main() {
println!(" Build time: {:?}", build_time); println!(" Build time: {:?}", build_time);
let exec_start = Instant::now(); let exec_start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let exec_time = exec_start.elapsed(); let exec_time = exec_start.elapsed();
println!(" Execution time: {:?}", exec_time); println!(" Execution time: {:?}", exec_time);
@@ -276,11 +273,9 @@ async fn main() {
println!(" Tree depth: {}", level); println!(" Tree depth: {}", level);
let exec_start = Instant::now(); let exec_start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let exec_time = exec_start.elapsed(); let exec_time = exec_start.elapsed();
println!(" Execution time: {:?}", exec_time); println!(" Execution time: {:?}", exec_time);
@@ -336,11 +331,9 @@ async fn main() {
println!(" Build time: {:?}", build_time); println!(" Build time: {:?}", build_time);
let exec_start = Instant::now(); let exec_start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let exec_time = exec_start.elapsed(); let exec_time = exec_start.elapsed();
println!(" Execution time: {:?}", exec_time); println!(" Execution time: {:?}", exec_time);
@@ -362,11 +355,9 @@ async fn main() {
.map(|i| dag.add_task(FormatNumber { id: i })) .map(|i| dag.add_task(FormatNumber { id: i }))
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Verify results are accessible // Verify results are accessible
let i32_sum: i32 = small_tasks let i32_sum: i32 = small_tasks
+4 -5
View File
@@ -48,6 +48,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::time::sleep; use tokio::time::sleep;
@@ -86,11 +87,9 @@ async fn main() {
// Measure execution time // Measure execution time
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let elapsed = start.elapsed(); let elapsed = start.elapsed();
// Verify all tasks completed correctly // Verify all tasks completed correctly
+16 -25
View File
@@ -153,6 +153,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
// Retry strategy configuration // Retry strategy configuration
@@ -355,11 +356,9 @@ async fn main() {
], ],
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result: {:?}\n", dag.get(api_call).unwrap()); println!("Result: {:?}\n", dag.get(api_call).unwrap());
} }
@@ -379,11 +378,9 @@ async fn main() {
], ],
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result: {:?}\n", dag.get(api_call).unwrap()); println!("Result: {:?}\n", dag.get(api_call).unwrap());
} }
@@ -407,11 +404,9 @@ async fn main() {
], ],
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result: {:?}\n", dag.get(api_call).unwrap()); println!("Result: {:?}\n", dag.get(api_call).unwrap());
} }
@@ -428,11 +423,9 @@ async fn main() {
vec![ApiError::AuthenticationFailed], vec![ApiError::AuthenticationFailed],
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result: {:?}\n", dag.get(api_call).unwrap()); println!("Result: {:?}\n", dag.get(api_call).unwrap());
} }
@@ -457,11 +450,9 @@ async fn main() {
], ],
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result: {:?}\n", dag.get(api_call).unwrap()); println!("Result: {:?}\n", dag.get(api_call).unwrap());
} }
+15 -24
View File
@@ -24,7 +24,7 @@
//! //!
//! **2. DAG-Level Timeout** - Entire workflow with timeout //! **2. DAG-Level Timeout** - Entire workflow with timeout
//! ```rust //! ```rust
//! match timeout(dag_timeout, dag.run(spawner)).await { //! match timeout(dag_timeout, dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await {
//! Ok(result) => { /* DAG completed */ }, //! Ok(result) => { /* DAG completed */ },
//! Err(_) => { /* DAG timed out */ }, //! Err(_) => { /* DAG timed out */ },
//! } //! }
@@ -99,6 +99,7 @@
//! ``` //! ```
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
use tokio::time::{sleep, timeout, Duration}; use tokio::time::{sleep, timeout, Duration};
// Fast task that completes quickly // Fast task that completes quickly
@@ -177,11 +178,9 @@ async fn main() {
timeout_ms: 500, timeout_ms: 500,
}); });
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Fast task: {:?}", dag.get(fast).unwrap()); println!("Fast task: {:?}", dag.get(fast).unwrap());
println!("Timed task: {:?}\n", dag.get(timed).unwrap()); println!("Timed task: {:?}\n", dag.get(timed).unwrap());
@@ -199,11 +198,9 @@ async fn main() {
let processed = dag.add_task(ProcessResult).depends_on(&timed); let processed = dag.add_task(ProcessResult).depends_on(&timed);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Result: {}\n", dag.get(processed).unwrap()); println!("Result: {}\n", dag.get(processed).unwrap());
} }
@@ -228,11 +225,9 @@ async fn main() {
timeout_ms: 200, timeout_ms: 200,
}); });
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Fast task 1: {:?}", dag.get(fast1).unwrap()); println!("Fast task 1: {:?}", dag.get(fast1).unwrap());
println!("Fast task 2: {:?}", dag.get(fast2).unwrap()); println!("Fast task 2: {:?}", dag.get(fast2).unwrap());
@@ -254,11 +249,9 @@ async fn main() {
let processed1 = dag.add_task(ProcessResult).depends_on(&source); let processed1 = dag.add_task(ProcessResult).depends_on(&source);
let processed2 = dag.add_task(ProcessResult).depends_on(&source); let processed2 = dag.add_task(ProcessResult).depends_on(&source);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("Processed 1: {}", dag.get(processed1).unwrap()); println!("Processed 1: {}", dag.get(processed1).unwrap());
println!("Processed 2: {}\n", dag.get(processed2).unwrap()); println!("Processed 2: {}\n", dag.get(processed2).unwrap());
@@ -277,9 +270,7 @@ async fn main() {
let dag_timeout = Duration::from_millis(500); let dag_timeout = Duration::from_millis(500);
match timeout( match timeout(
dag_timeout, dag_timeout,
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)),
tokio::spawn(fut);
}),
) )
.await .await
{ {
+10 -15
View File
@@ -34,6 +34,7 @@
//! //!
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
use tracing_subscriber::{fmt, EnvFilter}; use tracing_subscriber::{fmt, EnvFilter};
// Simple source task // Simple source task
@@ -95,11 +96,9 @@ async fn main() {
// Layer 2: Combine results // Layer 2: Combine results
let result = dag.add_task(Add).depends_on((&left, &right)); let result = dag.add_task(Add).depends_on((&left, &right));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("\nResult: {}", dag.get(result).unwrap()); println!("\nResult: {}", dag.get(result).unwrap());
println!("(Expected: 10+10 = 20, 10*10 = 100, 20+100 = 120)\n"); println!("(Expected: 10+10 = 20, 10*10 = 100, 20+100 = 120)\n");
@@ -118,11 +117,9 @@ async fn main() {
let c = dag.add_task(Multiply).depends_on((&b, &b)); let c = dag.add_task(Multiply).depends_on((&b, &b));
let d = dag.add_task(Add).depends_on((&c, &c)); let d = dag.add_task(Add).depends_on((&c, &c));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("\nResult: {}", dag.get(d).unwrap()); println!("\nResult: {}", dag.get(d).unwrap());
println!("(Expected: 1+1 = 2, 2*2 = 4, 4+4 = 8)\n"); println!("(Expected: 1+1 = 2, 2*2 = 4, 4+4 = 8)\n");
@@ -149,11 +146,9 @@ async fn main() {
let sum_all = dag.add_task(Add).depends_on((&sum12, &sum23)); let sum_all = dag.add_task(Add).depends_on((&sum12, &sum23));
let final_result = dag.add_task(Add).depends_on((&sum_all, &sum34)); let final_result = dag.add_task(Add).depends_on((&sum_all, &sum34));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
println!("\nResult: {}", dag.get(final_result).unwrap()); println!("\nResult: {}", dag.get(final_result).unwrap());
println!("(Expected: (1+2)+(2+3) = 8, 8+(3+4) = 15)\n"); println!("(Expected: (1+2)+(2+3) = 8, 8+(3+4) = 15)\n");
+4 -2
View File
@@ -31,6 +31,7 @@ impl IsUnitType for () {}
/// ///
/// ```no_run /// ```no_run
/// # use dagx::{task, DagRunner, Task}; /// # use dagx::{task, DagRunner, Task};
/// # use futures::FutureExt;
/// // Using tuple struct for simple constants /// // Using tuple struct for simple constants
/// struct Constant(i32); /// struct Constant(i32);
/// ///
@@ -65,7 +66,7 @@ impl IsUnitType for () {}
/// let b = b.depends_on(&a); /// let b = b.depends_on(&a);
/// // Now b is a TaskHandle<i32> /// // Now b is a TaskHandle<i32>
/// ///
/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
/// assert_eq!(dag.get(&b).unwrap(), 20); /// assert_eq!(dag.get(&b).unwrap(), 20);
/// # }; /// # };
/// ``` /// ```
@@ -89,6 +90,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
/// ///
/// ```no_run /// ```no_run
/// # use dagx::{task, DagRunner, Task}; /// # use dagx::{task, DagRunner, Task};
/// # use futures::FutureExt;
/// // Tuple struct /// // Tuple struct
/// struct Value(i32); /// struct Value(i32);
/// ///
@@ -125,7 +127,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
/// // Multiple dependencies: tuple form /// // Multiple dependencies: tuple form
/// let sum = dag.add_task(Add).depends_on((&x, &y)); /// let sum = dag.add_task(Add).depends_on((&x, &y));
/// ///
/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
/// # }; /// # };
/// ``` /// ```
pub fn depends_on<D>(self, deps: D) -> TaskHandle<Tk::Output> pub fn depends_on<D>(self, deps: D) -> TaskHandle<Tk::Output>
+1 -1
View File
@@ -63,7 +63,7 @@ impl std::fmt::Display for DagError {
f, f,
"Result not found for task #{}.\n\ "Result not found for task #{}.\n\
\n\ \n\
Call dag.run() before accessing results with get().", Call dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).",
task_id task_id
) )
} }
+1 -1
View File
@@ -51,7 +51,7 @@ fn test_dag_error_display_result_not_found() {
assert!(display.contains("Result not found")); assert!(display.contains("Result not found"));
assert!(display.contains("task #7")); assert!(display.contains("task #7"));
assert!(display.contains("Call dag.run()")); assert!(display.contains("Call dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))"));
} }
#[test] #[test]
+57 -89
View File
@@ -1,4 +1,4 @@
//! Input extraction for task dependencies via oneshot channels. //! Input extraction for task dependencies via a list of dependencies.
//! //!
//! # Legacy Trait for Internal Use Only //! # Legacy Trait for Internal Use Only
//! //!
@@ -11,7 +11,7 @@
//! //!
//! # How it works //! # How it works
//! //!
//! When a task executes, it needs to extract its inputs from type-erased oneshot receivers. //! When a task executes, it needs to extract its inputs from type-erased oneshot dependencies.
//! Outputs are wrapped in Arc for efficient fanout (cheap Arc clones instead of data clones). //! Outputs are wrapped in Arc for efficient fanout (cheap Arc clones instead of data clones).
//! //!
//! **Modern approach (via `#[task]` macro)**: //! **Modern approach (via `#[task]` macro)**:
@@ -35,9 +35,7 @@
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
use futures::channel::oneshot; /// Helper trait for async input extraction from list of dependencies.
/// Helper trait for async input extraction from oneshot channels.
/// ///
/// Type erasure occurs only at the ExecutableNode trait boundary - by the time /// Type erasure occurs only at the ExecutableNode trait boundary - by the time
/// we're in ExtractInput, we know the concrete type and can safely downcast. /// we're in ExtractInput, we know the concrete type and can safely downcast.
@@ -47,35 +45,34 @@ use futures::channel::oneshot;
/// which allows ANY type to work without requiring ExtractInput implementations. /// which allows ANY type to work without requiring ExtractInput implementations.
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) trait ExtractInput: Sized + Clone { pub(crate) trait ExtractInput: Sized + Clone {
fn extract_from_channels( fn extract_from_deps(
receivers: Vec<Box<dyn Any + Send>>, deps: Vec<Arc<dyn Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self, String>> + Send; ) -> impl std::future::Future<Output = Result<Self, String>> + Send;
} }
// Unit type - no dependencies // Unit type - no dependencies
impl ExtractInput for () { impl ExtractInput for () {
async fn extract_from_channels(_receivers: Vec<Box<dyn Any + Send>>) -> Result<Self, String> { async fn extract_from_deps(_deps: Vec<Arc<dyn Any + Send + Sync>>) -> Result<Self, String> {
Ok(()) Ok(())
} }
} }
// Implementations for common primitive and standard library types. // Implementations for common primitive and standard library types.
// These allow tasks to receive these types as single inputs without wrapping in tuples. // These allow tasks to receive these types as single inputs without wrapping in tuples.
// Receives Arc<T> from channel, clones the inner value for task consumption. // Receives Arc<T> from runner, clones the inner value for task consumption.
// For custom types, implement ExtractInput following the same pattern. // For custom types, implement ExtractInput following the same pattern.
macro_rules! impl_extract_single { macro_rules! impl_extract_single {
($($t:ty),+) => { ($($t:ty),+) => {
$( $(
impl ExtractInput for $t { impl ExtractInput for $t {
async fn extract_from_channels(mut receivers: Vec<Box<dyn Any + Send>>) -> Result<Self, String> { async fn extract_from_deps(mut dependencies: Vec<Arc<dyn Any + Send + Sync>>) -> Result<Self, String> {
if receivers.len() != 1 { if dependencies.len() != 1 {
return Err(format!("Expected 1 dependency, got {}", receivers.len())); return Err(format!("Expected 1 dependency, got {}", dependencies.len()));
} }
let rx = *receivers.pop() let arc_value = dependencies.pop()
.unwrap() .unwrap()
.downcast::<oneshot::Receiver<Arc<$t>>>() .downcast::<$t>()
.map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<$t>()))?; .map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<$t>()))?;
let arc_value = rx.await.map_err(|_| "Channel closed before receiving value".to_string())?;
Ok((*arc_value).clone()) Ok((*arc_value).clone())
} }
} }
@@ -93,16 +90,16 @@ where
T: 'static + Clone + Send + Sync, T: 'static + Clone + Send + Sync,
E: 'static + Clone + Send + Sync, E: 'static + Clone + Send + Sync,
{ {
async fn extract_from_channels( async fn extract_from_deps(
mut receivers: Vec<Box<dyn Any + Send>>, mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> { ) -> Result<Self, String> {
if receivers.len() != 1 { if dependencies.len() != 1 {
return Err(format!("Expected 1 dependency, got {}", receivers.len())); return Err(format!("Expected 1 dependency, got {}", dependencies.len()));
} }
let rx = *receivers let arc_value = dependencies
.pop() .pop()
.unwrap() .unwrap()
.downcast::<oneshot::Receiver<Arc<Result<T, E>>>>() .downcast::<Result<T, E>>()
.map_err(|_| { .map_err(|_| {
format!( format!(
"Type mismatch: expected Arc<Result<{}, {}>>", "Type mismatch: expected Arc<Result<{}, {}>>",
@@ -110,9 +107,6 @@ where
std::any::type_name::<E>() std::any::type_name::<E>()
) )
})?; })?;
let arc_value = rx
.await
.map_err(|_| "Channel closed before receiving value".to_string())?;
Ok((*arc_value).clone()) Ok((*arc_value).clone())
} }
} }
@@ -122,25 +116,22 @@ impl<T> ExtractInput for Option<T>
where where
T: 'static + Clone + Send + Sync, T: 'static + Clone + Send + Sync,
{ {
async fn extract_from_channels( async fn extract_from_deps(
mut receivers: Vec<Box<dyn Any + Send>>, mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> { ) -> Result<Self, String> {
if receivers.len() != 1 { if dependencies.len() != 1 {
return Err(format!("Expected 1 dependency, got {}", receivers.len())); return Err(format!("Expected 1 dependency, got {}", dependencies.len()));
} }
let rx = *receivers let arc_value = dependencies
.pop() .pop()
.unwrap() .unwrap()
.downcast::<oneshot::Receiver<Arc<Option<T>>>>() .downcast::<Option<T>>()
.map_err(|_| { .map_err(|_| {
format!( format!(
"Type mismatch: expected Arc<Option<{}>>", "Type mismatch: expected Arc<Option<{}>>",
std::any::type_name::<T>() std::any::type_name::<T>()
) )
})?; })?;
let arc_value = rx
.await
.map_err(|_| "Channel closed before receiving value".to_string())?;
Ok((*arc_value).clone()) Ok((*arc_value).clone())
} }
} }
@@ -150,25 +141,22 @@ impl<T> ExtractInput for Vec<T>
where where
T: 'static + Clone + Send + Sync, T: 'static + Clone + Send + Sync,
{ {
async fn extract_from_channels( async fn extract_from_deps(
mut receivers: Vec<Box<dyn Any + Send>>, mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> { ) -> Result<Self, String> {
if receivers.len() != 1 { if dependencies.len() != 1 {
return Err(format!("Expected 1 dependency, got {}", receivers.len())); return Err(format!("Expected 1 dependency, got {}", dependencies.len()));
} }
let rx = *receivers let arc_value = dependencies
.pop() .pop()
.unwrap() .unwrap()
.downcast::<oneshot::Receiver<Arc<Vec<T>>>>() .downcast::<Vec<T>>()
.map_err(|_| { .map_err(|_| {
format!( format!(
"Type mismatch: expected Arc<Vec<{}>>", "Type mismatch: expected Arc<Vec<{}>>",
std::any::type_name::<T>() std::any::type_name::<T>()
) )
})?; })?;
let arc_value = rx
.await
.map_err(|_| "Channel closed before receiving value".to_string())?;
Ok((*arc_value).clone()) Ok((*arc_value).clone())
} }
} }
@@ -179,16 +167,16 @@ where
K: 'static + Clone + Send + Sync + Eq + std::hash::Hash, K: 'static + Clone + Send + Sync + Eq + std::hash::Hash,
V: 'static + Clone + Send + Sync, V: 'static + Clone + Send + Sync,
{ {
async fn extract_from_channels( async fn extract_from_deps(
mut receivers: Vec<Box<dyn Any + Send>>, mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> { ) -> Result<Self, String> {
if receivers.len() != 1 { if dependencies.len() != 1 {
return Err(format!("Expected 1 dependency, got {}", receivers.len())); return Err(format!("Expected 1 dependency, got {}", dependencies.len()));
} }
let rx = *receivers let arc_value = dependencies
.pop() .pop()
.unwrap() .unwrap()
.downcast::<oneshot::Receiver<Arc<std::collections::HashMap<K, V>>>>() .downcast::<std::collections::HashMap<K, V>>()
.map_err(|_| { .map_err(|_| {
format!( format!(
"Type mismatch: expected Arc<HashMap<{}, {}>>", "Type mismatch: expected Arc<HashMap<{}, {}>>",
@@ -196,9 +184,6 @@ where
std::any::type_name::<V>() std::any::type_name::<V>()
) )
})?; })?;
let arc_value = rx
.await
.map_err(|_| "Channel closed before receiving value".to_string())?;
Ok((*arc_value).clone()) Ok((*arc_value).clone())
} }
} }
@@ -209,25 +194,22 @@ impl<T> ExtractInput for Arc<T>
where where
T: 'static + Clone + Send + Sync, T: 'static + Clone + Send + Sync,
{ {
async fn extract_from_channels( async fn extract_from_deps(
mut receivers: Vec<Box<dyn Any + Send>>, mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> { ) -> Result<Self, String> {
if receivers.len() != 1 { if dependencies.len() != 1 {
return Err(format!("Expected 1 dependency, got {}", receivers.len())); return Err(format!("Expected 1 dependency, got {}", dependencies.len()));
} }
let rx = *receivers let arc_arc_value = dependencies
.pop() .pop()
.unwrap() .unwrap()
.downcast::<oneshot::Receiver<Arc<Arc<T>>>>() .downcast::<Arc<T>>()
.map_err(|_| { .map_err(|_| {
format!( format!(
"Type mismatch: expected Arc<Arc<{}>>", "Type mismatch: expected Arc<Arc<{}>>",
std::any::type_name::<T>() std::any::type_name::<T>()
) )
})?; })?;
let arc_arc_value = rx
.await
.map_err(|_| "Channel closed before receiving value".to_string())?;
// Return the inner Arc (clone is cheap - just refcount increment) // Return the inner Arc (clone is cheap - just refcount increment)
Ok(Arc::clone(&*arc_arc_value)) Ok(Arc::clone(&*arc_arc_value))
} }
@@ -235,9 +217,6 @@ where
/// Macro to implement ExtractInput for tuple types. /// Macro to implement ExtractInput for tuple types.
/// ///
/// Generates an implementation that extracts values from multiple dependency channels
/// and combines them into a tuple. All channels are awaited concurrently using join_all.
///
/// We need separate implementations for each tuple size (2-8) because Rust doesn't support /// We need separate implementations for each tuple size (2-8) because Rust doesn't support
/// variadic generics. This is standard practice when working with tuples in Rust. /// variadic generics. This is standard practice when working with tuples in Rust.
/// ///
@@ -251,43 +230,32 @@ where
macro_rules! impl_extract_tuple { macro_rules! impl_extract_tuple {
($($T:ident : $idx:tt),+) => { ($($T:ident : $idx:tt),+) => {
impl<$($T: 'static + Clone + Send + Sync),+> ExtractInput for ($($T,)+) { impl<$($T: 'static + Clone + Send + Sync),+> ExtractInput for ($($T,)+) {
async fn extract_from_channels(receivers: Vec<Box<dyn Any + Send>>) -> Result<Self, String> { async fn extract_from_deps(dependencies: Vec<Arc<dyn Any + Send + Sync>>) -> Result<Self, String> {
// Verify we have the correct number of receivers // Verify we have the correct number of dependencies
let expected_count = [$($idx),+].len(); let expected_count = [$($idx),+].len();
if receivers.len() != expected_count { if dependencies.len() != expected_count {
return Err(format!("Expected {} dependencies, got {}", expected_count, receivers.len())); return Err(format!("Expected {} dependencies, got {}", expected_count, dependencies.len()));
} }
// Convert Vec to iterator so we can consume it // Convert Vec to iterator so we can consume it
let mut iter = receivers.into_iter(); let mut iter = dependencies.into_iter();
// Extract each receiver by consuming from the iterator // dependencies now contain Arc-wrapped values for efficient fanout
// Receivers now contain Arc-wrapped values for efficient fanout // Dereference Box after downcasting to get the parameter itself
// Dereference Box after downcasting to get the receiver itself
// Use type names as variable names to avoid numeric suffix issues // Use type names as variable names to avoid numeric suffix issues
$( let arc_results = (
#[allow(non_snake_case)]
let $T = *iter.next()
.ok_or_else(|| format!("Missing receiver at index {}", $idx))?
.downcast::<oneshot::Receiver<Arc<$T>>>()
.map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>",
$idx, std::any::type_name::<$T>()))?;
)+
// Await all channels concurrently - this is a key optimization!
// We can start receiving as soon as ANY dependency completes, not waiting
// for them sequentially.
let arc_results = futures::join!(
$( $(
async move { iter.next()
$T.await.map_err(|_| format!("Channel {} closed", $idx)) .ok_or_else(|| format!("Missing parameter at index {}", $idx))?
} .downcast::<$T>()
),+ .map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>",
$idx, std::any::type_name::<$T>()))?,
)+
); );
// Clone inner values from Arc (cheap for small types, necessary for ownership) // Clone inner values from Arc (cheap for small types, necessary for ownership)
Ok(($( Ok(($(
(*arc_results.$idx?).clone() (*arc_results.$idx).clone()
,)+)) ,)+))
} }
} }
+39 -127
View File
@@ -2,10 +2,9 @@
//! //!
//! These tests specifically target type mismatch error messages that aren't hit //! These tests specifically target type mismatch error messages that aren't hit
//! in normal usage because the #[task] macro generates type-safe code. //! in normal usage because the #[task] macro generates type-safe code.
//! We directly call extract_from_channels with mismatched types. //! We directly call extract_from_deps with mismatched types.
use crate::extract::ExtractInput; use crate::extract::ExtractInput;
use futures::channel::oneshot;
use std::any::Any; use std::any::Any;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@@ -13,17 +12,10 @@ use std::sync::Arc;
#[tokio::test] #[tokio::test]
async fn test_result_type_mismatch_coverage() { async fn test_result_type_mismatch_coverage() {
// Tests line 95 in extract.rs - Result<T,E> type mismatch error message // Tests line 95 in extract.rs - Result<T,E> type mismatch error message
// Create a channel that sends the WRONG type (String instead of Result<i32, String>) // Create a dependency that provides the WRONG type (String instead of Result<i32, String>)
let (tx, rx) = oneshot::channel::<Arc<String>>();
// Send a String value
tokio::spawn(async move {
let _ = tx.send(Arc::new("hello".to_string()));
});
// Try to extract as Result<i32, String> - this will fail with type mismatch // Try to extract as Result<i32, String> - this will fail with type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new("hello") as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_channels(deps).await; let result = Result::<i32, String>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information // Verify the error message contains the expected type information
assert!(result.is_err()); assert!(result.is_err());
@@ -45,17 +37,10 @@ async fn test_result_type_mismatch_coverage() {
#[tokio::test] #[tokio::test]
async fn test_option_type_mismatch_coverage() { async fn test_option_type_mismatch_coverage() {
// Tests line 124 in extract.rs - Option<T> type mismatch error message // Tests line 124 in extract.rs - Option<T> type mismatch error message
// Create a channel that sends the WRONG type (i32 instead of Option<i32>) // Create a dependency that provides the WRONG type (i32 instead of Option<i32>)
let (tx, rx) = oneshot::channel::<Arc<i32>>();
// Send an i32 value
tokio::spawn(async move {
let _ = tx.send(Arc::new(42));
});
// Try to extract as Option<i32> - this will fail with type mismatch // Try to extract as Option<i32> - this will fail with type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new(42) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::extract_from_channels(deps).await; let result = Option::<i32>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information // Verify the error message contains the expected type information
assert!(result.is_err()); assert!(result.is_err());
@@ -77,17 +62,10 @@ async fn test_option_type_mismatch_coverage() {
#[tokio::test] #[tokio::test]
async fn test_vec_type_mismatch_coverage() { async fn test_vec_type_mismatch_coverage() {
// Tests line 152 in extract.rs - Vec<T> type mismatch error message // Tests line 152 in extract.rs - Vec<T> type mismatch error message
// Create a channel that sends the WRONG type (String instead of Vec<u8>) // Create a dependency that provides the WRONG type (String instead of Vec<u8>)
let (tx, rx) = oneshot::channel::<Arc<String>>();
// Send a String value
tokio::spawn(async move {
let _ = tx.send(Arc::new("not a vec".to_string()));
});
// Try to extract as Vec<u8> - this will fail with type mismatch // Try to extract as Vec<u8> - this will fail with type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new("not a vec".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Vec::<u8>::extract_from_channels(deps).await; let result = Vec::<u8>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information // Verify the error message contains the expected type information
assert!(result.is_err()); assert!(result.is_err());
@@ -109,17 +87,10 @@ async fn test_vec_type_mismatch_coverage() {
#[tokio::test] #[tokio::test]
async fn test_hashmap_type_mismatch_coverage() { async fn test_hashmap_type_mismatch_coverage() {
// Tests line 181 in extract.rs - HashMap<K,V> type mismatch error message // Tests line 181 in extract.rs - HashMap<K,V> type mismatch error message
// Create a channel that sends the WRONG type (Vec<i32> instead of HashMap) // Create a dependency that provides the WRONG type (Vec<i32> instead of HashMap)
let (tx, rx) = oneshot::channel::<Arc<Vec<i32>>>();
// Send a Vec value
tokio::spawn(async move {
let _ = tx.send(Arc::new(vec![1, 2, 3]));
});
// Try to extract as HashMap<String, i32> - this will fail with type mismatch // Try to extract as HashMap<String, i32> - this will fail with type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new(vec![1, 2, 3]) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, i32>::extract_from_channels(deps).await; let result = HashMap::<String, i32>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information // Verify the error message contains the expected type information
assert!(result.is_err()); assert!(result.is_err());
@@ -142,18 +113,12 @@ async fn test_hashmap_type_mismatch_coverage() {
async fn test_arc_type_mismatch_coverage() { async fn test_arc_type_mismatch_coverage() {
// Tests line 211 in extract.rs - Arc<T> type mismatch error message // Tests line 211 in extract.rs - Arc<T> type mismatch error message
// Arc is special: it expects Arc<Arc<T>> from channel // Arc is special: it expects Arc<Arc<T>> from channel
// Create a channel that sends Arc<String> instead of Arc<Arc<String>> // Create a dependency that provides Arc<String> instead of Arc<Arc<String>>
let (tx, rx) = oneshot::channel::<Arc<String>>(); let deps = vec![Arc::new("not double arc".to_string()) as Arc<dyn Any + Send + Sync>];
// Send an Arc<String> value (missing one level of Arc wrapping)
tokio::spawn(async move {
let _ = tx.send(Arc::new("not double arc".to_string()));
});
// Try to extract as Arc<String> - this will fail with type mismatch // Try to extract as Arc<String> - this will fail with type mismatch
// because it expects Arc<Arc<String>> in the channel // because it expects Arc<Arc<String>> in the channel
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let result = Arc::<String>::extract_from_deps(deps).await;
let result = Arc::<String>::extract_from_channels(deps).await;
// Verify the error message contains the expected type information // Verify the error message contains the expected type information
assert!(result.is_err()); assert!(result.is_err());
@@ -177,15 +142,9 @@ async fn test_arc_type_mismatch_coverage() {
#[tokio::test] #[tokio::test]
async fn test_result_with_wrong_ok_type() { async fn test_result_with_wrong_ok_type() {
// Test Result with wrong Ok type // Test Result with wrong Ok type
let (tx, rx) = oneshot::channel::<Arc<Result<String, String>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Ok("test".to_string())));
});
// Try to extract as Result<i32, String> - wrong Ok type // Try to extract as Result<i32, String> - wrong Ok type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new(Ok::<_, String>("test".to_string())) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_channels(deps).await; let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.unwrap_err(); let err_msg = result.unwrap_err();
@@ -199,15 +158,9 @@ async fn test_result_with_wrong_ok_type() {
#[tokio::test] #[tokio::test]
async fn test_result_with_wrong_err_type() { async fn test_result_with_wrong_err_type() {
// Test Result with wrong Err type // Test Result with wrong Err type
let (tx, rx) = oneshot::channel::<Arc<Result<i32, i32>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Err(42)));
});
// Try to extract as Result<i32, String> - wrong Err type // Try to extract as Result<i32, String> - wrong Err type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new(Err::<i32, i32>(42)) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_channels(deps).await; let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.unwrap_err(); let err_msg = result.unwrap_err();
@@ -221,15 +174,9 @@ async fn test_result_with_wrong_err_type() {
#[tokio::test] #[tokio::test]
async fn test_option_with_wrong_inner_type() { async fn test_option_with_wrong_inner_type() {
// Test Option with wrong inner type // Test Option with wrong inner type
let (tx, rx) = oneshot::channel::<Arc<Option<String>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Some("test".to_string())));
});
// Try to extract as Option<i32> - wrong inner type // Try to extract as Option<i32> - wrong inner type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new(Some("test".to_string())) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::extract_from_channels(deps).await; let result = Option::<i32>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.unwrap_err(); let err_msg = result.unwrap_err();
@@ -243,15 +190,9 @@ async fn test_option_with_wrong_inner_type() {
#[tokio::test] #[tokio::test]
async fn test_vec_with_wrong_element_type() { async fn test_vec_with_wrong_element_type() {
// Test Vec with wrong element type // Test Vec with wrong element type
let (tx, rx) = oneshot::channel::<Arc<Vec<String>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(vec!["a".to_string(), "b".to_string()]));
});
// Try to extract as Vec<i32> - wrong element type // Try to extract as Vec<i32> - wrong element type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new(vec!["a".to_string(), "b".to_string()]) as Arc<dyn Any + Send + Sync>];
let result = Vec::<i32>::extract_from_channels(deps).await; let result = Vec::<i32>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.unwrap_err(); let err_msg = result.unwrap_err();
@@ -265,17 +206,11 @@ async fn test_vec_with_wrong_element_type() {
#[tokio::test] #[tokio::test]
async fn test_hashmap_with_wrong_key_type() { async fn test_hashmap_with_wrong_key_type() {
// Test HashMap with wrong key type // Test HashMap with wrong key type
let (tx, rx) = oneshot::channel::<Arc<HashMap<i32, String>>>(); let mut map = HashMap::new();
map.insert(1, "one".to_string());
tokio::spawn(async move {
let mut map = HashMap::new();
map.insert(1, "one".to_string());
let _ = tx.send(Arc::new(map));
});
// Try to extract as HashMap<String, String> - wrong key type // Try to extract as HashMap<String, String> - wrong key type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new(map) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, String>::extract_from_channels(deps).await; let result = HashMap::<String, String>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.unwrap_err(); let err_msg = result.unwrap_err();
@@ -289,17 +224,11 @@ async fn test_hashmap_with_wrong_key_type() {
#[tokio::test] #[tokio::test]
async fn test_hashmap_with_wrong_value_type() { async fn test_hashmap_with_wrong_value_type() {
// Test HashMap with wrong value type // Test HashMap with wrong value type
let (tx, rx) = oneshot::channel::<Arc<HashMap<String, i32>>>(); let mut map = HashMap::new();
map.insert("key".to_string(), 42);
tokio::spawn(async move {
let mut map = HashMap::new();
map.insert("key".to_string(), 42);
let _ = tx.send(Arc::new(map));
});
// Try to extract as HashMap<String, String> - wrong value type // Try to extract as HashMap<String, String> - wrong value type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new(map) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, String>::extract_from_channels(deps).await; let result = HashMap::<String, String>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.unwrap_err(); let err_msg = result.unwrap_err();
@@ -314,15 +243,9 @@ async fn test_hashmap_with_wrong_value_type() {
async fn test_arc_with_wrong_inner_type() { async fn test_arc_with_wrong_inner_type() {
// Test Arc with completely wrong inner type // Test Arc with completely wrong inner type
// Send Arc<i32> but expect Arc<String> // Send Arc<i32> but expect Arc<String>
let (tx, rx) = oneshot::channel::<Arc<Arc<i32>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Arc::new(42)));
});
// Try to extract as Arc<String> - wrong inner type // Try to extract as Arc<String> - wrong inner type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new(Arc::new(Arc::new(42))) as Arc<dyn Any + Send + Sync>];
let result = Arc::<String>::extract_from_channels(deps).await; let result = Arc::<String>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.unwrap_err(); let err_msg = result.unwrap_err();
@@ -336,15 +259,9 @@ async fn test_arc_with_wrong_inner_type() {
#[tokio::test] #[tokio::test]
async fn test_primitive_type_mismatch_for_completeness() { async fn test_primitive_type_mismatch_for_completeness() {
// Test primitive type mismatch (already covered in existing tests but good for completeness) // Test primitive type mismatch (already covered in existing tests but good for completeness)
let (tx, rx) = oneshot::channel::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("not an i32".to_string()));
});
// Try to extract as i32 - type mismatch // Try to extract as i32 - type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new("not an i32".to_string()) as Arc<dyn Any + Send + Sync>];
let result = i32::extract_from_channels(deps).await; let result = i32::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.unwrap_err(); let err_msg = result.unwrap_err();
@@ -358,15 +275,10 @@ async fn test_primitive_type_mismatch_for_completeness() {
#[tokio::test] #[tokio::test]
async fn test_complex_nested_result_option() { async fn test_complex_nested_result_option() {
// Test complex nested type: send Result<String, String>, expect Result<Option<i32>, String> // Test complex nested type: send Result<String, String>, expect Result<Option<i32>, String>
let (tx, rx) = oneshot::channel::<Arc<Result<String, String>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Ok("test".to_string())));
});
// Try to extract as Result<Option<i32>, String> - nested type mismatch // Try to extract as Result<Option<i32>, String> - nested type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps =
let result = Result::<Option<i32>, String>::extract_from_channels(deps).await; vec![Arc::new(Ok::<String, String>("test".to_string())) as Arc<dyn Any + Send + Sync>];
let result = Result::<Option<i32>, String>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.unwrap_err(); let err_msg = result.unwrap_err();
+61 -174
View File
@@ -4,7 +4,6 @@
use crate::extract::ExtractInput; use crate::extract::ExtractInput;
use crate::task::Task; use crate::task::Task;
use futures::channel::oneshot;
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
@@ -39,11 +38,11 @@ impl TestTaskWithInput {
async fn test_extract_result_type_single_dependency_error() { async fn test_extract_result_type_single_dependency_error() {
// Test Result<T,E> with wrong dependency count // Test Result<T,E> with wrong dependency count
let deps = vec![ let deps = vec![
Box::new(oneshot::channel::<Result<i32, String>>().1) as Box<dyn Any + Send>, Arc::new(Ok::<i32, String>(5)) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<Result<i32, String>>().1) as Box<dyn Any + Send>, Arc::new(Err::<i32, String>("test".to_string())) as Arc<dyn Any + Send + Sync>,
]; // Two dependencies when expecting 1 ]; // Two dependencies when expecting 1
let result = Result::<i32, String>::extract_from_channels(deps).await; let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2"));
} }
@@ -51,15 +50,9 @@ async fn test_extract_result_type_single_dependency_error() {
#[tokio::test] #[tokio::test]
async fn test_extract_result_type_success() { async fn test_extract_result_type_success() {
// Test Result<T,E> successful extraction // Test Result<T,E> successful extraction
let (tx, rx) = oneshot::channel::<Arc<Result<i32, String>>>();
// Send value in background let deps = vec![Arc::new(Ok::<i32, String>(42)) as Arc<dyn Any + Send + Sync>];
tokio::spawn(async move { let result = Result::<i32, String>::extract_from_deps(deps).await;
let _ = tx.send(Arc::new(Ok(42)));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Result::<i32, String>::extract_from_channels(deps).await;
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), Ok(42)); assert_eq!(result.unwrap(), Ok(42));
} }
@@ -68,12 +61,12 @@ async fn test_extract_result_type_success() {
async fn test_extract_option_type_single_dependency_error() { async fn test_extract_option_type_single_dependency_error() {
// Test Option<T> with wrong dependency count // Test Option<T> with wrong dependency count
let deps = vec![ let deps = vec![
Box::new(oneshot::channel::<Option<String>>().1) as Box<dyn Any + Send>, Arc::new(None::<String>) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<Option<String>>().1) as Box<dyn Any + Send>, Arc::new(None::<String>) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<Option<String>>().1) as Box<dyn Any + Send>, Arc::new(None::<String>) as Arc<dyn Any + Send + Sync>,
]; // Three dependencies when expecting 1 ]; // Three dependencies when expecting 1
let result = Option::<String>::extract_from_channels(deps).await; let result = Option::<String>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 3")); assert!(result.unwrap_err().contains("Expected 1 dependency, got 3"));
} }
@@ -81,15 +74,8 @@ async fn test_extract_option_type_single_dependency_error() {
#[tokio::test] #[tokio::test]
async fn test_extract_option_type_success() { async fn test_extract_option_type_success() {
// Test Option<T> successful extraction // Test Option<T> successful extraction
let (tx, rx) = oneshot::channel::<Arc<Option<i32>>>(); let deps = vec![Arc::new(Some(99)) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::extract_from_deps(deps).await;
// Send value in background
tokio::spawn(async move {
let _ = tx.send(Arc::new(Some(99)));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Option::<i32>::extract_from_channels(deps).await;
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(99)); assert_eq!(result.unwrap(), Some(99));
} }
@@ -99,7 +85,7 @@ async fn test_extract_vec_type_single_dependency_error() {
// Test Vec<T> with wrong dependency count // Test Vec<T> with wrong dependency count
let deps = vec![]; // Zero dependencies when expecting 1 let deps = vec![]; // Zero dependencies when expecting 1
let result = Vec::<u8>::extract_from_channels(deps).await; let result = Vec::<u8>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 0")); assert!(result.unwrap_err().contains("Expected 1 dependency, got 0"));
} }
@@ -107,15 +93,8 @@ async fn test_extract_vec_type_single_dependency_error() {
#[tokio::test] #[tokio::test]
async fn test_extract_vec_type_success() { async fn test_extract_vec_type_success() {
// Test Vec<T> successful extraction // Test Vec<T> successful extraction
let (tx, rx) = oneshot::channel::<Arc<Vec<i32>>>(); let deps = vec![Arc::new(vec![1, 2, 3]) as Arc<dyn Any + Send + Sync>];
let result = Vec::<i32>::extract_from_deps(deps).await;
// Send value in background
tokio::spawn(async move {
let _ = tx.send(Arc::new(vec![1, 2, 3]));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Vec::<i32>::extract_from_channels(deps).await;
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), vec![1, 2, 3]); assert_eq!(result.unwrap(), vec![1, 2, 3]);
} }
@@ -124,29 +103,29 @@ async fn test_extract_vec_type_success() {
async fn test_extract_primitive_single_dependency_error() { async fn test_extract_primitive_single_dependency_error() {
// Test primitive types with wrong dependency count // Test primitive types with wrong dependency count
let deps = vec![ let deps = vec![
Box::new(oneshot::channel::<i32>().1) as Box<dyn Any + Send>, Arc::new(0) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<i32>().1) as Box<dyn Any + Send>, Arc::new(0) as Arc<dyn Any + Send + Sync>,
]; // Two dependencies when expecting 1 ]; // Two dependencies when expecting 1
let result = i32::extract_from_channels(deps).await; let result = i32::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2"));
// Test with String // Test with String
let deps = vec![ let deps = vec![
Box::new(oneshot::channel::<String>().1) as Box<dyn Any + Send>, Arc::new(String::default()) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<String>().1) as Box<dyn Any + Send>, Arc::new(String::default()) as Arc<dyn Any + Send + Sync>,
]; ];
let result = String::extract_from_channels(deps).await; let result = String::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2"));
// Test with f64 // Test with f64
let deps = vec![ let deps = vec![
Box::new(oneshot::channel::<f64>().1) as Box<dyn Any + Send>, Arc::new(0.0) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<f64>().1) as Box<dyn Any + Send>, Arc::new(0.0) as Arc<dyn Any + Send + Sync>,
]; ];
let result = f64::extract_from_channels(deps).await; let result = f64::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2"));
} }
@@ -155,67 +134,38 @@ async fn test_extract_primitive_single_dependency_error() {
async fn test_extract_unit_type() { async fn test_extract_unit_type() {
// Test that unit type always succeeds regardless of dependencies // Test that unit type always succeeds regardless of dependencies
let deps = vec![]; let deps = vec![];
let result = <()>::extract_from_channels(deps).await; let result = <()>::extract_from_deps(deps).await;
assert!(result.is_ok()); assert!(result.is_ok());
// Even with dependencies (ignored), unit type succeeds // Even with dependencies (ignored), unit type succeeds
let deps = vec![ let deps = vec![
Box::new(oneshot::channel::<i32>().1) as Box<dyn Any + Send>, Arc::new(0) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<i32>().1) as Box<dyn Any + Send>, Arc::new(0) as Arc<dyn Any + Send + Sync>,
]; ];
let result = <()>::extract_from_channels(deps).await; let result = <()>::extract_from_deps(deps).await;
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test] #[tokio::test]
async fn test_extract_tuple_basic() { async fn test_extract_tuple_basic() {
// Test tuple extraction with channels // Test tuple extraction
let (tx1, rx1) = oneshot::channel::<Arc<i32>>();
let (tx2, rx2) = oneshot::channel::<Arc<i32>>();
// Send values in background
tokio::spawn(async move {
let _ = tx1.send(Arc::new(42));
let _ = tx2.send(Arc::new(100));
});
let deps = vec![ let deps = vec![
Box::new(rx1) as Box<dyn Any + Send>, Arc::new(42) as Arc<dyn Any + Send + Sync>,
Box::new(rx2) as Box<dyn Any + Send>, Arc::new(100) as Arc<dyn Any + Send + Sync>,
]; ];
// Extract a tuple (i32, i32) // Extract a tuple (i32, i32)
let result = <(i32, i32)>::extract_from_channels(deps).await; let result = <(i32, i32)>::extract_from_deps(deps).await;
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), (42, 100)); assert_eq!(result.unwrap(), (42, 100));
} }
#[tokio::test]
async fn test_extract_channel_closed() {
// Test error when channel is closed
let (tx, rx) = oneshot::channel::<Arc<i32>>();
// Explicitly drop tx to close the channel
drop(tx);
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = i32::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Channel closed"));
}
#[tokio::test] #[tokio::test]
async fn test_extract_type_mismatch() { async fn test_extract_type_mismatch() {
// Test type mismatch during downcast // Test type mismatch during downcast
let (tx, rx) = oneshot::channel::<String>();
// Send a String
tokio::spawn(async move {
let _ = tx.send("hello".to_string());
});
// Try to extract as wrong type - put wrong receiver type in deps // Try to extract as wrong type - put wrong receiver type in deps
let deps = vec![Box::new(rx) as Box<dyn Any + Send>]; let deps = vec![Arc::new("hello".to_string()) as Arc<dyn Any + Send + Sync>];
let result = i32::extract_from_channels(deps).await; let result = i32::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch")); assert!(result.unwrap_err().contains("Type mismatch"));
} }
@@ -225,14 +175,8 @@ async fn test_extract_all_primitive_types() {
// Test that all primitive types in impl_extract_single! work correctly // Test that all primitive types in impl_extract_single! work correctly
macro_rules! test_type { macro_rules! test_type {
($t:ty, $val:expr) => { ($t:ty, $val:expr) => {
let (tx, rx) = oneshot::channel::<Arc<$t>>(); let deps = vec![Arc::new($val) as Arc<dyn Any + Send + Sync>];
let result = <$t>::extract_from_deps(deps).await;
tokio::spawn(async move {
let _ = tx.send(Arc::new($val));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = <$t>::extract_from_channels(deps).await;
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), $val); assert_eq!(result.unwrap(), $val);
}; };
@@ -248,31 +192,16 @@ async fn test_extract_all_primitive_types() {
#[tokio::test] #[tokio::test]
async fn test_extract_larger_tuples() { async fn test_extract_larger_tuples() {
// Test extraction for larger tuple sizes // Test extraction for larger tuple sizes
let (tx1, rx1) = oneshot::channel::<Arc<i32>>();
let (tx2, rx2) = oneshot::channel::<Arc<i32>>();
let (tx3, rx3) = oneshot::channel::<Arc<i32>>();
let (tx4, rx4) = oneshot::channel::<Arc<i32>>();
let (tx5, rx5) = oneshot::channel::<Arc<i32>>();
// Send values in background
tokio::spawn(async move {
let _ = tx1.send(Arc::new(0));
let _ = tx2.send(Arc::new(1));
let _ = tx3.send(Arc::new(2));
let _ = tx4.send(Arc::new(3));
let _ = tx5.send(Arc::new(4));
});
let deps = vec![ let deps = vec![
Box::new(rx1) as Box<dyn Any + Send>, Arc::new(0) as Arc<dyn Any + Send + Sync>,
Box::new(rx2) as Box<dyn Any + Send>, Arc::new(1) as Arc<dyn Any + Send + Sync>,
Box::new(rx3) as Box<dyn Any + Send>, Arc::new(2) as Arc<dyn Any + Send + Sync>,
Box::new(rx4) as Box<dyn Any + Send>, Arc::new(3) as Arc<dyn Any + Send + Sync>,
Box::new(rx5) as Box<dyn Any + Send>, Arc::new(4) as Arc<dyn Any + Send + Sync>,
]; ];
// Extract a 5-element tuple // Extract a 5-element tuple
let result = <(i32, i32, i32, i32, i32)>::extract_from_channels(deps).await; let result = <(i32, i32, i32, i32, i32)>::extract_from_deps(deps).await;
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), (0, 1, 2, 3, 4)); assert_eq!(result.unwrap(), (0, 1, 2, 3, 4));
} }
@@ -280,17 +209,11 @@ async fn test_extract_larger_tuples() {
#[tokio::test] #[tokio::test]
async fn test_extract_arc_type() { async fn test_extract_arc_type() {
// Test Arc<T> extraction // Test Arc<T> extraction
let (tx, rx) = oneshot::channel::<Arc<Arc<String>>>();
let test_arc = Arc::new("test".to_string()); let test_arc = Arc::new("test".to_string());
let test_arc_clone = test_arc.clone(); let test_arc_clone = test_arc.clone();
tokio::spawn(async move { let deps = vec![Arc::new(test_arc) as Arc<dyn Any + Send + Sync>];
let _ = tx.send(Arc::new(test_arc)); let result = Arc::<String>::extract_from_deps(deps).await;
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Arc::<String>::extract_from_channels(deps).await;
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(*result.unwrap(), *test_arc_clone); assert_eq!(*result.unwrap(), *test_arc_clone);
} }
@@ -301,74 +224,43 @@ use std::collections::HashMap;
#[tokio::test] #[tokio::test]
async fn test_hashmap_extract_wrong_dependency_count() { async fn test_hashmap_extract_wrong_dependency_count() {
let deps = vec![ let deps = vec![
Box::new(oneshot::channel::<Arc<HashMap<String, i32>>>().1) as Box<dyn Any + Send>, Arc::new(HashMap::<String, i32>::default()) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<Arc<HashMap<String, i32>>>().1) as Box<dyn Any + Send>, Arc::new(HashMap::<String, i32>::default()) as Arc<dyn Any + Send + Sync>,
]; ];
let result = HashMap::<String, i32>::extract_from_channels(deps).await; let result = HashMap::<String, i32>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2"));
} }
#[tokio::test] #[tokio::test]
async fn test_hashmap_extract_type_mismatch() { async fn test_hashmap_extract_type_mismatch() {
let (tx, rx) = oneshot::channel::<Arc<String>>(); let deps = vec![Arc::new("wrong type".to_string()) as Arc<dyn Any + Send + Sync>];
tokio::spawn(async move { let result = HashMap::<String, i32>::extract_from_deps(deps).await;
let _ = tx.send(Arc::new("wrong type".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = HashMap::<String, i32>::extract_from_channels(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch")); assert!(result.unwrap_err().contains("Type mismatch"));
} }
#[tokio::test]
async fn test_hashmap_extract_channel_closed() {
let (tx, rx) = oneshot::channel::<Arc<HashMap<String, i32>>>();
drop(tx);
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = HashMap::<String, i32>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Channel closed"));
}
#[tokio::test] #[tokio::test]
async fn test_result_extract_downcast_failure() { async fn test_result_extract_downcast_failure() {
let (tx, rx) = oneshot::channel::<Arc<String>>(); let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
tokio::spawn(async move { let result = Result::<i32, String>::extract_from_deps(deps).await;
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Result::<i32, String>::extract_from_channels(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch")); assert!(result.unwrap_err().contains("Type mismatch"));
} }
#[tokio::test] #[tokio::test]
async fn test_option_extract_downcast_failure() { async fn test_option_extract_downcast_failure() {
let (tx, rx) = oneshot::channel::<Arc<String>>(); let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
tokio::spawn(async move { let result = Option::<i32>::extract_from_deps(deps).await;
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Option::<i32>::extract_from_channels(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch")); assert!(result.unwrap_err().contains("Type mismatch"));
} }
#[tokio::test] #[tokio::test]
async fn test_vec_extract_downcast_failure() { async fn test_vec_extract_downcast_failure() {
let (tx, rx) = oneshot::channel::<Arc<String>>(); let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
tokio::spawn(async move { let result = Vec::<i32>::extract_from_deps(deps).await;
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Vec::<i32>::extract_from_channels(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch")); assert!(result.unwrap_err().contains("Type mismatch"));
} }
@@ -376,20 +268,15 @@ async fn test_vec_extract_downcast_failure() {
#[tokio::test] #[tokio::test]
async fn test_arc_extract_wrong_dependency_count() { async fn test_arc_extract_wrong_dependency_count() {
let deps = vec![]; let deps = vec![];
let result = Arc::<i32>::extract_from_channels(deps).await; let result = Arc::<i32>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 0")); assert!(result.unwrap_err().contains("Expected 1 dependency, got 0"));
} }
#[tokio::test] #[tokio::test]
async fn test_arc_extract_downcast_failure() { async fn test_arc_extract_downcast_failure() {
let (tx, rx) = oneshot::channel::<Arc<String>>(); let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
tokio::spawn(async move { let result = Arc::<i32>::extract_from_deps(deps).await;
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Arc::<i32>::extract_from_channels(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch")); assert!(result.unwrap_err().contains("Type mismatch"));
} }
@@ -397,12 +284,12 @@ async fn test_arc_extract_downcast_failure() {
#[tokio::test] #[tokio::test]
async fn test_tuple_wrong_dependency_count() { async fn test_tuple_wrong_dependency_count() {
let deps = vec![ let deps = vec![
Box::new(oneshot::channel::<Arc<i32>>().1) as Box<dyn Any + Send>, Arc::new(0) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<Arc<i32>>().1) as Box<dyn Any + Send>, Arc::new(0) as Arc<dyn Any + Send + Sync>,
Box::new(oneshot::channel::<Arc<i32>>().1) as Box<dyn Any + Send>, Arc::new(0) as Arc<dyn Any + Send + Sync>,
]; ];
let result = <(i32, i32)>::extract_from_channels(deps).await; let result = <(i32, i32)>::extract_from_deps(deps).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result assert!(result
.unwrap_err() .unwrap_err()
+18 -11
View File
@@ -24,6 +24,7 @@
//! //!
//! ```no_run //! ```no_run
//! use dagx::{task, DagRunner, Task}; //! use dagx::{task, DagRunner, Task};
//! use futures::FutureExt; // for FutureExt::map
//! //!
//! // Source task with read-only state (tuple struct) //! // Source task with read-only state (tuple struct)
//! struct Value(i32); //! struct Value(i32);
@@ -52,7 +53,7 @@
//! let sum = dag.add_task(Add).depends_on((&x, &y)); //! let sum = dag.add_task(Add).depends_on((&x, &y));
//! //!
//! // Execute and retrieve results //! // Execute and retrieve results
//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); //! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
//! assert_eq!(dag.get(sum).unwrap(), 5); //! assert_eq!(dag.get(sum).unwrap(), 5);
//! # }; //! # };
//! ``` //! ```
@@ -64,7 +65,7 @@
//! dagx automatically optimizes execution for sequential workloads using an inline fast-path. //! dagx automatically optimizes execution for sequential workloads using an inline fast-path.
//! When a layer contains only a single task (common in deep chains and linear pipelines), //! When a layer contains only a single task (common in deep chains and linear pipelines),
//! that task executes inline rather than being spawned. This eliminates spawning overhead, //! that task executes inline rather than being spawned. This eliminates spawning overhead,
//! context switching, and channel creation. //! and context switching.
//! //!
//! **Panic handling**: To maintain consistent behavior between inline and spawned execution, //! **Panic handling**: To maintain consistent behavior between inline and spawned execution,
//! panics in inline tasks are caught using `FutureExt::catch_unwind()` and converted to //! panics in inline tasks are caught using `FutureExt::catch_unwind()` and converted to
@@ -290,6 +291,7 @@
//! //!
//! ```no_run //! ```no_run
//! use dagx::{task, DagRunner, Task}; //! use dagx::{task, DagRunner, Task};
//! use futures::FutureExt;
//! //!
//! // Source task (tuple struct) //! // Source task (tuple struct)
//! struct Value(i32); //! struct Value(i32);
@@ -322,7 +324,7 @@
//! let plus1 = dag.add_task(Add(1)).depends_on(&base); //! let plus1 = dag.add_task(Add(1)).depends_on(&base);
//! let times2 = dag.add_task(Scale(2)).depends_on(&base); //! let times2 = dag.add_task(Scale(2)).depends_on(&base);
//! //!
//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); //! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
//! //!
//! assert_eq!(dag.get(plus1).unwrap(), 11); //! assert_eq!(dag.get(plus1).unwrap(), 11);
//! assert_eq!(dag.get(times2).unwrap(), 20); //! assert_eq!(dag.get(times2).unwrap(), 20);
@@ -335,6 +337,7 @@
//! //!
//! ```no_run //! ```no_run
//! use dagx::{task, DagRunner, Task}; //! use dagx::{task, DagRunner, Task};
//! use futures::FutureExt;
//! //!
//! // Source task for String (tuple struct) //! // Source task for String (tuple struct)
//! struct Name(String); //! struct Name(String);
@@ -378,7 +381,7 @@
//! let active = dag.add_task(Active(true)); //! let active = dag.add_task(Active(true));
//! let result = dag.add_task(FormatUser).depends_on((&name, &age, &active)); //! let result = dag.add_task(FormatUser).depends_on((&name, &age, &active));
//! //!
//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); //! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
//! //!
//! assert_eq!(dag.get(result).unwrap(), "User: Alice, Age: 30, Active: true"); //! assert_eq!(dag.get(result).unwrap(), "User: Alice, Age: 30, Active: true");
//! # }; //! # };
@@ -390,6 +393,7 @@
//! //!
//! ```no_run //! ```no_run
//! use dagx::{task, DagRunner, Task}; //! use dagx::{task, DagRunner, Task};
//! use futures::FutureExt;
//! //!
//! // Source task (tuple struct) //! // Source task (tuple struct)
//! struct Value(i32); //! struct Value(i32);
@@ -430,7 +434,7 @@
//! // Layer 3: Final result //! // Layer 3: Final result
//! let total = dag.add_task(Add).depends_on((&sum_xy, &prod_yz)); // 5 + 15 = 20 //! let total = dag.add_task(Add).depends_on((&sum_xy, &prod_yz)); // 5 + 15 = 20
//! //!
//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); //! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
//! //!
//! assert_eq!(dag.get(total).unwrap(), 20); //! assert_eq!(dag.get(total).unwrap(), 20);
//! # }; //! # };
@@ -442,7 +446,8 @@
//! the `#[task]` macro generates the necessary extraction logic: //! the `#[task]` macro generates the necessary extraction logic:
//! //!
//! ```no_run //! ```no_run
//! use dagx::{task, DagRunner, Task}; //! # use dagx::{task, DagRunner, Task};
//! # use futures::FutureExt;
//! //!
//! // Just derive Clone - that's all you need! //! // Just derive Clone - that's all you need!
//! #[derive(Clone)] //! #[derive(Clone)]
@@ -473,7 +478,7 @@
//! let user = dag.add_task(CreateUser); //! let user = dag.add_task(CreateUser);
//! let formatted = dag.add_task(FormatUser).depends_on(&user); //! let formatted = dag.add_task(FormatUser).depends_on(&user);
//! //!
//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); //! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
//! //!
//! assert_eq!(dag.get(formatted).unwrap(), "Alice is 30 years old"); //! assert_eq!(dag.get(formatted).unwrap(), "Alice is 30 years old");
//! # }; //! # };
@@ -530,6 +535,7 @@
//! //!
//! ```no_run //! ```no_run
//! # use dagx::{task, DagRunner, Task}; //! # use dagx::{task, DagRunner, Task};
//! # use futures::FutureExt;
//! # struct Value(i32); //! # struct Value(i32);
//! # #[task] //! # #[task]
//! # impl Value { //! # impl Value {
@@ -539,11 +545,11 @@
//! # let dag = DagRunner::new(); //! # let dag = DagRunner::new();
//! # let node = dag.add_task(Value(42)); //! # let node = dag.add_task(Value(42));
//! // Simple approach with .unwrap() //! // Simple approach with .unwrap()
//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); //! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
//! let result = dag.get(node).unwrap(); //! let result = dag.get(node).unwrap();
//! //!
//! // Or handle errors explicitly //! // Or handle errors explicitly
//! match dag.run(|fut| { tokio::spawn(fut); }).await { //! match dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await {
//! Ok(_) => println!("DAG executed successfully"), //! Ok(_) => println!("DAG executed successfully"),
//! Err(e) => eprintln!("DAG execution failed: {}", e), //! Err(e) => eprintln!("DAG execution failed: {}", e),
//! } //! }
@@ -626,7 +632,8 @@
//! You output `T`, the framework handles the Arc wrapping: //! You output `T`, the framework handles the Arc wrapping:
//! //!
//! ``` //! ```
//! use dagx::{task, DagRunner, Task}; //! # use dagx::{task, DagRunner, Task};
//! # use futures::FutureExt;
//! //!
//! // ✅ CORRECT: Just output Vec<String>, framework wraps in Arc internally //! // ✅ CORRECT: Just output Vec<String>, framework wraps in Arc internally
//! struct FetchData; //! struct FetchData;
@@ -656,7 +663,7 @@
//! let task2 = dag.add_task(ProcessData).depends_on(&data); //! let task2 = dag.add_task(ProcessData).depends_on(&data);
//! let task3 = dag.add_task(ProcessData).depends_on(&data); //! let task3 = dag.add_task(ProcessData).depends_on(&data);
//! //!
//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); //! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
//! # }; //! # };
//! ``` //! ```
//! //!
+10 -54
View File
@@ -11,18 +11,10 @@ use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use futures::channel::oneshot;
use crate::error::DagError; use crate::error::DagError;
use crate::task::Task; use crate::task::Task;
use crate::types::NodeId; use crate::types::NodeId;
/// Type-erased channel endpoints (senders or receivers)
type ErasedChannels = Vec<Box<dyn Any + Send>>;
/// Type-erased channel pair (senders, receivers)
type ChannelPair = (ErasedChannels, ErasedChannels);
/// Type-erased async execution result (Arc-wrapped for efficient fanout) /// Type-erased async execution result (Arc-wrapped for efficient fanout)
type ExecuteFuture = type ExecuteFuture =
Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, DagError>> + Send>>; Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, DagError>> + Send>>;
@@ -33,26 +25,18 @@ type ExecuteFuture =
/// collection. While the public API (Task, Handle, Node) is fully type-safe, internally we /// collection. While the public API (Task, Handle, Node) is fully type-safe, internally we
/// need dynamic dispatch to execute tasks with different input/output types. /// need dynamic dispatch to execute tasks with different input/output types.
/// ///
/// This is the ONLY place where type erasure occurs - channels are created with full type /// This is the ONLY place where type erasure occurs - tasks are created with full type
/// information and only erased at this trait boundary. /// information and only erased at this trait boundary.
/// ///
/// This is an implementation detail and not part of the public API. /// This is an implementation detail and not part of the public API.
pub(crate) trait ExecutableNode: Send { pub(crate) trait ExecutableNode: Send {
/// Create type-erased output channels for this node's dependents. /// Execute the task with type-erased inputs and output
///
/// Returns (senders, receivers) where:
/// - senders: Type-erased senders that this node will use to send outputs
/// - receivers: Type-erased receivers that dependents will use to receive inputs
fn create_output_channels(&self, num_dependents: usize) -> ChannelPair;
/// Execute the task with type-erased input receivers and output senders.
/// ///
/// The TypedNode implementation knows the concrete types and can safely downcast. /// The TypedNode implementation knows the concrete types and can safely downcast.
/// Consumes the node and returns the output value. /// Consumes the node and returns the output value.
fn execute_with_channels( fn execute_with_deps(
self: Box<Self>, self: Box<Self>,
receivers: ErasedChannels, dependencies: Vec<Arc<dyn Any + Send + Sync>>,
senders: ErasedChannels,
) -> ExecuteFuture; ) -> ExecuteFuture;
} }
@@ -66,10 +50,7 @@ pub(crate) trait ExecutableNode: Send {
/// The task is owned directly (no Mutex needed) and consumed during execution. /// The task is owned directly (no Mutex needed) and consumed during execution.
/// Each task executes exactly once, taking ownership and producing an output. /// Each task executes exactly once, taking ownership and producing an output.
/// ///
/// # Oneshot Channels /// The output is also returned from execute_with_deps for central storage.
///
/// Outputs are communicated via oneshot channels for inter-task communication.
/// The output is also returned from execute_with_channels for central storage.
pub(crate) struct TypedNode<T: Task> { pub(crate) struct TypedNode<T: Task> {
pub(crate) id: NodeId, pub(crate) id: NodeId,
pub(crate) task: T, pub(crate) task: T,
@@ -86,33 +67,19 @@ where
T::Input: 'static + Clone, T::Input: 'static + Clone,
T::Output: 'static + Clone, T::Output: 'static + Clone,
{ {
fn create_output_channels(&self, num_dependents: usize) -> ChannelPair { fn execute_with_deps(
let mut senders = Vec::new();
let mut receivers = Vec::new();
for _ in 0..num_dependents {
// Create channels that pass Arc-wrapped outputs (efficient for fanout)
let (tx, rx) = oneshot::channel::<Arc<T::Output>>();
senders.push(Box::new(tx) as Box<dyn Any + Send>);
receivers.push(Box::new(rx) as Box<dyn Any + Send>);
}
(senders, receivers)
}
fn execute_with_channels(
self: Box<Self>, self: Box<Self>,
receivers: ErasedChannels, dependencies: Vec<Arc<dyn Any + Send + Sync>>,
senders: ErasedChannels,
) -> ExecuteFuture { ) -> ExecuteFuture {
Box::pin(async move { Box::pin(async move {
// Use the task's extract_and_run method which has inline extraction logic // Use the task's extract_and_run method which has inline extraction logic
// generated by the #[task] macro. This allows ANY type to work without // generated by the #[task] macro. This allows ANY type to work without
// requiring ExtractInput trait implementations. // requiring ExtractInput trait implementations.
let output = match self.task.extract_and_run(receivers).await { let output = match self.task.extract_and_run(dependencies).await {
Ok(output) => output, Ok(output) => output,
Err(_msg) => { Err(_msg) => {
// Failed to extract input - likely channel closed or type mismatch // Failed to extract input - likely type mismatch.
// This should technically be infallible, so it might be worth removing in the future.
return Err(DagError::InvalidDependency { task_id: self.id.0 }); return Err(DagError::InvalidDependency { task_id: self.id.0 });
} }
}; };
@@ -124,17 +91,6 @@ where
// Downstream tasks receive &T after extraction from Arc. // Downstream tasks receive &T after extraction from Arc.
let arc_output = Arc::new(output); let arc_output = Arc::new(output);
// Send Arc to all dependents via their channels
// TypedNode<T> knows T::Output, so downcast is safe
for sender_any in senders {
if let Ok(sender) = sender_any.downcast::<oneshot::Sender<Arc<T::Output>>>() {
// Clone Arc (just atomic refcount increment, NOT data clone)
// This is the key performance optimization for fan-out patterns
// Ignore send errors - dependent might have been cancelled
let _ = sender.send(Arc::clone(&arc_output));
}
}
// Return Arc-wrapped output (for storage by runner) // Return Arc-wrapped output (for storage by runner)
Ok(arc_output as Arc<dyn Any + Send + Sync>) Ok(arc_output as Arc<dyn Any + Send + Sync>)
}) })
+20 -92
View File
@@ -1,5 +1,7 @@
//! Unit tests for node module //! Unit tests for node module
use std::sync::Arc;
use crate::node::{ExecutableNode, TypedNode}; use crate::node::{ExecutableNode, TypedNode};
use crate::task::Task; use crate::task::Task;
use crate::types::NodeId; use crate::types::NodeId;
@@ -16,28 +18,6 @@ impl TestTask {
} }
} }
#[test]
fn test_create_output_channels() {
let node = TypedNode::new(NodeId(0), TestTask { value: 42 });
// Create channels for 3 dependents
let (senders, receivers) = node.create_output_channels(3);
assert_eq!(senders.len(), 3);
assert_eq!(receivers.len(), 3);
}
#[test]
fn test_create_output_channels_zero_dependents() {
let node = TypedNode::new(NodeId(0), TestTask { value: 42 });
// Create channels for 0 dependents (sink node)
let (senders, receivers) = node.create_output_channels(0);
assert_eq!(senders.len(), 0);
assert_eq!(receivers.len(), 0);
}
struct TaskWithDependency; struct TaskWithDependency;
#[crate::task] #[crate::task]
@@ -48,15 +28,14 @@ impl TaskWithDependency {
} }
#[tokio::test] #[tokio::test]
async fn test_execute_with_channels_missing_input() { async fn test_execute_with_deps_missing_input() {
// Test when task expects input but doesn't receive it // Test when task expects input but doesn't receive it
let node = Box::new(TypedNode::new(NodeId(0), TaskWithDependency)); let node = Box::new(TypedNode::new(NodeId(0), TaskWithDependency));
// TaskWithDependency expects an i32 input, but we provide no receivers // TaskWithDependency expects an i32 input, but we provide no dependencies
let receivers = vec![]; let dependencies = vec![];
let senders = vec![];
let result = node.execute_with_channels(receivers, senders).await; let result = node.execute_with_deps(dependencies).await;
assert!(result.is_err()); assert!(result.is_err());
if let Err(crate::error::DagError::InvalidDependency { task_id }) = result { if let Err(crate::error::DagError::InvalidDependency { task_id }) = result {
@@ -67,49 +46,38 @@ async fn test_execute_with_channels_missing_input() {
} }
#[tokio::test] #[tokio::test]
async fn test_execute_with_channels_success() { async fn test_execute_with_deps_success() {
// Test successful execution with channels // Test successful execution with channels
let dependency_node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 21 })); let dependency_node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 21 }));
let dependent_node = Box::new(TypedNode::new(NodeId(1), TaskWithDependency)); let dependent_node = Box::new(TypedNode::new(NodeId(1), TaskWithDependency));
// Create a channel from dependency to dependent (Arc-wrapped)
let (tx, rx) = futures::channel::oneshot::channel::<std::sync::Arc<i32>>();
// Execute dependency node with sender // Execute dependency node with sender
let dep_senders = vec![Box::new(tx) as Box<dyn std::any::Any + Send>]; let dep_dependencies = vec![];
let dep_receivers = vec![];
// Execute in background // Execute in background
let dep_handle = tokio::spawn(async move { let dep_result =
dependency_node tokio::spawn(async move { dependency_node.execute_with_deps(dep_dependencies).await })
.execute_with_channels(dep_receivers, dep_senders)
.await .await
}); .unwrap()
.unwrap();
// Execute dependent node with receiver // Execute dependent node with receiver
let receivers = vec![Box::new(rx) as Box<dyn std::any::Any + Send>]; let dependencies = vec![dep_result as Arc<dyn std::any::Any + Send + Sync>];
let senders = vec![];
let result = dependent_node let result = dependent_node.execute_with_deps(dependencies).await;
.execute_with_channels(receivers, senders)
.await;
// Wait for dependency to complete
let _ = dep_handle.await;
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[tokio::test] #[tokio::test]
async fn test_execute_with_channels_sink_node() { async fn test_execute_with_deps_sink_node() {
// Test that sink nodes (no dependents) return their output // Test that sink nodes (no dependents) return their output
let node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 42 })); let node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 42 }));
// No senders means this is a sink node // No senders means this is a sink node
let receivers = vec![]; let dependencies = vec![];
let senders = vec![];
let result = node.execute_with_channels(receivers, senders).await; let result = node.execute_with_deps(dependencies).await;
assert!(result.is_ok()); assert!(result.is_ok());
// Check output was returned (Arc-wrapped) // Check output was returned (Arc-wrapped)
@@ -119,58 +87,18 @@ async fn test_execute_with_channels_sink_node() {
} }
#[tokio::test] #[tokio::test]
async fn test_execute_with_channels_non_sink_node() { async fn test_execute_with_deps_non_sink_node() {
// Test that non-sink nodes send output via channels // Test that non-sink nodes send output via channels
let node = TypedNode::new(NodeId(0), TestTask { value: 42 }); let node = TypedNode::new(NodeId(0), TestTask { value: 42 });
// Create output channels for one dependent
let (senders, mut receivers) = node.create_output_channels(1);
// Execute with senders (non-sink node) // Execute with senders (non-sink node)
let node = Box::new(node); let node = Box::new(node);
let exec_receivers = vec![]; let result = node.execute_with_deps(vec![]).await;
let result = node.execute_with_channels(exec_receivers, senders).await;
assert!(result.is_ok()); assert!(result.is_ok());
// Verify output was sent through channel (Arc-wrapped) // Verify output was sent through channel (Arc-wrapped)
let rx = receivers let arc_value = result.unwrap().downcast::<i32>().unwrap();
.pop()
.unwrap()
.downcast::<futures::channel::oneshot::Receiver<std::sync::Arc<i32>>>()
.unwrap();
let received_arc = rx.await;
assert!(received_arc.is_ok());
assert_eq!(*received_arc.unwrap(), 42);
// Also verify output was returned (Arc-wrapped) // Also verify output was returned (Arc-wrapped)
let output = result.unwrap();
let arc_value = output.downcast::<i32>().unwrap();
assert_eq!(*arc_value, 42); assert_eq!(*arc_value, 42);
} }
#[tokio::test]
async fn test_execute_with_multiple_dependents() {
// Test that output is sent to multiple dependents
let node = TypedNode::new(NodeId(0), TestTask { value: 100 });
// Create output channels for three dependents
let (senders, receivers) = node.create_output_channels(3);
// Execute node
let node = Box::new(node);
let exec_receivers = vec![];
let result = node.execute_with_channels(exec_receivers, senders).await;
assert!(result.is_ok());
// Verify all three dependents receive the output (Arc-wrapped)
for receiver in receivers {
let rx = receiver
.downcast::<futures::channel::oneshot::Receiver<std::sync::Arc<i32>>>()
.unwrap();
let received_arc = rx.await;
assert!(received_arc.is_ok());
assert_eq!(*received_arc.unwrap(), 100);
}
}
+108 -131
View File
@@ -5,16 +5,18 @@
//! //!
//! Uses Mutex for interior mutability to enable builder pattern (`&self` instead of `&mut self`). //! Uses Mutex for interior mutability to enable builder pattern (`&self` instead of `&mut self`).
use std::any::Any;
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::{HashMap, HashSet, VecDeque};
use std::future::Future;
use std::panic::AssertUnwindSafe; use std::panic::AssertUnwindSafe;
use std::sync::{ use std::sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Arc,
}; };
use futures::channel::mpsc;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt}; use futures::stream::FuturesUnordered;
use futures::{FutureExt, StreamExt, TryFutureExt};
use parking_lot::Mutex; use parking_lot::Mutex;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@@ -54,6 +56,7 @@ impl<'a> Drop for RunGuard<'a> {
/// ///
/// ```no_run /// ```no_run
/// # use dagx::{task, DagRunner, Task}; /// # use dagx::{task, DagRunner, Task};
/// # use futures::FutureExt;
/// // Task with state constructed via ::new() /// // Task with state constructed via ::new()
/// struct LoadValue { value: i32 } /// struct LoadValue { value: i32 }
/// ///
@@ -82,7 +85,7 @@ impl<'a> Drop for RunGuard<'a> {
/// let y = dag.add_task(LoadValue::new(3)); /// let y = dag.add_task(LoadValue::new(3));
/// let sum = dag.add_task(Add).depends_on((&x, &y)); /// let sum = dag.add_task(Add).depends_on((&x, &y));
/// ///
/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
/// ///
/// assert_eq!(dag.get(sum).unwrap(), 5); /// assert_eq!(dag.get(sum).unwrap(), 5);
/// # }; /// # };
@@ -95,7 +98,7 @@ impl<'a> Drop for RunGuard<'a> {
/// Outputs are Arc-wrapped and stored separately for retrieval via get(). /// Outputs are Arc-wrapped and stored separately for retrieval via get().
/// Arc enables efficient sharing during fanout without cloning data. /// Arc enables efficient sharing during fanout without cloning data.
pub struct DagRunner { pub struct DagRunner {
pub(crate) nodes: Mutex<Vec<Option<Box<dyn ExecutableNode>>>>, pub(crate) nodes: Mutex<Vec<Option<Box<dyn ExecutableNode + Sync>>>>,
pub(crate) outputs: Mutex<HashMap<NodeId, std::sync::Arc<dyn std::any::Any + Send + Sync>>>, pub(crate) outputs: Mutex<HashMap<NodeId, std::sync::Arc<dyn std::any::Any + Send + Sync>>>,
pub(crate) edges: Mutex<HashMap<NodeId, Vec<NodeId>>>, // node -> dependencies pub(crate) edges: Mutex<HashMap<NodeId, Vec<NodeId>>>, // node -> dependencies
pub(crate) dependents: Mutex<HashMap<NodeId, Vec<NodeId>>>, // node -> tasks that depend on it pub(crate) dependents: Mutex<HashMap<NodeId, Vec<NodeId>>>, // node -> tasks that depend on it
@@ -147,6 +150,7 @@ impl DagRunner {
/// ///
/// ```no_run /// ```no_run
/// # use dagx::{task, DagRunner, Task}; /// # use dagx::{task, DagRunner, Task};
/// # use futures::FutureExt;
/// // Task with state - shows you construct with specific value /// // Task with state - shows you construct with specific value
/// struct LoadValue { /// struct LoadValue {
/// initial: i32, /// initial: i32,
@@ -188,13 +192,13 @@ impl DagRunner {
/// // Construct task with offset of 1 /// // Construct task with offset of 1
/// let inc = dag.add_task(AddOffset::new(1)).depends_on(&base); /// let inc = dag.add_task(AddOffset::new(1)).depends_on(&base);
/// ///
/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
/// assert_eq!(dag.get(&inc).unwrap(), 11); /// assert_eq!(dag.get(&inc).unwrap(), 11);
/// # }; /// # };
/// ``` /// ```
pub fn add_task<Tk>(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending> pub fn add_task<Tk>(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending>
where where
Tk: Task + 'static, Tk: Task + Sync + 'static,
Tk::Input: 'static + Clone, Tk::Input: 'static + Clone,
Tk::Output: 'static + Clone, Tk::Output: 'static + Clone,
{ {
@@ -230,10 +234,13 @@ impl DagRunner {
/// ///
/// # Parameters /// # Parameters
/// ///
/// - `spawner`: A function that spawns futures on the async runtime. Examples: /// - `spawner`: A function that spawns futures on the async runtime
/// - Tokio: `|fut| { tokio::spawn(fut); }` /// and returns a handle to the task. This is the only way to run tasks on separate threads. Examples:
/// - Smol: `|fut| { smol::spawn(fut).detach(); }` /// - Tokio: `|fut| { tokio::spawn(fut).map(Result::unwrap) }`
/// - Async-std: `|fut| { async_std::task::spawn(fut); }` /// - Smol: `|fut| { smol::spawn(fut) }`
/// - Async-std: `|fut| { async_std::task::spawn(fut) }`
/// - Single-threaded: `|fut| fut`
/// - For computationally light tasks, concurrency without parallelism can be significantly faster.
/// ///
/// # Errors /// # Errors
/// ///
@@ -249,6 +256,7 @@ impl DagRunner {
/// ///
/// ``` /// ```
/// # use dagx::{task, DagRunner, Task}; /// # use dagx::{task, DagRunner, Task};
/// # use futures::FutureExt;
/// // Tuple struct /// // Tuple struct
/// struct Value(i32); /// struct Value(i32);
/// ///
@@ -272,21 +280,15 @@ impl DagRunner {
/// let b = dag.add_task(Value(2)); /// let b = dag.add_task(Value(2));
/// let sum = dag.add_task(Add).depends_on((&a, &b)); /// let sum = dag.add_task(Add).depends_on((&a, &b));
/// ///
/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); // Executes all tasks /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); // Executes all tasks
/// # }; /// # };
/// ``` /// ```
///
/// # Implementation Note
///
/// Tasks communicate via oneshot channels created fresh during run().
/// This eliminates Mutex contention on outputs and enables true streaming execution.
/// Type erasure occurs only at the ExecutableNode trait boundary - channels are created
/// with full type information and type-erased just before passing to execute_with_channels.
#[inline] #[inline]
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))] #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))]
pub async fn run<S>(&self, spawner: S) -> DagResult<()> pub async fn run<S, F>(&self, spawner: S) -> DagResult<()>
where where
S: Fn(BoxFuture<'static, ()>), S: Fn(BoxFuture<'static, DagResult<(NodeId, Arc<dyn Any + Send + Sync>)>>) -> F,
F: Future<Output = DagResult<(NodeId, Arc<dyn Any + Send + Sync>)>>,
{ {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
info!("starting DAG execution"); info!("starting DAG execution");
@@ -320,47 +322,8 @@ impl DagRunner {
let edges = self.edges.lock().clone(); let edges = self.edges.lock().clone();
// Create all channels upfront let mut outputs: HashMap<NodeId, Arc<dyn Any + Send + Sync>> = HashMap::new();
// Map: (producer_id, consumer_id) -> receiver index let mut first_error = None;
// We store receivers in a Vec per consumer to maintain order
let mut consumer_receivers: HashMap<NodeId, Vec<Box<dyn std::any::Any + Send>>> =
HashMap::new();
let mut producer_senders: HashMap<NodeId, Vec<Box<dyn std::any::Any + Send>>> =
HashMap::new();
// Create channels for each producer-consumer relationship
// We need to lock nodes temporarily to call create_output_channels
{
let nodes_lock = self.nodes.lock();
for (consumer_id, producer_ids) in &edges {
for &producer_id in producer_ids {
// Get the producer node (it's still in the vector at this point)
if let Some(Some(producer_node)) = nodes_lock.get(producer_id.0) {
// Ask the producer to create ONE channel for this consumer
let (mut senders, mut receivers) = producer_node.create_output_channels(1);
// Store the sender for the producer
producer_senders
.entry(producer_id)
.or_default()
.push(senders.pop().unwrap());
// Store the receiver for the consumer (order matters!)
consumer_receivers
.entry(*consumer_id)
.or_default()
.push(receivers.pop().unwrap());
}
}
}
}
// Create a channel to collect Arc-wrapped outputs from all tasks
let (output_tx, mut output_rx) = mpsc::unbounded::<(
NodeId,
Result<std::sync::Arc<dyn std::any::Any + Send + Sync>, DagError>,
)>();
for layer in layers { for layer in layers {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@@ -375,7 +338,6 @@ impl DagRunner {
// pipelines), we execute it inline rather than spawning it. This provides // pipelines), we execute it inline rather than spawning it. This provides
// 10-100x performance improvements for sequential workloads by eliminating: // 10-100x performance improvements for sequential workloads by eliminating:
// - Task spawning overhead // - Task spawning overhead
// - Channel creation/destruction for layer coordination
// - Context switching to/from the runtime // - Context switching to/from the runtime
// //
// CRITICAL: Panic handling is required to maintain behavioral consistency. // CRITICAL: Panic handling is required to maintain behavioral consistency.
@@ -402,8 +364,6 @@ impl DagRunner {
"executing task inline (single-task layer optimization)" "executing task inline (single-task layer optimization)"
); );
let out_tx = output_tx.clone();
// Take ownership of the node // Take ownership of the node
let node = { let node = {
let mut nodes_lock = self.nodes.lock(); let mut nodes_lock = self.nodes.lock();
@@ -411,11 +371,11 @@ impl DagRunner {
}; };
if let Some(node) = node { if let Some(node) = node {
// Take the receivers for this consumer let dependencies: Vec<_> = edges[&node_id]
let receivers = consumer_receivers.remove(&node_id).unwrap_or_default(); .iter()
.flat_map(|dep| outputs.get(dep))
// Take the senders for this producer .map(|value| value.clone())
let senders = producer_senders.remove(&node_id).unwrap_or_default(); .collect();
// Execute inline with panic handling. // Execute inline with panic handling.
// //
@@ -423,7 +383,7 @@ impl DagRunner {
// DagError::TaskPanicked, matching the behavior of async runtimes when // DagError::TaskPanicked, matching the behavior of async runtimes when
// they spawn tasks. This guarantees consistent error handling whether // they spawn tasks. This guarantees consistent error handling whether
// a task executes inline (single-task layer) or spawned (multi-task layer). // a task executes inline (single-task layer) or spawned (multi-task layer).
let result = AssertUnwindSafe(node.execute_with_channels(receivers, senders)) let result = AssertUnwindSafe(node.execute_with_deps(dependencies))
.catch_unwind() .catch_unwind()
.await .await
.unwrap_or_else(|panic_payload| { .unwrap_or_else(|panic_payload| {
@@ -450,80 +410,96 @@ impl DagRunner {
}) })
}); });
// Send the output (ignore send errors - receiver may be dropped) match result {
let _ = out_tx.unbounded_send((node_id, result)); Ok(output) => {
outputs.insert(node_id, output);
}
Err(e) => {
first_error.get_or_insert(e);
}
}
} }
} else { } else {
// Slow path: Multiple tasks require spawning and coordination // Slow path: Multiple tasks require spawning and coordination
// Create a channel to track task completion for this layer
let (tx, mut rx) = mpsc::channel::<()>(layer.len());
// Spawn each task in this layer // Spawn each task in this layer
for &node_id in &layer { let mut futures: FuturesUnordered<_> = layer
#[cfg(feature = "tracing")] .into_iter()
trace!(task_id = node_id.0, "spawning task"); .filter_map(|node_id| {
let mut task_tx = tx.clone(); #[cfg(feature = "tracing")]
let out_tx = output_tx.clone(); trace!(task_id = node_id.0, "spawning task");
// Take ownership of the node // Take ownership of the node
let node = { let node = {
let mut nodes_lock = self.nodes.lock(); let mut nodes_lock = self.nodes.lock();
nodes_lock[node_id.0].take() nodes_lock[node_id.0].take()
};
if let Some(node) = node {
// Take the receivers for this consumer
let receivers = consumer_receivers.remove(&node_id).unwrap_or_default();
// Take the senders for this producer
let senders = producer_senders.remove(&node_id).unwrap_or_default();
// Create a 'static future by taking ownership
let inner_future = async move {
let task_future = node.execute_with_channels(receivers, senders);
let result = task_future.await;
// Send the output (ignore send errors - receiver may be dropped)
let _ = out_tx.unbounded_send((node_id, result));
// Signal completion (ignore send errors - receiver may be dropped)
let _ = task_tx.try_send(());
}; };
if let Some(node) = node {
let dependencies: Vec<_> = edges[&node_id]
.iter()
.flat_map(|dep| outputs.get(dep))
.map(|value| value.clone())
.collect();
// Spawn the task using the provided spawner // Spawn the task using the provided spawner
spawner(Box::pin(inner_future)); let inner_future = async move {
let result = node.execute_with_deps(dependencies).await?;
Ok((node_id, result))
};
Some(
AssertUnwindSafe(spawner(Box::pin(inner_future)))
.catch_unwind()
.unwrap_or_else(move |panic_payload| {
// Convert panic to error
let panic_message =
if let Some(s) = panic_payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) =
panic_payload.downcast_ref::<String>()
{
s.clone()
} else {
"unknown panic".to_string()
};
#[cfg(feature = "tracing")]
error!(
task_id = node_id.0,
panic_message = %panic_message,
"task panicked during inline execution"
);
Err(DagError::TaskPanicked {
task_id: node_id.0,
panic_message,
})
}),
)
} else {
None
}
})
.collect();
while let Some(out) = futures.next().await {
match out {
Ok(output) => {
outputs.insert(output.0, output.1);
}
Err(e) => {
first_error.get_or_insert(e);
}
} }
} }
// Drop the original sender so the channel closes when all tasks complete
drop(tx);
// Wait for all tasks in this layer to complete
while rx.next().await.is_some() {}
}
}
// Drop the output sender so the channel closes
drop(output_tx);
// Collect all outputs and check for errors
let mut collected = Vec::new();
let mut first_error = None;
while let Some((node_id, result)) = output_rx.next().await {
match result {
Ok(output) => collected.push((node_id, output)),
Err(e) if first_error.is_none() => first_error = Some(e),
Err(_) => {} // Ignore subsequent errors
} }
} }
// Insert all successful outputs (avoiding holding lock across await) // Insert all successful outputs (avoiding holding lock across await)
let mut outputs = self.outputs.lock(); let mut outputs_lock = self.outputs.lock();
for (node_id, output) in collected { for (node_id, output) in outputs {
outputs.insert(node_id, output); outputs_lock.insert(node_id, output);
} }
drop(outputs); drop(outputs_lock);
// Return first error if any // Return first error if any
if let Some(err) = first_error { if let Some(err) = first_error {
@@ -554,6 +530,7 @@ impl DagRunner {
/// ///
/// ```no_run /// ```no_run
/// # use dagx::{task, DagRunner, Task}; /// # use dagx::{task, DagRunner, Task};
/// # use futures::FutureExt;
/// struct Configuration { /// struct Configuration {
/// setting: i32, /// setting: i32,
/// } /// }
@@ -575,7 +552,7 @@ impl DagRunner {
/// // Construct task with specific setting value /// // Construct task with specific setting value
/// let task = dag.add_task(Configuration::new(42)); /// let task = dag.add_task(Configuration::new(42));
/// ///
/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
/// ///
/// assert_eq!(dag.get(task).unwrap(), 42); /// assert_eq!(dag.get(task).unwrap(), 42);
/// # }; /// # };
+19 -30
View File
@@ -6,6 +6,7 @@ use crate::error::DagError;
use crate::runner::DagRunner; use crate::runner::DagRunner;
use crate::task::Task; use crate::task::Task;
use crate::types::TaskHandle; use crate::types::TaskHandle;
use futures::FutureExt;
use std::collections::HashMap; use std::collections::HashMap;
#[tokio::test] #[tokio::test]
@@ -20,11 +21,9 @@ async fn test_get_type_mismatch_error() {
// Task outputs i32 // Task outputs i32
let source = dag.add_task(task_fn(|_: ()| async { 42i32 })); let source = dag.add_task(task_fn(|_: ()| async { 42i32 }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Try to get as String - TYPE MISMATCH! // Try to get as String - TYPE MISMATCH!
// Create a handle with the same ID but wrong type // Create a handle with the same ID but wrong type
@@ -56,11 +55,9 @@ async fn test_get_type_mismatch_i32_to_vec() {
// Task outputs i32 // Task outputs i32
let int_task = dag.add_task(task_fn(|_: ()| async { 123i32 })); let int_task = dag.add_task(task_fn(|_: ()| async { 123i32 }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Try to get as Vec<u8> // Try to get as Vec<u8>
let wrong_handle: TaskHandle<Vec<u8>> = TaskHandle { let wrong_handle: TaskHandle<Vec<u8>> = TaskHandle {
@@ -102,11 +99,9 @@ async fn test_get_type_mismatch_struct_to_hashmap() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let task_handle = dag.add_task(CustomTask); let task_handle = dag.add_task(CustomTask);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Try to get as HashMap // Try to get as HashMap
let wrong_handle: TaskHandle<HashMap<String, i32>> = TaskHandle { let wrong_handle: TaskHandle<HashMap<String, i32>> = TaskHandle {
@@ -136,11 +131,9 @@ async fn test_get_type_mismatch_vec_different_types() {
// Task outputs Vec<i32> // Task outputs Vec<i32>
let vec_task = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] })); let vec_task = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Try to get as Vec<String> // Try to get as Vec<String>
let wrong_handle: TaskHandle<Vec<String>> = TaskHandle { let wrong_handle: TaskHandle<Vec<String>> = TaskHandle {
@@ -170,11 +163,9 @@ async fn test_get_type_mismatch_option_types() {
// Task outputs Option<i32> // Task outputs Option<i32>
let opt_task = dag.add_task(task_fn(|_: ()| async { Some(42) })); let opt_task = dag.add_task(task_fn(|_: ()| async { Some(42) }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Try to get as Option<String> // Try to get as Option<String>
let wrong_handle: TaskHandle<Option<String>> = TaskHandle { let wrong_handle: TaskHandle<Option<String>> = TaskHandle {
@@ -206,11 +197,9 @@ async fn test_multiple_type_mismatches_same_dag() {
let string_task = dag.add_task(task_fn(|_: ()| async { "hello".to_string() })); let string_task = dag.add_task(task_fn(|_: ()| async { "hello".to_string() }));
let bool_task = dag.add_task(task_fn(|_: ()| async { true })); let bool_task = dag.add_task(task_fn(|_: ()| async { true }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Try to get int_task as String // Try to get int_task as String
let wrong_int_handle: TaskHandle<String> = TaskHandle { let wrong_int_handle: TaskHandle<String> = TaskHandle {
+14 -28
View File
@@ -5,6 +5,8 @@ use crate::runner::DagRunner;
use crate::task::Task; use crate::task::Task;
use crate::types::TaskHandle; use crate::types::TaskHandle;
use futures::FutureExt;
// Initialize tracing subscriber for tests (idempotent) // Initialize tracing subscriber for tests (idempotent)
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
fn init_tracing() { fn init_tracing() {
@@ -68,11 +70,9 @@ async fn test_get_wrong_type() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let handle = dag.add_task(TestTask { value: 42 }); let handle = dag.add_task(TestTask { value: 42 });
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Try to get with wrong type - downcast will fail // Try to get with wrong type - downcast will fail
let fake_handle: TaskHandle<String> = TaskHandle { let fake_handle: TaskHandle<String> = TaskHandle {
@@ -145,18 +145,12 @@ async fn test_concurrent_run_protection() {
// Start two runs concurrently // Start two runs concurrently
let handle1 = tokio::spawn(async move { let handle1 = tokio::spawn(async move {
barrier1.wait().await; barrier1.wait().await;
dag1.run(|fut| { dag1.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await
tokio::spawn(fut);
})
.await
}); });
let handle2 = tokio::spawn(async move { let handle2 = tokio::spawn(async move {
barrier2.wait().await; barrier2.wait().await;
dag2.run(|fut| { dag2.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await
tokio::spawn(fut);
})
.await
}); });
// One should succeed, one should fail (concurrent execution not supported) // One should succeed, one should fail (concurrent execution not supported)
@@ -205,11 +199,9 @@ async fn test_multiple_get_calls() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let handle = dag.add_task(TestTask { value: 100 }); let handle = dag.add_task(TestTask { value: 100 });
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Multiple get calls should all return the same value // Multiple get calls should all return the same value
let result1 = dag.get(&handle).unwrap(); let result1 = dag.get(&handle).unwrap();
@@ -233,11 +225,9 @@ async fn test_invalid_node_id_in_get() {
_phantom: std::marker::PhantomData, _phantom: std::marker::PhantomData,
}; };
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let result = dag.get(invalid_handle); let result = dag.get(invalid_handle);
assert!(result.is_err()); assert!(result.is_err());
@@ -288,11 +278,7 @@ async fn test_task_panic_in_multi_task_layer() {
let _good_task = dag.add_task(GoodTask).depends_on(&source); let _good_task = dag.add_task(GoodTask).depends_on(&source);
// Run the DAG - the panic should be caught // Run the DAG - the panic should be caught
let result = dag let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
// The run should fail due to the panic // The run should fail due to the panic
// Note: Tokio catches panics in spawned tasks, so the behavior depends on the runtime // Note: Tokio catches panics in spawned tasks, so the behavior depends on the runtime
+6 -6
View File
@@ -3,7 +3,7 @@
//! This module defines the core Task trait that all DAG nodes must implement, //! This module defines the core Task trait that all DAG nodes must implement,
//! along with convenience functions for creating tasks from closures. //! along with convenience functions for creating tasks from closures.
use std::future::Future; use std::{future::Future, sync::Arc};
/// A unit of async work with typed inputs and outputs. /// A unit of async work with typed inputs and outputs.
/// ///
@@ -87,7 +87,7 @@ use std::future::Future;
/// # Output Wrapping /// # Output Wrapping
/// ///
/// **Task outputs are automatically wrapped in `Arc<T>` internally** for efficient fan-out. /// **Task outputs are automatically wrapped in `Arc<T>` internally** for efficient fan-out.
/// You define `Output = T`, but the framework wraps it in `Arc<T>` before sending to channels. /// You define `Output = T`, but the framework wraps it in `Arc<T>` before sending to dependents.
/// This enables O(1) sharing for 1:N dependencies - Arc is cloned (cheap), not your data. /// This enables O(1) sharing for 1:N dependencies - Arc is cloned (cheap), not your data.
/// ///
/// Downstream tasks receive `&T` after ExtractInput extracts from the Arc. This is transparent /// Downstream tasks receive `&T` after ExtractInput extracts from the Arc. This is transparent
@@ -98,7 +98,7 @@ pub trait Task: Send {
fn run(self, input: Self::Input) -> impl Future<Output = Self::Output> + Send; fn run(self, input: Self::Input) -> impl Future<Output = Self::Output> + Send;
/// Internal method: Extract input from type-erased channels and execute the task. /// Internal method: Extract input from list of type-erased dependencies and execute the task.
/// ///
/// This method is implemented automatically by the #[task] macro with inline /// This method is implemented automatically by the #[task] macro with inline
/// extraction logic based on the task's parameter signature. This allows ANY /// extraction logic based on the task's parameter signature. This allows ANY
@@ -106,7 +106,7 @@ pub trait Task: Send {
#[doc(hidden)] #[doc(hidden)]
fn extract_and_run( fn extract_and_run(
self, self,
receivers: Vec<Box<dyn std::any::Any + Send>>, dependencies: Vec<Arc<dyn std::any::Any + Send + Sync>>,
) -> impl Future<Output = Result<Self::Output, String>> + Send; ) -> impl Future<Output = Result<Self::Output, String>> + Send;
} }
@@ -149,10 +149,10 @@ where
async fn extract_and_run( async fn extract_and_run(
self, self,
receivers: Vec<Box<dyn std::any::Any + Send>>, dependencies: Vec<Arc<dyn std::any::Any + Send + Sync>>,
) -> Result<Self::Output, String> { ) -> Result<Self::Output, String> {
// For task_fn, we use ExtractInput since we don't have macro-generated extraction // For task_fn, we use ExtractInput since we don't have macro-generated extraction
let input = I::extract_from_channels(receivers) let input = I::extract_from_deps(dependencies)
.await .await
.map_err(|e| format!("Failed to extract input: {}", e))?; .map_err(|e| format!("Failed to extract input: {}", e))?;
+2 -1
View File
@@ -3,6 +3,7 @@
use crate::task::{task_fn, Task}; use crate::task::{task_fn, Task};
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc;
// Test the task_fn function and TaskFn struct // Test the task_fn function and TaskFn struct
#[tokio::test] #[tokio::test]
@@ -87,7 +88,7 @@ impl Task for CustomTask {
#[allow(refining_impl_trait)] #[allow(refining_impl_trait)]
fn extract_and_run( fn extract_and_run(
self, self,
_receivers: Vec<Box<dyn std::any::Any + Send>>, _dependencies: Vec<Arc<dyn std::any::Any + Send + Sync>>,
) -> Pin<Box<dyn Future<Output = Result<Self::Output, String>> + Send>> { ) -> Pin<Box<dyn Future<Output = Result<Self::Output, String>> + Send>> {
Box::pin(async move { Box::pin(async move {
// This is a test helper - just return an error // This is a test helper - just return an error
+4 -3
View File
@@ -6,9 +6,9 @@
use crate::builder::{IsUnitType, TaskBuilder}; use crate::builder::{IsUnitType, TaskBuilder};
use crate::task::Task; use crate::task::Task;
/// Opaque node identifier (internal use) /// Opaque node identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) struct NodeId(pub usize); pub struct NodeId(pub usize);
/// Opaque, typed token for a node's output. /// Opaque, typed token for a node's output.
/// ///
@@ -23,6 +23,7 @@ pub(crate) struct NodeId(pub usize);
/// ///
/// ```no_run /// ```no_run
/// # use dagx::{task, DagRunner, Task}; /// # use dagx::{task, DagRunner, Task};
/// # use futures::FutureExt;
/// # struct LoadValue { value: i32 } /// # struct LoadValue { value: i32 }
/// # impl LoadValue { pub fn new(v: i32) -> Self { Self { value: v } } } /// # impl LoadValue { pub fn new(v: i32) -> Self { Self { value: v } } }
/// # #[task] /// # #[task]
@@ -33,7 +34,7 @@ pub(crate) struct NodeId(pub usize);
/// let dag = DagRunner::new(); /// let dag = DagRunner::new();
/// let node = dag.add_task(LoadValue::new(42)); /// let node = dag.add_task(LoadValue::new(42));
/// ///
/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
/// ///
/// assert_eq!(dag.get(node).unwrap(), 42); /// assert_eq!(dag.get(node).unwrap(), 42);
/// # }; /// # };
+16 -25
View File
@@ -4,6 +4,7 @@
use crate::types::TaskHandle; use crate::types::TaskHandle;
use crate::{task, DagRunner, Task}; use crate::{task, DagRunner, Task};
use futures::FutureExt;
#[tokio::test] #[tokio::test]
async fn test_unit_type_taskbuilder_to_handle_conversion() { async fn test_unit_type_taskbuilder_to_handle_conversion() {
@@ -31,11 +32,9 @@ async fn test_unit_type_taskbuilder_to_handle_conversion() {
// specifically covering line 71 // specifically covering line 71
let handle: TaskHandle<i32> = builder.into(); let handle: TaskHandle<i32> = builder.into();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Verify the task ran and we can get its output // Verify the task ran and we can get its output
assert_eq!(dag.get(handle).unwrap(), 42); assert_eq!(dag.get(handle).unwrap(), 42);
@@ -81,11 +80,9 @@ async fn test_unit_type_conversion_with_multiple_tasks() {
let second_handle: TaskHandle<i32> = second_builder.into(); let second_handle: TaskHandle<i32> = second_builder.into();
let third_handle: TaskHandle<bool> = third_builder.into(); let third_handle: TaskHandle<bool> = third_builder.into();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Verify all outputs // Verify all outputs
assert_eq!(dag.get(first_handle).unwrap(), "first"); assert_eq!(dag.get(first_handle).unwrap(), "first");
@@ -122,11 +119,9 @@ async fn test_unit_struct_with_state() {
// Explicit conversion - triggers line 71 // Explicit conversion - triggers line 71
let handle: TaskHandle<i32> = builder.into(); let handle: TaskHandle<i32> = builder.into();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(handle).unwrap(), 42); assert_eq!(dag.get(handle).unwrap(), 42);
} }
@@ -151,11 +146,9 @@ async fn test_taskbuilder_conversion_assignment() {
let handle: TaskHandle<Vec<i32>> = builder.into(); let handle: TaskHandle<Vec<i32>> = builder.into();
let id = handle.id; let id = handle.id;
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let result = dag.get(handle).unwrap(); let result = dag.get(handle).unwrap();
assert_eq!(result, vec![1, 2, 3]); assert_eq!(result, vec![1, 2, 3]);
@@ -197,11 +190,9 @@ async fn test_mixed_unit_and_dependent_tasks() {
// Doubler needs dependencies, so we use it differently // Doubler needs dependencies, so we use it differently
let doubler = dag.add_task(Doubler).depends_on(source_handle); let doubler = dag.add_task(Doubler).depends_on(source_handle);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(source_handle).unwrap(), 10); assert_eq!(dag.get(source_handle).unwrap(), 10);
assert_eq!(dag.get(doubler).unwrap(), 20); assert_eq!(dag.get(doubler).unwrap(), 20);
+54 -91
View File
@@ -10,6 +10,7 @@
//! These tests serve as both validation and documentation for proper Arc usage. //! These tests serve as both validation and documentation for proper Arc usage.
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -43,11 +44,9 @@ async fn test_arc_basic_pipeline() {
let producer = dag.add_task(Producer); let producer = dag.add_task(Producer);
let consumer = dag.add_task(Consumer).depends_on(&producer); let consumer = dag.add_task(Consumer).depends_on(&producer);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Note: producer is not a sink (consumer depends on it), so we can't get() its value // Note: producer is not a sink (consumer depends on it), so we can't get() its value
// Only sink nodes (nodes with no dependents) store their outputs for retrieval // Only sink nodes (nodes with no dependents) store their outputs for retrieval
@@ -101,11 +100,9 @@ async fn test_arc_with_primitives() {
.add_task(AllConsumer) .add_task(AllConsumer)
.depends_on((&i32_prod, &f64_prod, &bool_prod)); .depends_on((&i32_prod, &f64_prod, &bool_prod));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Note: producers are not sinks (consumer depends on them), so we can't get() their values // Note: producers are not sinks (consumer depends on them), so we can't get() their values
// The consumer output verifies all three producers worked correctly // The consumer output verifies all three producers worked correctly
@@ -177,11 +174,9 @@ async fn test_arc_with_collections() {
.add_task(CollectionConsumer) .add_task(CollectionConsumer)
.depends_on((&vec_prod, &map_prod, &set_prod)); .depends_on((&vec_prod, &map_prod, &set_prod));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Note: producers are not sinks (consumer depends on them), so we can't get() their values // Note: producers are not sinks (consumer depends on them), so we can't get() their values
// The consumer output verifies all three collection producers worked correctly // The consumer output verifies all three collection producers worked correctly
@@ -250,11 +245,9 @@ async fn test_arc_with_custom_structs() {
let age_ext = dag.add_task(AgeExtractor).depends_on(&person_prod); let age_ext = dag.add_task(AgeExtractor).depends_on(&person_prod);
let email_ext = dag.add_task(EmailCountExtractor).depends_on(&person_prod); let email_ext = dag.add_task(EmailCountExtractor).depends_on(&person_prod);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Note: person_prod is not a sink (3 extractors depend on it), so we can't get() its value // Note: person_prod is not a sink (3 extractors depend on it), so we can't get() its value
// The extractor outputs verify the person was created and shared correctly // The extractor outputs verify the person was created and shared correctly
@@ -318,11 +311,9 @@ async fn test_arc_fan_out_three_consumers() {
let c2 = dag.add_task(FirstConsumer).depends_on(&producer); let c2 = dag.add_task(FirstConsumer).depends_on(&producer);
let c3 = dag.add_task(JoinConsumer).depends_on(&producer); let c3 = dag.add_task(JoinConsumer).depends_on(&producer);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(c1).unwrap(), 5); assert_eq!(dag.get(c1).unwrap(), 5);
assert_eq!(dag.get(c2).unwrap(), "item1"); assert_eq!(dag.get(c2).unwrap(), "item1");
@@ -394,11 +385,9 @@ async fn test_arc_fan_out_five_consumers_large_data() {
let c4 = dag.add_task(PatternCounter).depends_on(&producer); let c4 = dag.add_task(PatternCounter).depends_on(&producer);
let c5 = dag.add_task(TotalBytesComputer).depends_on(&producer); let c5 = dag.add_task(TotalBytesComputer).depends_on(&producer);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Note: producer is not a sink (has 5 consumers), so we can't get() its value // Note: producer is not a sink (has 5 consumers), so we can't get() its value
// We verify correctness through the consumer outputs instead // We verify correctness through the consumer outputs instead
@@ -486,11 +475,9 @@ async fn test_arc_nested_fan_out_tree() {
let l2_negate = dag.add_task(Level2Negate).depends_on(l1_prod); let l2_negate = dag.add_task(Level2Negate).depends_on(l1_prod);
let l2_mod = dag.add_task(Level2Mod10).depends_on(l1_prod); let l2_mod = dag.add_task(Level2Mod10).depends_on(l1_prod);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Note: l1_sum and l1_prod are not sinks (they have level 2 tasks depending on them) // Note: l1_sum and l1_prod are not sinks (they have level 2 tasks depending on them)
// Only the level 2 tasks are sinks and can be retrieved // Only the level 2 tasks are sinks and can be retrieved
@@ -556,11 +543,9 @@ async fn test_arc_performance_vs_clone() {
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let elapsed = start.elapsed(); let elapsed = start.elapsed();
@@ -658,11 +643,9 @@ async fn test_arc_with_result() {
let consumer3 = dag.add_task(ArcResultConsumer2).depends_on(&arc_result_err); let consumer3 = dag.add_task(ArcResultConsumer2).depends_on(&arc_result_err);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(consumer1).unwrap(), "Got Ok: Success"); assert_eq!(dag.get(consumer1).unwrap(), "Got Ok: Success");
assert_eq!(dag.get(consumer2).unwrap(), "Got Arc: Success"); assert_eq!(dag.get(consumer2).unwrap(), "Got Arc: Success");
@@ -767,11 +750,9 @@ async fn test_arc_with_option() {
let c3 = dag.add_task(OptionArcConsumer1).depends_on(&opt_arc_some); let c3 = dag.add_task(OptionArcConsumer1).depends_on(&opt_arc_some);
let c4 = dag.add_task(OptionArcConsumer2).depends_on(&opt_arc_none); let c4 = dag.add_task(OptionArcConsumer2).depends_on(&opt_arc_none);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(c1).unwrap(), "Some(42)"); assert_eq!(dag.get(c1).unwrap(), "Some(42)");
assert_eq!(dag.get(c2).unwrap(), "None"); assert_eq!(dag.get(c2).unwrap(), "None");
@@ -824,11 +805,9 @@ async fn test_arc_diamond_dependency() {
let right = dag.add_task(RightBranch).depends_on(&top); let right = dag.add_task(RightBranch).depends_on(&top);
let bottom = dag.add_task(Bottom).depends_on((&left, &right)); let bottom = dag.add_task(Bottom).depends_on((&left, &right));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(bottom).unwrap(), "Sum: 15, Product: 120"); assert_eq!(dag.get(bottom).unwrap(), "Sum: 15, Product: 120");
} }
@@ -894,11 +873,9 @@ async fn test_arc_empty_collections() {
.add_task(EmptyConsumer) .add_task(EmptyConsumer)
.depends_on((&vec_task, &string_task, &map_task)); .depends_on((&vec_task, &string_task, &map_task));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!( assert_eq!(
dag.get(consumer).unwrap(), dag.get(consumer).unwrap(),
@@ -967,11 +944,9 @@ async fn test_arc_very_large_data() {
let last = dag.add_task(LastSampler).depends_on(&producer); let last = dag.add_task(LastSampler).depends_on(&producer);
let middle = dag.add_task(MiddleSampler).depends_on(&producer); let middle = dag.add_task(MiddleSampler).depends_on(&producer);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(counter).unwrap(), SIZE); assert_eq!(dag.get(counter).unwrap(), SIZE);
assert!(dag.get(first).unwrap().contains("Item number 0")); assert!(dag.get(first).unwrap().contains("Item number 0"));
@@ -1006,11 +981,9 @@ async fn test_arc_of_arc() {
let producer = dag.add_task(NestedProducer); let producer = dag.add_task(NestedProducer);
let consumer = dag.add_task(NestedConsumer).depends_on(&producer); let consumer = dag.add_task(NestedConsumer).depends_on(&producer);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(consumer).unwrap(), "Value: Nested Arc"); assert_eq!(dag.get(consumer).unwrap(), "Value: Nested Arc");
} }
@@ -1072,11 +1045,9 @@ async fn test_arc_mixed_with_non_arc() {
let unwrap = dag.add_task(Unwrapper).depends_on(&arc_prod); let unwrap = dag.add_task(Unwrapper).depends_on(&arc_prod);
let wrap = dag.add_task(Wrapper).depends_on(&non_arc_prod); let wrap = dag.add_task(Wrapper).depends_on(&non_arc_prod);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!( assert_eq!(
dag.get(mixed).unwrap(), dag.get(mixed).unwrap(),
@@ -1142,11 +1113,9 @@ async fn test_arc_parallel_execution() {
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let elapsed = start.elapsed(); let elapsed = start.elapsed();
@@ -1193,11 +1162,9 @@ async fn test_arc_reference_counting() {
let producer = dag.add_task(RefCountProducer); let producer = dag.add_task(RefCountProducer);
let checker = dag.add_task(RefCountChecker).depends_on(&producer); let checker = dag.add_task(RefCountChecker).depends_on(&producer);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Note: producer is not a sink (checker depends on it), so we can't get() its value // Note: producer is not a sink (checker depends on it), so we can't get() its value
// The checker task verifies the Arc was passed correctly // The checker task verifies the Arc was passed correctly
@@ -1271,9 +1238,7 @@ async fn test_arc_massive_fanout_performance_comparison() {
let start_without_arc = Instant::now(); let start_without_arc = Instant::now();
dag_without_arc dag_without_arc
.run(|fut| { .run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut);
})
.await .await
.unwrap(); .unwrap();
let duration_without_arc = start_without_arc.elapsed(); let duration_without_arc = start_without_arc.elapsed();
@@ -1325,9 +1290,7 @@ async fn test_arc_massive_fanout_performance_comparison() {
let start_with_arc = Instant::now(); let start_with_arc = Instant::now();
dag_with_arc dag_with_arc
.run(|fut| { .run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut);
})
.await .await
.unwrap(); .unwrap();
let duration_with_arc = start_with_arc.elapsed(); let duration_with_arc = start_with_arc.elapsed();
+10 -36
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -19,10 +20,7 @@ async fn test_1000_level_deep_chain() -> DagResult<()> {
.depends_on(current); .depends_on(current);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(current)?, 999); assert_eq!(dag.get(current)?, 999);
Ok(()) Ok(())
@@ -52,10 +50,7 @@ async fn test_deep_chain_with_branches() -> DagResult<()> {
} }
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(main_chain)?, 499); assert_eq!(dag.get(main_chain)?, 499);
@@ -113,10 +108,7 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
&chain_ends[7], &chain_ends[7],
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Each chain: start + 500 // Each chain: start + 500
// Chain 0: 0 + 500 = 500 // Chain 0: 0 + 500 = 500
@@ -161,10 +153,7 @@ async fn test_deep_chain_execution_order() -> DagResult<()> {
.depends_on(current); .depends_on(current);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(order.load(Ordering::SeqCst), 100); assert_eq!(order.load(Ordering::SeqCst), 100);
assert_eq!(dag.get(current)?, 99); assert_eq!(dag.get(current)?, 99);
@@ -192,10 +181,7 @@ async fn test_deep_binary_tree() -> DagResult<()> {
let root = build_tree(&dag, 10, 1); let root = build_tree(&dag, 10, 1);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Sum of all values in a complete binary tree with values 1024..2047 // Sum of all values in a complete binary tree with values 1024..2047
// This is sum from 2^10 to 2^11-1 = 1024 * 1023 / 2 + 1024 * 512 = 1047552 // This is sum from 2^10 to 2^11-1 = 1024 * 1023 / 2 + 1024 * 512 = 1047552
@@ -225,10 +211,7 @@ async fn test_fibonacci_chain() -> DagResult<()> {
prev1 = next; prev1 = next;
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// 50th Fibonacci number (0-indexed) = 7778742049 // 50th Fibonacci number (0-indexed) = 7778742049
assert_eq!(dag.get(prev1)?, 7778742049); assert_eq!(dag.get(prev1)?, 7778742049);
@@ -251,10 +234,7 @@ async fn test_deep_chain_with_accumulator() -> DagResult<()> {
.depends_on(current); .depends_on(current);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let result = dag.get(current)?; let result = dag.get(current)?;
assert_eq!(result, 20100); // sum of 1..=200 assert_eq!(result, 20100); // sum of 1..=200
@@ -292,10 +272,7 @@ async fn test_zigzag_dependencies() -> DagResult<()> {
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((&chain_a, &chain_b)); .depends_on((&chain_a, &chain_b));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let result = dag.get(final_task)?; let result = dag.get(final_task)?;
assert!(result > 100); assert!(result > 100);
@@ -318,10 +295,7 @@ async fn test_10000_level_chain_stress() -> DagResult<()> {
.depends_on(current); .depends_on(current);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(current)?, 9_999); assert_eq!(dag.get(current)?, 9_999);
Ok(()) Ok(())
+77 -20
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -25,10 +26,7 @@ async fn test_10000_independent_tasks() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify all tasks completed // Verify all tasks completed
assert_eq!(counter.load(Ordering::Relaxed), 10_000); assert_eq!(counter.load(Ordering::Relaxed), 10_000);
@@ -103,10 +101,78 @@ async fn test_5000_node_pyramid() -> DagResult<()> {
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((&current_layer[0], &current_layer[1])); .depends_on((&current_layer[0], &current_layer[1]));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
}) // We created 1024 ones and 1024 zeros at the bottom
.await?; // The sum should propagate up to 1024
let result = dag.get(root)?;
assert!(result > 0, "Expected positive result, got {}", result);
Ok(())
}
#[tokio::test]
async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> {
// Create a pyramid structure with ~5000 nodes
// Layer sizes: 2048 -> 512 -> 128 -> 32 -> 8 -> 2 -> 1
let dag = DagRunner::new();
// Bottom layer: 2048 nodes - convert to TaskHandles
let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048)
.map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move {
if i < 1024 {
1
} else {
0
}
}));
(&task).into()
})
.collect();
let layer_sizes = vec![512, 128, 32, 8, 2];
for layer_size in layer_sizes {
let mut next_layer = Vec::with_capacity(layer_size);
let group_size = current_layer.len() / layer_size;
for i in 0..layer_size {
let start = i * group_size;
let deps: Vec<_> = current_layer[start..start + group_size.min(4)]
.iter()
.take(4)
.collect();
let task = match deps.len() {
1 => dag
.add_task(task_fn(|a: i32| async move { a }))
.depends_on(deps[0]),
2 => dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((deps[0], deps[1])),
3 => dag
.add_task(task_fn(
|(a, b, c): (i32, i32, i32)| async move { a + b + c },
))
.depends_on((deps[0], deps[1], deps[2])),
4 => dag
.add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move {
a + b + c + d
}))
.depends_on((deps[0], deps[1], deps[2], deps[3])),
_ => unreachable!(),
};
next_layer.push(task);
}
current_layer = next_layer;
}
// Final layer combines the last 2
let root = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((&current_layer[0], &current_layer[1]));
dag.run(|fut| fut).await?;
// We created 1024 ones and 1024 zeros at the bottom // We created 1024 ones and 1024 zeros at the bottom
// The sum should propagate up to 1024 // The sum should propagate up to 1024
@@ -139,10 +205,7 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify some sinks completed correctly // Verify some sinks completed correctly
let result0 = dag.get(sinks[0])?; let result0 = dag.get(sinks[0])?;
@@ -195,10 +258,7 @@ async fn test_10000_node_linear_chain_segments() -> DagResult<()> {
chain_ends.push(current); chain_ends.push(current);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// All 10,000 tasks should have executed // All 10,000 tasks should have executed
assert_eq!(counter.load(Ordering::Relaxed), 10_000); assert_eq!(counter.load(Ordering::Relaxed), 10_000);
@@ -239,10 +299,7 @@ async fn test_100000_nodes_stress() -> DagResult<()> {
.collect(); .collect();
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(completed.load(Ordering::Relaxed), 100_000); assert_eq!(completed.load(Ordering::Relaxed), 100_000);
Ok(()) Ok(())
+11 -40
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::marker::PhantomData; use std::marker::PhantomData;
#[tokio::test] #[tokio::test]
@@ -38,10 +39,7 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> {
})) }))
.depends_on(&t2); .depends_on(&t2);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(t1)?, Empty); assert_eq!(dag.get(t1)?, Empty);
assert_eq!(dag.get(t2)?, 42); assert_eq!(dag.get(t2)?, 42);
@@ -76,10 +74,7 @@ async fn test_large_value_types() -> DagResult<()> {
})) }))
.depends_on(&t1); .depends_on(&t1);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(t2)?, 1042); assert_eq!(dag.get(t2)?, 1042);
@@ -98,10 +93,7 @@ async fn test_unit_type_chains() -> DagResult<()> {
.add_task(task_fn(|_: ()| async { "done" })) .add_task(task_fn(|_: ()| async { "done" }))
.depends_on(&t3); .depends_on(&t3);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
dag.get(t1)?; // Verify no error dag.get(t1)?; // Verify no error
dag.get(t2)?; dag.get(t2)?;
@@ -132,10 +124,7 @@ async fn test_nested_option_result_types() -> DagResult<()> {
})) }))
.depends_on(&t1); .depends_on(&t1);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(t2)?, 55); assert_eq!(dag.get(t2)?, 55);
@@ -159,10 +148,7 @@ async fn test_tuple_of_tuples() -> DagResult<()> {
)) ))
.depends_on((&t1, &t2)); .depends_on((&t1, &t2));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(t3)?, 36); assert_eq!(dag.get(t3)?, 36);
@@ -196,10 +182,7 @@ async fn test_string_types_variety() -> DagResult<()> {
)) ))
.depends_on((&t1, &t2, &t3, &t4, &t5, &t6)); .depends_on((&t1, &t2, &t3, &t4, &t5, &t6));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let result = dag.get(combined)?; let result = dag.get(combined)?;
assert!(result.contains("static str")); assert!(result.contains("static str"));
@@ -228,10 +211,7 @@ async fn test_generic_type_constraints() -> DagResult<()> {
)) ))
.depends_on((&t1, &t2, &t3)); .depends_on((&t1, &t2, &t3));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let result = dag.get(combined)?; let result = dag.get(combined)?;
assert!(result.contains("Wrapper(42)")); assert!(result.contains("Wrapper(42)"));
@@ -257,10 +237,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
.add_task(task_fn(|val: i32| async move { val * 2 })) .add_task(task_fn(|val: i32| async move { val * 2 }))
.depends_on(&t1); .depends_on(&t1);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(t2)?, 200); assert_eq!(dag.get(t2)?, 200);
@@ -285,10 +262,7 @@ async fn test_array_types() -> DagResult<()> {
})) }))
.depends_on((&t1, &t2)); .depends_on((&t1, &t2));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(t3)?, 55); assert_eq!(dag.get(t3)?, 55);
@@ -327,10 +301,7 @@ async fn test_enum_variants() -> DagResult<()> {
)) ))
.depends_on((&t1, &t2, &t3, &t4)); .depends_on((&t1, &t2, &t3, &t4));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert!(dag.get(combined)?); assert!(dag.get(combined)?);
+12 -44
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{task, DagResult, DagRunner, Task}; use dagx::{task, DagResult, DagRunner, Task};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -10,10 +11,7 @@ async fn test_empty_dag_execution() -> DagResult<()> {
// Test that an empty DAG can be created and run without issues // Test that an empty DAG can be created and run without issues
let dag = DagRunner::new(); let dag = DagRunner::new();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Should complete successfully with no tasks // Should complete successfully with no tasks
Ok(()) Ok(())
@@ -26,10 +24,7 @@ async fn test_single_task_no_deps() -> DagResult<()> {
let task = dag.add_task(task_fn(|_: ()| async { 42 })); let task = dag.add_task(task_fn(|_: ()| async { 42 }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(task)?, 42); assert_eq!(dag.get(task)?, 42);
Ok(()) Ok(())
@@ -50,10 +45,7 @@ async fn test_single_task_unit_input_output() -> DagResult<()> {
} }
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
dag.get(task)?; // Just verify it doesn't error dag.get(task)?; // Just verify it doesn't error
assert_eq!(executed.load(Ordering::SeqCst), 1); assert_eq!(executed.load(Ordering::SeqCst), 1);
@@ -70,10 +62,7 @@ async fn test_zero_sized_type_task() -> DagResult<()> {
let task = dag.add_task(task_fn(|_: ()| async { ZeroSized })); let task = dag.add_task(task_fn(|_: ()| async { ZeroSized }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(&task)?, ZeroSized); assert_eq!(dag.get(&task)?, ZeroSized);
Ok(()) Ok(())
@@ -85,10 +74,7 @@ async fn test_empty_dag_multiple_runs() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
for _ in 0..10 { for _ in 0..10 {
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
} }
Ok(()) Ok(())
@@ -103,10 +89,7 @@ async fn test_dag_with_only_source_tasks() -> DagResult<()> {
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) .map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
assert_eq!(dag.get(task)?, i); assert_eq!(dag.get(task)?, i);
@@ -132,10 +115,7 @@ async fn test_single_self_contained_stateful_task() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let task = dag.add_task(Counter { value: 21 }); let task = dag.add_task(Counter { value: 21 });
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(task)?, 42); assert_eq!(dag.get(task)?, 42);
Ok(()) Ok(())
@@ -149,17 +129,11 @@ async fn test_alternating_empty_and_filled_dag_runs() -> DagResult<()> {
if i % 2 == 0 { if i % 2 == 0 {
// Empty DAG // Empty DAG
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
} else { } else {
// DAG with a task // DAG with a task
let task = dag.add_task(task_fn(move |_: ()| async move { i })); let task = dag.add_task(task_fn(move |_: ()| async move { i }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(task)?, i); assert_eq!(dag.get(task)?, i);
} }
} }
@@ -179,10 +153,7 @@ async fn test_dag_with_never_type_simulation() -> DagResult<()> {
// This task completes immediately but returns a Result containing "never" type // This task completes immediately but returns a Result containing "never" type
let task = dag.add_task(task_fn(|_: ()| async { Result::<i32, Never>::Ok(42) })); let task = dag.add_task(task_fn(|_: ()| async { Result::<i32, Never>::Ok(42) }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(&task)?, Ok(42)); assert_eq!(dag.get(&task)?, Ok(42));
Ok(()) Ok(())
@@ -199,10 +170,7 @@ async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> {
1337 1337
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(task)?, 1337); assert_eq!(dag.get(task)?, 1337);
Ok(()) Ok(())
+13 -20
View File
@@ -1,5 +1,6 @@
// Test that custom types work without any trait implementations // Test that custom types work without any trait implementations
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
struct Person { struct Person {
@@ -56,11 +57,9 @@ async fn test_custom_type_single_dependency() {
let fetch = dag.add_task(FetchPerson); let fetch = dag.add_task(FetchPerson);
let process = dag.add_task(ProcessPerson).depends_on(&fetch); let process = dag.add_task(ProcessPerson).depends_on(&fetch);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let result = dag.get(process).unwrap(); let result = dag.get(process).unwrap();
assert_eq!(result, "Alice is 30 years old"); assert_eq!(result, "Alice is 30 years old");
@@ -74,11 +73,9 @@ async fn test_custom_type_passthrough() {
let transform = dag.add_task(PersonToCompany).depends_on(&fetch); let transform = dag.add_task(PersonToCompany).depends_on(&fetch);
let process = dag.add_task(ProcessPerson).depends_on(&fetch); let process = dag.add_task(ProcessPerson).depends_on(&fetch);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let person_result = dag.get(process).unwrap(); let person_result = dag.get(process).unwrap();
let company_result = dag.get(transform).unwrap(); let company_result = dag.get(transform).unwrap();
@@ -99,11 +96,9 @@ async fn test_custom_type_diamond_pattern() {
let path_a = dag.add_task(ProcessPerson).depends_on(&fetch); let path_a = dag.add_task(ProcessPerson).depends_on(&fetch);
let path_b = dag.add_task(PersonToCompany).depends_on(&fetch); let path_b = dag.add_task(PersonToCompany).depends_on(&fetch);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let result_a = dag.get(path_a).unwrap(); let result_a = dag.get(path_a).unwrap();
let result_b = dag.get(path_b).unwrap(); let result_b = dag.get(path_b).unwrap();
@@ -121,11 +116,9 @@ async fn test_custom_type_fan_out() {
let process2 = dag.add_task(ProcessPerson).depends_on(&fetch); let process2 = dag.add_task(ProcessPerson).depends_on(&fetch);
let process3 = dag.add_task(ProcessPerson).depends_on(&fetch); let process3 = dag.add_task(ProcessPerson).depends_on(&fetch);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let result1 = dag.get(process1).unwrap(); let result1 = dag.get(process1).unwrap();
let result2 = dag.get(process2).unwrap(); let result2 = dag.get(process2).unwrap();
+4 -5
View File
@@ -4,6 +4,7 @@
//! prevents it through the type-state pattern. //! prevents it through the type-state pattern.
use dagx::{task, DagRunner, Task, TaskHandle}; use dagx::{task, DagRunner, Task, TaskHandle};
use futures::FutureExt;
/// Demonstrates that TaskBuilder is consumed by depends_on, /// Demonstrates that TaskBuilder is consumed by depends_on,
/// preventing you from wiring the same task multiple times /// preventing you from wiring the same task multiple times
@@ -270,11 +271,9 @@ async fn test_type_safety_prevents_invalid_graphs() {
let result = dag.add_task(Aggregate).depends_on((&t1, &t2)); let result = dag.add_task(Aggregate).depends_on((&t1, &t2));
// Execute and verify // Execute and verify
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(result).unwrap(), 40); // (10*2) + (10*2) assert_eq!(dag.get(result).unwrap(), 40); // (10*2) + (10*2)
+5 -16
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[tokio::test] #[tokio::test]
@@ -81,10 +82,7 @@ async fn test_dependency_resolution_order() -> DagResult<()> {
.depends_on(d) .depends_on(d)
}; };
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(e)?, 5); assert_eq!(dag.get(e)?, 5);
@@ -118,10 +116,7 @@ async fn test_transitive_dependencies() -> DagResult<()> {
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn(|x: i32| async move { x + 5 }))
.depends_on(b); .depends_on(b);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(&a)?, 10); assert_eq!(dag.get(&a)?, 10);
assert_eq!(dag.get(b)?, 20); assert_eq!(dag.get(b)?, 20);
@@ -144,10 +139,7 @@ async fn test_deep_chain_50_levels() -> DagResult<()> {
handles.push(next); handles.push(next);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(handles.last().unwrap())?, 50); assert_eq!(dag.get(handles.last().unwrap())?, 50);
Ok(()) Ok(())
@@ -166,10 +158,7 @@ async fn test_deep_chain_200_levels() -> DagResult<()> {
.depends_on(current); .depends_on(current);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(current)?, 201); assert_eq!(dag.get(current)?, 201);
Ok(()) Ok(())
+6 -20
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
#[tokio::test] #[tokio::test]
async fn test_single_dependency() -> DagResult<()> { async fn test_single_dependency() -> DagResult<()> {
@@ -12,10 +13,7 @@ async fn test_single_dependency() -> DagResult<()> {
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(&source); .depends_on(&source);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(dependent)?, 43); assert_eq!(dag.get(dependent)?, 43);
Ok(()) Ok(())
@@ -42,10 +40,7 @@ async fn test_dependency_data_flow() -> DagResult<()> {
.add_task(task_fn(|(s, p): (i32, i32)| async move { s + p })) .add_task(task_fn(|(s, p): (i32, i32)| async move { s + p }))
.depends_on((&sum, &product)); .depends_on((&sum, &product));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Note: sum and product are not sinks (final_result depends on them) // Note: sum and product are not sinks (final_result depends on them)
// Only final_result is a sink and can be retrieved // Only final_result is a sink and can be retrieved
@@ -69,10 +64,7 @@ async fn test_dependencies_with_different_types() -> DagResult<()> {
})) }))
.depends_on((&int_source, &string_source, &bool_source)); .depends_on((&int_source, &string_source, &bool_source));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(combined)?, "hello: 42 (true)"); assert_eq!(dag.get(combined)?, "hello: 42 (true)");
Ok(()) Ok(())
@@ -93,10 +85,7 @@ async fn test_shared_dependencies() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify all dependents got the correct value from shared // Verify all dependents got the correct value from shared
for (i, handle) in dependents.iter().enumerate() { for (i, handle) in dependents.iter().enumerate() {
@@ -115,10 +104,7 @@ async fn test_multiple_source_nodes() -> DagResult<()> {
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 10 }))) .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 10 })))
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
for (i, source) in sources.iter().enumerate() { for (i, source) in sources.iter().enumerate() {
assert_eq!(dag.get(source)?, i as i32 * 10); assert_eq!(dag.get(source)?, i as i32 * 10);
+8 -28
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
#[tokio::test] #[tokio::test]
async fn test_two_dependencies() -> DagResult<()> { async fn test_two_dependencies() -> DagResult<()> {
@@ -13,10 +14,7 @@ async fn test_two_dependencies() -> DagResult<()> {
.add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y }))
.depends_on((&a, &b)); .depends_on((&a, &b));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(sum)?, 30); assert_eq!(dag.get(sum)?, 30);
Ok(()) Ok(())
@@ -35,10 +33,7 @@ async fn test_three_dependencies() -> DagResult<()> {
)) ))
.depends_on((&a, &b, &c)); .depends_on((&a, &b, &c));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(sum)?, 6); assert_eq!(dag.get(sum)?, 6);
Ok(()) Ok(())
@@ -58,10 +53,7 @@ async fn test_four_dependencies() -> DagResult<()> {
})) }))
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3])); .depends_on((&deps[0], &deps[1], &deps[2], &deps[3]));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(sum)?, 10); // 1+2+3+4 assert_eq!(dag.get(sum)?, 10); // 1+2+3+4
Ok(()) Ok(())
@@ -81,10 +73,7 @@ async fn test_five_dependencies() -> DagResult<()> {
)) ))
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4])); .depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4]));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(sum)?, 15); // 1+2+3+4+5 assert_eq!(dag.get(sum)?, 15); // 1+2+3+4+5
Ok(()) Ok(())
@@ -106,10 +95,7 @@ async fn test_six_dependencies() -> DagResult<()> {
&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5] &deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5]
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(sum)?, 21); // 1+2+3+4+5+6 assert_eq!(dag.get(sum)?, 21); // 1+2+3+4+5+6
Ok(()) Ok(())
@@ -133,10 +119,7 @@ async fn test_seven_dependencies() -> DagResult<()> {
&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6], &deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6],
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(sum)?, 28); // 1+2+3+4+5+6+7 assert_eq!(dag.get(sum)?, 28); // 1+2+3+4+5+6+7
Ok(()) Ok(())
@@ -160,10 +143,7 @@ async fn test_eight_dependencies() -> DagResult<()> {
&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6], &deps[7], &deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6], &deps[7],
)); ));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(sum)?, 36); // Sum of 1..=8 assert_eq!(dag.get(sum)?, 36); // Sum of 1..=8
Ok(()) Ok(())
+18 -35
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagError, DagRunner}; use dagx::{DagError, DagRunner};
use futures::FutureExt;
#[tokio::test] #[tokio::test]
async fn test_result_not_found_error() { async fn test_result_not_found_error() {
@@ -14,11 +15,9 @@ async fn test_result_not_found_error() {
assert!(matches!(result, Err(DagError::ResultNotFound { .. }))); assert!(matches!(result, Err(DagError::ResultNotFound { .. })));
// Run and verify it works after // Run and verify it works after
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(&t1).unwrap(), 42); assert_eq!(dag.get(&t1).unwrap(), 42);
} }
@@ -34,11 +33,9 @@ async fn test_nested_result_error_handling() {
let task2 = dag.add_task(task_fn(|_: ()| async { Result::<i32, String>::Ok(42) })); let task2 = dag.add_task(task_fn(|_: ()| async { Result::<i32, String>::Ok(42) }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Both tasks complete successfully (they return Results) // Both tasks complete successfully (they return Results)
assert_eq!(dag.get(&task1).unwrap(), Err("Internal error".to_string())); assert_eq!(dag.get(&task1).unwrap(), Err("Internal error".to_string()));
@@ -51,11 +48,9 @@ async fn test_option_none_not_error() {
let task = dag.add_task(task_fn(|_: ()| async { None::<i32> })); let task = dag.add_task(task_fn(|_: ()| async { None::<i32> }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// None is a valid result, not an error // None is a valid result, not an error
assert_eq!(dag.get(&task).unwrap(), None); assert_eq!(dag.get(&task).unwrap(), None);
@@ -82,11 +77,9 @@ async fn test_custom_error_types() {
}) })
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Check custom errors are preserved // Check custom errors are preserved
match dag.get(&task1).unwrap() { match dag.get(&task1).unwrap() {
@@ -113,11 +106,7 @@ async fn test_error_in_tuple_dependency() {
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((&ok_task, &err_task)); .depends_on((&ok_task, &err_task));
let result = dag let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
assert!(result.is_err()); assert!(result.is_err());
assert!(dag.get(dependent).is_err()); assert!(dag.get(dependent).is_err());
@@ -135,11 +124,7 @@ async fn test_string_panic_vs_structured_panic() {
panic!("Formatted panic: value={}, code={}", 42, "ERROR"); panic!("Formatted panic: value={}, code={}", 42, "ERROR");
})); }));
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
// Both should result in errors // Both should result in errors
assert!(dag.get(&string_panic).is_err()); assert!(dag.get(&string_panic).is_err());
@@ -159,11 +144,9 @@ async fn test_anyhow_like_error_chain() {
outer outer
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Get the chained error // Get the chained error
let result = dag.get(&task).unwrap(); let result = dag.get(&task).unwrap();
+13 -40
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::DagRunner; use dagx::DagRunner;
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -22,11 +23,7 @@ async fn test_linear_error_propagation() {
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(t3); .depends_on(t3);
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
// t1 succeeds // t1 succeeds
assert_eq!(dag.get(&t1).unwrap(), 1); assert_eq!(dag.get(&t1).unwrap(), 1);
@@ -65,11 +62,7 @@ async fn test_diamond_error_propagation() {
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
.depends_on((&left, &right)); .depends_on((&left, &right));
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
assert_eq!(dag.get(&source).unwrap(), 100); assert_eq!(dag.get(&source).unwrap(), 100);
assert_eq!(dag.get(left).unwrap(), 50); assert_eq!(dag.get(left).unwrap(), 50);
@@ -119,11 +112,7 @@ async fn test_error_stops_at_boundary() {
} }
})); }));
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
// Error branch: source runs but dependent doesn't // Error branch: source runs but dependent doesn't
assert!(dag.get(&error_source).is_err()); assert!(dag.get(&error_source).is_err());
@@ -156,11 +145,7 @@ async fn test_multiple_error_sources_convergence() {
.add_task(task_fn(|(_, _, _): (i32, i32, i32)| async { 100 })) .add_task(task_fn(|(_, _, _): (i32, i32, i32)| async { 100 }))
.depends_on((&error1, &error2, &success)); .depends_on((&error1, &error2, &success));
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
// All error sources fail // All error sources fail
assert!(dag.get(&error1).is_err()); assert!(dag.get(&error1).is_err());
@@ -189,11 +174,7 @@ async fn test_error_in_wide_fanout() {
}) })
.collect(); .collect();
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
// Source fails // Source fails
assert!(dag.get(&source).is_err()); assert!(dag.get(&source).is_err());
@@ -234,11 +215,9 @@ async fn test_selective_error_propagation() {
})) }))
.depends_on(handler); .depends_on(handler);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Source returns an Err variant (not a panic) // Source returns an Err variant (not a panic)
assert_eq!(dag.get(&source).unwrap(), Err("Source error".to_string())); assert_eq!(dag.get(&source).unwrap(), Err("Source error".to_string()));
@@ -294,11 +273,7 @@ async fn test_error_propagation_timing() {
})) }))
.depends_on(&t2); .depends_on(&t2);
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
let order = propagation_order.lock().clone(); let order = propagation_order.lock().clone();
@@ -373,11 +348,9 @@ async fn test_partial_branch_failure_propagation() {
)) ))
.depends_on((&a3, &b3, &c3)); .depends_on((&a3, &b3, &c3));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .ok();
.await
.ok();
// Branch A succeeds // Branch A succeeds
assert_eq!(dag.get(a3).unwrap(), 106); assert_eq!(dag.get(a3).unwrap(), 106);
+9 -35
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -23,11 +24,7 @@ async fn test_fallback_on_error() -> DagResult<()> {
let _selector = dag.add_task(task_fn(|_: ()| async {})); let _selector = dag.add_task(task_fn(|_: ()| async {}));
// Run DAG // Run DAG
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
// Primary should fail // Primary should fail
assert!(dag.get(&primary).is_err()); assert!(dag.get(&primary).is_err());
@@ -63,10 +60,7 @@ async fn test_retry_pattern_simulation() -> DagResult<()> {
} }
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Task completes with error on first "attempt" // Task completes with error on first "attempt"
assert_eq!(dag.get(&task)?, Err("Simulated failure")); assert_eq!(dag.get(&task)?, Err("Simulated failure"));
@@ -109,10 +103,7 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// First few tasks fail and open circuit // First few tasks fail and open circuit
for task in &tasks[0..3] { for task in &tasks[0..3] {
@@ -144,11 +135,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> {
vec![0, 10, 20, 0, 40, 50, 0, 70, 80, 0] // 0 for failed tasks vec![0, 10, 20, 0, 40, 50, 0, 70, 80, 0] // 0 for failed tasks
})); }));
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
let result = dag.get(&aggregator)?; let result = dag.get(&aggregator)?;
assert_eq!(result.len(), 10); assert_eq!(result.len(), 10);
@@ -178,10 +165,7 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Check accumulated errors // Check accumulated errors
let error_list = errors.lock().clone(); let error_list = errors.lock().clone();
@@ -215,10 +199,7 @@ async fn test_graceful_degradation() -> DagResult<()> {
})) }))
.depends_on(&critical); .depends_on(&critical);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Critical path works // Critical path works
assert_eq!(dag.get(final_task)?, 15); assert_eq!(dag.get(final_task)?, 15);
@@ -249,10 +230,7 @@ async fn test_error_boundary_isolation() -> DagResult<()> {
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn(|x: i32| async move { x + 5 }))
.depends_on(b2); .depends_on(b2);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Group A fails (returns Err) // Group A fails (returns Err)
assert_eq!(dag.get(a2)?, Err("Group A error")); assert_eq!(dag.get(a2)?, Err("Group A error"));
@@ -287,11 +265,7 @@ async fn test_compensation_action() -> DagResult<()> {
} }
})); }));
let _ = dag let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
// Action failed // Action failed
assert!(dag.get(&action).is_err()); assert!(dag.get(&action).is_err());
+41 -66
View File
@@ -1,27 +1,24 @@
// Basic execution tests for DagRunner // Basic execution tests for DagRunner
use crate::common::task_fn; use crate::common::task_fn;
use futures::FutureExt;
use dagx::*; use dagx::*;
#[tokio::test] #[tokio::test]
async fn test_empty_dag() { async fn test_empty_dag() {
let dag = DagRunner::new(); let dag = DagRunner::new();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap(); // Should complete without error
.await
.unwrap(); // Should complete without error
} }
#[tokio::test] #[tokio::test]
async fn test_single_task() { async fn test_single_task() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { 42 })); let task = dag.add_task(task_fn(|_: ()| async { 42 }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(task).unwrap(), 42); assert_eq!(dag.get(task).unwrap(), 42);
} }
@@ -43,11 +40,9 @@ async fn test_deep_chain() {
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(d); .depends_on(d);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Note: a, b, c, d are not sinks (they have dependents in the chain) // Note: a, b, c, d are not sinks (they have dependents in the chain)
// Only e is a sink and can be retrieved // Only e is a sink and can be retrieved
@@ -62,11 +57,9 @@ async fn test_wide_parallel() {
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 })))
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
assert_eq!(dag.get(task).unwrap(), i * 2); assert_eq!(dag.get(task).unwrap(), i * 2);
@@ -88,11 +81,9 @@ async fn test_diamond_dependency() {
.add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y }))
.depends_on((&b, &c)); .depends_on((&b, &c));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Note: a, b, and c are not sinks (they have dependents) // Note: a, b, and c are not sinks (they have dependents)
// Only d is a sink and can be retrieved // Only d is a sink and can be retrieved
@@ -118,11 +109,9 @@ async fn test_different_output_types() {
} }
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(&dag.get(string_task).unwrap(), "hello"); assert_eq!(&dag.get(string_task).unwrap(), "hello");
assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]); assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]);
@@ -153,11 +142,9 @@ async fn test_multiple_sinks() {
.add_task(task_fn(|x: i32| async move { x * 3 })) .add_task(task_fn(|x: i32| async move { x * 3 }))
.depends_on(branch2); .depends_on(branch2);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap(); // Should wait for both sinks
.await
.unwrap(); // Should wait for both sinks
assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1 assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1
assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3 assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3
@@ -177,11 +164,9 @@ async fn test_single_task_inline_path() {
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn(|x: i32| async move { x + 5 }))
.depends_on(t2); .depends_on(t2);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5 assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5
} }
@@ -204,11 +189,9 @@ async fn test_multi_task_spawn_path() {
.add_task(task_fn(|x: i32| async move { x + 3 })) .add_task(task_fn(|x: i32| async move { x + 3 }))
.depends_on(&source); .depends_on(&source);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(t1).unwrap(), 101); assert_eq!(dag.get(t1).unwrap(), 101);
assert_eq!(dag.get(t2).unwrap(), 102); assert_eq!(dag.get(t2).unwrap(), 102);
@@ -230,18 +213,16 @@ async fn test_producer_consumer_channels() {
)) ))
.depends_on(&producer); .depends_on(&producer);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(consumer).unwrap(), 15); assert_eq!(dag.get(consumer).unwrap(), 15);
} }
#[tokio::test] #[tokio::test]
async fn test_multi_consumer_fanout() { async fn test_multi_consumer_fanout() {
// Test fanout with multiple consumers from one producer (channel creation) // Test fanout with multiple consumers from one producer
let dag = DagRunner::new(); let dag = DagRunner::new();
let producer = dag.add_task(task_fn(|_: ()| async { 42 })); let producer = dag.add_task(task_fn(|_: ()| async { 42 }));
@@ -257,11 +238,9 @@ async fn test_multi_consumer_fanout() {
.add_task(task_fn(|x: i32| async move { x - 5 })) .add_task(task_fn(|x: i32| async move { x - 5 }))
.depends_on(&producer); .depends_on(&producer);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(c1).unwrap(), 84); assert_eq!(dag.get(c1).unwrap(), 84);
assert_eq!(dag.get(c2).unwrap(), 52); assert_eq!(dag.get(c2).unwrap(), 52);
@@ -287,11 +266,9 @@ async fn test_compute_layers_with_dependents_tracking() {
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
.depends_on((&left, &right)); .depends_on((&left, &right));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(sink).unwrap(), 5); // (1+1) + (1+2) = 5 assert_eq!(dag.get(sink).unwrap(), 5); // (1+1) + (1+2) = 5
} }
@@ -337,11 +314,9 @@ async fn test_layer_execution_order() {
})) }))
.depends_on(t2); .depends_on(t2);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let order = execution_order.lock().clone(); let order = execution_order.lock().clone();
assert_eq!(order, vec![1, 2, 3]); assert_eq!(order, vec![1, 2, 3]);
+13 -20
View File
@@ -3,6 +3,7 @@ use crate::common::task_fn;
use crate::common::tasks::*; use crate::common::tasks::*;
use dagx::*; use dagx::*;
use futures::FutureExt;
#[tokio::test] #[tokio::test]
async fn test_example_a_fan_out() { async fn test_example_a_fan_out() {
@@ -12,11 +13,9 @@ async fn test_example_a_fan_out() {
let plus1 = dag.add_task(Inc::new()).depends_on(&base); // 11 let plus1 = dag.add_task(Inc::new()).depends_on(&base); // 11
let plus2 = dag.add_task(Inc::new()).depends_on(&base); // 11 let plus2 = dag.add_task(Inc::new()).depends_on(&base); // 11
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap(); // SPEC API with spawner
.await
.unwrap(); // SPEC API with spawner
assert_eq!(dag.get(plus1).unwrap(), 11); assert_eq!(dag.get(plus1).unwrap(), 11);
assert_eq!(dag.get(plus2).unwrap(), 11); assert_eq!(dag.get(plus2).unwrap(), 11);
@@ -32,11 +31,9 @@ async fn test_example_b_fan_in() {
let output = dag.add_task(OutputResult::new()).depends_on((&s, &n, &f)); let output = dag.add_task(OutputResult::new()).depends_on((&s, &n, &f));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(&dag.get(output).unwrap(), "hi:42:true"); assert_eq!(&dag.get(output).unwrap(), "hi:42:true");
} }
@@ -52,11 +49,9 @@ async fn test_example_c_many_to_many() {
let prod_yz = dag.add_task(Mul::new()).depends_on((&y, &z)); // 15 let prod_yz = dag.add_task(Mul::new()).depends_on((&y, &z)); // 15
let total = dag.add_task(Add::new()).depends_on((&sum_xy, &prod_yz)); // 20 let total = dag.add_task(Add::new()).depends_on((&sum_xy, &prod_yz)); // 20
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(total).unwrap(), 20); assert_eq!(dag.get(total).unwrap(), 20);
} }
@@ -76,11 +71,9 @@ async fn test_single_input_forms() {
.add_task(task_fn(|s: String| async move { s.to_uppercase() })) .add_task(task_fn(|s: String| async move { s.to_uppercase() }))
.depends_on((&s,)); .depends_on((&s,));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(&dag.get(upper1).unwrap(), "HELLO"); assert_eq!(&dag.get(upper1).unwrap(), "HELLO");
assert_eq!(&dag.get(upper2).unwrap(), "HELLO"); assert_eq!(&dag.get(upper2).unwrap(), "HELLO");
+33 -26
View File
@@ -1,7 +1,8 @@
//! Integration tests for end-to-end functionality //! Integration tests for end-to-end functionality
use dagx::{DagResult, DagRunner, Task};
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner, Task};
use futures::FutureExt;
use std::time::Instant; use std::time::Instant;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
@@ -18,7 +19,7 @@ impl LoadValue {
#[dagx::task] #[dagx::task]
impl LoadValue { impl LoadValue {
async fn run(&self) -> i32 { async fn run(&self) -> i32 {
self.value // Read-only state: use &self self.value // Read-only state: use &self
} }
} }
@@ -33,7 +34,7 @@ impl Multiply {
#[dagx::task] #[dagx::task]
impl Multiply { impl Multiply {
async fn run(&self, input: &i32) -> i32 { async fn run(&self, input: &i32) -> i32 {
input * self.factor // Read-only state: use &self input * self.factor // Read-only state: use &self
} }
} }
@@ -41,7 +42,7 @@ struct Add;
#[dagx::task] #[dagx::task]
impl Add { impl Add {
async fn run(a: &i32, b: &i32) -> i32 { async fn run(a: &i32, b: &i32) -> i32 {
a + b // Stateless: no self needed a + b // Stateless: no self needed
} }
} }
@@ -67,7 +68,7 @@ async fn integration_test_etl_pipeline() -> DagResult<()> {
let final_result = dag.add_task(Add).depends_on((&combine12, &transform3)); let final_result = dag.add_task(Add).depends_on((&combine12, &transform3));
// Execute pipeline // Execute pipeline
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify // Verify
let result = dag.get(final_result)?; let result = dag.get(final_result)?;
@@ -97,7 +98,7 @@ async fn integration_test_multi_layer_dag() -> DagResult<()> {
// Layer 4: Final result // Layer 4: Final result
let final_result = dag.add_task(Add).depends_on((&double_ab, &double_bc)); let final_result = dag.add_task(Add).depends_on((&double_ab, &double_bc));
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
let result = dag.get(final_result)?; let result = dag.get(final_result)?;
// (1+2)*2 + (2+3)*2 = 6 + 10 = 16 // (1+2)*2 + (2+3)*2 = 6 + 10 = 16
@@ -118,7 +119,7 @@ async fn integration_test_multiple_sinks() -> DagResult<()> {
let branch2 = dag.add_task(Multiply::new(3)).depends_on(&source); let branch2 = dag.add_task(Multiply::new(3)).depends_on(&source);
let branch3 = dag.add_task(Multiply::new(4)).depends_on(&source); let branch3 = dag.add_task(Multiply::new(4)).depends_on(&source);
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// All branches should complete // All branches should complete
assert_eq!(dag.get(branch1)?, 20); assert_eq!(dag.get(branch1)?, 20);
@@ -144,7 +145,7 @@ async fn integration_test_diamond_cascade() -> DagResult<()> {
let right2 = dag.add_task(Multiply::new(3)).depends_on(sink1); let right2 = dag.add_task(Multiply::new(3)).depends_on(sink1);
let sink2 = dag.add_task(Add).depends_on((&left2, &right2)); let sink2 = dag.add_task(Add).depends_on((&left2, &right2));
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
let result = dag.get(sink2)?; let result = dag.get(sink2)?;
// Diamond 1: (10*2) + (10*3) = 50 // Diamond 1: (10*2) + (10*3) = 50
@@ -175,7 +176,7 @@ async fn integration_test_wide_and_deep() -> DagResult<()> {
final_nodes.push(step3); final_nodes.push(step3);
} }
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify each chain // Verify each chain
for (i, node) in final_nodes.iter().enumerate() { for (i, node) in final_nodes.iter().enumerate() {
@@ -194,7 +195,7 @@ async fn integration_test_error_recovery() -> DagResult<()> {
let source = dag.add_task(LoadValue::new(42)); let source = dag.add_task(LoadValue::new(42));
let transform = dag.add_task(Multiply::new(2)).depends_on(&source); let transform = dag.add_task(Multiply::new(2)).depends_on(&source);
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Should be able to access results // Should be able to access results
assert_eq!(dag.get(transform)?, 84); assert_eq!(dag.get(transform)?, 84);
@@ -230,7 +231,7 @@ async fn integration_test_large_scale() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify some results // Verify some results
assert_eq!(dag.get(aggregations[0])?, 1); // First two assert_eq!(dag.get(aggregations[0])?, 1); // First two
@@ -254,7 +255,7 @@ async fn integration_test_reused_values() -> DagResult<()> {
// Combine some of the results // Combine some of the results
let combined = dag.add_task(Add).depends_on((&double, &triple)); let combined = dag.add_task(Add).depends_on((&double, &triple));
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
assert_eq!(dag.get(double)?, 200); assert_eq!(dag.get(double)?, 200);
assert_eq!(dag.get(triple)?, 300); assert_eq!(dag.get(triple)?, 300);
@@ -283,7 +284,7 @@ async fn integration_test_parallel_execution_timing() -> DagResult<()> {
.collect(); .collect();
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
// Verify all tasks completed // Verify all tasks completed
@@ -322,23 +323,29 @@ async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> {
// Layer 2: Sequential chain of 3 tasks depending on the first parallel task // Layer 2: Sequential chain of 3 tasks depending on the first parallel task
// (should add ~150ms) // (should add ~150ms)
let seq1 = dag.add_task(task_fn(move |x: i32| async move { let seq1 = dag
sleep(sleep_duration).await; .add_task(task_fn(move |x: i32| async move {
x + 1 sleep(sleep_duration).await;
})).depends_on(&parallel_tasks[0]); x + 1
}))
.depends_on(&parallel_tasks[0]);
let seq2 = dag.add_task(task_fn(move |x: i32| async move { let seq2 = dag
sleep(sleep_duration).await; .add_task(task_fn(move |x: i32| async move {
x + 1 sleep(sleep_duration).await;
})).depends_on(seq1); x + 1
}))
.depends_on(seq1);
let seq3 = dag.add_task(task_fn(move |x: i32| async move { let seq3 = dag
sleep(sleep_duration).await; .add_task(task_fn(move |x: i32| async move {
x + 1 sleep(sleep_duration).await;
})).depends_on(seq2); x + 1
}))
.depends_on(seq2);
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { tokio::spawn(fut); }).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
// Verify results // Verify results
+13 -25
View File
@@ -3,6 +3,7 @@ use crate::common::task_fn;
use crate::common::tasks::Counter; use crate::common::tasks::Counter;
use dagx::*; use dagx::*;
use futures::FutureExt;
#[tokio::test] #[tokio::test]
async fn test_result_type() { async fn test_result_type() {
@@ -13,11 +14,9 @@ async fn test_result_type() {
Err::<i32, String>("error".to_string()) Err::<i32, String>("error".to_string())
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(success_task).unwrap(), Ok(42)); assert_eq!(dag.get(success_task).unwrap(), Ok(42));
assert_eq!(dag.get(error_task).unwrap(), Err("error".to_string())); assert_eq!(dag.get(error_task).unwrap(), Err("error".to_string()));
@@ -30,10 +29,7 @@ async fn test_dag_result_type_works() {
// Test that DagResult<()> works with ? operator pattern // Test that DagResult<()> works with ? operator pattern
async fn run_dag(dag: &DagRunner) -> DagResult<()> { async fn run_dag(dag: &DagRunner) -> DagResult<()> {
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
Ok(()) Ok(())
} }
@@ -68,7 +64,7 @@ async fn test_error_messages_are_helpful() {
let err_msg = format!("{}", result.unwrap_err()); let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("Result not found")); assert!(err_msg.contains("Result not found"));
assert!(err_msg.contains("task #0")); // Task ID assert!(err_msg.contains("task #0")); // Task ID
assert!(err_msg.contains("Call dag.run()")); assert!(err_msg.contains("Call dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))"));
} }
#[tokio::test] #[tokio::test]
@@ -77,11 +73,7 @@ async fn test_successful_execution_returns_ok() {
let task = dag.add_task(task_fn(|_: ()| async { 42 })); let task = dag.add_task(task_fn(|_: ()| async { 42 }));
// Should return Ok(()) // Should return Ok(())
let result = dag let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
.run(|fut| {
tokio::spawn(fut);
})
.await;
assert!(result.is_ok()); assert!(result.is_ok());
// Should be able to get value // Should be able to get value
@@ -95,11 +87,9 @@ async fn test_stateful_tasks() {
let input = dag.add_task(task_fn(|_: ()| async { 5 })); let input = dag.add_task(task_fn(|_: ()| async { 5 }));
let counter = dag.add_task(Counter::new(10)).depends_on(&input); let counter = dag.add_task(Counter::new(10)).depends_on(&input);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(counter).unwrap(), 15); // 10 + 5 assert_eq!(dag.get(counter).unwrap(), 15); // 10 + 5
} }
@@ -117,11 +107,9 @@ async fn test_task_fn_with_captured_state() {
.add_task(task_fn(move |x: i32| async move { x * multiplier })) .add_task(task_fn(move |x: i32| async move { x * multiplier }))
.depends_on(&base); .depends_on(&base);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(scaled).unwrap(), 35); assert_eq!(dag.get(scaled).unwrap(), 35);
} }
+32 -47
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::DagRunner; use dagx::DagRunner;
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -14,7 +15,12 @@ async fn test_concurrent_run_calls() {
// Add some tasks // Add some tasks
let tasks: Vec<_> = (0..10) let tasks: Vec<_> = (0..10)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) .map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
tokio::time::sleep(Duration::from_millis(5)).await;
i
}))
})
.collect(); .collect();
// Try to run the DAG multiple times concurrently // Try to run the DAG multiple times concurrently
@@ -22,12 +28,7 @@ async fn test_concurrent_run_calls() {
for _ in 0..5 { for _ in 0..5 {
let dag = Arc::clone(&dag); let dag = Arc::clone(&dag);
handles.spawn(async move { handles.spawn(async move { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await });
dag.run(|fut| {
tokio::spawn(fut);
})
.await
});
} }
// Only one run should succeed, others should fail with concurrent execution error // Only one run should succeed, others should fail with concurrent execution error
@@ -62,11 +63,9 @@ async fn test_concurrent_execution_and_building() {
.collect(); .collect();
// Execute the initial DAG // Execute the initial DAG
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Now try to add more tasks while also reading results // Now try to add more tasks while also reading results
let mut handles = JoinSet::new(); let mut handles = JoinSet::new();
@@ -93,11 +92,9 @@ async fn test_concurrent_execution_and_building() {
} }
// Execute again with the new tasks // Execute again with the new tasks
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
} }
#[tokio::test] #[tokio::test]
@@ -139,11 +136,9 @@ async fn test_dag_builder_thread_safety() {
assert_eq!(counter.load(Ordering::SeqCst), 10); assert_eq!(counter.load(Ordering::SeqCst), 10);
// Execute the combined DAG // Execute the combined DAG
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Verify each pipeline computed correctly // Verify each pipeline computed correctly
for task in final_tasks.iter() { for task in final_tasks.iter() {
@@ -163,11 +158,9 @@ async fn test_concurrent_get_operations() {
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) .map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Now access results concurrently // Now access results concurrently
let mut handles = JoinSet::new(); let mut handles = JoinSet::new();
@@ -217,11 +210,9 @@ async fn test_no_race_in_dependency_tracking() {
} }
// Execute // Execute
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// All dependents should have correct values // All dependents should have correct values
for dep in dependents.iter() { for dep in dependents.iter() {
@@ -277,11 +268,9 @@ async fn test_high_concurrency_stress() {
assert_eq!(completed.load(Ordering::SeqCst), 1000); assert_eq!(completed.load(Ordering::SeqCst), 1000);
// The DAG should still be executable // The DAG should still be executable
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
} }
#[tokio::test] #[tokio::test]
@@ -369,11 +358,9 @@ async fn test_atomic_dag_operations() {
assert_eq!(deps.len(), 50); assert_eq!(deps.len(), 50);
// Execute and verify // Execute and verify
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
for dep in deps.iter() { for dep in deps.iter() {
let result = dag.get(dep).unwrap(); let result = dag.get(dep).unwrap();
@@ -424,9 +411,7 @@ async fn test_no_deadlock_on_circular_waiting() {
assert!(result.is_ok(), "Deadlock detected - operation timed out"); assert!(result.is_ok(), "Deadlock detected - operation timed out");
// Should be able to execute the DAG // Should be able to execute the DAG
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
} }
+5 -16
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::time::sleep; use tokio::time::sleep;
@@ -62,10 +63,7 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> {
.depends_on((&layer1[0], &layer1[1], &layer1[2])) .depends_on((&layer1[0], &layer1[1], &layer1[2]))
}; };
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1) assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1)
@@ -185,10 +183,7 @@ async fn test_completion_order_matches_dependencies() -> DagResult<()> {
.depends_on(c) .depends_on(c)
}; };
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(d)?, 6); // (1 + 2) * 2 assert_eq!(dag.get(d)?, 6); // (1 + 2) * 2
@@ -271,10 +266,7 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
.depends_on(c) .depends_on(c)
}; };
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify results // Verify results
assert_eq!(dag.get(a)?, 1); assert_eq!(dag.get(a)?, 1);
@@ -344,10 +336,7 @@ async fn test_independent_tasks_dont_wait() -> DagResult<()> {
})) }))
}; };
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Check results // Check results
assert_eq!(dag.get(fast1)?, "fast1"); assert_eq!(dag.get(fast1)?, "fast1");
+2 -4
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::time::sleep; use tokio::time::sleep;
@@ -24,10 +25,7 @@ async fn test_parallelism_proof_1000_tasks() -> DagResult<()> {
.collect(); .collect();
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
// Verify all tasks completed // Verify all tasks completed
+15 -11
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
@@ -16,16 +17,22 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
// Create 10 independent tasks // Create 10 independent tasks
let tasks: Vec<_> = (0..10) let tasks: Vec<_> = (0..10)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) .map(|i| {
dag.add_task(task_fn({
let value = spawn_count.clone();
move |_: ()| {
let value = value.clone();
async move {
value.fetch_add(1, Ordering::SeqCst);
i
}
}
}))
})
.collect(); .collect();
// Custom spawner that counts invocations // Custom spawner that counts invocations
let counter = spawn_count.clone(); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
dag.run(move |fut| {
counter.fetch_add(1, Ordering::SeqCst);
tokio::spawn(fut);
})
.await?;
// Verify all tasks were spawned // Verify all tasks were spawned
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
@@ -71,10 +78,7 @@ async fn test_tasks_not_running_inline() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify results // Verify results
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
+7 -24
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -40,10 +41,7 @@ async fn test_tasks_run_on_different_threads() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify all tasks completed // Verify all tasks completed
for task in tasks.iter() { for task in tasks.iter() {
@@ -112,10 +110,7 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify all tasks completed // Verify all tasks completed
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
@@ -186,10 +181,7 @@ async fn test_diamond_parallel_execution() -> DagResult<()> {
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((&parallel1, &parallel2)); .depends_on((&parallel1, &parallel2));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(sink)?, 500); // (100 * 2) + (100 * 3) assert_eq!(dag.get(sink)?, 500); // (100 * 2) + (100 * 3)
@@ -276,10 +268,7 @@ async fn test_parallel_branches_with_dependencies() -> DagResult<()> {
.depends_on(b2_1) .depends_on(b2_1)
}; };
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify results // Verify results
assert_eq!(dag.get(b1_2)?, 21); // (10 * 2) + 1 assert_eq!(dag.get(b1_2)?, 21); // (10 * 2) + 1
@@ -371,10 +360,7 @@ async fn test_wide_fanout_parallel_execution() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify results // Verify results
for (i, task) in dependents.iter().enumerate() { for (i, task) in dependents.iter().enumerate() {
@@ -445,10 +431,7 @@ async fn test_massive_parallel_fanout() -> DagResult<()> {
.collect(); .collect();
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
// All should complete // All should complete
+3 -8
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner, Task}; use dagx::{DagResult, DagRunner, Task};
use futures::FutureExt;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -26,10 +27,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
.collect(); .collect();
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
// Verify all tasks completed // Verify all tasks completed
@@ -113,10 +111,7 @@ async fn test_sync_tasks_dont_block_async_runtime() -> DagResult<()> {
})) }))
}; };
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify results // Verify results
assert_eq!(dag.get(sync1)?, 20); assert_eq!(dag.get(sync1)?, 20);
+5 -16
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -44,10 +45,7 @@ async fn test_butterfly_network() -> DagResult<()> {
.depends_on((&layer1[3], &layer1[7])), .depends_on((&layer1[3], &layer1[7])),
]; ];
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify some outputs exist // Verify some outputs exist
for task in &layer2 { for task in &layer2 {
@@ -100,10 +98,7 @@ async fn test_recursive_butterfly() -> DagResult<()> {
let outputs = build_butterfly(&dag, inputs); let outputs = build_butterfly(&dag, inputs);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(outputs.len(), 4); assert_eq!(outputs.len(), 4);
for output in outputs { for output in outputs {
@@ -143,10 +138,7 @@ async fn test_benes_network() -> DagResult<()> {
.depends_on((&stage1[1], &stage1[3])), .depends_on((&stage1[1], &stage1[3])),
]; ];
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
for output in outputs { for output in outputs {
let _ = dag.get(output)?; let _ = dag.get(output)?;
@@ -209,10 +201,7 @@ async fn test_shuffle_exchange_network() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(counter.load(Ordering::Relaxed), 8); assert_eq!(counter.load(Ordering::Relaxed), 8);
+6 -20
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -59,10 +60,7 @@ async fn test_workflow_with_conditional_paths() -> DagResult<()> {
})) }))
.depends_on(combine); .depends_on(combine);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(process)?, 84); assert_eq!(dag.get(process)?, 84);
let result = dag.get(final_result)?; let result = dag.get(final_result)?;
@@ -140,10 +138,7 @@ async fn test_map_reduce_pattern() -> DagResult<()> {
)) ))
.depends_on((&reducers[0], &reducers[1], &reducers[2], &reducers[3])); .depends_on((&reducers[0], &reducers[1], &reducers[2], &reducers[3]));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let result = dag.get(final_task)?; let result = dag.get(final_task)?;
assert!(result > 0); assert!(result > 0);
@@ -200,10 +195,7 @@ async fn test_pipeline_with_feedback() -> DagResult<()> {
})) }))
.depends_on(stage3); .depends_on(stage3);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(stage1)?, 2); assert_eq!(dag.get(stage1)?, 2);
assert_eq!(dag.get(stage3)?, 12); assert_eq!(dag.get(stage3)?, 12);
@@ -247,10 +239,7 @@ async fn test_scatter_gather_pattern() -> DagResult<()> {
)) ))
.depends_on((&workers[0], &workers[1], &workers[2], &workers[3])); .depends_on((&workers[0], &workers[1], &workers[2], &workers[3]));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Sum of 10+20+30+40+50+60+70+80 = 360 // Sum of 10+20+30+40+50+60+70+80 = 360
assert_eq!(dag.get(gather)?, 360); assert_eq!(dag.get(gather)?, 360);
@@ -339,10 +328,7 @@ async fn test_fork_join_with_barriers() -> DagResult<()> {
})) }))
.depends_on(barrier); .depends_on(barrier);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let _ = dag.get(join)?; let _ = dag.get(join)?;
assert_eq!(phase_counter.load(Ordering::SeqCst), 2); assert_eq!(phase_counter.load(Ordering::SeqCst), 2);
+5 -16
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
#[tokio::test] #[tokio::test]
async fn test_2d_grid_pattern() -> DagResult<()> { async fn test_2d_grid_pattern() -> DagResult<()> {
@@ -33,10 +34,7 @@ async fn test_2d_grid_pattern() -> DagResult<()> {
} }
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Check corner values // Check corner values
let bottom_right = dag.get(grid[3][3].as_ref().unwrap())?; let bottom_right = dag.get(grid[3][3].as_ref().unwrap())?;
@@ -99,10 +97,7 @@ async fn test_3d_grid_pattern() -> DagResult<()> {
} }
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Check that all cells have values // Check that all cells have values
for row in &grid { for row in &grid {
@@ -178,10 +173,7 @@ async fn test_hexagonal_grid() -> DagResult<()> {
hex_grid.insert((q, r), task); hex_grid.insert((q, r), task);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify center and some ring values // Verify center and some ring values
assert_eq!(dag.get(hex_grid.get(&(0, 0)).unwrap())?, 100); assert_eq!(dag.get(hex_grid.get(&(0, 0)).unwrap())?, 100);
@@ -230,10 +222,7 @@ async fn test_toroidal_grid() -> DagResult<()> {
} }
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify all torus tasks completed // Verify all torus tasks completed
for task in torus_tasks { for task in torus_tasks {
+7 -24
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
#[tokio::test] #[tokio::test]
async fn test_binary_tree_reduction() -> DagResult<()> { async fn test_binary_tree_reduction() -> DagResult<()> {
@@ -35,10 +36,7 @@ async fn test_binary_tree_reduction() -> DagResult<()> {
let root = current_level[0]; let root = current_level[0];
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Sum of powers of 2 from 0 to 7: 1+2+4+8+16+32+64+128 = 255 // Sum of powers of 2 from 0 to 7: 1+2+4+8+16+32+64+128 = 255
assert_eq!(dag.get(root)?, 255); assert_eq!(dag.get(root)?, 255);
@@ -69,10 +67,7 @@ async fn test_n_ary_tree() -> DagResult<()> {
let root = build_tree(&dag, 3, 1); let root = build_tree(&dag, 3, 1);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let result = dag.get(root)?; let result = dag.get(root)?;
assert!(result > 0); assert!(result > 0);
@@ -119,10 +114,7 @@ async fn test_unbalanced_tree() -> DagResult<()> {
.add_task(task_fn(|(a, b): (i32, i32)| async move { a * b })) .add_task(task_fn(|(a, b): (i32, i32)| async move { a * b }))
.depends_on((&a, &b)); .depends_on((&a, &b));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(&f)?, 1); assert_eq!(dag.get(&f)?, 1);
assert_eq!(dag.get(c)?, 2); assert_eq!(dag.get(c)?, 2);
@@ -170,10 +162,7 @@ async fn test_trie_like_structure() -> DagResult<()> {
.add_task(task_fn(|v: String| async move { format!("{}/log", v) })) .add_task(task_fn(|v: String| async move { format!("{}/log", v) }))
.depends_on(var); .depends_on(var);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(usr_bin)?, "root/usr/bin"); assert_eq!(dag.get(usr_bin)?, "root/usr/bin");
assert_eq!(dag.get(usr_lib)?, "root/usr/lib"); assert_eq!(dag.get(usr_lib)?, "root/usr/lib");
@@ -230,10 +219,7 @@ async fn test_balanced_k_ary_tree() -> DagResult<()> {
let root = create_balanced_tree(&dag, DEPTH, K, 0); let root = create_balanced_tree(&dag, DEPTH, K, 0);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let result = dag.get(root)?; let result = dag.get(root)?;
assert!(result > 0); assert!(result > 0);
@@ -280,10 +266,7 @@ async fn test_merkle_tree_pattern() -> DagResult<()> {
let root_hash = current_level[0]; let root_hash = current_level[0];
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
let result = dag.get(root_hash)?; let result = dag.get(root_hash)?;
assert!(result.starts_with("hash")); assert!(result.starts_with("hash"));
+4 -5
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::DagRunner; use dagx::DagRunner;
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -23,11 +24,9 @@ async fn test_memory_usage_10000_nodes() {
println!("Memory for 10k nodes: ~{} bytes", build_memory); println!("Memory for 10k nodes: ~{} bytes", build_memory);
// Execute // Execute
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let after_exec = memory_usage_hint(); let after_exec = memory_usage_hint();
let exec_memory = after_exec.saturating_sub(after_build); let exec_memory = after_exec.saturating_sub(after_build);
+7 -10
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::DagRunner; use dagx::DagRunner;
use futures::FutureExt;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -26,11 +27,9 @@ async fn test_deep_chain_100_levels() {
// Execute // Execute
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let exec_time = start.elapsed(); let exec_time = start.elapsed();
println!("Executed 100-level chain in {:?}", exec_time); println!("Executed 100-level chain in {:?}", exec_time);
@@ -89,11 +88,9 @@ async fn test_stress_mixed_patterns() {
// Execute // Execute
let start = Instant::now(); let start = Instant::now();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let exec_time = start.elapsed(); let exec_time = start.elapsed();
println!("Executed mixed pattern stress test in {:?}", exec_time); println!("Executed mixed pattern stress test in {:?}", exec_time);
+4 -4
View File
@@ -1,7 +1,7 @@
//! Runtime compatibility tests for async-std //! Runtime compatibility tests for async-std
use dagx::{DagRunner, Task};
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagRunner, Task};
struct Value(i32); struct Value(i32);
#[dagx::task] #[dagx::task]
@@ -27,7 +27,7 @@ async fn test_basic_dag_async_std() {
let y = dag.add_task(Value(3)); let y = dag.add_task(Value(3));
let sum = dag.add_task(Add).depends_on((&x, &y)); let sum = dag.add_task(Add).depends_on((&x, &y));
dag.run(|fut| { async_std::task::spawn(fut); }).await.unwrap(); dag.run(|fut| async_std::task::spawn(fut)).await.unwrap();
assert_eq!(dag.get(sum).unwrap(), 5); assert_eq!(dag.get(sum).unwrap(), 5);
} }
@@ -40,7 +40,7 @@ async fn test_parallel_execution_async_std() {
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 })))
.collect(); .collect();
dag.run(|fut| { async_std::task::spawn(fut); }).await.unwrap(); dag.run(|fut| async_std::task::spawn(fut)).await.unwrap();
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
assert_eq!(dag.get(task).unwrap(), i * 2); assert_eq!(dag.get(task).unwrap(), i * 2);
@@ -58,7 +58,7 @@ async fn test_complex_dependencies_async_std() {
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(sum); .depends_on(sum);
dag.run(|fut| { async_std::task::spawn(fut); }).await.unwrap(); dag.run(|fut| async_std::task::spawn(fut)).await.unwrap();
assert_eq!(dag.get(double).unwrap(), 60); assert_eq!(dag.get(double).unwrap(), 60);
} }
+2 -4
View File
@@ -1,6 +1,7 @@
//! Runtime integration tests - verify dagx works correctly across different runtimes //! Runtime integration tests - verify dagx works correctly across different runtimes
use dagx::{DagResult, DagRunner, Task}; use dagx::{DagResult, DagRunner, Task};
use futures::FutureExt;
struct Value(i32); struct Value(i32);
#[dagx::task] #[dagx::task]
@@ -26,10 +27,7 @@ async fn runtime_integration_tokio() -> DagResult<()> {
let b = dag.add_task(Value(20)); let b = dag.add_task(Value(20));
let sum = dag.add_task(Add).depends_on((&a, &b)); let sum = dag.add_task(Add).depends_on((&a, &b));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(sum)?, 30); assert_eq!(dag.get(sum)?, 30);
Ok(()) Ok(())
+3
View File
@@ -2,3 +2,6 @@
pub mod compatibility; pub mod compatibility;
pub mod quirky; pub mod quirky;
mod async_std;
mod smol;
+52 -100
View File
@@ -15,6 +15,8 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::DagRunner; use dagx::DagRunner;
use futures::task::SpawnExt;
use futures_executor::ThreadPool;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -36,11 +38,7 @@ fn test_async_executor_basic() {
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((&x, &y)); .depends_on((&x, &y));
dag.run(|fut| { dag.run(|fut| executor.spawn(fut)).await.unwrap();
executor.spawn(fut).detach();
})
.await
.unwrap();
assert_eq!(dag.get(sum).unwrap(), 30); assert_eq!(dag.get(sum).unwrap(), 30);
}; };
@@ -70,11 +68,7 @@ fn test_async_executor_complex_dag() {
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
.depends_on((&left, &right)); .depends_on((&left, &right));
dag.run(|fut| { dag.run(|fut| executor.spawn(fut)).await.unwrap();
executor.spawn(fut).detach();
})
.await
.unwrap();
assert_eq!(dag.get(sink).unwrap(), 25); // (5*2) + (5*3) = 25 assert_eq!(dag.get(sink).unwrap(), 25); // (5*2) + (5*3) = 25
}; };
@@ -104,11 +98,7 @@ fn test_async_executor_parallel_tasks() {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| executor.spawn(fut)).await.unwrap();
executor.spawn(fut).detach();
})
.await
.unwrap();
// All tasks should have run // All tasks should have run
assert_eq!(counter.load(Ordering::SeqCst), 10); assert_eq!(counter.load(Ordering::SeqCst), 10);
@@ -132,11 +122,7 @@ fn test_async_executor_with_local_spawn() {
let x = dag.add_task(task_fn(|_: ()| async { 42 })); let x = dag.add_task(task_fn(|_: ()| async { 42 }));
dag.run(|fut| { dag.run(|fut| executor.spawn(fut)).await.unwrap();
executor.spawn(fut).detach();
})
.await
.unwrap();
assert_eq!(dag.get(x).unwrap(), 42); assert_eq!(dag.get(x).unwrap(), 42);
}; };
@@ -163,11 +149,7 @@ fn test_async_executor_deep_chain() {
.depends_on(current); .depends_on(current);
} }
dag.run(|fut| { dag.run(|fut| executor.spawn(fut)).await.unwrap();
executor.spawn(fut).detach();
})
.await
.unwrap();
assert_eq!(dag.get(current).unwrap(), 50); assert_eq!(dag.get(current).unwrap(), 50);
}; };
@@ -190,13 +172,9 @@ fn test_pollster_basic() {
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(&x); .depends_on(&x);
dag.run(|fut| { dag.run(|fut| async { pollster::block_on(fut) })
// pollster blocks, so we need to spawn on a different executor .await
// Use futures::executor for spawning .unwrap();
futures_executor::block_on(fut);
})
.await
.unwrap();
assert_eq!(dag.get(doubled).unwrap(), 200); assert_eq!(dag.get(doubled).unwrap(), 200);
}); });
@@ -217,11 +195,9 @@ fn test_pollster_complex() {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| async { pollster::block_on(fut) })
futures_executor::block_on(fut); .await
}) .unwrap();
.await
.unwrap();
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
assert_eq!(dag.get(task).unwrap(), 7 * i as i32); assert_eq!(dag.get(task).unwrap(), 7 * i as i32);
@@ -237,11 +213,9 @@ fn test_pollster_error_handling() {
// Task that returns Result // Task that returns Result
let task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(42) })); let task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(42) }));
dag.run(|fut| { dag.run(|fut| async { pollster::block_on(fut) })
futures_executor::block_on(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(task).unwrap(), Ok(42)); assert_eq!(dag.get(task).unwrap(), Ok(42));
}); });
@@ -262,13 +236,11 @@ fn test_futures_executor_basic() {
.add_task(task_fn(|(a, b): (i32, i32)| async move { a * b })) .add_task(task_fn(|(a, b): (i32, i32)| async move { a * b }))
.depends_on((&x, &y)); .depends_on((&x, &y));
dag.run(|fut| { let spawner = ThreadPool::new().unwrap();
// Use ThreadPool for spawning
let pool = futures_executor::ThreadPool::new().unwrap(); dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
pool.spawn_ok(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(product).unwrap(), 375); // 15 * 25 assert_eq!(dag.get(product).unwrap(), 375); // 15 * 25
}); });
@@ -276,8 +248,8 @@ fn test_futures_executor_basic() {
#[test] #[test]
fn test_futures_executor_threadpool() { fn test_futures_executor_threadpool() {
let pool = std::sync::Arc::new(futures_executor::ThreadPool::new().unwrap()); let pool = futures_executor::ThreadPool::new().unwrap();
let pool_clone = pool.clone(); let spawner = pool.clone();
pool.spawn_ok(async move { pool.spawn_ok(async move {
let dag = DagRunner::new(); let dag = DagRunner::new();
@@ -287,11 +259,9 @@ fn test_futures_executor_threadpool() {
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * i }))) .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * i })))
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
pool_clone.spawn_ok(fut); .await
}) .unwrap();
.await
.unwrap();
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
assert_eq!(dag.get(task).unwrap(), (i * i) as i32); assert_eq!(dag.get(task).unwrap(), (i * i) as i32);
@@ -325,12 +295,11 @@ fn test_futures_executor_diamond_pattern() {
.add_task(task_fn(|(x, y): (i32, i32)| async move { x * y })) .add_task(task_fn(|(x, y): (i32, i32)| async move { x * y }))
.depends_on((&b, &c)); .depends_on((&b, &c));
let pool = futures_executor::ThreadPool::new().unwrap(); let spawner = ThreadPool::new().unwrap();
dag.run(|fut| {
pool.spawn_ok(fut); dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
}) .await
.await .unwrap();
.unwrap();
assert_eq!(dag.get(d).unwrap(), 60); // (8+2) * (8-2) = 10 * 6 assert_eq!(dag.get(d).unwrap(), 60); // (8+2) * (8-2) = 10 * 6
}); });
@@ -350,7 +319,7 @@ fn test_all_runtimes_compatibility() {
executor.run(async { executor.run(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 123 })); let x = dag.add_task(task_fn(|_: ()| async { 123 }));
dag.run(|fut| executor.spawn(fut).detach()).await.unwrap(); dag.run(|fut| executor.spawn(fut)).await.unwrap();
assert_eq!(dag.get(x).unwrap(), 123); assert_eq!(dag.get(x).unwrap(), 123);
}); });
} }
@@ -360,7 +329,9 @@ fn test_all_runtimes_compatibility() {
pollster::block_on(async { pollster::block_on(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 123 })); let x = dag.add_task(task_fn(|_: ()| async { 123 }));
dag.run(futures_executor::block_on).await.unwrap(); dag.run(|fut| async { pollster::block_on(fut) })
.await
.unwrap();
assert_eq!(dag.get(x).unwrap(), 123); assert_eq!(dag.get(x).unwrap(), 123);
}); });
} }
@@ -370,8 +341,10 @@ fn test_all_runtimes_compatibility() {
futures_executor::block_on(async { futures_executor::block_on(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 123 })); let x = dag.add_task(task_fn(|_: ()| async { 123 }));
let pool = futures_executor::ThreadPool::new().unwrap(); let spawner = ThreadPool::new().unwrap();
dag.run(|fut| pool.spawn_ok(fut)).await.unwrap(); dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
.await
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
assert_eq!(dag.get(x).unwrap(), 123); assert_eq!(dag.get(x).unwrap(), 123);
}); });
@@ -393,11 +366,7 @@ fn test_runtime_edge_cases() {
42 42
})); }));
dag.run(|fut| { dag.run(|fut| executor.spawn(fut)).await.unwrap();
executor.spawn(fut).detach();
})
.await
.unwrap();
assert_eq!(dag.get(x).unwrap(), 42); assert_eq!(dag.get(x).unwrap(), 42);
}); });
@@ -424,11 +393,7 @@ fn test_async_executor_stress() {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| executor.spawn(fut)).await.unwrap();
executor.spawn(fut).detach();
})
.await
.unwrap();
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
assert_eq!(dag.get(task).unwrap(), i as i32); assert_eq!(dag.get(task).unwrap(), i as i32);
@@ -443,13 +408,9 @@ fn test_pollster_nested_blocks() {
let dag1 = DagRunner::new(); let dag1 = DagRunner::new();
let x = dag1.add_task(task_fn(|_: ()| async { 10 })); let x = dag1.add_task(task_fn(|_: ()| async { 10 }));
dag1.run(|fut| { dag1.run(|fut| async { pollster::block_on(fut) })
pollster::block_on(async { .await
futures_executor::block_on(fut); .unwrap();
});
})
.await
.unwrap();
assert_eq!(dag1.get(x).unwrap(), 10); assert_eq!(dag1.get(x).unwrap(), 10);
}); });
@@ -459,19 +420,16 @@ fn test_pollster_nested_blocks() {
fn test_futures_executor_multi_pool() { fn test_futures_executor_multi_pool() {
// Test with multiple thread pools // Test with multiple thread pools
let pool1 = futures_executor::ThreadPool::new().unwrap(); let pool1 = futures_executor::ThreadPool::new().unwrap();
let pool2 = futures_executor::ThreadPool::new().unwrap(); let spawner = pool1.clone();
pool1.spawn_ok(async move { pool1.spawn_ok(async move {
let dag = DagRunner::new(); let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 50 })); let x = dag.add_task(task_fn(|_: ()| async { 50 }));
dag.run(|fut| { dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
// Spawn on different pool .await
pool2.spawn_ok(fut); .unwrap();
})
.await
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(50)); std::thread::sleep(std::time::Duration::from_millis(50));
assert_eq!(dag.get(x).unwrap(), 50); assert_eq!(dag.get(x).unwrap(), 50);
@@ -507,11 +465,7 @@ fn test_sync_tasks_on_async_executor() {
let y = dag.add_task(task_fn(|_: ()| async { 7 })); let y = dag.add_task(task_fn(|_: ()| async { 7 }));
let sum = dag.add_task(SyncAdd).depends_on((&x, &y)); let sum = dag.add_task(SyncAdd).depends_on((&x, &y));
dag.run(|fut| { dag.run(|fut| executor.spawn(fut)).await.unwrap();
executor.spawn(fut).detach();
})
.await
.unwrap();
assert_eq!(dag.get(sum).unwrap(), 12); assert_eq!(dag.get(sum).unwrap(), 12);
}); });
@@ -536,11 +490,9 @@ fn test_sync_tasks_on_pollster() {
let x = dag.add_task(task_fn(|_: ()| async { 21 })); let x = dag.add_task(task_fn(|_: ()| async { 21 }));
let doubled = dag.add_task(SyncDouble).depends_on(&x); let doubled = dag.add_task(SyncDouble).depends_on(&x);
dag.run(|fut| { dag.run(|fut| async { pollster::block_on(fut) })
futures_executor::block_on(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(doubled).unwrap(), 42); assert_eq!(dag.get(doubled).unwrap(), 42);
}); });
+4 -4
View File
@@ -1,7 +1,7 @@
//! Runtime compatibility tests for smol //! Runtime compatibility tests for smol
use dagx::{DagRunner, Task};
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagRunner, Task};
struct Value(i32); struct Value(i32);
#[dagx::task] #[dagx::task]
@@ -28,7 +28,7 @@ fn test_basic_dag_smol() {
let y = dag.add_task(Value(3)); let y = dag.add_task(Value(3));
let sum = dag.add_task(Add).depends_on((&x, &y)); let sum = dag.add_task(Add).depends_on((&x, &y));
dag.run(|fut| { smol::spawn(fut).detach(); }).await.unwrap(); dag.run(|fut| smol::spawn(fut)).await.unwrap();
assert_eq!(dag.get(sum).unwrap(), 5); assert_eq!(dag.get(sum).unwrap(), 5);
}); });
@@ -43,7 +43,7 @@ fn test_parallel_execution_smol() {
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 })))
.collect(); .collect();
dag.run(|fut| { smol::spawn(fut).detach(); }).await.unwrap(); dag.run(|fut| smol::spawn(fut)).await.unwrap();
for (i, task) in tasks.iter().enumerate() { for (i, task) in tasks.iter().enumerate() {
assert_eq!(dag.get(task).unwrap(), i * 2); assert_eq!(dag.get(task).unwrap(), i * 2);
@@ -63,7 +63,7 @@ fn test_complex_dependencies_smol() {
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(sum); .depends_on(sum);
dag.run(|fut| { smol::spawn(fut).detach(); }).await.unwrap(); dag.run(|fut| smol::spawn(fut)).await.unwrap();
assert_eq!(dag.get(double).unwrap(), 60); assert_eq!(dag.get(double).unwrap(), 60);
}); });
+10 -15
View File
@@ -3,6 +3,7 @@ use crate::common::task_fn;
// Comprehensive test showing all three patterns working together // Comprehensive test showing all three patterns working together
use dagx::*; use dagx::*;
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -52,11 +53,9 @@ async fn test_all_three_task_patterns() {
// Pattern 3: Mutable accumulation // Pattern 3: Mutable accumulation
let accumulated = dag.add_task(MutableAccumulator(10)).depends_on(doubled); // 10 + 16 = 26 let accumulated = dag.add_task(MutableAccumulator(10)).depends_on(doubled); // 10 + 16 = 26
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(sum).unwrap(), 8); assert_eq!(dag.get(sum).unwrap(), 8);
assert_eq!(dag.get(doubled).unwrap(), 16); assert_eq!(dag.get(doubled).unwrap(), 16);
@@ -97,11 +96,9 @@ async fn test_parallel_execution() {
} }
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// All three tasks should have run // All three tasks should have run
assert_eq!(counter.load(Ordering::SeqCst), 3); assert_eq!(counter.load(Ordering::SeqCst), 3);
@@ -120,11 +117,9 @@ async fn test_task_fn_with_captured_state() {
.add_task(task_fn(move |x: i32| async move { x * multiplier })) .add_task(task_fn(move |x: i32| async move { x * multiplier }))
.depends_on(&base); .depends_on(&base);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(scaled).unwrap(), 35); assert_eq!(dag.get(scaled).unwrap(), 35);
} }
+10 -15
View File
@@ -3,6 +3,7 @@ use crate::common::task_fn;
// These tests verify the read-only state pattern using &self // These tests verify the read-only state pattern using &self
use dagx::*; use dagx::*;
use futures::FutureExt;
// Read-only state tasks // Read-only state tasks
struct ReadOnlyValue(i32); struct ReadOnlyValue(i32);
@@ -42,11 +43,9 @@ async fn test_readonly_state_basic() {
let value = dag.add_task(ReadOnlyValue(42)); let value = dag.add_task(ReadOnlyValue(42));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(value).unwrap(), 42); assert_eq!(dag.get(value).unwrap(), 42);
} }
@@ -58,11 +57,9 @@ async fn test_readonly_state_with_input() {
let input = dag.add_task(task_fn(|_: ()| async { 5 })); let input = dag.add_task(task_fn(|_: ()| async { 5 }));
let result = dag.add_task(ReadOnlyMultiplier(7)).depends_on(&input); let result = dag.add_task(ReadOnlyMultiplier(7)).depends_on(&input);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(result).unwrap(), 35); assert_eq!(dag.get(result).unwrap(), 35);
} }
@@ -87,11 +84,9 @@ async fn test_readonly_state_multiple_uses() {
}; };
let result2 = dag.add_task(config2).depends_on(&input2); let result2 = dag.add_task(config2).depends_on(&input2);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(result1).unwrap(), 35); // (10 * 3) + 5 assert_eq!(dag.get(result1).unwrap(), 35); // (10 * 3) + 5
assert_eq!(dag.get(result2).unwrap(), 65); // (20 * 3) + 5 assert_eq!(dag.get(result2).unwrap(), 65); // (20 * 3) + 5
} }
+25 -40
View File
@@ -1,5 +1,6 @@
// Stateless task pattern tests // Stateless task pattern tests
use crate::common::task_fn; use crate::common::task_fn;
use futures::FutureExt;
use dagx::*; use dagx::*;
@@ -85,11 +86,9 @@ async fn test_stateless_basic() {
let y = dag.add_task(task_fn(|_: ()| async { 20 })); let y = dag.add_task(task_fn(|_: ()| async { 20 }));
let sum = dag.add_task(StatelessAdd).depends_on((&x, &y)); let sum = dag.add_task(StatelessAdd).depends_on((&x, &y));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(sum).unwrap(), 30); assert_eq!(dag.get(sum).unwrap(), 30);
} }
@@ -101,11 +100,9 @@ async fn test_stateless_single_input() {
let input = dag.add_task(task_fn(|_: ()| async { 21 })); let input = dag.add_task(task_fn(|_: ()| async { 21 }));
let doubled = dag.add_task(StatelessDouble).depends_on(&input); let doubled = dag.add_task(StatelessDouble).depends_on(&input);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(doubled).unwrap(), 42); assert_eq!(dag.get(doubled).unwrap(), 42);
} }
@@ -116,11 +113,9 @@ async fn test_stateless_no_input() {
let source = dag.add_task(StatelessSource); let source = dag.add_task(StatelessSource);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(source).unwrap(), 42); assert_eq!(dag.get(source).unwrap(), 42);
} }
@@ -137,11 +132,9 @@ async fn test_stateless_multiple_inputs() {
.add_task(StatelessFormat) .add_task(StatelessFormat)
.depends_on((&num, &text, &flag)); .depends_on((&num, &text, &flag));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(result).unwrap(), "test: 42 (true)"); assert_eq!(dag.get(result).unwrap(), "test: 42 (true)");
} }
@@ -178,11 +171,9 @@ async fn test_stateless_mixed_with_stateful() {
// Stateful counter // Stateful counter
let counted = dag.add_task(Counter::new(10)).depends_on(product); let counted = dag.add_task(Counter::new(10)).depends_on(product);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(product).unwrap(), 15); assert_eq!(dag.get(product).unwrap(), 15);
assert_eq!(dag.get(counted).unwrap(), 25); // 10 + 15 assert_eq!(dag.get(counted).unwrap(), 25); // 10 + 15
@@ -198,11 +189,9 @@ async fn test_stateless_chain() {
let step2 = dag.add_task(StatelessInc).depends_on(step1); let step2 = dag.add_task(StatelessInc).depends_on(step1);
let step3 = dag.add_task(StatelessInc).depends_on(step2); let step3 = dag.add_task(StatelessInc).depends_on(step2);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(step1).unwrap(), 1); assert_eq!(dag.get(step1).unwrap(), 1);
assert_eq!(dag.get(step2).unwrap(), 2); assert_eq!(dag.get(step2).unwrap(), 2);
@@ -221,11 +210,9 @@ async fn test_stateless_parallel() {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(tasks[0]).unwrap(), 1); assert_eq!(dag.get(tasks[0]).unwrap(), 1);
assert_eq!(dag.get(tasks[1]).unwrap(), 4); assert_eq!(dag.get(tasks[1]).unwrap(), 4);
@@ -244,11 +231,9 @@ async fn test_stateless_with_string_types() {
let greeting = dag.add_task(StatelessConcat).depends_on((&hello, &world)); let greeting = dag.add_task(StatelessConcat).depends_on((&hello, &world));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(greeting).unwrap(), "Hello, World!"); assert_eq!(dag.get(greeting).unwrap(), "Hello, World!");
} }
+8 -28
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@@ -23,10 +24,7 @@ async fn test_concurrent_result_retrieval() -> DagResult<()> {
.collect(); .collect();
// Execute the DAG // Execute the DAG
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Now try to get results concurrently from multiple threads // Now try to get results concurrently from multiple threads
let mut get_handles = Vec::new(); let mut get_handles = Vec::new();
@@ -77,10 +75,7 @@ async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> {
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
// Execute the DAG // Execute the DAG
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Get the additional tasks // Get the additional tasks
let additional_tasks = build_handle.await.unwrap(); let additional_tasks = build_handle.await.unwrap();
@@ -94,10 +89,7 @@ async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> {
// (they need another run cycle) // (they need another run cycle)
if building_complete.load(Ordering::SeqCst) { if building_complete.load(Ordering::SeqCst) {
// Run again for the new tasks // Run again for the new tasks
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Now they should have results // Now they should have results
for (i, task) in additional_tasks.iter().enumerate() { for (i, task) in additional_tasks.iter().enumerate() {
@@ -141,10 +133,7 @@ async fn test_concurrent_dependency_resolution() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Check all completed // Check all completed
assert_eq!(resolution_counter.load(Ordering::SeqCst), 101); assert_eq!(resolution_counter.load(Ordering::SeqCst), 101);
@@ -198,10 +187,7 @@ async fn test_read_write_race_patterns() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Verify readers saw their writer's data // Verify readers saw their writer's data
for reader in &readers { for reader in &readers {
@@ -257,10 +243,7 @@ async fn test_atomic_counter_races() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Final counter should be 200 * 100 = 20000 // Final counter should be 200 * 100 = 20000
assert_eq!(counter.load(Ordering::SeqCst), 20000); assert_eq!(counter.load(Ordering::SeqCst), 20000);
@@ -296,10 +279,7 @@ async fn test_multiple_dag_runners_interaction() -> DagResult<()> {
}) })
.collect(); .collect();
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Collect results // Collect results
let mut results = Vec::new(); let mut results = Vec::new();
+5 -16
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner}; use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Barrier; use tokio::sync::Barrier;
@@ -66,10 +67,7 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
)) ))
.depends_on((&t1, &t2, &t3)); .depends_on((&t1, &t2, &t3));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(collector)?, 6); assert_eq!(dag.get(collector)?, 6);
@@ -168,10 +166,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
.depends_on(chain_b); .depends_on(chain_b);
} }
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Check alternating pattern // Check alternating pattern
let event_list = events.lock().clone(); let event_list = events.lock().clone();
@@ -239,10 +234,7 @@ async fn test_layered_interleaving() -> DagResult<()> {
})) }))
.depends_on((&layer2[0], &layer2[1])); .depends_on((&layer2[0], &layer2[1]));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
// Sum of 0+1+2+3 = 6 // Sum of 0+1+2+3 = 6
assert_eq!(dag.get(layer3)?, 6); assert_eq!(dag.get(layer3)?, 6);
@@ -335,10 +327,7 @@ async fn test_cross_branch_synchronization() -> DagResult<()> {
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((&a2, &b2)); .depends_on((&a2, &b2));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
tokio::spawn(fut);
})
.await?;
assert_eq!(dag.get(merge)?, 13); // 2 + 11 assert_eq!(dag.get(merge)?, 13); // 2 + 11
+13 -20
View File
@@ -1,6 +1,7 @@
//! Tests with tracing feature enabled //! Tests with tracing feature enabled
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
use tracing_subscriber::{fmt, EnvFilter}; use tracing_subscriber::{fmt, EnvFilter};
struct Value(i32); struct Value(i32);
@@ -44,11 +45,9 @@ async fn test_tracing_with_subscriber() {
let b = dag.add_task(Value(3)); let b = dag.add_task(Value(3));
let sum = dag.add_task(Add).depends_on((&a, &b)); let sum = dag.add_task(Add).depends_on((&a, &b));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(sum).unwrap(), 5); assert_eq!(dag.get(sum).unwrap(), 5);
} }
@@ -69,11 +68,9 @@ async fn test_tracing_with_complex_dag() {
let right = dag.add_task(Multiply).depends_on((&source, &source)); let right = dag.add_task(Multiply).depends_on((&source, &source));
let sink = dag.add_task(Add).depends_on((&left, &right)); let sink = dag.add_task(Add).depends_on((&left, &right));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// source = 10 // source = 10
// left = 10 + 10 = 20 // left = 10 + 10 = 20
@@ -97,11 +94,9 @@ async fn test_tracing_inline_execution() {
let b = dag.add_task(Add).depends_on((&a, &a)); let b = dag.add_task(Add).depends_on((&a, &a));
let c = dag.add_task(Multiply).depends_on((&b, &b)); let c = dag.add_task(Multiply).depends_on((&b, &b));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// a = 1 // a = 1
// b = 1 + 1 = 2 // b = 1 + 1 = 2
@@ -133,11 +128,9 @@ async fn test_tracing_multiple_layers() {
// Layer 2 // Layer 2
let final_sum = dag.add_task(Add).depends_on((&sum12, &mul23)); let final_sum = dag.add_task(Add).depends_on((&sum12, &mul23));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// v1 = 1, v2 = 2, v3 = 3 // v1 = 1, v2 = 2, v3 = 3
// sum12 = 1 + 2 = 3 // sum12 = 1 + 2 = 3
+7 -10
View File
@@ -1,6 +1,7 @@
//! Tests that the library works correctly without tracing feature //! Tests that the library works correctly without tracing feature
use dagx::{task, DagRunner, Task}; use dagx::{task, DagRunner, Task};
use futures::FutureExt;
struct Value(i32); struct Value(i32);
@@ -28,11 +29,9 @@ async fn test_basic_dag_without_tracing() {
let b = dag.add_task(Value(3)); let b = dag.add_task(Value(3));
let sum = dag.add_task(Add).depends_on((&a, &b)); let sum = dag.add_task(Add).depends_on((&a, &b));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(sum).unwrap(), 5); assert_eq!(dag.get(sum).unwrap(), 5);
} }
@@ -51,11 +50,9 @@ async fn test_complex_dag_without_tracing() {
let sum34 = dag.add_task(Add).depends_on((&v3, &v4)); let sum34 = dag.add_task(Add).depends_on((&v3, &v4));
let final_sum = dag.add_task(Add).depends_on((&sum12, &sum34)); let final_sum = dag.add_task(Add).depends_on((&sum12, &sum34));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(final_sum).unwrap(), 10); assert_eq!(dag.get(final_sum).unwrap(), 10);
} }
+37 -60
View File
@@ -2,6 +2,7 @@
use crate::common::task_fn; use crate::common::task_fn;
use dagx::*; use dagx::*;
use futures::FutureExt;
#[tokio::test] #[tokio::test]
async fn test_complex_type_safety() { async fn test_complex_type_safety() {
@@ -21,11 +22,9 @@ async fn test_complex_type_safety() {
} }
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Correct type works // Correct type works
let custom = dag.get(&custom_task).unwrap(); let custom = dag.get(&custom_task).unwrap();
@@ -47,11 +46,9 @@ async fn test_generic_type_safety() {
vec!["a".to_string(), "b".to_string()] vec!["a".to_string(), "b".to_string()]
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Correct types work // Correct types work
assert_eq!(dag.get(&vec_i32).unwrap(), vec![1, 2, 3]); assert_eq!(dag.get(&vec_i32).unwrap(), vec![1, 2, 3]);
@@ -69,11 +66,9 @@ async fn test_option_result_type_safety() {
// Task returning Result<i32, String> // Task returning Result<i32, String>
let result_task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(42) })); let result_task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(42) }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Correct wrapped types work // Correct wrapped types work
assert_eq!(dag.get(&opt_task).unwrap(), Some(42)); assert_eq!(dag.get(&opt_task).unwrap(), Some(42));
@@ -94,11 +89,9 @@ async fn test_zero_sized_type_safety() {
let zst1_task = dag.add_task(task_fn(|_: ()| async { Zst1 })); let zst1_task = dag.add_task(task_fn(|_: ()| async { Zst1 }));
let _zst2_task = dag.add_task(task_fn(|_: ()| async { Zst2 })); let _zst2_task = dag.add_task(task_fn(|_: ()| async { Zst2 }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Correct ZSTs work // Correct ZSTs work
assert!(dag.get(&zst1_task).is_ok()); assert!(dag.get(&zst1_task).is_ok());
@@ -117,11 +110,9 @@ async fn test_type_safety_with_many_types() {
let string_task = dag.add_task(task_fn(|_: ()| async { "42".to_string() })); let string_task = dag.add_task(task_fn(|_: ()| async { "42".to_string() }));
let bool_task = dag.add_task(task_fn(|_: ()| async { true })); let bool_task = dag.add_task(task_fn(|_: ()| async { true }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Each type works correctly // Each type works correctly
assert_eq!(dag.get(&i32_task).unwrap(), 42i32); assert_eq!(dag.get(&i32_task).unwrap(), 42i32);
@@ -145,11 +136,9 @@ async fn test_type_safety_single_dependency() {
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(&source); .depends_on(&source);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(transform).unwrap(), 84); assert_eq!(dag.get(transform).unwrap(), 84);
} }
@@ -169,11 +158,9 @@ async fn test_type_safety_tuple_dependencies() {
})) }))
.depends_on((&a, &b, &c)); .depends_on((&a, &b, &c));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(combine).unwrap(), "hello: 10 (true)"); assert_eq!(dag.get(combine).unwrap(), "hello: 10 (true)");
} }
@@ -186,11 +173,9 @@ async fn test_type_safety_different_output_types() {
let str_source = dag.add_task(task_fn(|_: ()| async { "test".to_string() })); let str_source = dag.add_task(task_fn(|_: ()| async { "test".to_string() }));
let vec_source = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] })); let vec_source = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(&int_source).unwrap(), 42); assert_eq!(dag.get(&int_source).unwrap(), 42);
assert_eq!(dag.get(&str_source).unwrap(), "test"); assert_eq!(dag.get(&str_source).unwrap(), "test");
@@ -214,11 +199,9 @@ async fn test_type_safety_complex_types() {
vec![Some(1), None, Some(2), Some(3)] vec![Some(1), None, Some(2), Some(3)]
})); }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
let tuple_result = dag.get(tuple_task).unwrap(); let tuple_result = dag.get(tuple_task).unwrap();
assert_eq!(tuple_result.0, 42); assert_eq!(tuple_result.0, 42);
@@ -245,11 +228,9 @@ async fn test_type_safety_with_generics() {
)) ))
.depends_on(&source); .depends_on(&source);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(transform).unwrap(), Some(84)); assert_eq!(dag.get(transform).unwrap(), Some(84));
@@ -261,11 +242,9 @@ async fn test_type_safety_with_generics() {
)) ))
.depends_on(&source2); .depends_on(&source2);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(transform2).unwrap(), None); assert_eq!(dag.get(transform2).unwrap(), None);
} }
@@ -285,11 +264,9 @@ async fn test_large_tuple_input() {
)) ))
.depends_on((&a, &b, &c, &d, &e)); .depends_on((&a, &b, &c, &d, &e));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(sum).unwrap(), 15); assert_eq!(dag.get(sum).unwrap(), 15);
} }
+10 -15
View File
@@ -1,5 +1,6 @@
// Type safety tests for TaskHandle // Type safety tests for TaskHandle
use crate::common::task_fn; use crate::common::task_fn;
use futures::FutureExt;
// These tests verify dagx's compile-time type safety guarantees. // These tests verify dagx's compile-time type safety guarantees.
// Tests cover both runtime behavior (wrong types return None) and // Tests cover both runtime behavior (wrong types return None) and
// compile-time checking (type mismatches fail to compile). // compile-time checking (type mismatches fail to compile).
@@ -15,11 +16,9 @@ async fn test_get_with_wrong_type_returns_none() {
let int_task = dag.add_task(task_fn(|_: ()| async { 42i32 })); let int_task = dag.add_task(task_fn(|_: ()| async { 42i32 }));
// Run DAG // Run DAG
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Verify correct type works // Verify correct type works
assert_eq!(dag.get(&int_task), Ok(42i32)); assert_eq!(dag.get(&int_task), Ok(42i32));
@@ -38,11 +37,9 @@ async fn test_handle_preserves_type() {
let string_task = dag.add_task(task_fn(|_: ()| async { "hello".to_string() })); let string_task = dag.add_task(task_fn(|_: ()| async { "hello".to_string() }));
let int_task = dag.add_task(task_fn(|_: ()| async { 42i32 })); let int_task = dag.add_task(task_fn(|_: ()| async { 42i32 }));
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
// Get with correct types // Get with correct types
assert_eq!(dag.get(&string_task).unwrap(), "hello".to_string()); assert_eq!(dag.get(&string_task).unwrap(), "hello".to_string());
@@ -66,11 +63,9 @@ async fn test_type_safety_handle_reuse() {
.add_task(task_fn(|x: i32| async move { x * 3 })) .add_task(task_fn(|x: i32| async move { x * 3 }))
.depends_on(&source); .depends_on(&source);
dag.run(|fut| { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
tokio::spawn(fut); .await
}) .unwrap();
.await
.unwrap();
assert_eq!(dag.get(double).unwrap(), 200); assert_eq!(dag.get(double).unwrap(), 200);
assert_eq!(dag.get(triple).unwrap(), 300); assert_eq!(dag.get(triple).unwrap(), 300);