From e4fc0b0d4489326f4a3adf57208fbe667c8089a6 Mon Sep 17 00:00:00 2001 From: TechnoPorg Date: Tue, 17 Feb 2026 19:27:40 +0100 Subject: [PATCH 1/2] refactor: make DagRunner interface single-threaded --- CONTRIBUTING.md | 15 +- Cargo.lock | 48 ----- Cargo.toml | 1 - README.md | 10 +- SECURITY.md | 1 - dagx-macros/src/lib.rs | 5 +- dagx-test/src/lib.rs | 6 +- docs/COMPARISONS.md | 2 +- docs/CYCLE_PREVENTION.md | 4 +- src/builder.rs | 156 ++++++++++++++-- src/{types => builder}/coverage_tests.rs | 13 +- src/builder/tests.rs | 21 +-- src/deps.rs | 64 ++----- src/deps/tests.rs | 8 +- src/error.rs | 10 - src/error/tests.rs | 14 +- src/lib.rs | 72 ++++---- src/node.rs | 25 ++- src/output.rs | 83 +++++++++ src/runner.rs | 221 ++++------------------- src/runner/tests.rs | 103 +++-------- src/task.rs | 10 +- src/task/tests.rs | 5 +- src/types.rs | 86 --------- src/types/tests.rs | 50 ----- 25 files changed, 416 insertions(+), 617 deletions(-) rename src/{types => builder}/coverage_tests.rs (81%) create mode 100644 src/output.rs delete mode 100644 src/types.rs delete mode 100644 src/types/tests.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83c02e3..1d6fb63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,12 @@ cargo clippy -- -D warnings ## Development Setup **Prerequisites:** + - Rust 1.81+ (edition 2021) - Git **Optional tools:** + ```bash rustup component add rustfmt clippy cargo install cargo-llvm-cov # For coverage @@ -67,7 +69,8 @@ cargo build --examples # Examples compile - All doc examples must compile Example: -```rust + +````rust /// Adds a task to the DAG. /// /// # Examples @@ -75,23 +78,25 @@ Example: /// ``` /// use dagx::DagRunner; /// -/// let dag = DagRunner::new(); +/// let mut dag = DagRunner::new(); /// let task = dag.add_task(MyTask::new()); /// ``` pub fn add_task(&self, task: T) -> TaskBuilder { // ... } -``` +```` ## Testing **Test organization:** + - Unit tests: `#[cfg(test)]` modules in source files - Integration tests: `tests/` directory - Examples: `examples/` directory (must compile) - Doctests: In `///` comments (must pass) **Runtime testing:** + ```bash cargo test # Default (tokio) cargo test --test async_std_runtime # async-std @@ -99,6 +104,7 @@ cargo test --test smol_runtime # smol ``` **Benchmarks:** + ```bash cargo bench # View results: open target/criterion/report/index.html @@ -130,6 +136,7 @@ Closes #123 **Types**: `feat`, `fix`, `docs`, `test`, `refactor`, `perf`, `chore` **Examples:** + - `feat: add timeout support for tasks` - `fix: correct cycle detection in diamond DAG` - `docs: clarify TaskHandle usage in README` @@ -137,12 +144,14 @@ Closes #123 ## Pull Requests **PR description should include:** + - What changed and why - How it was tested - Any breaking changes - Link to related issue (if applicable) **Review process:** + - Maintainer reviews within 1 week - Address feedback - Once approved, maintainer merges diff --git a/Cargo.lock b/Cargo.lock index fc6ce91..6c14f14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,7 +381,6 @@ dependencies = [ "dagx-test", "futures-executor", "futures-util", - "parking_lot", "pollster", "smol", "test-case", @@ -628,15 +627,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.28" @@ -733,29 +723,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -865,15 +832,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - [[package]] name = "regex" version = "1.11.3" @@ -943,12 +901,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "serde" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index c53da27..698c97d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ workspace = true [dependencies] futures-util = { version = "0.3", default-features = false, features = ["std"] } -parking_lot = "0.12" dagx-macros = { version = "0.3.1", path = "./dagx-macros" } tracing = { version = "0.1", optional = true } diff --git a/README.md b/README.md index d902944..3c4823c 100644 --- a/README.md +++ b/README.md @@ -80,21 +80,21 @@ impl Add { #[tokio::main] async fn main() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Add source tasks with no dependencies let x = dag.add_task(Value(2)); - let y: TaskHandle<_> = dag.add_task(Value(3)).into(); + let y = dag.add_task(Value(3)); // Add task that depends on both x and y. // Here, y could be reused as a dependency for other tasks, while x could not. let sum = dag.add_task(Add).depends_on((x, &y)); // Execute with true parallelism - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); + let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); // Retrieve results - assert_eq!(dag.get(sum).unwrap(), 5); + assert_eq!(output.get(sum).unwrap(), 5); } ``` @@ -171,7 +171,7 @@ impl Counter { The `DagRunner` orchestrates task execution: ```rust -let dag = DagRunner::new(); +let mut dag = DagRunner::new(); let handle = dag.add_task(MyTask::new()); ``` diff --git a/SECURITY.md b/SECURITY.md index fd05350..ceb1178 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,7 +17,6 @@ dagx is a task execution library designed for safety and predictability. This do - **Compile-time type safety**: Dependencies are validated at compile time, preventing type errors - **Cycle detection**: Runtime detection of circular dependencies - **Memory safety**: Rust's ownership system prevents memory corruption -- **Thread safety**: Interior mutability uses `parking_lot::Mutex` for safe concurrent access ### What dagx Does NOT Provide diff --git a/dagx-macros/src/lib.rs b/dagx-macros/src/lib.rs index e4a3d26..5dc1d23 100644 --- a/dagx-macros/src/lib.rs +++ b/dagx-macros/src/lib.rs @@ -332,11 +332,10 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream { // Generate the Task trait implementation based on whether we have self and async/sync quote! { - impl ::dagx::Task for #struct_name { - type Input = #input_type; + impl ::dagx::Task<#input_type> for #struct_name { type Output = #output_type; - async fn run(mut self, mut input: ::dagx::TaskInput<'_, Self::Input>) -> Self::Output { + async fn run(mut self, mut input: ::dagx::TaskInput<'_, #input_type>) -> Self::Output { #param_destructure #run_call } diff --git a/dagx-test/src/lib.rs b/dagx-test/src/lib.rs index 226b891..889973b 100644 --- a/dagx-test/src/lib.rs +++ b/dagx-test/src/lib.rs @@ -21,13 +21,13 @@ where _phantom: std::marker::PhantomData O>, } -impl Task for TaskFn +impl Task for TaskFn where for<'input> F: FnMut(I::Retv<'input>) -> O + Send, I: ExtractInput + Send + Sync + 'static, - O: Send + Sync, + I::Input: Send + Sync + 'static, + O: Send + Sync + 'static, { - type Input = I::Input; type Output = O; async fn run(mut self, input: TaskInput<'_, I::Input>) -> O { diff --git a/docs/COMPARISONS.md b/docs/COMPARISONS.md index 108b425..8bbafdd 100644 --- a/docs/COMPARISONS.md +++ b/docs/COMPARISONS.md @@ -15,7 +15,7 @@ impl Value { async fn run(&self) -> i32 { self.0 } } #[task] impl Add { async fn run(a: &i32, b: &i32) -> i32 { a + b } } -let dag = DagRunner::new(); +let mut dag = DagRunner::new(); let x = dag.add_task(Value(2)); let y = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((x, y)); diff --git a/docs/CYCLE_PREVENTION.md b/docs/CYCLE_PREVENTION.md index 5ee861f..5cc2699 100644 --- a/docs/CYCLE_PREVENTION.md +++ b/docs/CYCLE_PREVENTION.md @@ -8,7 +8,7 @@ Most DAG libraries check for cycles when you execute the graph: ```rust // Other libraries: cycle detected at RUNTIME -let dag = DagRunner::new(); +let mut dag = DagRunner::new(); let a = dag.add_task(TaskA); let b = dag.add_task(TaskB); a.depends_on(b); @@ -20,7 +20,7 @@ dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) ```rust // dagx: cycle prevented at COMPILE TIME -let dag = DagRunner::new(); +let mut dag = DagRunner::new(); let a_builder = dag.add_task(TaskA); let b_builder = dag.add_task(TaskB); diff --git a/src/builder.rs b/src/builder.rs index 5708782..babe8fd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -3,10 +3,11 @@ //! TaskBuilder uses compile-time type checking to ensure dependencies are wired correctly. //! The type parameter tracks whether dependencies have been specified yet. +use std::marker::PhantomData; + use crate::deps::DepsTuple; use crate::runner::DagRunner; use crate::task::Task; -use crate::types::{NodeId, TaskHandle}; #[cfg(feature = "tracing")] use tracing::debug; @@ -57,17 +58,26 @@ use tracing::debug; /// let b = b.depends_on(a); /// // Now b is a TaskHandle /// -/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); -/// assert_eq!(dag.get(b).unwrap(), 20); +///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +/// assert_eq!(output.get(b).unwrap(), 20); /// # }; /// ``` -pub struct TaskBuilder<'a, Tk: Task, Deps> { +#[must_use] +pub struct TaskBuilder<'a, Input, Tk> +where + Tk: Task, + Input: Send + Sync + 'static, +{ pub(crate) id: NodeId, - pub(crate) dag: &'a DagRunner, - pub(crate) _phantom: std::marker::PhantomData<(Tk, Deps)>, + pub(crate) dag: &'a mut DagRunner, + pub(crate) _phantom: PhantomData<(Tk, Input)>, } -impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> { +impl<'a, Input, Tk> TaskBuilder<'a, Input, Tk> +where + Tk: Task, + Input: Send + Sync + 'static, +{ /// Provide all dependencies exactly once as a tuple. /// /// The dependencies must match the task's `Input` type exactly: @@ -107,7 +117,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> { /// } /// /// # async { - /// let dag = DagRunner::new(); + /// let mut dag = DagRunner::new(); /// /// let x = dag.add_task(Value(2)).into(); /// let y = dag.add_task(Value(3)); @@ -118,13 +128,13 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> { /// // Multiple dependencies: tuple form /// let sum = dag.add_task(Add).depends_on((&x, y)); /// - /// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); + ///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); /// # }; /// ``` #[allow(private_bounds)] pub fn depends_on(self, deps: D) -> TaskHandle where - D: DepsTuple, + D: DepsTuple, { // Register dependencies in the DAG let dep_ids = deps.to_node_ids(); @@ -137,27 +147,141 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> { "wiring task dependencies" ); - let mut edges = self.dag.edges.lock(); - let mut dependents = self.dag.dependents.lock(); - for &dep_id in &dep_ids { // Add edge from this node to dependency - if let Some(node_edges) = edges.get_mut(&self.id) { + if let Some(node_edges) = self.dag.edges.get_mut(&self.id) { node_edges.push(dep_id); } // Add this node as dependent of the dependency - if let Some(node_dependents) = dependents.get_mut(&dep_id) { + if let Some(node_dependents) = self.dag.dependents.get_mut(&dep_id) { node_dependents.push(self.id); } } TaskHandle { id: self.id, - _phantom: std::marker::PhantomData, + _phantom: PhantomData, } } } +/// Opaque node identifier +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub struct NodeId(pub u32); + +/// Opaque, typed token for a node's output. +/// +/// A `TaskHandle` provides compile-time type-safe access to a task's output. +/// You can: +/// 1. Pass it to [`crate::TaskBuilder::depends_on`] to wire up dependencies +/// 2. Use it with [`crate::DagOutput::get`] to retrieve the output after [`crate::DagRunner::run`] +/// +/// Handles are cheap to clone and copy. +/// +/// # Examples +/// +/// ```no_run +/// # use dagx::{task, DagRunner, Task}; +/// # +/// # struct LoadValue { value: i32 } +/// # impl LoadValue { pub fn new(v: i32) -> Self { Self { value: v } } } +/// # #[task] +/// # impl LoadValue { +/// # async fn run(&mut self) -> i32 { self.value } +/// # } +/// # async { +/// let mut dag = DagRunner::new(); +/// let node = dag.add_task(LoadValue::new(42)); +/// +///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +/// +/// assert_eq!(output.get(node).unwrap(), 42); +/// # }; +/// ``` +pub struct TaskHandle { + pub(crate) id: NodeId, + pub(crate) _phantom: PhantomData T>, +} + +impl Clone for TaskHandle { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for TaskHandle {} + +// TaskHandle can be converted from &TaskHandle (for .get() calls) +impl From<&TaskHandle> for TaskHandle { + fn from(handle: &TaskHandle) -> Self { + *handle + } +} + +/// Takes a task and converts it to either a TaskBuilder or a TaskHandle, +/// depending on whether it has inputs or not. +/// +/// This is useful to enforce at compile time that a task with unit input never has +/// depends_on called, and that it can be used directly as a dependency +/// without converting it manually to a TaskHandle. +pub trait TaskWire: Task + Sync + 'static +where + Input: Send + Sync + 'static, +{ + type Retval<'dag>; + + fn new_from_dag<'dag>(id: NodeId, dag: &'dag mut DagRunner) -> Self::Retval<'dag>; +} + +impl TaskWire<()> for Tk +where + Tk: Task<()> + Sync + 'static, +{ + type Retval<'dag> = TaskHandle; + + fn new_from_dag(id: NodeId, _dag: &mut DagRunner) -> Self::Retval<'static> { + Self::Retval { + id, + _phantom: PhantomData, + } + } +} + +/// Macro to implement TaskWire for different tuple sizes. +macro_rules! impl_wire_tuple { + ($($T:ident),+) => { + // For tuples of any combination of &TaskHandle, TaskHandle, and TaskBuilder + impl TaskWire<($($T,)+)> for Tk + where + Tk: Task<($($T,)+)> + Sync + 'static + { + type Retval<'dag> = TaskBuilder<'dag, ($($T,)+), Tk>; + + fn new_from_dag<'dag>(id: NodeId, dag: &'dag mut DagRunner) -> Self::Retval<'dag> { + Self::Retval { + id, + dag, + _phantom: PhantomData, + } + } + } + }; +} + +// Generate DepsTuple implementations for tuples of size 2-8. +// Supporting up to 8 elements covers the vast majority of use cases. +impl_wire_tuple!(T1); +impl_wire_tuple!(T1, T2); +impl_wire_tuple!(T1, T2, T3); +impl_wire_tuple!(T1, T2, T3, T4); +impl_wire_tuple!(T1, T2, T3, T4, T5); +impl_wire_tuple!(T1, T2, T3, T4, T5, T6); +impl_wire_tuple!(T1, T2, T3, T4, T5, T6, T7); +impl_wire_tuple!(T1, T2, T3, T4, T5, T6, T7, T8); + #[cfg(test)] mod tests; + +#[cfg(test)] +mod coverage_tests; diff --git a/src/types/coverage_tests.rs b/src/builder/coverage_tests.rs similarity index 81% rename from src/types/coverage_tests.rs rename to src/builder/coverage_tests.rs index f81ee93..e3e9565 100644 --- a/src/types/coverage_tests.rs +++ b/src/builder/coverage_tests.rs @@ -25,19 +25,20 @@ async fn test_unit_struct_with_state() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Create task with initial state let builder = dag.add_task(StatefulTask::new(21)); // Explicit conversion - triggers line 71 - let handle: TaskHandle<_> = builder.into(); + let handle = builder; - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - assert_eq!(dag.get(handle).unwrap(), 42); + assert_eq!(output.get(handle).unwrap(), 42); } #[test] @@ -51,12 +52,12 @@ fn test_taskbuilder_conversion_without_running() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let builder = dag.add_task(TestTask); let builder_id = builder.id; // Convert without running the DAG - let handle: TaskHandle = builder.into(); + let handle: TaskHandle = builder; // Verify the ID was transferred correctly assert_eq!(handle.id, builder_id); diff --git a/src/builder/tests.rs b/src/builder/tests.rs index e94cfe4..164a669 100644 --- a/src/builder/tests.rs +++ b/src/builder/tests.rs @@ -1,7 +1,6 @@ //! Unit tests for builder module use crate::runner::DagRunner; -use crate::types::TaskHandle; // Initialize tracing subscriber for tests (idempotent) #[cfg(feature = "tracing")] @@ -47,11 +46,11 @@ impl TestTaskWithInput { #[test] fn test_task_builder_from_conversion() { // Test line 165 in builder.rs - the From implementation - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let builder = dag.add_task(TestTask { value: 42 }); // Convert builder to TaskHandle - let handle: TaskHandle<_> = builder.into(); + let handle = builder; // Check that the ID is preserved assert_eq!(handle.id.0, 0); // First task should have ID 0 @@ -59,17 +58,17 @@ fn test_task_builder_from_conversion() { #[test] fn test_task_builder_chain() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Test chaining multiple tasks - let t1 = dag.add_task(TestTask { value: 1 }).into(); + let t1 = dag.add_task(TestTask { value: 1 }); let t2 = dag.add_task(TestTaskWithInput).depends_on(t1); let t3 = dag.add_task(TestTaskWithInput).depends_on(t2); // All should return TaskHandles - let _: TaskHandle<_> = t1; - let _: TaskHandle<_> = t2; - let _: TaskHandle<_> = t3; + let _ = t1; + let _ = t2; + let _ = t3; } #[test] @@ -84,7 +83,7 @@ fn test_multiple_dependencies() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let a = dag.add_task(TestTask { value: 10 }); let b = dag.add_task(TestTask { value: 20 }); @@ -93,12 +92,12 @@ fn test_multiple_dependencies() { let sum = dag.add_task(AddTask).depends_on((a, b)); // Should return a TaskHandle - let _: TaskHandle<_> = sum; + let _ = sum; } #[test] fn test_task_builder_stores_correct_id() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let builder1 = dag.add_task(TestTask { value: 1 }); let builder2 = dag.add_task(TestTask { value: 2 }); diff --git a/src/deps.rs b/src/deps.rs index a92c796..9f82eb1 100644 --- a/src/deps.rs +++ b/src/deps.rs @@ -7,36 +7,7 @@ //! This trait is internal and not meant for external implementation. Users should only //! use the provided implementations. -use crate::builder::TaskBuilder; -use crate::task::Task; -use crate::types::{NodeId, TaskHandle}; - -trait IntoNodeRef { - type Output; - fn into_node_ref(self) -> NodeId; -} - -impl IntoNodeRef for &TaskHandle { - type Output = T; - fn into_node_ref(self) -> NodeId { - self.id - } -} - -impl IntoNodeRef for TaskHandle { - type Output = T; - fn into_node_ref(self) -> NodeId { - self.id - } -} - -impl<'runner, Tk: Task, Deps> IntoNodeRef for TaskBuilder<'runner, Tk, Deps> { - type Output = Tk::Output; - - fn into_node_ref(self) -> NodeId { - self.id - } -} +use crate::builder::{NodeId, TaskHandle}; pub(crate) trait DepsTuple { fn to_node_ids(self) -> Vec; @@ -50,21 +21,21 @@ impl DepsTuple<()> for () { } // Implementation for single dependency (all forms) -impl DepsTuple<(T::Output,)> for T +impl DepsTuple<(O,)> for T where - T: IntoNodeRef, + T: Into>, { fn to_node_ids(self) -> Vec { - vec![self.into_node_ref()] + vec![self.into().id] } } -impl DepsTuple for (T,) +impl DepsTuple for (T,) where - T: IntoNodeRef, + T: Into>, { fn to_node_ids(self) -> Vec { - vec![self.0.into_node_ref()] + vec![self.0.into().id] } } @@ -82,13 +53,14 @@ where /// task.depends_on((&dag.add_task(TaskA), &dag.add_task(TaskB))) /// ``` macro_rules! impl_deps_tuple { - ($($T:ident),+) => { + ($($T:ident : $O:ident),+) => { // For tuples of any combination of &TaskHandle, TaskHandle, and TaskBuilder - impl<$($T: IntoNodeRef),+> DepsTuple<($($T::Output,)+)> for ($($T,)+) { + impl<$($T: Into>, $O),+> DepsTuple<($($O,)+)> for ($($T,)+) + { #[allow(non_snake_case)] fn to_node_ids(self) -> Vec { let ($($T,)+) = self; - vec![$($T.into_node_ref(),)+] + vec![$($T.into().id,)+] } } }; @@ -96,13 +68,13 @@ macro_rules! impl_deps_tuple { // Generate DepsTuple implementations for tuples of size 2-8. // Supporting up to 8 elements covers the vast majority of use cases. -impl_deps_tuple!(T1, T2); -impl_deps_tuple!(T1, T2, T3); -impl_deps_tuple!(T1, T2, T3, T4); -impl_deps_tuple!(T1, T2, T3, T4, T5); -impl_deps_tuple!(T1, T2, T3, T4, T5, T6); -impl_deps_tuple!(T1, T2, T3, T4, T5, T6, T7); -impl_deps_tuple!(T1, T2, T3, T4, T5, T6, T7, T8); +impl_deps_tuple!(T1:O1, T2:O2); +impl_deps_tuple!(T1:O1, T2:O2, T3:O3); +impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4); +impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5); +impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5, T6:O6); +impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5, T6:O6, T7:O7); +impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5, T6:O6, T7:O7, T8:O8); #[cfg(test)] mod tests; diff --git a/src/deps/tests.rs b/src/deps/tests.rs index 203c850..c58af16 100644 --- a/src/deps/tests.rs +++ b/src/deps/tests.rs @@ -1,8 +1,8 @@ //! Unit tests for deps module +use crate::builder::{NodeId, TaskHandle}; use crate::deps::DepsTuple; use crate::runner::DagRunner; -use crate::types::{NodeId, TaskHandle}; // Test task for dependency testing struct TestTask { @@ -78,7 +78,7 @@ fn test_deps_tuple_single_handle_tuple_owned() { #[test] fn test_deps_tuple_task_builder_ref() { // Test lines 51-55 in deps.rs - &TaskBuilder - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let builder = dag.add_task(TestTask { value: 42 }); let builder_id = builder.id; @@ -92,7 +92,7 @@ fn test_deps_tuple_task_builder_ref() { #[test] fn test_deps_tuple_task_builder_tuple() { // Test lines 57-61 in deps.rs - (&TaskBuilder,) - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let builder = dag.add_task(TestTask { value: 100 }); let builder_id = builder.id; @@ -134,7 +134,7 @@ fn test_deps_tuple_multiple_handles() { #[test] fn test_deps_tuple_multiple_builders() { // Test macro-generated implementations for multiple builders - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let builder1 = dag.add_task(TestTask { value: 10 }); let builder1_id = builder1.id; let builder2 = dag.add_task(TestTask { value: 20 }); diff --git a/src/error.rs b/src/error.rs index 4f98c55..8bc24c4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -17,8 +17,6 @@ pub enum DagError { TaskPanicked { task_id: u32, panic_message: String }, /// Result not found for task ResultNotFound { task_id: u32 }, - /// Attempted to run the DAG while it's already running - ConcurrentExecution, } impl std::fmt::Display for DagError { @@ -65,14 +63,6 @@ impl std::fmt::Display for DagError { task_id ) } - DagError::ConcurrentExecution => { - write!( - f, - "DAG is already running - concurrent execution not supported.\n\ - \n\ - Wait for the current execution to complete before starting another." - ) - } } } } diff --git a/src/error/tests.rs b/src/error/tests.rs index c4ef7e0..4e3d87a 100644 --- a/src/error/tests.rs +++ b/src/error/tests.rs @@ -51,17 +51,9 @@ fn test_dag_error_display_result_not_found() { assert!(display.contains("Result not found")); assert!(display.contains("task #7")); - assert!(display.contains("Call dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })")); -} - -#[test] -fn test_dag_error_display_concurrent_execution() { - let err = DagError::ConcurrentExecution; - let display = format!("{}", err); - - assert!(display.contains("already running")); - assert!(display.contains("concurrent execution not supported")); - assert!(display.contains("Wait for the current execution")); + assert!(display.contains( + "Calllet mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })" + )); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 443e15e..06294d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,18 +42,18 @@ //! } //! //! # async { -//! let dag = DagRunner::new(); +//! let mut dag = DagRunner::new(); //! //! // Add source tasks //! let x = dag.add_task(Value(2)); -//! let y: TaskHandle<_> = dag.add_task(Value(3)).into(); // Allows y to be reused as a dependency +//! let y = dag.add_task(Value(3)).into(); // Allows y to be reused as a dependency //! //! // Add task with dependencies //! let sum = dag.add_task(Add).depends_on((x, &y)); //! //! // Execute and retrieve results -//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); -//! assert_eq!(dag.get(sum).unwrap(), 5); +//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +//! assert_eq!(output.get(sum).unwrap(), 5); //! # }; //! ``` //! @@ -212,7 +212,7 @@ //! //! A [`TaskHandle`] is a typed, opaque reference to a task's output. Use it to: //! - Wire dependencies between tasks -//! - Retrieve results after execution with [`DagRunner::get`] +//! - Retrieve results after execution with [`DagOutput::get`] //! //! ## Dependency Patterns //! @@ -230,7 +230,7 @@ //! # async fn run(&self) -> i32 { self.0 } //! # } //! # async { -//! # let dag = DagRunner::new(); +//! # let mut dag = DagRunner::new(); //! let source = dag.add_task(Value(42)); //! # }; //! ``` @@ -252,7 +252,7 @@ //! async fn run(input: &i32) -> i32 { input * 2 } //! } //! # async { -//! # let dag = DagRunner::new(); +//! # let mut dag = DagRunner::new(); //! # let source = dag.add_task(Value(42)); //! let doubled = dag.add_task(Double).depends_on(source); //! # }; @@ -275,7 +275,7 @@ //! async fn run(a: &i32, b: &i32) -> i32 { a + b } //! } //! # async { -//! # let dag = DagRunner::new(); +//! # let mut dag = DagRunner::new(); //! # let x = dag.add_task(Value(2)); //! # let y = dag.add_task(Value(3)); //! let sum = dag.add_task(Add).depends_on((x, y)); @@ -317,16 +317,16 @@ //! } //! //! # async { -//! let dag = DagRunner::new(); +//! let mut dag = DagRunner::new(); //! //! let base = dag.add_task(Value(10)).into(); //! let plus1 = dag.add_task(Add(1)).depends_on(&base); //! let times2 = dag.add_task(Scale(2)).depends_on(&base); //! -//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); //! -//! assert_eq!(dag.get(plus1).unwrap(), 11); -//! assert_eq!(dag.get(times2).unwrap(), 20); +//! assert_eq!(output.get(plus1).unwrap(), 11); +//! assert_eq!(output.get(times2).unwrap(), 20); //! # }; //! ``` //! @@ -373,16 +373,16 @@ //! } //! //! # async { -//! let dag = DagRunner::new(); +//! let mut dag = DagRunner::new(); //! //! let name = dag.add_task(Name("Alice".to_string())); //! let age = dag.add_task(Age(30)); //! let active = dag.add_task(Active(true)); //! let result = dag.add_task(FormatUser).depends_on((name, age, active)); //! -//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); //! -//! assert_eq!(dag.get(result).unwrap(), "User: Alice, Age: 30, Active: true"); +//! assert_eq!(output.get(result).unwrap(), "User: Alice, Age: 30, Active: true"); //! # }; //! ``` //! @@ -419,7 +419,7 @@ //! } //! //! # async { -//! let dag = DagRunner::new(); +//! let mut dag = DagRunner::new(); //! //! // Layer 1: Sources //! let x = dag.add_task(Value(2)); @@ -433,9 +433,9 @@ //! // Layer 3: Final result //! let total = dag.add_task(Add).depends_on((&sum_xy, &prod_yz)); // 5 + 15 = 20 //! -//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); //! -//! assert_eq!(dag.get(total).unwrap(), 20); +//! assert_eq!(output.get(total).unwrap(), 20); //! # }; //! ``` //! @@ -470,14 +470,14 @@ //! } //! //! # async { -//! let dag = DagRunner::new(); +//! let mut dag = DagRunner::new(); //! //! let user = dag.add_task(CreateUser); //! let formatted = dag.add_task(FormatUser).depends_on(user); //! -//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); //! -//! assert_eq!(dag.get(formatted).unwrap(), "Alice is 30 years old"); +//! assert_eq!(output.get(formatted).unwrap(), "Alice is 30 years old"); //! # }; //! ``` //! @@ -497,21 +497,21 @@ //! // With Tokio //! #[tokio::main] //! async fn main() { -//! let dag = DagRunner::new(); +//! let mut dag = DagRunner::new(); //! // ... build and run DAG //! } //! //! // With async-std //! #[async_std::main] //! async fn main() { -//! let dag = DagRunner::new(); +//! let mut dag = DagRunner::new(); //! // ... build and run DAG //! } //! //! // With smol //! fn main() { //! smol::block_on(async { -//! let dag = DagRunner::new(); +//! let mut dag = DagRunner::new(); //! // ... build and run DAG //! }); //! } @@ -522,9 +522,9 @@ //! dagx uses [`DagResult`] (an alias for `Result`) for operations that //! can fail: //! -//! - [`DagRunner::run`] returns `DagResult<()>` and can fail if tasks panic or if multiple +//! - [`DagRunner::run`] returns `DagResult` and can fail if tasks panic or if multiple //! concurrent runs are attempted -//! - [`DagRunner::get`] returns `DagResult` and can fail if the task hasn't executed or +//! - [`DagOutput::get`] returns `DagResult` and can fail if the task hasn't executed or //! the handle is invalid //! //! You can handle errors explicitly or use `.unwrap()` for simple cases: @@ -538,14 +538,14 @@ //! # async fn run(&self) -> i32 { self.0 } //! # } //! # async { -//! # let dag = DagRunner::new(); +//! # let mut dag = DagRunner::new(); //! # let node = dag.add_task(Value(42)); //! // Simple approach with .unwrap() -//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); -//! let result = dag.get(node).unwrap(); +//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +//! let result = output.get(node).unwrap(); //! //! // Or handle errors explicitly -//! match dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await { +//! matchlet mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await { //! Ok(_) => println!("DAG executed successfully"), //! Err(e) => eprintln!("DAG execution failed: {}", e), //! } @@ -651,7 +651,7 @@ //! } //! //! # async { -//! let dag = DagRunner::new(); +//! let mut dag = DagRunner::new(); //! let data = dag.add_task(FetchData).into(); //! //! // All three tasks get efficient Arc-wrapped sharing automatically @@ -659,7 +659,7 @@ //! let task2 = dag.add_task(ProcessData).depends_on(&data); //! let task3 = dag.add_task(ProcessData).depends_on(&data); //! -//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); //! # }; //! ``` //! @@ -668,7 +668,7 @@ //! - Framework wraps it in `Arc` internally //! - For fan-out (1→N), Arc is cloned N times (just atomic pointer increments - O(1)) //! - Each downstream task receives `&T` after extracting from Arc -//! - When you call `dag.get()`, the Arc is cloned and inner value extracted +//! - When you call `output.get()`, the Arc is cloned and inner value extracted //! //! **Performance characteristics:** //! - **Heap types** (Vec, String, HashMap): Arc overhead is negligible (~few ns) @@ -771,16 +771,16 @@ mod builder; mod deps; mod error; mod node; +mod output; mod runner; mod task; -mod types; // Public re-exports -pub use builder::TaskBuilder; +pub use builder::{TaskBuilder, TaskHandle}; pub use error::{DagError, DagResult}; +pub use output::DagOutput; pub use runner::DagRunner; pub use task::{Task, TaskInput}; -pub use types::{Pending, TaskHandle}; // Re-export the procedural macro pub use dagx_macros::task; diff --git a/src/node.rs b/src/node.rs index d566a8b..440bae1 100644 --- a/src/node.rs +++ b/src/node.rs @@ -8,6 +8,7 @@ use std::any::Any; use std::future::Future; +use std::marker::PhantomData; use std::pin::Pin; use std::sync::Arc; @@ -50,20 +51,32 @@ pub(crate) trait ExecutableNode: Send { /// Each task executes exactly once, taking ownership and producing an output. /// /// The output is also returned from execute_with_deps for central storage. -pub(crate) struct TypedNode { +pub(crate) struct TypedNode +where + Input: Send + Sync + 'static, + T: Task + 'static, +{ pub(crate) task: T, + phantom: PhantomData, } -impl TypedNode { +impl TypedNode +where + T: Task, + Input: Send + Sync + 'static, +{ pub(crate) fn new(task: T) -> Self { - Self { task } + Self { + task, + phantom: PhantomData, + } } } -impl ExecutableNode for TypedNode +impl ExecutableNode for TypedNode where - T::Input: 'static, - T::Output: 'static, + T: Task, + Input: Send + Sync + 'static, { fn execute_with_deps( self: Box, diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..309ad2f --- /dev/null +++ b/src/output.rs @@ -0,0 +1,83 @@ +use std::{any::Any, sync::Arc}; + +use crate::{builder::NodeId, runner::PassThroughHashMap, DagError, DagResult, TaskHandle}; + +pub struct DagOutput { + outputs: PassThroughHashMap>, +} + +impl DagOutput { + /// Retrieve a task's output after [`crate::DagRunner::run`]. + /// + /// # Behavior + /// + /// All task outputs are stored after execution and can be retrieved via get(). + /// Each task's output can only be taken once, after which it will return `DagError::ResultNotFound`. + /// + /// # Errors + /// + /// Returns `DagError::ResultNotFound` if: + /// - The task hasn't been executed yet + /// - The handle is invalid + /// + /// # Examples + /// + /// ```no_run + /// # use dagx::{task, DagRunner, Task}; + /// # + /// struct Configuration { + /// setting: i32, + /// } + /// + /// impl Configuration { + /// fn new(setting: i32) -> Self { + /// Self { setting } + /// } + /// } + /// + /// #[task] + /// impl Configuration { + /// async fn run(&mut self) -> i32 { self.setting } + /// } + /// + /// # async { + /// let mut dag = DagRunner::new(); + /// + /// // Construct task with specific setting value + /// let task = dag.add_task(Configuration::new(42)); + /// + ///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); + /// + /// assert_eq!(output.get(task).unwrap(), 42); + /// # }; + /// ``` + pub fn get(&mut self, handle: H) -> DagResult + where + H: Into>, + { + let handle: TaskHandle = handle.into(); + + let arc_output = self + .outputs + .remove(&handle.id) + .ok_or(DagError::ResultNotFound { + task_id: handle.id.0, + })?; + + // Downcast Arc to Arc + let output = arc_output + .downcast::() + .map_err(|_| DagError::TypeMismatch { + expected: std::any::type_name::(), + found: "unknown", + })?; + + Ok(Arc::into_inner(output).unwrap()) + } + + pub(super) fn new( + outputs: PassThroughHashMap>, + ) -> Self { + Self { outputs } + } +} diff --git a/src/runner.rs b/src/runner.rs index 335de5c..9d9b019 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -10,34 +10,18 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::future::Future; use std::hash::{BuildHasher, Hasher}; use std::panic::AssertUnwindSafe; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; +use std::sync::Arc; use futures_util::future::BoxFuture; use futures_util::{stream::FuturesUnordered, FutureExt, StreamExt, TryFutureExt}; -use parking_lot::Mutex; #[cfg(feature = "tracing")] use tracing::{debug, error, info, trace}; -use crate::builder::TaskBuilder; +use crate::builder::{NodeId, TaskWire}; use crate::error::{DagError, DagResult}; use crate::node::{ExecutableNode, TypedNode}; -use crate::task::Task; -use crate::types::{NodeId, Pending, TaskHandle}; - -// Guard to ensure run_lock is released even on early return or panic -struct RunGuard<'a> { - lock: &'a AtomicBool, -} - -impl<'a> Drop for RunGuard<'a> { - fn drop(&mut self) { - self.lock.store(false, Ordering::SeqCst); - } -} +use crate::DagOutput; #[derive(Default, Clone)] pub(crate) struct PassThroughHasher { @@ -76,10 +60,10 @@ pub(crate) type PassThroughHashMap = HashMap; /// # Workflow /// /// 1. Create a new DAG with [`DagRunner::new`] -/// 2. Add tasks with [`DagRunner::add_task`] to get [`TaskBuilder`] builders -/// 3. Wire dependencies with [`TaskBuilder::depends_on`] +/// 2. Add tasks with [`DagRunner::add_task`] to get builders +/// 3. Wire dependencies with [`crate::TaskBuilder::depends_on`] /// 4. Execute all tasks with [`DagRunner::run`] -/// 5. Optionally retrieve outputs with [`DagRunner::get`] +/// 5. Optionally retrieve outputs with [`DagOutput::get`] /// /// # Examples /// @@ -107,16 +91,16 @@ pub(crate) type PassThroughHashMap = HashMap; /// } /// /// # async { -/// let dag = DagRunner::new(); +/// let mut dag = DagRunner::new(); /// /// // Construct instances using ::new() pattern /// let x = dag.add_task(LoadValue::new(2)); /// let y = dag.add_task(LoadValue::new(3)); /// let sum = dag.add_task(Add).depends_on((x, y)); /// -/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); /// -/// assert_eq!(dag.get(sum).unwrap(), 5); +/// assert_eq!(output.get(sum).unwrap(), 5); /// # }; /// ``` /// @@ -127,13 +111,9 @@ pub(crate) type PassThroughHashMap = HashMap; /// Outputs are Arc-wrapped and stored separately for retrieval via get(). /// Arc enables efficient sharing during fanout without cloning data. pub struct DagRunner { - pub(crate) nodes: Mutex>>>, - pub(crate) outputs: - Mutex>>, - pub(crate) edges: Mutex>>, // node -> dependencies - pub(crate) dependents: Mutex>>, // node -> tasks that depend on it - pub(crate) next_id: Mutex, - pub(crate) run_lock: AtomicBool, // Ensures only one run() at a time + pub(crate) nodes: Vec>>, + pub(crate) edges: PassThroughHashMap>, // node -> dependencies + pub(crate) dependents: PassThroughHashMap>, // node -> tasks that depend on it } impl Default for DagRunner { @@ -150,31 +130,21 @@ impl DagRunner { /// ``` /// use dagx::DagRunner; /// - /// let dag = DagRunner::new(); + /// let mut dag = DagRunner::new(); /// ``` pub fn new() -> Self { Self { - nodes: Mutex::new(Vec::new()), - outputs: Mutex::new(HashMap::default()), - edges: Mutex::new(HashMap::default()), - dependents: Mutex::new(HashMap::default()), - next_id: Mutex::new(0), - run_lock: AtomicBool::new(false), + nodes: Vec::new(), + edges: HashMap::default(), + dependents: HashMap::default(), } } - pub(crate) fn alloc_id(&self) -> NodeId { - let mut next_id = self.next_id.lock(); - let id = NodeId(*next_id); - *next_id += 1; - id - } - /// Add a task instance to the DAG, returning a node builder for wiring dependencies. /// - /// The returned [`TaskBuilder`](TaskBuilder) can be used to: - /// - Specify dependencies via [`TaskBuilder::depends_on`] - /// - Used directly as a [`TaskHandle`] to the task's output + /// If the task has no dependencies, a [`crate::TaskHandle`] will be returned. + /// If not, the returned [`crate::TaskBuilder`] can be used to + /// specify dependencies via [`crate::TaskBuilder::depends_on`]. /// /// # Examples /// @@ -214,7 +184,7 @@ impl DagRunner { /// } /// /// # async { - /// let dag = DagRunner::new(); + /// let mut dag = DagRunner::new(); /// /// // Construct task with initial value of 10 /// let base = dag.add_task(LoadValue::new(10)); @@ -222,17 +192,16 @@ impl DagRunner { /// // Construct task with offset of 1 /// let inc = dag.add_task(AddOffset::new(1)).depends_on(base); /// - /// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); - /// assert_eq!(dag.get(inc).unwrap(), 11); + ///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); + /// assert_eq!(output.get(inc).unwrap(), 11); /// # }; /// ``` - pub fn add_task(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending> + pub fn add_task<'dag, Input, Tk>(&'dag mut self, task: Tk) -> Tk::Retval<'dag> where - Tk: Task + Sync + 'static, - Tk::Input: 'static, - Tk::Output: 'static, + Tk: TaskWire, + Input: Send + Sync + 'static, { - let id = self.alloc_id(); + let id = NodeId(self.nodes.len() as u32); #[cfg(feature = "tracing")] debug!( @@ -242,15 +211,11 @@ impl DagRunner { ); let node = TypedNode::new(task); - self.nodes.lock().push(Some(Box::new(node))); - self.edges.lock().insert(id, Vec::new()); - self.dependents.lock().insert(id, Vec::new()); + self.nodes.push(Some(Box::new(node))); + self.edges.insert(id, Vec::new()); + self.dependents.insert(id, Vec::new()); - TaskBuilder { - id, - dag: self, - _phantom: std::marker::PhantomData, - } + Tk::new_from_dag(id, self) } /// Run the entire DAG to completion using the provided spawner. @@ -299,41 +264,24 @@ impl DagRunner { /// } /// /// # async { - /// let dag = DagRunner::new(); + /// let mut dag = DagRunner::new(); /// /// let a = dag.add_task(Value(1)); /// let b = dag.add_task(Value(2)); /// let sum = dag.add_task(Add).depends_on((a, b)); /// - /// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); // Executes all tasks + ///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); // Executes all tasks /// # }; /// ``` #[inline] #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))] - pub async fn run(&self, spawner: S) -> DagResult<()> + pub async fn run(mut self, spawner: S) -> DagResult where S: Fn(BoxFuture<'static, DagResult>>) -> F, F: Future>>, { #[cfg(feature = "tracing")] info!("starting DAG execution"); - // Acquire run lock to prevent concurrent executions - // Use atomic compare_exchange to check if already running - if self - .run_lock - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) - .is_err() - { - #[cfg(feature = "tracing")] - error!("DAG is already running - concurrent execution not supported"); - - return Err(DagError::ConcurrentExecution); - } - - // Guard ensures lock is released on drop (even on early return or panic) - let _run_guard = RunGuard { - lock: &self.run_lock, - }; // Build topological layers let layers = self.compute_layers()?; @@ -346,8 +294,6 @@ impl DagRunner { total_tasks, "computed topological layers" ); - let edges = self.edges.lock().clone(); - let mut outputs: PassThroughHashMap> = HashMap::with_capacity_and_hasher(total_tasks, PassThroughHasher::default()); let mut first_error = None; @@ -392,13 +338,10 @@ impl DagRunner { ); // Take ownership of the node - let node = { - let mut nodes_lock = self.nodes.lock(); - nodes_lock[node_id.0 as usize].take() - }; + let node = self.nodes[node_id.0 as usize].take(); if let Some(node) = node { - let dependencies: Vec<_> = edges[&node_id] + let dependencies: Vec<_> = self.edges[&node_id] .iter() .flat_map(|dep| outputs.get(dep)) .cloned() @@ -456,12 +399,9 @@ impl DagRunner { trace!(task_id = node_id.0, "spawning task"); // Take ownership of the node - let node = { - let mut nodes_lock = self.nodes.lock(); - nodes_lock[node_id.0 as usize].take() - }; + let node = self.nodes[node_id.0 as usize].take(); if let Some(node) = node { - let dependencies: Vec<_> = edges[&node_id] + let dependencies: Vec<_> = self.edges[&node_id] .iter() .flat_map(|dep| outputs.get(dep)) .cloned() @@ -522,13 +462,6 @@ impl DagRunner { } } - // Insert all successful outputs (avoiding holding lock across await) - let mut outputs_lock = self.outputs.lock(); - for (node_id, output) in outputs { - outputs_lock.insert(node_id, output); - } - drop(outputs_lock); - // Return first error if any if let Some(err) = first_error { #[cfg(feature = "tracing")] @@ -539,73 +472,7 @@ impl DagRunner { #[cfg(feature = "tracing")] info!("DAG execution completed successfully"); - Ok(()) - } - - /// Retrieve a task's output after [`DagRunner::run`]. - /// - /// # Behavior - /// - /// All task outputs are stored after execution and can be retrieved via get(). - /// Each task's output can only be taken once, after which it will return `DagError::ResultNotFound`. - /// - /// # Errors - /// - /// Returns `DagError::ResultNotFound` if: - /// - The task hasn't been executed yet - /// - The handle is invalid - /// - /// # Examples - /// - /// ```no_run - /// # use dagx::{task, DagRunner, Task}; - /// # - /// struct Configuration { - /// setting: i32, - /// } - /// - /// impl Configuration { - /// fn new(setting: i32) -> Self { - /// Self { setting } - /// } - /// } - /// - /// #[task] - /// impl Configuration { - /// async fn run(&mut self) -> i32 { self.setting } - /// } - /// - /// # async { - /// let dag = DagRunner::new(); - /// - /// // Construct task with specific setting value - /// let task = dag.add_task(Configuration::new(42)); - /// - /// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); - /// - /// assert_eq!(dag.get(task).unwrap(), 42); - /// # }; - /// ``` - pub fn get(&self, handle: H) -> DagResult - where - H: Into>, - { - let handle: TaskHandle = handle.into(); - let mut outputs = self.outputs.lock(); - - let arc_output = outputs.remove(&handle.id).ok_or(DagError::ResultNotFound { - task_id: handle.id.0, - })?; - - // Downcast Arc to Arc - let output = arc_output - .downcast::() - .map_err(|_| DagError::TypeMismatch { - expected: std::any::type_name::(), - found: "unknown", - })?; - - Ok(Arc::into_inner(output).unwrap()) + Ok(DagOutput::new(outputs)) } #[inline] @@ -617,18 +484,11 @@ impl DagRunner { let mut layers = Vec::new(); // Calculate in-degrees: for each node, count how many dependencies it has - let edges = self.edges.lock(); - // Compiler optimization hint: provides bounds information for HashSet sizing. - // This variable guides the optimizer's understanding of graph size, improving - // hash table and iterator performance in the topological sort below. - let _total_nodes = edges.len(); - - for (&node, deps) in edges.iter() { + for (&node, deps) in self.edges.iter() { let degree = deps.len(); in_degree.insert(node, degree); } - drop(edges); // Release lock early // Find all nodes with in-degree 0 (sources - nodes with no dependencies) let mut queue: VecDeque = in_degree @@ -653,8 +513,7 @@ impl DagRunner { visited.insert(node); // For each node that depends on the current node - let dependents = self.dependents.lock(); - if let Some(deps) = dependents.get(&node) { + if let Some(deps) = self.dependents.get(&node) { for &dependent in deps { if let Some(degree) = in_degree.get_mut(&dependent) { *degree -= 1; diff --git a/src/runner/tests.rs b/src/runner/tests.rs index b6b2c4f..ceef534 100644 --- a/src/runner/tests.rs +++ b/src/runner/tests.rs @@ -3,9 +3,10 @@ use std::collections::HashMap; use std::hash::Hasher; +use crate::builder::TaskHandle; use crate::error::DagError; use crate::runner::{DagRunner, PassThroughHasher}; -use crate::types::TaskHandle; +use crate::DagOutput; // Initialize tracing subscriber for tests (idempotent) #[cfg(feature = "tracing")] @@ -40,16 +41,16 @@ impl TestTask { #[test] fn test_dag_runner_new() { // Test that DagRunner::new() creates a new instance - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Initial state should be empty - assert_eq!(dag.nodes.lock().len(), 0); - assert_eq!(dag.edges.lock().len(), 0); - assert_eq!(dag.dependents.lock().len(), 0); + assert_eq!(dag.nodes.len(), 0); + assert_eq!(dag.edges.len(), 0); + assert_eq!(dag.dependents.len(), 0); // Adding a task works dag.add_task(TestTask { value: 42 }); - assert_eq!(dag.nodes.lock().len(), 1); + assert_eq!(dag.nodes.len(), 1); } #[test] @@ -58,19 +59,20 @@ fn test_dag_runner_default() { let dag = DagRunner::default(); // Should behave the same as new() - assert_eq!(dag.nodes.lock().len(), 0); - assert_eq!(dag.edges.lock().len(), 0); - assert_eq!(dag.dependents.lock().len(), 0); + assert_eq!(dag.nodes.len(), 0); + assert_eq!(dag.edges.len(), 0); + assert_eq!(dag.dependents.len(), 0); } #[tokio::test] async fn test_get_wrong_type() { init_tracing(); // Test getting with wrong type - this should return TypeMismatch - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let handle = dag.add_task(TestTask { value: 42 }); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); @@ -80,7 +82,7 @@ async fn test_get_wrong_type() { _phantom: std::marker::PhantomData, }; - let result = dag.get(fake_handle); + let result = output.get(fake_handle); assert!(result.is_err()); // When downcast fails, we get TypeMismatch @@ -96,11 +98,12 @@ async fn test_get_wrong_type() { async fn test_get_result_not_found() { init_tracing(); // Test getting a result before running the DAG - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let handle = dag.add_task(TestTask { value: 42 }); let handle_id = handle.id.0; // Save ID before moving handle + let mut output = DagOutput::new(HashMap::default()); - let result = dag.get(handle); + let result = output.get(handle); assert!(result.is_err()); match result.unwrap_err() { DagError::ResultNotFound { task_id } => { @@ -110,67 +113,6 @@ async fn test_get_result_not_found() { } } -#[tokio::test] -async fn test_concurrent_run_protection() { - init_tracing(); - // Test that run_lock prevents concurrent runs - use std::sync::Arc; - use std::time::Duration; - use tokio::time::sleep; - - let dag = DagRunner::new(); - - // Add a task that takes some time - struct SlowTask; - #[crate::task] - impl SlowTask { - async fn run(&self) -> i32 { - sleep(Duration::from_millis(100)).await; - 42 - } - } - - dag.add_task(SlowTask); - - // Wrap in Arc for sharing between tasks - let dag = Arc::new(dag); - let dag1 = Arc::clone(&dag); - let dag2 = Arc::clone(&dag); - - // Use a barrier to ensure both runs start at exactly the same time - let barrier = Arc::new(tokio::sync::Barrier::new(2)); - let barrier1 = barrier.clone(); - let barrier2 = barrier.clone(); - - // Start two runs concurrently - let handle1 = tokio::spawn(async move { - barrier1.wait().await; - dag1.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - }); - - let handle2 = tokio::spawn(async move { - barrier2.wait().await; - dag2.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - }); - - // One should succeed, one should fail (concurrent execution not supported) - let result1 = handle1.await.unwrap(); - let result2 = handle2.await.unwrap(); - - // Exactly one should be ok, one should be err - assert!(result1.is_ok() != result2.is_ok()); - - // The error should be about concurrent execution - if let Err(e) = result1 { - assert!(matches!(e, crate::error::DagError::ConcurrentExecution)); - } - if let Err(e) = result2 { - assert!(matches!(e, crate::error::DagError::ConcurrentExecution)); - } -} - #[tokio::test] async fn test_invalid_node_id_in_get() { init_tracing(); @@ -179,15 +121,16 @@ async fn test_invalid_node_id_in_get() { // Create a handle with an ID that doesn't exist let invalid_handle: TaskHandle = TaskHandle { - id: crate::types::NodeId(999), + id: crate::builder::NodeId(999), _phantom: std::marker::PhantomData, }; - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - let result = dag.get(invalid_handle); + let result = output.get(invalid_handle); assert!(result.is_err()); // With centralized output storage, we get ResultNotFound for invalid node IDs @@ -228,8 +171,8 @@ async fn test_task_panic_in_multi_task_layer() { } } - let dag = DagRunner::new(); - let source: TaskHandle<_> = dag.add_task(Source).into(); + let mut dag = DagRunner::new(); + let source = dag.add_task(Source); // Create two tasks in the same layer - one panics, one doesn't let _panic_task = dag.add_task(PanicTask).depends_on(source); diff --git a/src/task.rs b/src/task.rs index 0b40a18..ff42e47 100644 --- a/src/task.rs +++ b/src/task.rs @@ -92,11 +92,13 @@ use std::{any::Any, future::Future, marker::PhantomData, slice::Iter, sync::Arc} /// /// Downstream tasks receive `&T` after ExtractInput extracts from the Arc. This is transparent /// to your task code - you work with `T`, the framework handles Arc wrapping/unwrapping. -pub trait Task: Send { - type Input: Send; - type Output: Send + Sync; // Sync required for Arc-wrapping and cross-thread sharing +pub trait Task: Send +where + Input: Send + Sync + 'static, +{ + type Output: Send + Sync + 'static; // Sync required for Arc-wrapping and cross-thread sharing - fn run(self, input: TaskInput) -> impl Future + Send; + fn run(self, input: TaskInput) -> impl Future + Send; } pub struct TaskInput<'inputs, Input: Send> { diff --git a/src/task/tests.rs b/src/task/tests.rs index 1cdb494..8828fd8 100644 --- a/src/task/tests.rs +++ b/src/task/tests.rs @@ -9,11 +9,10 @@ struct CustomTask { state: i32, } -impl Task for CustomTask { - type Input = (i32,); +impl Task<(i32,)> for CustomTask { type Output = i32; - async fn run(mut self, input: TaskInput<'_, Self::Input>) -> i32 { + async fn run(mut self, input: TaskInput<'_, (i32,)>) -> i32 { self.state += input.next().0; self.state } diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 63f45ad..0000000 --- a/src/types.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Core type definitions for DAG nodes and handles. -//! -//! This module defines the fundamental types used throughout the DAG system, -//! including node identifiers, task handles, and type-state markers. - -use crate::builder::TaskBuilder; -use crate::task::Task; - -/// Opaque node identifier -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub struct NodeId(pub u32); - -/// Opaque, typed token for a node's output. -/// -/// A `TaskHandle` provides compile-time type-safe access to a task's output. -/// You can: -/// 1. Pass it to [`crate::TaskBuilder::depends_on`] to wire up dependencies -/// 2. Use it with [`crate::DagRunner::get`] to retrieve the output after [`crate::DagRunner::run`] -/// -/// Handles are cheap to clone and copy. -/// -/// # Examples -/// -/// ```no_run -/// # use dagx::{task, DagRunner, Task}; -/// # -/// # struct LoadValue { value: i32 } -/// # impl LoadValue { pub fn new(v: i32) -> Self { Self { value: v } } } -/// # #[task] -/// # impl LoadValue { -/// # async fn run(&mut self) -> i32 { self.value } -/// # } -/// # async { -/// let dag = DagRunner::new(); -/// let node = dag.add_task(LoadValue::new(42)); -/// -/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); -/// -/// assert_eq!(dag.get(node).unwrap(), 42); -/// # }; -/// ``` -pub struct TaskHandle { - pub(crate) id: NodeId, - pub(crate) _phantom: std::marker::PhantomData T>, -} - -impl Clone for TaskHandle { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for TaskHandle {} - -/// Type-level marker for empty dependency list. -/// -/// This type appears in the type signature of [`crate::TaskBuilder`](crate::TaskBuilder) before -/// dependencies are specified with [`crate::TaskBuilder::depends_on`]. -/// -/// You don't interact with this type directly; it's part of the type-state -/// pattern that ensures dependencies are wired correctly at compile time. -pub struct Pending; - -// TaskBuilder can be converted to a TaskHandle ONLY if Input is () (unit type) -// This enforces at compile time that tasks with non-unit inputs MUST call .depends_on() -impl<'a, Tk: Task, Deps> From> for TaskHandle { - fn from(node: TaskBuilder<'a, Tk, Deps>) -> Self { - TaskHandle { - id: node.id, - _phantom: std::marker::PhantomData, - } - } -} - -// TaskHandle can be converted from &TaskHandle (for .get() calls) -impl From<&TaskHandle> for TaskHandle { - fn from(handle: &TaskHandle) -> Self { - *handle - } -} - -#[cfg(test)] -mod tests; - -#[cfg(test)] -mod coverage_tests; diff --git a/src/types/tests.rs b/src/types/tests.rs deleted file mode 100644 index a8717ba..0000000 --- a/src/types/tests.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Unit tests for types module - -use crate::types::{NodeId, TaskHandle}; -use std::marker::PhantomData; - -#[test] -#[allow(clippy::clone_on_copy)] -fn test_task_handle_clone() { - let handle: TaskHandle<(i32,)> = TaskHandle { - id: NodeId(42), - _phantom: PhantomData, - }; - - // Test Clone implementation - let cloned = handle.clone(); - assert_eq!(handle.id.0, cloned.id.0); - - let handle2 = handle.clone(); - let handle3 = handle2.clone(); - assert_eq!(handle3.id.0, 42); -} - -#[test] -fn test_task_handle_copy() { - let handle: TaskHandle = TaskHandle { - id: NodeId(99), - _phantom: PhantomData, - }; - - // Test that TaskHandle implements Copy - let copied = handle; - let copied2 = handle; // This wouldn't compile if not Copy - assert_eq!(copied.id.0, 99); - assert_eq!(copied2.id.0, 99); -} - -#[test] -fn test_task_handle_ref_from_conversion() { - // Test From<&TaskHandle> for TaskHandle (line 90-93) - let handle: TaskHandle = TaskHandle { - id: NodeId(123), - _phantom: PhantomData, - }; - - let handle_ref = &handle; - let converted: TaskHandle = handle_ref.into(); - - assert_eq!(converted.id.0, 123); - assert_eq!(handle.id, converted.id); -} From 72a83e29dcf615be8183f9b1d58e02edb30f508a Mon Sep 17 00:00:00 2001 From: TechnoPorg Date: Tue, 17 Feb 2026 20:50:08 +0100 Subject: [PATCH 2/2] test: update tests/examples/benches --- benches/basic/creation.rs | 2 +- benches/basic/scaling.rs | 5 +- benches/comparisons/deep_chain.rs | 10 +- benches/comparisons/diamond_pattern.rs | 10 +- benches/comparisons/etl_pipeline.rs | 7 +- benches/comparisons/large_scale.rs | 5 +- benches/comparisons/linear_pipeline.rs | 7 +- benches/comparisons/wide_fanout.rs | 8 +- benches/patterns/breakdown.rs | 12 +- benches/patterns/diamond.rs | 10 +- benches/patterns/fanout.rs | 24 +- benches/realistic/etl.rs | 33 +- benches/realistic/memory_efficiency.rs | 19 +- benches/realistic/mixed.rs | 19 +- dagx-macros/src/lib.rs | 6 +- dagx-test/src/lib.rs | 6 +- examples/01_basic.rs | 15 +- examples/02_fan_out.rs | 17 +- examples/03_fan_in.rs | 7 +- examples/04_parallel_computation.rs | 7 +- examples/circuit_breaker.rs | 49 +- examples/data_pipeline.rs | 7 +- examples/error_handling.rs | 28 +- scripts/release_check.sh | 4 +- src/error/tests.rs | 4 +- src/lib.rs | 5 +- tests/boundaries/deep_chains.rs | 53 ++- tests/boundaries/type_limits.rs | 56 +-- tests/boundaries/zero_tasks.rs | 21 +- tests/custom_type_test.rs | 33 +- tests/cycle_prevention.rs | 29 +- tests/dependencies/resolution.rs | 7 +- tests/dependencies/tuples.rs | 59 +-- tests/errors/propagation.rs | 120 ++--- tests/errors/recovery.rs | 45 +- tests/execution/mod.rs | 70 +-- .../interleaving.rs => interleaving/mod.rs} | 67 +-- tests/lib_tests.rs | 2 +- tests/parallelism/concurrent.rs | 425 ------------------ tests/parallelism/mod.rs | 1 - tests/parallelism/spawning.rs | 7 +- tests/parallelism/threads.rs | 22 +- tests/parallelism/timing.rs | 16 +- tests/runtimes/mod.rs | 18 +- tests/timing/concurrent_access.rs | 331 -------------- tests/timing/mod.rs | 6 - tests/tracing/with_tracing.rs | 16 +- 47 files changed, 491 insertions(+), 1239 deletions(-) rename tests/{timing/interleaving.rs => interleaving/mod.rs} (78%) delete mode 100644 tests/parallelism/concurrent.rs delete mode 100644 tests/timing/concurrent_access.rs delete mode 100644 tests/timing/mod.rs diff --git a/benches/basic/creation.rs b/benches/basic/creation.rs index 496b5fd..97d4aa1 100644 --- a/benches/basic/creation.rs +++ b/benches/basic/creation.rs @@ -21,7 +21,7 @@ pub fn bench_dag_creation(c: &mut Criterion) { c.bench_function("add_100_tasks", |b| { b.iter(|| { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); for i in 0..100 { black_box(dag.add_task(Value(i))); } diff --git a/benches/basic/scaling.rs b/benches/basic/scaling.rs index 1be3a1c..2ce8e9c 100644 --- a/benches/basic/scaling.rs +++ b/benches/basic/scaling.rs @@ -15,11 +15,12 @@ pub fn bench_dag_scaling(c: &mut Criterion) { group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| { b.iter(|| { rt.block_on(async { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); for i in 0..size { dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) diff --git a/benches/comparisons/deep_chain.rs b/benches/comparisons/deep_chain.rs index 932601d..055fca5 100644 --- a/benches/comparisons/deep_chain.rs +++ b/benches/comparisons/deep_chain.rs @@ -3,7 +3,6 @@ //! Long sequential chain: 100 tasks in sequence use criterion::Criterion; -use dagx::TaskHandle; pub fn bench_deep_chain(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -16,9 +15,9 @@ pub fn bench_deep_chain(c: &mut Criterion) { use dagx::DagRunner; use dagx_test::task_fn; - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let first: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)).into(); + let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); let mut prev = dag .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(first); @@ -29,10 +28,11 @@ pub fn bench_deep_chain(c: &mut Criterion) { .depends_on(prev); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - dag.get(prev).unwrap() + output.get(prev).unwrap() }) }); }); diff --git a/benches/comparisons/diamond_pattern.rs b/benches/comparisons/diamond_pattern.rs index 5aae415..2088e6f 100644 --- a/benches/comparisons/diamond_pattern.rs +++ b/benches/comparisons/diamond_pattern.rs @@ -3,7 +3,6 @@ //! Classic diamond: A → B,C → D use criterion::Criterion; -use dagx::TaskHandle; pub fn bench_diamond_pattern(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -16,9 +15,9 @@ pub fn bench_diamond_pattern(c: &mut Criterion) { use dagx::DagRunner; use dagx_test::task_fn; - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); + let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); let b = dag .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(a); @@ -29,10 +28,11 @@ pub fn bench_diamond_pattern(c: &mut Criterion) { .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y)) .depends_on((&b, &c)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - dag.get(d).unwrap() + output.get(d).unwrap() }) }); }); diff --git a/benches/comparisons/etl_pipeline.rs b/benches/comparisons/etl_pipeline.rs index 1363546..75fe567 100644 --- a/benches/comparisons/etl_pipeline.rs +++ b/benches/comparisons/etl_pipeline.rs @@ -15,7 +15,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { use dagx::DagRunner; use dagx_test::task_fn; - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Extract from 3 sources let source1 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5])); @@ -70,10 +70,11 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { })) .depends_on(validate); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - dag.get(load).unwrap() + output.get(load).unwrap() }) }); }); diff --git a/benches/comparisons/large_scale.rs b/benches/comparisons/large_scale.rs index b23a562..16446ed 100644 --- a/benches/comparisons/large_scale.rs +++ b/benches/comparisons/large_scale.rs @@ -17,12 +17,13 @@ pub fn bench_large_scale(c: &mut Criterion) { use dagx::DagRunner; use dagx_test::task_fn; - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); for i in 0..10_000 { dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) diff --git a/benches/comparisons/linear_pipeline.rs b/benches/comparisons/linear_pipeline.rs index 6e787c0..4a1f0c0 100644 --- a/benches/comparisons/linear_pipeline.rs +++ b/benches/comparisons/linear_pipeline.rs @@ -15,7 +15,7 @@ pub fn bench_linear_pipeline(c: &mut Criterion) { use dagx::DagRunner; use dagx_test::task_fn; - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); let b = dag @@ -31,10 +31,11 @@ pub fn bench_linear_pipeline(c: &mut Criterion) { .add_task(task_fn::(|&x: &i32| x * 3)) .depends_on(d); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - dag.get(e).unwrap() + output.get(e).unwrap() }) }); }); diff --git a/benches/comparisons/wide_fanout.rs b/benches/comparisons/wide_fanout.rs index fa5149c..60881b0 100644 --- a/benches/comparisons/wide_fanout.rs +++ b/benches/comparisons/wide_fanout.rs @@ -3,7 +3,6 @@ //! One source task feeding 100 dependent tasks use criterion::Criterion; -use dagx::TaskHandle; pub fn bench_wide_fanout(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -16,8 +15,8 @@ pub fn bench_wide_fanout(c: &mut Criterion) { use dagx::DagRunner; use dagx_test::task_fn; - let dag = DagRunner::new(); - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); + let mut dag = DagRunner::new(); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); // Create 100 tasks that all depend on source for i in 0..100 { @@ -25,7 +24,8 @@ pub fn bench_wide_fanout(c: &mut Criterion) { .depends_on(source); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) diff --git a/benches/patterns/breakdown.rs b/benches/patterns/breakdown.rs index 196d857..78e8942 100644 --- a/benches/patterns/breakdown.rs +++ b/benches/patterns/breakdown.rs @@ -12,7 +12,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { // a) Adding 10k tasks (construction only) group.bench_function("construction", |b| { b.iter(|| { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); for i in 0..10_000 { dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } @@ -23,11 +23,12 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { group.bench_function("full_execution", |b| { b.iter(|| { rt.block_on(async { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); for i in 0..10_000 { dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) @@ -38,7 +39,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { group.bench_function("execution_only", |b| { b.iter_batched( || { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); for i in 0..10_000 { dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } @@ -46,7 +47,8 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { }, |dag| { rt.block_on(async { - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) diff --git a/benches/patterns/diamond.rs b/benches/patterns/diamond.rs index 75a2263..14f1fda 100644 --- a/benches/patterns/diamond.rs +++ b/benches/patterns/diamond.rs @@ -1,7 +1,7 @@ //! Diamond pattern benchmarks (fan-out then fan-in) use criterion::Criterion; -use dagx::{DagRunner, TaskHandle}; +use dagx::DagRunner; use dagx_test::task_fn; pub fn bench_diamond(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -9,11 +9,10 @@ pub fn bench_diamond(c: &mut Criterion) { c.bench_function("diamond_pattern_50", |b| { b.iter(|| { rt.block_on(async { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); for i in 0..50 { - let source: TaskHandle<_> = - dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into(); + let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); let left = dag .add_task(task_fn::(|&x: &i32| x * 2)) @@ -26,7 +25,8 @@ pub fn bench_diamond(c: &mut Criterion) { .depends_on((&left, &right)); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) diff --git a/benches/patterns/fanout.rs b/benches/patterns/fanout.rs index 0b49035..e411a9e 100644 --- a/benches/patterns/fanout.rs +++ b/benches/patterns/fanout.rs @@ -1,7 +1,7 @@ //! Fan-out pattern benchmarks (1 → N dependencies) use criterion::Criterion; -use dagx::{DagRunner, TaskHandle}; +use dagx::DagRunner; use dagx_test::task_fn; pub fn bench_fanout(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -10,15 +10,16 @@ pub fn bench_fanout(c: &mut Criterion) { c.bench_function("fanout_1_to_100_i32", |b| { b.iter(|| { rt.block_on(async { - let dag = DagRunner::new(); - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); + let mut dag = DagRunner::new(); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); for i in 0..100 { dag.add_task(task_fn::(move |&x: &i32| x + i)) .depends_on(source); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) @@ -29,13 +30,11 @@ pub fn bench_fanout(c: &mut Criterion) { c.bench_function("fanout_1_to_100_vec", |b| { b.iter(|| { rt.block_on(async { - let dag = DagRunner::new(); - let source: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(|_: ()| { - // Create 1000 strings - framework wraps in Arc automatically - (0..1000).map(|i| format!("Item {}", i)).collect::>() - })) - .into(); + let mut dag = DagRunner::new(); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| { + // Create 1000 strings - framework wraps in Arc automatically + (0..1000).map(|i| format!("Item {}", i)).collect::>() + })); for i in 0..100 { dag.add_task(task_fn::, _, _>(move |data: &Vec| { @@ -45,7 +44,8 @@ pub fn bench_fanout(c: &mut Criterion) { .depends_on(source); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) diff --git a/benches/realistic/etl.rs b/benches/realistic/etl.rs index 2be6428..13381c4 100644 --- a/benches/realistic/etl.rs +++ b/benches/realistic/etl.rs @@ -1,7 +1,7 @@ //! ETL (Extract, Transform, Load) pipeline benchmark use criterion::{BenchmarkId, Criterion}; -use dagx::{DagRunner, TaskHandle}; +use dagx::DagRunner; use dagx_test::task_fn; pub fn bench_etl_pipeline(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -17,23 +17,21 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { |b, &count| { b.iter(|| { rt.block_on(async { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Extract: Load JSON-like data (framework wraps in Arc automatically) - let extract: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(move |_: ()| { - (0..count) - .map(|i| { - format!( - "{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}", - i, - i, - i * 10 - ) - }) - .collect::>() - })) - .into(); + let extract = dag.add_task(task_fn::<(), _, _>(move |_: ()| { + (0..count) + .map(|i| { + format!( + "{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}", + i, + i, + i * 10 + ) + }) + .collect::>() + })); // Transform: Three parallel transformations @@ -92,7 +90,8 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { )) .depends_on((&validate, &scores, &names)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) diff --git a/benches/realistic/memory_efficiency.rs b/benches/realistic/memory_efficiency.rs index 61ae2c8..3ea0e35 100644 --- a/benches/realistic/memory_efficiency.rs +++ b/benches/realistic/memory_efficiency.rs @@ -1,7 +1,7 @@ //! Memory efficiency benchmark with large data structures use criterion::{BenchmarkId, Criterion}; -use dagx::{DagRunner, TaskHandle}; +use dagx::DagRunner; use dagx_test::task_fn; use std::hint::black_box; @@ -10,21 +10,19 @@ pub fn bench_memory_efficiency(c: &mut Criterion) { let mut group = c.benchmark_group("memory_efficiency"); // Test with different data sizes (in MB) - for mb_size in [1, 5, 10].iter() { + for mb_size in [1, 10].iter() { group.bench_with_input( BenchmarkId::new("large_data_20_consumers", format!("{}MB", mb_size)), mb_size, |b, &mb| { b.iter(|| { rt.block_on(async { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let source: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(move |_: ()| { - // Create MB of data - framework wraps in Arc automatically - vec![0u8; mb * 1024 * 1024] - })) - .into(); + let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| { + // Create MB of data - framework wraps in Arc automatically + vec![0u8; mb * 1024 * 1024] + })); // 20 consumers share the data via automatic Arc wrapping for i in 0..20 { @@ -41,7 +39,8 @@ pub fn bench_memory_efficiency(c: &mut Criterion) { .depends_on(source); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) diff --git a/benches/realistic/mixed.rs b/benches/realistic/mixed.rs index 5f34d8d..e9d2d08 100644 --- a/benches/realistic/mixed.rs +++ b/benches/realistic/mixed.rs @@ -1,7 +1,7 @@ //! Mixed realistic patterns - fan-out, fan-in, and processing use criterion::Criterion; -use dagx::{DagRunner, TaskHandle}; +use dagx::DagRunner; use dagx_test::task_fn; pub fn bench_mixed_patterns(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -9,16 +9,14 @@ pub fn bench_mixed_patterns(c: &mut Criterion) { c.bench_function("mixed_patterns_realistic", |b| { b.iter(|| { rt.block_on(async { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Stage 1: Load data (framework wraps in Arc automatically) - let data: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(|_: ()| { - (0..1000) - .map(|i| format!("Record-{:04}", i)) - .collect::>() - })) - .into(); + let data = dag.add_task(task_fn::<(), _, _>(|_: ()| { + (0..1000) + .map(|i| format!("Record-{:04}", i)) + .collect::>() + })); // Stage 2: Fan-out for parallel analysis let analysis1 = dag @@ -54,7 +52,8 @@ pub fn bench_mixed_patterns(c: &mut Criterion) { .depends_on(summary); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); }) diff --git a/dagx-macros/src/lib.rs b/dagx-macros/src/lib.rs index 5dc1d23..2abcc6c 100644 --- a/dagx-macros/src/lib.rs +++ b/dagx-macros/src/lib.rs @@ -152,8 +152,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret /// } /// /// // Generated: -/// impl Task for Add { -/// type Input = (i32, i32); +/// impl Task<(i32, i32)> for Add { /// type Output = i32; /// /// async fn run(&mut self, input: Self::Input) -> Self::Output { @@ -183,8 +182,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret /// } /// /// // Generated: -/// impl Task for Counter { -/// type Input = i32; +/// impl Task<(i32)> for Counter { /// type Output = i32; /// /// async fn run(&mut self, input: Self::Input) -> Self::Output { diff --git a/dagx-test/src/lib.rs b/dagx-test/src/lib.rs index 889973b..ab72a8a 100644 --- a/dagx-test/src/lib.rs +++ b/dagx-test/src/lib.rs @@ -5,6 +5,8 @@ #![cfg(not(tarpaulin_include))] +use std::marker::PhantomData; + use crate::extract::ExtractInput; use dagx::{Task, TaskInput}; @@ -18,7 +20,7 @@ where O: Send, { f: F, - _phantom: std::marker::PhantomData O>, + _phantom: PhantomData O>, } impl Task for TaskFn @@ -45,6 +47,6 @@ where { TaskFn { f, - _phantom: std::marker::PhantomData, + _phantom: PhantomData, } } diff --git a/examples/01_basic.rs b/examples/01_basic.rs index 5bd065c..be8ac2e 100644 --- a/examples/01_basic.rs +++ b/examples/01_basic.rs @@ -77,13 +77,13 @@ impl Add { #[tokio::main] async fn main() { // Step 3: Create a new DAG - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Step 4: Add source tasks (tasks with no dependencies) // // add_task() returns a TaskHandle where T is the task's output type - let x = dag.add_task(Value::new(2)).into(); // TaskHandle - let y = dag.add_task(Value::new(3)).into(); // TaskHandle + let x = dag.add_task(Value::new(2)); // TaskHandle + let y = dag.add_task(Value::new(3)); // TaskHandle // Step 5: Add a task with dependencies // @@ -105,7 +105,8 @@ async fn main() { // 1. Tasks with no dependencies run first (x and y in parallel) // 2. Once x and y complete, sum runs (it depends on both) println!("Running DAG...\n"); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); @@ -113,9 +114,9 @@ async fn main() { // // Use .get() with a TaskHandle to retrieve the output. // Results are not cloned, so you can only retrieve them once. - println!("\nResult: {}", dag.get(sum).unwrap()); + println!("\nResult: {}", output.get(sum).unwrap()); // Note: You can also retrieve intermediate results - assert_eq!(dag.get(x).unwrap(), 2); - assert_eq!(dag.get(y).unwrap(), 3); + assert_eq!(output.get(x).unwrap(), 2); + assert_eq!(output.get(y).unwrap(), 3); } diff --git a/examples/02_fan_out.rs b/examples/02_fan_out.rs index f387a79..25513cb 100644 --- a/examples/02_fan_out.rs +++ b/examples/02_fan_out.rs @@ -55,7 +55,7 @@ //! Squared: 100 //! ``` -use dagx::{task, DagRunner, TaskHandle}; +use dagx::{task, DagRunner}; // Source task with a value field struct Source { @@ -127,10 +127,10 @@ impl Power { #[tokio::main] async fn main() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Single source task - constructed with ::new() - let source: TaskHandle<_> = dag.add_task(Source::new(10)).into(); + let source = dag.add_task(Source::new(10)); // Multiple downstream tasks consuming the same value // Each constructed differently to show flexibility @@ -140,14 +140,15 @@ async fn main() { // Run the DAG println!("Running fan-out DAG...\n"); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); // Print all results println!("\nResults:"); - println!(" Source: {}", dag.get(source).unwrap()); - println!(" Plus One: {}", dag.get(plus_one).unwrap()); - println!(" Times Two: {}", dag.get(times_two).unwrap()); - println!(" Squared: {}", dag.get(squared).unwrap()); + println!(" Source: {}", output.get(source).unwrap()); + println!(" Plus One: {}", output.get(plus_one).unwrap()); + println!(" Times Two: {}", output.get(times_two).unwrap()); + println!(" Squared: {}", output.get(squared).unwrap()); } diff --git a/examples/03_fan_in.rs b/examples/03_fan_in.rs index 48596c1..05db4f6 100644 --- a/examples/03_fan_in.rs +++ b/examples/03_fan_in.rs @@ -118,7 +118,7 @@ impl BuildProfile { #[tokio::main] async fn main() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Multiple independent source tasks let name = dag.add_task(FetchName); @@ -133,10 +133,11 @@ async fn main() { // Run the DAG println!("Running fan-in DAG...\n"); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); // Print result - println!("\n{}", dag.get(profile).unwrap()); + println!("\n{}", output.get(profile).unwrap()); } diff --git a/examples/04_parallel_computation.rs b/examples/04_parallel_computation.rs index 1c57389..4036740 100644 --- a/examples/04_parallel_computation.rs +++ b/examples/04_parallel_computation.rs @@ -115,7 +115,7 @@ async fn main() -> DagResult<()> { println!("Running PARALLEL computation (4 workers on separate threads)..."); let (parallel_time, parallel_result) = { let start = Instant::now(); - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Create 4 workers - dagx will run them in parallel let sum1 = dag.add_task(ComputeSum::new(1, 251, WORK_DELAY)); @@ -125,10 +125,11 @@ async fn main() -> DagResult<()> { let total = dag.add_task(Aggregate).depends_on((sum1, sum2, sum3, sum4)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - let result = dag.get(total)?; + let result = output.get(total)?; let elapsed = start.elapsed(); println!( diff --git a/examples/circuit_breaker.rs b/examples/circuit_breaker.rs index c661448..1c65b1e 100644 --- a/examples/circuit_breaker.rs +++ b/examples/circuit_breaker.rs @@ -381,7 +381,7 @@ async fn main() { // Create shared circuit breaker: 3 failures trigger open, 3 second recovery timeout let circuit = Arc::new(CircuitBreaker::new(3, Duration::from_secs(3))); - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Request 1: Success (circuit closed) let call1 = dag.add_task(ProtectedServiceCall::new( @@ -390,56 +390,60 @@ async fn main() { false, // success )); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - println!("Result 1: {:?}\n", dag.get(call1).unwrap()); + println!("Result 1: {:?}\n", output.get(call1).unwrap()); // Request 2: Failure (circuit closed, count = 1) - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let call2 = dag.add_task(ProtectedServiceCall::new( "ServiceCall-2", circuit.clone(), true, // fail )); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - println!("Result 2: {:?}\n", dag.get(call2).unwrap()); + println!("Result 2: {:?}\n", output.get(call2).unwrap()); // Request 3: Failure (circuit closed, count = 2) - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let call3 = dag.add_task(ProtectedServiceCall::new( "ServiceCall-3", circuit.clone(), true, // fail )); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - println!("Result 3: {:?}\n", dag.get(call3).unwrap()); + println!("Result 3: {:?}\n", output.get(call3).unwrap()); // Request 4: Failure (circuit closed, count = 3, opens!) - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let call4 = dag.add_task(ProtectedServiceCall::new( "ServiceCall-4", circuit.clone(), true, // fail )); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - println!("Result 4: {:?}\n", dag.get(call4).unwrap()); + println!("Result 4: {:?}\n", output.get(call4).unwrap()); // Request 5: Circuit is now open, fails fast with fallback - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let call5 = dag.add_task(ProtectedServiceCall::new( "ServiceCall-5", circuit.clone(), @@ -450,11 +454,12 @@ async fn main() { .add_task(FallbackService::new("ServiceCall-5")) .depends_on(call5); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - println!("Result 5: {:?}\n", dag.get(fallback5).unwrap()); + println!("Result 5: {:?}\n", output.get(fallback5).unwrap()); // Example 2: Circuit recovery after timeout println!("Example 2: Circuit recovery after timeout\n"); @@ -462,32 +467,34 @@ async fn main() { sleep(Duration::from_secs(3)).await; // Request 6: Circuit should be half-open, test succeeds, closes circuit - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let call6 = dag.add_task(ProtectedServiceCall::new( "ServiceCall-6", circuit.clone(), false, // success )); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - println!("Result 6: {:?}\n", dag.get(call6).unwrap()); + println!("Result 6: {:?}\n", output.get(call6).unwrap()); // Request 7: Circuit is now closed again, normal operation - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let call7 = dag.add_task(ProtectedServiceCall::new( "ServiceCall-7", circuit.clone(), false, // success )); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - println!("Result 7: {:?}\n", dag.get(call7).unwrap()); + println!("Result 7: {:?}\n", output.get(call7).unwrap()); } println!("=== Circuit Breaker Example Complete ==="); diff --git a/examples/data_pipeline.rs b/examples/data_pipeline.rs index 67cc0ce..e012fe1 100644 --- a/examples/data_pipeline.rs +++ b/examples/data_pipeline.rs @@ -178,7 +178,7 @@ impl GenerateReport { #[tokio::main] async fn main() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); println!("Building data pipeline DAG...\n"); @@ -220,10 +220,11 @@ async fn main() { // Execute the pipeline println!("Executing pipeline...\n"); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); // Print final report - println!("{}", dag.get(report).unwrap()); + println!("{}", output.get(report).unwrap()); } diff --git a/examples/error_handling.rs b/examples/error_handling.rs index c4dde6b..67cea84 100644 --- a/examples/error_handling.rs +++ b/examples/error_handling.rs @@ -209,7 +209,7 @@ async fn main() { // Example 1: Successful path println!("Example 1: Valid input"); { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let input = dag.add_task(StringSource("42")); @@ -222,11 +222,12 @@ async fn main() { let processed = dag.add_task(ProcessData).depends_on(validated); let logged = dag.add_task(LogError).depends_on(processed); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - match dag.get(logged).unwrap().as_ref() { + match output.get(logged).unwrap().as_ref() { Ok(result) => println!("Final result: {}\n", result), Err(e) => eprintln!("Final error: {}\n", e), } @@ -235,7 +236,7 @@ async fn main() { // Example 2: Parse error println!("Example 2: Invalid input (parse error)"); { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let input = dag.add_task(StringSource("not a number")); @@ -248,11 +249,12 @@ async fn main() { let processed = dag.add_task(ProcessData).depends_on(validated); let logged = dag.add_task(LogError).depends_on(processed); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - match dag.get(logged).unwrap().as_ref() { + match output.get(logged).unwrap().as_ref() { Ok(result) => println!("Final result: {}\n", result), Err(e) => eprintln!("Final error: {}\n", e), } @@ -261,7 +263,7 @@ async fn main() { // Example 3: Validation error println!("Example 3: Out of range"); { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let input = dag.add_task(StringSource("150")); @@ -274,11 +276,12 @@ async fn main() { let processed = dag.add_task(ProcessData).depends_on(validated); let logged = dag.add_task(LogError).depends_on(processed); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - match dag.get(logged).unwrap().as_ref() { + match output.get(logged).unwrap().as_ref() { Ok(result) => println!("Final result: {}\n", result), Err(e) => eprintln!("Final error: {}\n", e), } @@ -287,7 +290,7 @@ async fn main() { // Example 4: Error recovery with fallback println!("Example 4: Error recovery with fallback"); { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let input = dag.add_task(StringSource("invalid")); @@ -297,11 +300,12 @@ async fn main() { .add_task(WithFallback { fallback: 0 }) .depends_on(parsed); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - let result = dag.get(with_fallback).unwrap(); + let result = output.get(with_fallback).unwrap(); println!("Recovered with fallback: {}\n", result); } diff --git a/scripts/release_check.sh b/scripts/release_check.sh index bc5725f..e90afbc 100755 --- a/scripts/release_check.sh +++ b/scripts/release_check.sh @@ -157,14 +157,14 @@ section "TESTING" echo "→ Running library unit tests..." cargo test --lib || check_fail "Library unit tests failed" -check_pass "Library unit tests pass (70 tests)" +check_pass "Library unit tests pass" echo "" echo "→ Running integration tests with all feature combinations..." cargo test --test '*' --no-default-features || check_fail "Integration tests failed (no features)" cargo test --test '*' --all-features || check_fail "Integration tests failed (all features)" cargo test --test '*' || check_fail "Integration tests failed (default features)" -check_pass "Integration tests pass across all feature combinations (280+ tests total)" +check_pass "Integration tests pass across all feature combinations" echo "" echo "→ Running doc tests with all features..." diff --git a/src/error/tests.rs b/src/error/tests.rs index 4e3d87a..3e9d680 100644 --- a/src/error/tests.rs +++ b/src/error/tests.rs @@ -51,9 +51,7 @@ fn test_dag_error_display_result_not_found() { assert!(display.contains("Result not found")); assert!(display.contains("task #7")); - assert!(display.contains( - "Calllet mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })" - )); + assert!(display.contains("Call dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })")); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 06294d4..d7b9a03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -541,11 +541,12 @@ //! # let mut dag = DagRunner::new(); //! # let node = dag.add_task(Value(42)); //! // Simple approach with .unwrap() -//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); +//! let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); //! let result = output.get(node).unwrap(); +//! # let mut dag = DagRunner::new(); //! //! // Or handle errors explicitly -//! matchlet mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await { +//! match dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await { //! Ok(_) => println!("DAG executed successfully"), //! Err(e) => eprintln!("DAG execution failed: {}", e), //! } diff --git a/tests/boundaries/deep_chains.rs b/tests/boundaries/deep_chains.rs index a6bb29e..c38a008 100644 --- a/tests/boundaries/deep_chains.rs +++ b/tests/boundaries/deep_chains.rs @@ -9,13 +9,13 @@ use std::sync::Arc; #[tokio::test] async fn test_multiple_parallel_deep_chains() -> DagResult<()> { // Test 10 parallel chains of depth 500 each - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let mut chain_ends = Vec::new(); for chain_id in 0..8 { let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| chain_id * 1000)); - let mut current: dagx::TaskHandle = first.into(); + let mut current: dagx::TaskHandle = first; for _ in 0..500 { current = dag @@ -51,7 +51,8 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> { &chain_ends[7], )); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // Each chain: start + 500 @@ -59,7 +60,7 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> { // Chain 1: 1000 + 500 = 1500 // ... // Sum = 500*8 + (0+1000+2000+...+7000) = 4000 + 28000 = 32000 - assert_eq!(dag.get(final_task)?, 32000); + assert_eq!(output.get(final_task)?, 32000); Ok(()) } @@ -67,7 +68,7 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> { #[tokio::test] async fn test_deep_chain_execution_order() -> DagResult<()> { // Verify tasks in a deep chain execute in correct order - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let order = Arc::new(AtomicUsize::new(0)); let first = dag.add_task(task_fn::<(), _, _>({ @@ -78,7 +79,7 @@ async fn test_deep_chain_execution_order() -> DagResult<()> { 0 } })); - let mut current: dagx::TaskHandle = first.into(); + let mut current: dagx::TaskHandle = first; for expected in 1..100 { current = dag @@ -93,11 +94,12 @@ async fn test_deep_chain_execution_order() -> DagResult<()> { .depends_on(current); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; assert_eq!(order.load(Ordering::SeqCst), 100); - assert_eq!(dag.get(current)?, 99); + assert_eq!(output.get(current)?, 99); Ok(()) } @@ -105,12 +107,12 @@ async fn test_deep_chain_execution_order() -> DagResult<()> { #[tokio::test] async fn test_deep_binary_tree() -> DagResult<()> { // Test a deep binary tree structure (depth 10 = 1023 nodes) - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle { + fn build_tree(dag: &mut DagRunner, depth: usize, value: i32) -> dagx::TaskHandle { if depth == 0 { let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value)); - task.into() // Convert TaskBuilder to TaskHandle + task // Convert TaskBuilder to TaskHandle } else { let left = build_tree(dag, depth - 1, value * 2); let right = build_tree(dag, depth - 1, value * 2 + 1); @@ -122,14 +124,15 @@ async fn test_deep_binary_tree() -> DagResult<()> { } } - let root = build_tree(&dag, 10, 1); + let root = build_tree(&mut dag, 10, 1); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // 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 - let result = dag.get(root)?; + let result = output.get(root)?; assert!(result > 0); Ok(()) @@ -138,13 +141,13 @@ async fn test_deep_binary_tree() -> DagResult<()> { #[tokio::test] async fn test_fibonacci_chain() -> DagResult<()> { // Test a Fibonacci-like dependency pattern - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let f0_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64)); let f1_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 1i64)); - let mut prev2: dagx::TaskHandle = f0_builder.into(); - let mut prev1: dagx::TaskHandle = f1_builder.into(); + let mut prev2: dagx::TaskHandle = f0_builder; + let mut prev1: dagx::TaskHandle = f1_builder; for _ in 2..50 { let next = dag @@ -155,11 +158,12 @@ async fn test_fibonacci_chain() -> DagResult<()> { prev1 = next; } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // 50th Fibonacci number (0-indexed) = 7778742049 - assert_eq!(dag.get(prev1)?, 7778742049); + assert_eq!(output.get(prev1)?, 7778742049); Ok(()) } @@ -167,13 +171,13 @@ async fn test_fibonacci_chain() -> DagResult<()> { #[tokio::test] async fn test_zigzag_dependencies() -> DagResult<()> { // Test a zigzag pattern: two chains that cross-reference each other - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let a_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); let b_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 2)); - let mut chain_a: dagx::TaskHandle = a_first.into(); - let mut chain_b: dagx::TaskHandle = b_first.into(); + let mut chain_a: dagx::TaskHandle = a_first; + let mut chain_b: dagx::TaskHandle = b_first; for i in 0..100 { if i % 2 == 0 { @@ -194,10 +198,11 @@ async fn test_zigzag_dependencies() -> DagResult<()> { .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((&chain_a, &chain_b)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - let result = dag.get(final_task)?; + let result = output.get(final_task)?; assert!(result > 100); Ok(()) diff --git a/tests/boundaries/type_limits.rs b/tests/boundaries/type_limits.rs index 79a3f21..11d1e30 100644 --- a/tests/boundaries/type_limits.rs +++ b/tests/boundaries/type_limits.rs @@ -1,6 +1,6 @@ //! Tests for type system edge cases and limits -use dagx::{DagResult, DagRunner, TaskHandle}; +use dagx::{DagResult, DagRunner}; use dagx_test::task_fn; use std::marker::PhantomData; @@ -16,13 +16,13 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> { _phantom: PhantomData, } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Task producing ZST let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Empty)); // Task that also produces ZST (independent since we can't extract custom types) - let t2: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); + let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); // Task with PhantomData let _t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| PhantomWrapper:: { @@ -37,12 +37,13 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> { })) .depends_on(t2); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(t1)?, Empty); - assert_eq!(dag.get(t2)?, 42); - assert_eq!(dag.get(t4)?, "success"); + assert_eq!(output.get(t1)?, Empty); + assert_eq!(output.get(t2)?, 42); + assert_eq!(output.get(t4)?, "success"); Ok(()) } @@ -56,7 +57,7 @@ async fn test_large_value_types() -> DagResult<()> { more_data: Vec, } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| { let large = LargeStruct { @@ -73,37 +74,11 @@ async fn test_large_value_types() -> DagResult<()> { })) .depends_on(t1); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(t2)?, 1042); - - Ok(()) -} - -#[tokio::test] -async fn test_unit_type_chains() -> DagResult<()> { - // Test chains of unit type operations - let dag = DagRunner::new(); - - let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| {})).into(); - let t2 = dag - .add_task(task_fn::<(), _, _>(|_: ()| {})) - .depends_on((t1,)); - let t3 = dag - .add_task(task_fn::<(), _, _>(|_: ()| {})) - .depends_on((t2,)); - let t4 = dag - .add_task(task_fn::<(), _, _>(|_: ()| "done")) - .depends_on((t3,)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - dag.get(t1)?; // Verify no error - dag.get(t2)?; - dag.get(t3)?; - assert_eq!(dag.get(t4)?, "done"); + assert_eq!(output.get(t2)?, 1042); Ok(()) } @@ -113,7 +88,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> { // Test types that wrap references use std::sync::{Arc, Mutex}; - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| { let arc = Arc::new(Mutex::new(100)); @@ -126,10 +101,11 @@ async fn test_reference_wrapper_types() -> DagResult<()> { .add_task(task_fn::(|&val: &i32| val * 2)) .depends_on(t1); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(t2)?, 200); + assert_eq!(output.get(t2)?, 200); Ok(()) } diff --git a/tests/boundaries/zero_tasks.rs b/tests/boundaries/zero_tasks.rs index 3548a63..b9c2100 100644 --- a/tests/boundaries/zero_tasks.rs +++ b/tests/boundaries/zero_tasks.rs @@ -8,17 +8,18 @@ use tokio::task::yield_now; #[tokio::test] async fn test_dag_with_only_source_tasks() -> DagResult<()> { // Test a DAG with multiple source tasks but no dependencies - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let tasks: Vec<_> = (0..100) .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task)?, i); + assert_eq!(output.get(task)?, i); } Ok(()) @@ -38,20 +39,21 @@ async fn test_single_self_contained_stateful_task() -> DagResult<()> { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let task = dag.add_task(Counter { value: 21 }); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(task)?, 42); + assert_eq!(output.get(task)?, 42); Ok(()) } #[tokio::test] async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> { // Test a task that does the absolute minimum async work - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); struct YieldTask; @@ -65,9 +67,10 @@ async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> { let task = dag.add_task(YieldTask); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(task)?, 1337); + assert_eq!(output.get(task)?, 1337); Ok(()) } diff --git a/tests/custom_type_test.rs b/tests/custom_type_test.rs index f7d5374..607c4cc 100644 --- a/tests/custom_type_test.rs +++ b/tests/custom_type_test.rs @@ -1,5 +1,5 @@ // Test that custom types work with the #[task] macro -use dagx::{task, DagRunner, TaskHandle}; +use dagx::{task, DagRunner}; #[derive(Clone, Debug, PartialEq)] struct Person { @@ -51,33 +51,35 @@ impl PersonToCompany { #[tokio::test] async fn test_custom_type_single_dependency() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let fetch = dag.add_task(FetchPerson); let process = dag.add_task(ProcessPerson).depends_on(fetch); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - let result = dag.get(process).unwrap(); + let result = output.get(process).unwrap(); assert_eq!(result, "Alice is 30 years old"); } #[tokio::test] async fn test_custom_type_passthrough() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let fetch: TaskHandle<_> = dag.add_task(FetchPerson).into(); + let fetch = dag.add_task(FetchPerson); let transform = dag.add_task(PersonToCompany).depends_on(fetch); let process = dag.add_task(ProcessPerson).depends_on(fetch); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - let person_result = dag.get(process).unwrap(); - let company_result = dag.get(transform).unwrap(); + let person_result = output.get(process).unwrap(); + let company_result = output.get(transform).unwrap(); assert_eq!(person_result.as_str(), "Alice is 30 years old"); assert_eq!(company_result.name, "Alice's Company"); @@ -86,20 +88,21 @@ async fn test_custom_type_passthrough() { #[tokio::test] async fn test_custom_type_fan_out() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let fetch: TaskHandle<_> = dag.add_task(FetchPerson).into(); + let fetch = dag.add_task(FetchPerson); let process1 = 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); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - let result1 = dag.get(process1).unwrap(); - let result2 = dag.get(process2).unwrap(); - let result3 = dag.get(process3).unwrap(); + let result1 = output.get(process1).unwrap(); + let result2 = output.get(process2).unwrap(); + let result3 = output.get(process3).unwrap(); assert_eq!(result1, "Alice is 30 years old"); assert_eq!(result2, "Alice is 30 years old"); diff --git a/tests/cycle_prevention.rs b/tests/cycle_prevention.rs index 7365115..1c6e6ff 100644 --- a/tests/cycle_prevention.rs +++ b/tests/cycle_prevention.rs @@ -3,7 +3,7 @@ //! These tests show that even if you try to create cycles, the Rust compiler //! prevents it through the type-state pattern. -use dagx::{task, DagRunner, TaskHandle}; +use dagx::{task, DagRunner}; /// Demonstrates that TaskBuilder is consumed by depends_on, /// preventing you from wiring the same task multiple times @@ -25,7 +25,7 @@ fn test_builder_consumed_by_depends_on() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let source = dag.add_task(Source); let consumer_builder = dag.add_task(Consumer); @@ -49,11 +49,11 @@ fn test_handle_is_immutable_reference() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let builder = dag.add_task(NoInput); // Convert to handle (only works because Input = ()) - let handle: TaskHandle<_> = builder.into(); + let handle = builder; // TaskHandle is Copy/Clone - just an ID wrapper let _copy1 = handle; @@ -85,7 +85,7 @@ fn test_ordering_prevents_cycles() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let a_builder = dag.add_task(TaskA); let b_builder = dag.add_task(TaskB); @@ -107,7 +107,7 @@ fn test_ordering_prevents_cycles() { // So we can only create DAGs, never cycles! // Let's create a valid DAG instead: A→B (B depends on A) - let a_handle: TaskHandle<_> = a_builder.into(); // A has no deps (unit input) + let a_handle = a_builder; // A has no deps (unit input) let _b_handle = b_builder.depends_on(a_handle); // Now we could try: a_handle.depends_on(b_handle)? // But TaskHandle has no such method! @@ -132,13 +132,13 @@ fn test_unit_tasks_cannot_create_cycles() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let task1 = dag.add_task(Task1); let task2 = dag.add_task(Task2); // Both have unit input, so we can convert to handles - let _handle1: TaskHandle<_> = task1.into(); - let _handle2: TaskHandle<_> = task2.into(); + let _handle1 = task1; + let _handle2 = task2; // Now we have two handles, but... // TaskHandle has no methods to add dependencies! @@ -176,7 +176,7 @@ fn test_three_way_cycle_impossible() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let _a_builder = dag.add_task(A); let _b_builder = dag.add_task(B); let _c_builder = dag.add_task(C); @@ -205,7 +205,7 @@ fn test_self_loop_prevented() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let _builder = dag.add_task(SelfReferential); // To create A→A (self-loop): @@ -256,7 +256,7 @@ async fn test_type_safety_prevents_invalid_graphs() { } } - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Build a valid DAG: two sources, two transforms, one aggregator let s1 = dag.add_task(Source); @@ -270,11 +270,12 @@ async fn test_type_safety_prevents_invalid_graphs() { let result = dag.add_task(Aggregate).depends_on((&t1, &t2)); // Execute and verify - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - assert_eq!(dag.get(result).unwrap(), 40); // (10*2) + (10*2) + assert_eq!(output.get(result).unwrap(), 40); // (10*2) + (10*2) // Now if we wanted to add a cycle, we'd need to: // - Make one of the Source tasks depend on the Aggregate result diff --git a/tests/dependencies/resolution.rs b/tests/dependencies/resolution.rs index 31ac727..4aecc5c 100644 --- a/tests/dependencies/resolution.rs +++ b/tests/dependencies/resolution.rs @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex}; #[tokio::test] async fn test_dependency_resolution_order() -> DagResult<()> { // Test that dependencies are resolved in the correct order - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let execution_log = Arc::new(Mutex::new(Vec::new())); @@ -79,10 +79,11 @@ async fn test_dependency_resolution_order() -> DagResult<()> { .depends_on(d) }; - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(e)?, 5); + assert_eq!(output.get(e)?, 5); // Verify execution order let log = execution_log.lock().unwrap(); diff --git a/tests/dependencies/tuples.rs b/tests/dependencies/tuples.rs index aafbd3a..51b6385 100644 --- a/tests/dependencies/tuples.rs +++ b/tests/dependencies/tuples.rs @@ -5,7 +5,7 @@ use dagx_test::task_fn; #[tokio::test] async fn test_two_dependencies() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 20)); @@ -13,16 +13,17 @@ async fn test_two_dependencies() -> DagResult<()> { .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y)) .depends_on((a, b)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(sum)?, 30); + assert_eq!(output.get(sum)?, 30); Ok(()) } #[tokio::test] async fn test_three_dependencies() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 2)); @@ -33,19 +34,20 @@ async fn test_three_dependencies() -> DagResult<()> { )) .depends_on((a, b, c)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(sum)?, 6); + assert_eq!(output.get(sum)?, 6); Ok(()) } #[tokio::test] async fn test_four_dependencies() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let deps: Vec<_> = (1..=4) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); let sum = dag @@ -54,19 +56,20 @@ async fn test_four_dependencies() -> DagResult<()> { )) .depends_on((&deps[0], &deps[1], &deps[2], &deps[3])); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(sum)?, 10); // 1+2+3+4 + assert_eq!(output.get(sum)?, 10); // 1+2+3+4 Ok(()) } #[tokio::test] async fn test_five_dependencies() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let deps: Vec<_> = (1..=5) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); let sum = dag @@ -75,19 +78,20 @@ async fn test_five_dependencies() -> DagResult<()> { )) .depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4])); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(sum)?, 15); // 1+2+3+4+5 + assert_eq!(output.get(sum)?, 15); // 1+2+3+4+5 Ok(()) } #[tokio::test] async fn test_six_dependencies() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let deps: Vec<_> = (1..=6) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); let sum = dag @@ -96,19 +100,20 @@ async fn test_six_dependencies() -> DagResult<()> { )) .depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5])); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(sum)?, 21); // 1+2+3+4+5+6 + assert_eq!(output.get(sum)?, 21); // 1+2+3+4+5+6 Ok(()) } #[tokio::test] async fn test_seven_dependencies() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let deps: Vec<_> = (1..=7) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); let sum = dag @@ -121,19 +126,20 @@ async fn test_seven_dependencies() -> DagResult<()> { &deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6], )); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(sum)?, 28); // 1+2+3+4+5+6+7 + assert_eq!(output.get(sum)?, 28); // 1+2+3+4+5+6+7 Ok(()) } #[tokio::test] async fn test_eight_dependencies() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let deps: Vec<_> = (1..=8) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); let sum = dag @@ -146,9 +152,10 @@ async fn test_eight_dependencies() -> DagResult<()> { &deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6], &deps[7], )); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(sum)?, 36); // Sum of 1..=8 + assert_eq!(output.get(sum)?, 36); // Sum of 1..=8 Ok(()) } diff --git a/tests/errors/propagation.rs b/tests/errors/propagation.rs index 44501c9..abd0459 100644 --- a/tests/errors/propagation.rs +++ b/tests/errors/propagation.rs @@ -1,17 +1,17 @@ //! Tests for error propagation through the DAG -use dagx::{task, DagRunner, TaskHandle}; +use dagx::{task, DagError, DagRunner}; use dagx_test::task_fn; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::time::sleep; #[tokio::test] async fn test_linear_error_propagation() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into(); + let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); let t2 = dag .add_task(task_fn::(|&x: &i32| -> i32 { panic!("Error at t2 with input {}", x); @@ -20,28 +20,19 @@ async fn test_linear_error_propagation() { let t3 = dag .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t2); - let t4 = dag - .add_task(task_fn::(|&x: &i32| x * 2)) + dag.add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(t3); - let _ = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - // t1 succeeds - assert_eq!(dag.get(t1).unwrap(), 1); - - // t2 fails - assert!(dag.get(t2).is_err()); - - // t3 and t4 fail due to dependency - assert!(dag.get(t3).is_err()); - assert!(dag.get(t4).is_err()); + assert!(matches!( + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await, + Err(DagError::TaskPanicked { .. }) + )); } #[tokio::test] async fn test_diamond_error_propagation() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // source // / \ @@ -49,7 +40,7 @@ async fn test_diamond_error_propagation() { // \ / // sink - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into(); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); let left = dag .add_task(task_fn::(|&x: &i32| x / 2)) @@ -61,61 +52,45 @@ async fn test_diamond_error_propagation() { })) .depends_on(source); - let sink = dag - .add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) + dag.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) .depends_on((&left, &right)); - let _ = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - assert_eq!(dag.get(source).unwrap(), 100); - assert_eq!(dag.get(left).unwrap(), 50); - assert!(dag.get(right).is_err()); - assert!(dag.get(sink).is_err()); // Fails due to right dependency + assert!(matches!( + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await, + Err(DagError::TaskPanicked { .. }) + )); } #[tokio::test] async fn test_error_in_wide_fanout() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let source: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(|_: ()| -> i32 { - panic!("Source fails"); - })) - .into(); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| -> i32 { + panic!("Source fails"); + })); // Create wide fanout from failing source - let dependents: Vec<_> = (0..20) - .map(|i| { - dag.add_task(task_fn::(move |&x: &i32| x + i)) - .depends_on(source) - }) - .collect(); - - let _ = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - // Source fails - assert!(dag.get(source).is_err()); - - // All dependents fail - for dependent in &dependents { - assert!(dag.get(dependent).is_err()); + for i in 0..20 { + dag.add_task(task_fn::(move |&x: &i32| x + i)) + .depends_on(source); } + + assert!(matches!( + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await, + Err(DagError::TaskPanicked { .. }) + )); } #[tokio::test] async fn test_selective_error_propagation() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Source that produces a Result - let source: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(|_: ()| { - Result::::Err("Source error".to_string()) - })) - .into(); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| { + Result::::Err("Source error".to_string()) + })); // Handler that processes the Result let handler = dag @@ -140,34 +115,35 @@ async fn test_selective_error_propagation() { })) .depends_on(handler); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); // Source returns an Err variant (not a panic) - assert_eq!(dag.get(source).unwrap(), Err("Source error".to_string())); + assert_eq!(output.get(source).unwrap(), Err("Source error".to_string())); // Handler processes it - assert_eq!(dag.get(handler).unwrap(), -1); + assert_eq!(output.get(handler).unwrap(), -1); // Final task handles the error case - assert_eq!(dag.get(final_task).unwrap(), "Handled error case"); + assert_eq!(output.get(final_task).unwrap(), "Handled error case"); } #[tokio::test] async fn test_error_propagation_timing() { - let dag = DagRunner::new(); - let propagation_order = Arc::new(parking_lot::Mutex::new(Vec::new())); + let mut dag = DagRunner::new(); + let propagation_order = Arc::new(Mutex::new(Vec::new())); struct T1Task { - order: Arc>>, + order: Arc>>, } #[task] impl T1Task { async fn run(&self) -> i32 { sleep(Duration::from_millis(10)).await; - self.order.lock().push("t1_complete"); + self.order.lock().unwrap().push("t1_complete"); 1 } } @@ -177,13 +153,13 @@ async fn test_error_propagation_timing() { }); struct T2Task { - order: Arc>>, + order: Arc>>, } #[task] impl T2Task { async fn run(&self, x: &i32) -> i32 { - self.order.lock().push("t2_start"); + self.order.lock().unwrap().push("t2_start"); sleep(Duration::from_millis(10)).await; panic!("t2 fails with {x}"); } @@ -200,7 +176,7 @@ async fn test_error_propagation_timing() { let order = propagation_order.clone(); move |x: &i32| { let order = order.clone(); - order.lock().push("t3_should_not_run"); + order.lock().unwrap().push("t3_should_not_run"); x + 1 } })) @@ -210,7 +186,7 @@ async fn test_error_propagation_timing() { .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await; - let order = propagation_order.lock().clone(); + let order = propagation_order.lock().unwrap().clone(); // t1 completes, t2 starts and fails, t3 never runs assert!(order.contains(&"t1_complete")); diff --git a/tests/errors/recovery.rs b/tests/errors/recovery.rs index e1dc5b9..ef96102 100644 --- a/tests/errors/recovery.rs +++ b/tests/errors/recovery.rs @@ -4,11 +4,11 @@ use dagx::{DagResult, DagRunner}; use dagx_test::task_fn; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; #[tokio::test] async fn test_retry_pattern_simulation() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let attempt_counter = Arc::new(AtomicUsize::new(0)); // Task that would fail on first attempts (simulated) @@ -29,18 +29,19 @@ async fn test_retry_pattern_simulation() -> DagResult<()> { } })); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // Task completes with error on first "attempt" - assert_eq!(dag.get(task)?, Err("Simulated failure")); + assert_eq!(output.get(task)?, Err("Simulated failure")); Ok(()) } #[tokio::test] async fn test_circuit_breaker_pattern() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let failure_count = Arc::new(AtomicUsize::new(0)); let circuit_open = Arc::new(AtomicBool::new(false)); @@ -71,12 +72,13 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> { }) .collect(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // First few tasks fail and open circuit for task in tasks.into_iter().take(4) { - assert!(dag.get(task)?.is_err()); + assert!(output.get(task)?.is_err()); } Ok(()) @@ -84,8 +86,8 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> { #[tokio::test] async fn test_error_accumulation_pattern() -> DagResult<()> { - let dag = DagRunner::new(); - let errors = Arc::new(parking_lot::Mutex::new(Vec::new())); + let mut dag = DagRunner::new(); + let errors = Arc::new(Mutex::new(Vec::new())); // Tasks that may produce errors let mut tasks: Vec<_> = (0..10) @@ -94,7 +96,10 @@ async fn test_error_accumulation_pattern() -> DagResult<()> { dag.add_task(task_fn::<(), _, _>(move |_: ()| { let errors = errors.clone(); if i % 2 == 0 { - errors.lock().push(format!("Error from task {}", i)); + errors + .lock() + .unwrap() + .push(format!("Error from task {}", i)); return Err(format!("Task {} error", i)); } Ok(i) @@ -102,11 +107,12 @@ async fn test_error_accumulation_pattern() -> DagResult<()> { }) .collect(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // Check accumulated errors - let error_list = errors.lock().clone(); + let error_list = errors.lock().unwrap().clone(); assert_eq!(error_list.len(), 5); // Even numbered tasks tasks.truncate(2); @@ -114,15 +120,15 @@ async fn test_error_accumulation_pattern() -> DagResult<()> { let [first, second] = <[_; 2]>::try_from(tasks).map_err(|_e| ()).unwrap(); // Verify specific results - assert!(dag.get(first)?.is_err()); - assert_eq!(dag.get(second)?, Ok(1)); + assert!(output.get(first)?.is_err()); + assert_eq!(output.get(second)?, Ok(1)); Ok(()) } #[tokio::test] async fn test_error_boundary_isolation() -> DagResult<()> { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Group A: contains an error - use Result let a1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); @@ -143,15 +149,16 @@ async fn test_error_boundary_isolation() -> DagResult<()> { .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(b2); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // Group A fails (returns Err) - assert_eq!(dag.get(a2)?, Err("Group A error")); + assert_eq!(output.get(a2)?, Err("Group A error")); // Groups B and C succeed - assert_eq!(dag.get(b2)?, 40); - assert_eq!(dag.get(c1)?, 45); + assert_eq!(output.get(b2)?, 40); + assert_eq!(output.get(c1)?, 45); Ok(()) } diff --git a/tests/execution/mod.rs b/tests/execution/mod.rs index 9f29dd3..88bbb12 100644 --- a/tests/execution/mod.rs +++ b/tests/execution/mod.rs @@ -6,43 +6,46 @@ use dagx::*; #[tokio::test] async fn test_empty_dag() { let dag = DagRunner::new(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _ = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); // Should complete without error } #[tokio::test] async fn test_single_task() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - assert_eq!(dag.get(task).unwrap(), 42); + assert_eq!(output.get(task).unwrap(), 42); } #[tokio::test] async fn test_wide_parallel() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let tasks: Vec<_> = (0..10) .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) .collect(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), i * 2); + assert_eq!(output.get(task).unwrap(), i * 2); } } #[tokio::test] async fn test_diamond_dependency() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); + let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); let b = dag .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(a); @@ -53,13 +56,14 @@ async fn test_diamond_dependency() { .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y)) .depends_on((&b, &c)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); // Note: a, b, and c are not sinks (they have dependents) // Only d is a sink and can be retrieved - assert_eq!(dag.get(d).unwrap(), 35); // 15 + 20 + assert_eq!(output.get(d).unwrap(), 35); // 15 + 20 } #[derive(Clone)] @@ -70,7 +74,7 @@ struct User { #[tokio::test] async fn test_different_output_types() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string())); let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3])); @@ -79,22 +83,23 @@ async fn test_different_output_types() { age: 30, })); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - assert_eq!(&dag.get(string_task).unwrap(), "hello"); - assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]); - let user = dag.get(struct_task).unwrap(); + assert_eq!(&output.get(string_task).unwrap(), "hello"); + assert_eq!(output.get(vec_task).unwrap(), vec![1, 2, 3]); + let user = output.get(struct_task).unwrap(); assert_eq!(user.name, "Alice"); assert_eq!(user.age, 30); } #[tokio::test] async fn test_multiple_sinks() { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); // Branch 1 let branch1 = dag @@ -112,21 +117,22 @@ async fn test_multiple_sinks() { .add_task(task_fn::(|&x: &i32| x * 3)) .depends_on(branch2); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); // Should wait for both sinks - assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1 - assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3 + assert_eq!(output.get(sink1).unwrap(), 21); // (10 * 2) + 1 + assert_eq!(output.get(sink2).unwrap(), 45); // (10 + 5) * 3 } #[tokio::test] async fn test_single_task_inline_path() { // Test single-task layer inline execution path (line 367-410 in runner.rs) - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Single task in its own layer - let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); + let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); let t2 = dag .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(t1); @@ -134,19 +140,20 @@ async fn test_single_task_inline_path() { .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(t2); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5 + assert_eq!(output.get(t3).unwrap(), 25); // (10 * 2) + 5 } #[tokio::test] async fn test_multi_consumer_fanout() { // Test fanout with multiple consumers from one producer - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); - let producer: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); + let producer = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); // Multiple consumers let c1 = dag @@ -159,11 +166,12 @@ async fn test_multi_consumer_fanout() { .add_task(task_fn::(|&x: &i32| x - 5)) .depends_on(producer); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - assert_eq!(dag.get(c1).unwrap(), 84); - assert_eq!(dag.get(c2).unwrap(), 52); - assert_eq!(dag.get(c3).unwrap(), 37); + assert_eq!(output.get(c1).unwrap(), 84); + assert_eq!(output.get(c2).unwrap(), 52); + assert_eq!(output.get(c3).unwrap(), 37); } diff --git a/tests/timing/interleaving.rs b/tests/interleaving/mod.rs similarity index 78% rename from tests/timing/interleaving.rs rename to tests/interleaving/mod.rs index cb430d9..acd84ef 100644 --- a/tests/timing/interleaving.rs +++ b/tests/interleaving/mod.rs @@ -4,19 +4,19 @@ use dagx::{task, DagResult, DagRunner}; use dagx_test::task_fn; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use tokio::sync::Barrier; #[tokio::test] async fn test_forced_interleaving_with_barriers() -> DagResult<()> { // Force specific interleaving using barriers - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let barrier = Arc::new(Barrier::new(3)); - let order = Arc::new(parking_lot::Mutex::new(Vec::new())); + let order = Arc::new(Mutex::new(Vec::new())); struct InterleaveTask { barrier: Arc, - order: Arc>>, + order: Arc>>, prefix: &'static str, value: i32, } @@ -24,9 +24,15 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> { #[task] impl InterleaveTask { async fn run(&self) -> i32 { - self.order.lock().push(format!("{}_start", self.prefix)); + self.order + .lock() + .unwrap() + .push(format!("{}_start", self.prefix)); self.barrier.wait().await; - self.order.lock().push(format!("{}_end", self.prefix)); + self.order + .lock() + .unwrap() + .push(format!("{}_end", self.prefix)); self.value } } @@ -58,13 +64,14 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> { )) .depends_on((t1, t2, t3)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(collector)?, 6); + assert_eq!(output.get(collector)?, 6); // Check interleaving pattern - let events = order.lock().clone(); + let events = order.lock().unwrap().clone(); assert_eq!(events.len(), 6); // All starts should come before all ends due to barrier @@ -80,14 +87,13 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> { #[tokio::test] async fn test_alternating_execution_pattern() -> DagResult<()> { // Create an alternating execution pattern - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let turn = Arc::new(AtomicUsize::new(0)); - let events: Arc>> = - Arc::new(parking_lot::Mutex::new(Vec::new())); + let events: Arc>> = Arc::new(Mutex::new(Vec::new())); struct FirstA { turn: Arc, - events: Arc>>, + events: Arc>>, } #[task] @@ -96,22 +102,20 @@ async fn test_alternating_execution_pattern() -> DagResult<()> { while self.turn.load(Ordering::SeqCst) != 0 { tokio::task::yield_now().await; } - self.events.lock().push("A0".to_string()); + self.events.lock().unwrap().push("A0".to_string()); self.turn.store(1, Ordering::SeqCst); 0 } } - let first_a = dag.add_task(FirstA { + let mut chain_a = dag.add_task(FirstA { turn: turn.clone(), events: events.clone(), }); - let mut chain_a: dagx::TaskHandle = first_a.into(); - struct FirstB { turn: Arc, - events: Arc>>, + events: Arc>>, } #[task] @@ -120,22 +124,20 @@ async fn test_alternating_execution_pattern() -> DagResult<()> { while self.turn.load(Ordering::SeqCst) != 1 { tokio::task::yield_now().await; } - self.events.lock().push("B0".to_string()); + self.events.lock().unwrap().push("B0".to_string()); self.turn.store(2, Ordering::SeqCst); 0 } } - let first_b = dag.add_task(FirstB { + let mut chain_b = dag.add_task(FirstB { turn: turn.clone(), events: events.clone(), }); - let mut chain_b: dagx::TaskHandle = first_b.into(); - struct ChainA { turn: Arc, - events: Arc>>, + events: Arc>>, idx: usize, } @@ -145,7 +147,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> { while self.turn.load(Ordering::SeqCst) != self.idx * 2 { tokio::task::yield_now().await; } - self.events.lock().push(format!("A{}", self.idx)); + self.events.lock().unwrap().push(format!("A{}", self.idx)); self.turn.store(self.idx * 2 + 1, Ordering::SeqCst); x + 1 } @@ -153,7 +155,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> { struct ChainB { turn: Arc, - events: Arc>>, + events: Arc>>, idx: usize, } @@ -163,7 +165,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> { while self.turn.load(Ordering::SeqCst) != self.idx * 2 + 1 { tokio::task::yield_now().await; } - self.events.lock().push(format!("B{}", self.idx)); + self.events.lock().unwrap().push(format!("B{}", self.idx)); self.turn.store(self.idx * 2 + 2, Ordering::SeqCst); x + 1 } @@ -187,11 +189,12 @@ async fn test_alternating_execution_pattern() -> DagResult<()> { .depends_on(chain_b); } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let _output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // Check alternating pattern - let event_list = events.lock().clone(); + let event_list = events.lock().unwrap().clone(); assert_eq!( event_list, vec!["A0", "B0", "A1", "B1", "A2", "B2", "A3", "B3", "A4", "B4"] @@ -203,7 +206,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> { #[tokio::test] async fn test_layered_interleaving() -> DagResult<()> { // Test interleaving across DAG layers - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let layer_active = Arc::new(AtomicUsize::new(0)); struct Layer1Task { @@ -228,7 +231,6 @@ async fn test_layered_interleaving() -> DagResult<()> { active: layer_active.clone(), idx: i, }) - .into() }) .collect(); @@ -264,11 +266,12 @@ async fn test_layered_interleaving() -> DagResult<()> { }) .depends_on((&layer2[0], &layer2[1])); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // Sum of 0+1+2+3 = 6 - assert_eq!(dag.get(layer3)?, 6); + assert_eq!(output.get(layer3)?, 6); Ok(()) } diff --git a/tests/lib_tests.rs b/tests/lib_tests.rs index 3fecea2..fed5629 100644 --- a/tests/lib_tests.rs +++ b/tests/lib_tests.rs @@ -8,7 +8,7 @@ mod boundaries; mod dependencies; mod errors; mod execution; +mod interleaving; mod parallelism; mod runtimes; -mod timing; mod tracing; diff --git a/tests/parallelism/concurrent.rs b/tests/parallelism/concurrent.rs deleted file mode 100644 index fd746a6..0000000 --- a/tests/parallelism/concurrent.rs +++ /dev/null @@ -1,425 +0,0 @@ -//! Concurrent access tests - -use dagx::{task, DagRunner}; -use dagx_test::task_fn; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use tokio::task::JoinSet; - -struct SleepTask(usize); - -#[task] -impl SleepTask { - async fn run(&self) -> usize { - tokio::time::sleep(Duration::from_millis(5)).await; - self.0 - } -} - -#[tokio::test] -async fn test_concurrent_run_calls() { - // Test that multiple run() calls are handled correctly - let dag = Arc::new(DagRunner::new()); - - // Add some tasks - let tasks: Vec<_> = (0..10).map(|i| dag.add_task(SleepTask(i))).collect(); - - // Try to run the DAG multiple times concurrently - let mut handles = JoinSet::new(); - - for _ in 0..5 { - let dag = Arc::clone(&dag); - handles.spawn(async move { - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - }); - } - - // Only one run should succeed, others should fail with concurrent execution error - let mut success_count = 0; - let mut error_count = 0; - - while let Some(result) = handles.join_next().await { - match result.unwrap() { - Ok(_) => success_count += 1, - Err(_) => error_count += 1, - } - } - - // Exactly one should succeed, the rest should fail - assert_eq!(success_count, 1); - assert_eq!(error_count, 4); - - // Verify results are consistent from the successful run - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), i); - } -} - -#[tokio::test] -async fn test_concurrent_execution_and_building() { - // Test that we can build and execute concurrently - let dag = Arc::new(DagRunner::new()); - - // Start with some initial tasks - let initial: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) - .collect(); - - // Execute the initial DAG - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Now try to add more tasks while also reading results - let mut handles = JoinSet::new(); - - // Reader tasks - for task in initial { - let dag = Arc::clone(&dag); - let task_handle: dagx::TaskHandle = task.into(); - handles.spawn(async move { dag.get(task_handle).unwrap() }); - } - - // Builder tasks (adding new nodes) - for i in 10..20 { - let dag = Arc::clone(&dag); - handles.spawn(async move { - dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - i - }); - } - - // Wait for all operations - while let Some(result) = handles.join_next().await { - result.unwrap(); - } - - // Execute again with the new tasks - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); -} - -#[tokio::test] -async fn test_dag_builder_thread_safety() { - // Test that the DAG builder is thread-safe - let dag = Arc::new(DagRunner::new()); - let counter = Arc::new(AtomicUsize::new(0)); - - let mut handles = JoinSet::new(); - - // Spawn tasks that build complex sub-graphs concurrently - for thread_id in 0..10 { - let dag = Arc::clone(&dag); - let counter = Arc::clone(&counter); - - handles.spawn(async move { - // Each thread creates a small pipeline - let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| thread_id)); - - let transform1 = dag - .add_task(task_fn::(move |&x: &i32| x * 10)) - .depends_on(source); - - let transform2 = dag - .add_task(task_fn::(move |&x: &i32| x + thread_id)) - .depends_on(transform1); - - counter.fetch_add(1, Ordering::SeqCst); - transform2 - }); - } - - let mut final_tasks = vec![]; - while let Some(result) = handles.join_next().await { - final_tasks.push(result.unwrap()); - } - - // Verify all threads completed - assert_eq!(counter.load(Ordering::SeqCst), 10); - - // Execute the combined DAG - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Verify each pipeline computed correctly - for task in final_tasks.iter() { - let result = dag.get(task).unwrap(); - // Each pipeline: thread_id * 10 + thread_id - assert!((0..100).contains(&result)); - } -} - -#[tokio::test] -async fn test_concurrent_get_operations() { - // Test concurrent get() operations - let dag = Arc::new(DagRunner::new()); - - // Create and execute tasks - let tasks: Vec<_> = (0..100) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Now access results concurrently - let mut handles = JoinSet::new(); - - for task in tasks.into_iter() { - let dag = Arc::clone(&dag); - let task_handle: dagx::TaskHandle = task.into(); - handles.spawn(async move { dag.get(task_handle).unwrap() }); - } - - // Collect and verify results - let mut results = vec![]; - while let Some(result) = handles.join_next().await { - results.push(result.unwrap()); - } - - // Results should contain all values 0..100 - results.sort(); - for (i, val) in results.iter().enumerate() { - assert_eq!(*val, i as i32); - } -} - -#[tokio::test] -async fn test_no_race_in_dependency_tracking() { - // Test that concurrent dependency modifications don't cause races - let dag = Arc::new(DagRunner::new()); - let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - - let mut handles = JoinSet::new(); - - // Multiple threads create tasks depending on the same source - let source_handle: dagx::TaskHandle = source.into(); - for i in 0..50 { - let dag = Arc::clone(&dag); - let source_clone = source_handle; - - handles.spawn(async move { - dag.add_task(task_fn::(move |&x: &i32| x + i)) - .depends_on(source_clone) - }); - } - - let mut dependents = vec![]; - while let Some(result) = handles.join_next().await { - dependents.push(result.unwrap()); - } - - // Execute - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // All dependents should have correct values - for dep in dependents.iter() { - let result = dag.get(dep).unwrap(); - assert!((42..92).contains(&result)); // 42 + (0..50) - } -} - -#[tokio::test] -async fn test_high_concurrency_stress() { - // Stress test with high concurrency - let dag = Arc::new(DagRunner::new()); - let completed = Arc::new(AtomicUsize::new(0)); - - let mut handles = JoinSet::new(); - - // 1000 concurrent operations - for i in 0..1000 { - let dag = Arc::clone(&dag); - let completed = Arc::clone(&completed); - - handles.spawn(async move { - // Mix of operations - match i % 3 { - 0 => { - // Add a source task - dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - } - 1 => { - // Add a task with a simple dependency - let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - dag.add_task(task_fn::(move |&x: &i32| x * 2)) - .depends_on(source); - } - 2 => { - // Add a more complex subgraph - let a = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - let b = dag.add_task(task_fn::<(), _, _>(move |_: ()| i + 1)); - dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| { - x + y - })) - .depends_on((a, b)); - } - _ => unreachable!(), - } - completed.fetch_add(1, Ordering::SeqCst); - }); - } - - // Wait for all operations to complete - while let Some(result) = handles.join_next().await { - result.unwrap(); - } - - assert_eq!(completed.load(Ordering::SeqCst), 1000); - - // The DAG should still be executable - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); -} - -#[tokio::test] -async fn test_lock_contention_measurement() { - // Test to measure lock contention under concurrent load - let dag = Arc::new(DagRunner::new()); - let start = tokio::time::Instant::now(); - let operations = Arc::new(AtomicUsize::new(0)); - - let mut handles = JoinSet::new(); - - // Create many concurrent operations that will contend for locks - for i in 0..100 { - let dag = Arc::clone(&dag); - let ops = Arc::clone(&operations); - - handles.spawn(async move { - for j in 0..10 { - dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10 + j)); - ops.fetch_add(1, Ordering::SeqCst); - - // Small yield to increase contention likelihood - tokio::task::yield_now().await; - } - }); - } - - // Wait for completion - while let Some(result) = handles.join_next().await { - result.unwrap(); - } - - let duration = start.elapsed(); - let total_ops = operations.load(Ordering::SeqCst); - - println!( - "Completed {} operations in {:?} ({:.0} ops/sec)", - total_ops, - duration, - total_ops as f64 / duration.as_secs_f64() - ); - - // Should complete reasonably quickly even with contention - assert!( - duration < Duration::from_secs(5), - "Too much lock contention" - ); -} - -#[tokio::test] -async fn test_atomic_dag_operations() { - // Test that DAG operations are atomic (all-or-nothing) - let dag = Arc::new(DagRunner::new()); - - // Create a base task - let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); - - // Try to create dependent tasks concurrently - let mut handles = JoinSet::new(); - let success_count = Arc::new(AtomicUsize::new(0)); - - let base_handle: dagx::TaskHandle = base.into(); - for i in 0..50 { - let dag = Arc::clone(&dag); - let base_clone = base_handle; - let success = Arc::clone(&success_count); - - handles.spawn(async move { - let dep = dag - .add_task(task_fn::(move |&x: &i32| x + i)) - .depends_on(base_clone); - - success.fetch_add(1, Ordering::SeqCst); - dep - }); - } - - let mut deps = vec![]; - while let Some(result) = handles.join_next().await { - deps.push(result.unwrap()); - } - - // All operations should have succeeded - assert_eq!(success_count.load(Ordering::SeqCst), 50); - assert_eq!(deps.len(), 50); - - // Execute and verify - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - for dep in deps.iter() { - let result = dag.get(dep).unwrap(); - assert!((100..150).contains(&result)); - } -} - -#[tokio::test] -async fn test_no_deadlock_on_circular_waiting() { - // Test that we don't deadlock even with complex concurrent operations - let dag = Arc::new(DagRunner::new()); - - // Create multiple threads that might cause circular waiting patterns - let mut handles = JoinSet::new(); - - for i in 0..10 { - let dag = Arc::clone(&dag); - - handles.spawn(async move { - // Each thread creates a chain of tasks - let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - let mut chain = vec![first.into()]; - - for j in 0..5 { - let next = dag - .add_task(task_fn::(move |&x: &i32| x + j)) - .depends_on(chain.last().unwrap()); - chain.push(next); - - // Yield to potentially cause interleaving - tokio::task::yield_now().await; - } - - *chain.last().unwrap() - }); - } - - // Set a timeout to detect deadlocks - let result = tokio::time::timeout(Duration::from_secs(5), async { - let mut finals = vec![]; - while let Some(result) = handles.join_next().await { - finals.push(result.unwrap()); - } - finals - }) - .await; - - assert!(result.is_ok(), "Deadlock detected - operation timed out"); - - // Should be able to execute the DAG - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); -} diff --git a/tests/parallelism/mod.rs b/tests/parallelism/mod.rs index 93421db..adf6362 100644 --- a/tests/parallelism/mod.rs +++ b/tests/parallelism/mod.rs @@ -1,6 +1,5 @@ //! Parallelism tests module -pub mod concurrent; pub mod spawning; pub mod threads; pub mod timing; diff --git a/tests/parallelism/spawning.rs b/tests/parallelism/spawning.rs index 1ddf148..a6070c4 100644 --- a/tests/parallelism/spawning.rs +++ b/tests/parallelism/spawning.rs @@ -9,7 +9,7 @@ use std::sync::Arc; #[tokio::test] async fn test_spawner_actually_spawns_tasks() -> DagResult<()> { // Verify that the spawner function is actually called for each task - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let spawn_count = Arc::new(AtomicUsize::new(0)); // Create 10 independent tasks @@ -27,12 +27,13 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> { .collect(); // Custom spawner that counts invocations - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // Verify all tasks were spawned for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task)?, i as i32); + assert_eq!(output.get(task)?, i as i32); } // The spawner should have been called once for each task diff --git a/tests/parallelism/threads.rs b/tests/parallelism/threads.rs index 59cbd29..6636ebb 100644 --- a/tests/parallelism/threads.rs +++ b/tests/parallelism/threads.rs @@ -1,6 +1,6 @@ //! Tests for multi-threading and concurrent execution -use dagx::{task, DagResult, DagRunner, TaskHandle}; +use dagx::{task, DagResult, DagRunner}; use dagx_test::task_fn; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -11,7 +11,7 @@ use tokio::time::sleep; #[tokio::test] async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> { // Prove concurrent execution using atomic counters to track simultaneous tasks - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let concurrent_count = Arc::new(AtomicUsize::new(0)); let max_concurrent = Arc::new(AtomicUsize::new(0)); @@ -62,12 +62,13 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> { }) .collect(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; // Verify all tasks completed for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task)?, i); + assert_eq!(output.get(task)?, i); } // Check that we achieved real parallelism @@ -90,13 +91,13 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> { #[tokio::test] async fn test_massive_parallel_fanout() -> DagResult<()> { // Test extreme parallelism with 1000 parallel tasks - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let completed = Arc::new(AtomicUsize::new(0)); let max_concurrent = Arc::new(AtomicUsize::new(0)); let current = Arc::new(AtomicUsize::new(0)); - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into(); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); struct FanoutTask { comp: Arc, @@ -147,7 +148,8 @@ async fn test_massive_parallel_fanout() -> DagResult<()> { .collect(); let start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; let elapsed = start.elapsed(); @@ -155,9 +157,9 @@ async fn test_massive_parallel_fanout() -> DagResult<()> { assert_eq!(completed.load(Ordering::SeqCst), 1000); // Check some results - assert_eq!(dag.get(tasks[0])?, 1); - assert_eq!(dag.get(tasks[500])?, 501); - assert_eq!(dag.get(tasks[999])?, 1000); + assert_eq!(output.get(tasks[0])?, 1); + assert_eq!(output.get(tasks[500])?, 501); + assert_eq!(output.get(tasks[999])?, 1000); let max_seen = max_concurrent.load(Ordering::SeqCst); println!( diff --git a/tests/parallelism/timing.rs b/tests/parallelism/timing.rs index 8903ea0..9ecc4d7 100644 --- a/tests/parallelism/timing.rs +++ b/tests/parallelism/timing.rs @@ -22,7 +22,7 @@ impl SleepTask { #[tokio::test] async fn test_parallel_execution_speedup() -> DagResult<()> { // Prove parallel execution by comparing timing - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let work_duration = Duration::from_millis(50); let num_tasks = 10; @@ -33,13 +33,14 @@ async fn test_parallel_execution_speedup() -> DagResult<()> { .collect(); let start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; let elapsed = start.elapsed(); // Verify all tasks completed for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task)?, i); + assert_eq!(output.get(task)?, i); } // If tasks ran sequentially: 10 * 50ms = 500ms @@ -53,7 +54,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> { // Parallel execution should be significantly faster assert!( - elapsed < sequential_time / 4, + elapsed < sequential_time / 2, "Parallel execution too slow: {:?} vs sequential {:?}", elapsed, sequential_time @@ -65,7 +66,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> { #[tokio::test] async fn test_layers_execute_in_parallel() -> DagResult<()> { // Test that tasks in the same topological layer execute in parallel - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let layer_timing = Arc::new(Mutex::new(Vec::new())); @@ -142,10 +143,11 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> { .depends_on((&layer1[0], &layer1[1], &layer1[2])) }; - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await?; - assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1) + assert_eq!(output.get(final_task)?, 33); // (0+1) + (10+1) + (20+1) // Analyze timing let timing = layer_timing.lock().unwrap(); diff --git a/tests/runtimes/mod.rs b/tests/runtimes/mod.rs index 740a1da..ca424fe 100644 --- a/tests/runtimes/mod.rs +++ b/tests/runtimes/mod.rs @@ -95,15 +95,15 @@ impl Add { #[test_case(AsyncExecutorRuntimeTest)] fn test_basic_dag(runner: impl RuntimeTest) { runner.run_dagx_test(|spawner| async { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let x = dag.add_task(Value(2)); let y = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((x, y)); - dag.run(spawner).await.unwrap(); + let mut output = dag.run(spawner).await.unwrap(); - assert_eq!(dag.get(sum).unwrap(), 5); + assert_eq!(output.get(sum).unwrap(), 5); }) } @@ -113,16 +113,16 @@ fn test_basic_dag(runner: impl RuntimeTest) { #[test_case(AsyncExecutorRuntimeTest)] fn test_parallel_execution(runner: impl RuntimeTest) { runner.run_dagx_test(|spawner| async { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let tasks: Vec<_> = (0..10) .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) .collect(); - dag.run(spawner).await.unwrap(); + let mut output = dag.run(spawner).await.unwrap(); for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), i * 2); + assert_eq!(output.get(task).unwrap(), i * 2); } }); } @@ -133,7 +133,7 @@ fn test_parallel_execution(runner: impl RuntimeTest) { #[test_case(AsyncExecutorRuntimeTest)] fn test_complex_dependencies(runner: impl RuntimeTest) { runner.run_dagx_test(|spawner| async { - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let a = dag.add_task(Value(10)); let b = dag.add_task(Value(20)); @@ -142,8 +142,8 @@ fn test_complex_dependencies(runner: impl RuntimeTest) { .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(sum); - dag.run(spawner).await.unwrap(); + let mut output = dag.run(spawner).await.unwrap(); - assert_eq!(dag.get(double).unwrap().clone(), 60); + assert_eq!(output.get(double).unwrap().clone(), 60); }); } diff --git a/tests/timing/concurrent_access.rs b/tests/timing/concurrent_access.rs deleted file mode 100644 index ad5b147..0000000 --- a/tests/timing/concurrent_access.rs +++ /dev/null @@ -1,331 +0,0 @@ -//! Tests for concurrent access patterns and race conditions - -use dagx::{task, DagResult, DagRunner, TaskHandle}; -use dagx_test::task_fn; - -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::RwLock; - -#[tokio::test] -async fn test_concurrent_result_retrieval() -> DagResult<()> { - // Test concurrent get() calls after execution - let dag = Arc::new(DagRunner::new()); - - struct DelayTask(usize, Duration); - - #[task] - impl DelayTask { - async fn run(&self) -> usize { - tokio::time::sleep(self.1).await; - self.0 - } - } - - // Add tasks with minimal delays - let tasks: Vec> = (0..10) - .map(|i| { - let task = dag.add_task(DelayTask(i, Duration::from_millis(1))); - task.into() - }) - .collect(); - - // Execute the DAG - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Now try to get results concurrently from multiple threads - let mut get_handles = Vec::new(); - for (i, task) in tasks.iter().enumerate() { - let dag_clone = dag.clone(); - let task = *task; - get_handles.push(tokio::spawn(async move { - // Results should be available immediately - dag_clone.get(task).map(|v| (i, v)) - })); - } - - // Verify all gets succeeded - for handle in get_handles { - let (i, result) = handle.await.unwrap()?; - assert_eq!(result, i); - } - - Ok(()) -} - -#[tokio::test] -async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> { - // Test building DAG while another part is executing - let dag = Arc::new(DagRunner::new()); - let building_complete = Arc::new(AtomicBool::new(false)); - - // Start with a few initial tasks - let initial: Vec<_> = (0..5) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) - .collect(); - - // Start building more tasks in background - let dag_build = dag.clone(); - let building = building_complete.clone(); - let build_handle = tokio::spawn(async move { - let mut tasks: Vec> = Vec::new(); - for i in 5..50 { - let task = dag_build.add_task(task_fn::<(), _, _>(move |_: ()| i)); - tasks.push(task.into()); - tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; - } - building.store(true, Ordering::SeqCst); - tasks - }); - - // Wait a bit for some tasks to be added - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - - // Execute the DAG - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Get the additional tasks - let additional_tasks = build_handle.await.unwrap(); - - // Initial tasks should have results - for (i, task) in initial.into_iter().enumerate() { - assert_eq!(dag.get(task)?, i); - } - - // Additional tasks added during execution won't have results yet - // (they need another run cycle) - if building_complete.load(Ordering::SeqCst) { - // Run again for the new tasks - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Now they should have results - for (i, task) in additional_tasks.iter().enumerate() { - assert_eq!(dag.get(task)?, (i + 5) as i32); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_concurrent_dependency_resolution() -> DagResult<()> { - // Test multiple tasks resolving dependencies concurrently - let dag = DagRunner::new(); - let resolution_counter = Arc::new(AtomicUsize::new(0)); - - // Create a source task - let source: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>({ - let counter = resolution_counter.clone(); - move |_: ()| { - let counter = counter.clone(); - counter.fetch_add(1, Ordering::SeqCst); - 42 - } - })) - .into(); - - // Create many tasks depending on the same source - let dependents: Vec<_> = (0..100) - .map(|i| { - let counter = resolution_counter.clone(); - dag.add_task(task_fn::(move |val: &i32| { - let counter = counter.clone(); - counter.fetch_add(1, Ordering::SeqCst); - val + i - })) - .depends_on(source) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Check all completed - assert_eq!(resolution_counter.load(Ordering::SeqCst), 101); - - // Verify results - for (i, task) in dependents.iter().enumerate() { - assert_eq!(dag.get(task)?, 42 + i as i32); - } - - Ok(()) -} - -#[tokio::test] -async fn test_read_write_race_patterns() -> DagResult<()> { - // Test read-write patterns with shared state - let dag = DagRunner::new(); - let shared_state = Arc::new(RwLock::new(Vec::new())); - - struct WriterTask { - state: Arc>>, - idx: i32, - } - - #[task] - impl WriterTask { - async fn run(&self) -> i32 { - let mut guard = self.state.write().await; - guard.push(self.idx); - tokio::task::yield_now().await; // Force potential race - guard.push(self.idx * 10); - self.idx - } - } - - // Writers - let writers: Vec<_> = (0..10) - .map(|i| { - dag.add_task(WriterTask { - state: shared_state.clone(), - idx: i, - }) - .into() - }) - .collect(); - - struct ReaderTask { - state: Arc>>, - } - - struct ReaderTaskOutput((i32, bool, usize)); - - #[task] - impl ReaderTask { - async fn run(&self, writer_id: &i32) -> ReaderTaskOutput { - let guard = self.state.read().await; - let contains_writer = guard.contains(&writer_id); - let len = guard.len(); - ReaderTaskOutput((*writer_id, contains_writer, len)) - } - } - - // Readers (depend on writers to ensure ordering) - let readers: Vec<_> = writers - .iter() - .map(|writer| { - dag.add_task(ReaderTask { - state: shared_state.clone(), - }) - .depends_on(writer) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify readers saw their writer's data - for reader in &readers { - let ReaderTaskOutput((writer_id, saw_writer, _len)) = dag.get(reader)?; - assert!(saw_writer, "Reader didn't see writer {}", writer_id); - } - - // Final state should have all writes - let final_state = shared_state.read().await; - assert_eq!(final_state.len(), 20); // 10 writers * 2 writes each - - Ok(()) -} - -#[tokio::test] -async fn test_atomic_counter_races() -> DagResult<()> { - // Test atomic operations under high concurrency - let dag = DagRunner::new(); - let counter = Arc::new(AtomicUsize::new(0)); - let max_seen = Arc::new(AtomicUsize::new(0)); - - // Create tasks that increment and track max - let _tasks: Vec<_> = (0..200) - .map(|_| { - let counter = counter.clone(); - let max_seen = max_seen.clone(); - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let counter = counter.clone(); - let max_seen = max_seen.clone(); - let mut local_max = 0; - for _ in 0..100 { - let val = counter.fetch_add(1, Ordering::SeqCst); - local_max = local_max.max(val); - - // Update global max - let mut current_max = max_seen.load(Ordering::SeqCst); - while val > current_max { - match max_seen.compare_exchange_weak( - current_max, - val, - Ordering::SeqCst, - Ordering::SeqCst, - ) { - Ok(_) => break, - Err(x) => current_max = x, - } - } - } - })) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Final counter should be 200 * 100 = 20000 - assert_eq!(counter.load(Ordering::SeqCst), 20000); - - // Max seen should be at least 19999 (last increment - 1) - assert!(max_seen.load(Ordering::SeqCst) >= 19999); - - Ok(()) -} - -#[tokio::test] -async fn test_multiple_dag_runners_interaction() -> DagResult<()> { - // Test multiple DAG runners potentially interfering - let shared_counter = Arc::new(AtomicUsize::new(0)); - - let mut handles = Vec::new(); - - for dag_id in 0..5 { - let counter = shared_counter.clone(); - let handle = tokio::spawn(async move { - let dag = DagRunner::new(); - - let tasks: Vec<_> = (0..20) - .map(|task_id| { - let counter = counter.clone(); - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let counter = counter.clone(); - counter.fetch_add(1, Ordering::SeqCst); - (dag_id, task_id) - })) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Collect results - let mut results = Vec::new(); - for task in tasks { - results.push(dag.get(task)?); - } - - Ok::<_, dagx::DagError>(results) - }); - handles.push(handle); - } - - // Wait for all DAGs to complete - for handle in handles { - handle.await.unwrap()?; - } - - // Should have executed 5 * 20 = 100 tasks total - assert_eq!(shared_counter.load(Ordering::SeqCst), 100); - - Ok(()) -} diff --git a/tests/timing/mod.rs b/tests/timing/mod.rs deleted file mode 100644 index 58d63cf..0000000 --- a/tests/timing/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Timing and race condition tests -//! -//! Tests for temporal edge cases, execution ordering, and race conditions. - -mod concurrent_access; -mod interleaving; diff --git a/tests/tracing/with_tracing.rs b/tests/tracing/with_tracing.rs index c087143..d8ef38a 100644 --- a/tests/tracing/with_tracing.rs +++ b/tests/tracing/with_tracing.rs @@ -39,17 +39,18 @@ async fn test_tracing_with_subscriber() { .with_test_writer() .try_init(); - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); let a = dag.add_task(Value(2)); let b = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((a, b)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); - assert_eq!(dag.get(sum).unwrap(), 5); + assert_eq!(output.get(sum).unwrap(), 5); } #[tokio::test(flavor = "multi_thread")] @@ -60,19 +61,20 @@ async fn test_tracing_inline_execution() { .with_test_writer() .try_init(); - let dag = DagRunner::new(); + let mut dag = DagRunner::new(); // Create a linear chain (should trigger inline execution) - let a = dag.add_task(Value(1)).into(); + let a = dag.add_task(Value(1)); let b = dag.add_task(Add).depends_on((&a, &a)); let c = dag.add_task(Multiply).depends_on((&b, &b)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + let mut output = dag + .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) .await .unwrap(); // a = 1 // b = 1 + 1 = 2 // c = 2 * 2 = 4 - assert_eq!(dag.get(c).unwrap(), 4); + assert_eq!(output.get(c).unwrap(), 4); }