Merge pull request #12 from TechnoPorg/push-wummzrynoysn
Make DagRunner construction single-threaded
This commit is contained in:
68 files changed
+900
-1849
No files matched your search
+12
-3
@@ -21,10 +21,12 @@ cargo clippy -- -D warnings
|
|||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
**Prerequisites:**
|
**Prerequisites:**
|
||||||
|
|
||||||
- Rust 1.81+ (edition 2021)
|
- Rust 1.81+ (edition 2021)
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
**Optional tools:**
|
**Optional tools:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rustup component add rustfmt clippy
|
rustup component add rustfmt clippy
|
||||||
cargo install cargo-llvm-cov # For coverage
|
cargo install cargo-llvm-cov # For coverage
|
||||||
@@ -67,7 +69,8 @@ cargo build --examples # Examples compile
|
|||||||
- All doc examples must compile
|
- All doc examples must compile
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```rust
|
|
||||||
|
````rust
|
||||||
/// Adds a task to the DAG.
|
/// Adds a task to the DAG.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
@@ -75,23 +78,25 @@ Example:
|
|||||||
/// ```
|
/// ```
|
||||||
/// use dagx::DagRunner;
|
/// use dagx::DagRunner;
|
||||||
///
|
///
|
||||||
/// let dag = DagRunner::new();
|
/// let mut dag = DagRunner::new();
|
||||||
/// let task = dag.add_task(MyTask::new());
|
/// let task = dag.add_task(MyTask::new());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn add_task<T: Task>(&self, task: T) -> TaskBuilder<T> {
|
pub fn add_task<T: Task>(&self, task: T) -> TaskBuilder<T> {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
````
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
**Test organization:**
|
**Test organization:**
|
||||||
|
|
||||||
- Unit tests: `#[cfg(test)]` modules in source files
|
- Unit tests: `#[cfg(test)]` modules in source files
|
||||||
- Integration tests: `tests/` directory
|
- Integration tests: `tests/` directory
|
||||||
- Examples: `examples/` directory (must compile)
|
- Examples: `examples/` directory (must compile)
|
||||||
- Doctests: In `///` comments (must pass)
|
- Doctests: In `///` comments (must pass)
|
||||||
|
|
||||||
**Runtime testing:**
|
**Runtime testing:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test # Default (tokio)
|
cargo test # Default (tokio)
|
||||||
cargo test --test async_std_runtime # async-std
|
cargo test --test async_std_runtime # async-std
|
||||||
@@ -99,6 +104,7 @@ cargo test --test smol_runtime # smol
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Benchmarks:**
|
**Benchmarks:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo bench
|
cargo bench
|
||||||
# View results: open target/criterion/report/index.html
|
# View results: open target/criterion/report/index.html
|
||||||
@@ -130,6 +136,7 @@ Closes #123
|
|||||||
**Types**: `feat`, `fix`, `docs`, `test`, `refactor`, `perf`, `chore`
|
**Types**: `feat`, `fix`, `docs`, `test`, `refactor`, `perf`, `chore`
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
- `feat: add timeout support for tasks`
|
- `feat: add timeout support for tasks`
|
||||||
- `fix: correct cycle detection in diamond DAG`
|
- `fix: correct cycle detection in diamond DAG`
|
||||||
- `docs: clarify TaskHandle usage in README`
|
- `docs: clarify TaskHandle usage in README`
|
||||||
@@ -137,12 +144,14 @@ Closes #123
|
|||||||
## Pull Requests
|
## Pull Requests
|
||||||
|
|
||||||
**PR description should include:**
|
**PR description should include:**
|
||||||
|
|
||||||
- What changed and why
|
- What changed and why
|
||||||
- How it was tested
|
- How it was tested
|
||||||
- Any breaking changes
|
- Any breaking changes
|
||||||
- Link to related issue (if applicable)
|
- Link to related issue (if applicable)
|
||||||
|
|
||||||
**Review process:**
|
**Review process:**
|
||||||
|
|
||||||
- Maintainer reviews within 1 week
|
- Maintainer reviews within 1 week
|
||||||
- Address feedback
|
- Address feedback
|
||||||
- Once approved, maintainer merges
|
- Once approved, maintainer merges
|
||||||
|
|||||||
Generated
-48
@@ -381,7 +381,6 @@ dependencies = [
|
|||||||
"dagx-test",
|
"dagx-test",
|
||||||
"futures-executor",
|
"futures-executor",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"parking_lot",
|
|
||||||
"pollster",
|
"pollster",
|
||||||
"smol",
|
"smol",
|
||||||
"test-case",
|
"test-case",
|
||||||
@@ -628,15 +627,6 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
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]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.28"
|
version = "0.4.28"
|
||||||
@@ -733,29 +723,6 @@ version = "2.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
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]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -865,15 +832,6 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"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]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.3"
|
version = "1.11.3"
|
||||||
@@ -943,12 +901,6 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
parking_lot = "0.12"
|
|
||||||
dagx-macros = { version = "0.3.1", path = "./dagx-macros" }
|
dagx-macros = { version = "0.3.1", path = "./dagx-macros" }
|
||||||
tracing = { version = "0.1", optional = true }
|
tracing = { version = "0.1", optional = true }
|
||||||
|
|
||||||
|
|||||||
@@ -80,21 +80,21 @@ impl Add {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Add source tasks with no dependencies
|
// Add source tasks with no dependencies
|
||||||
let x = dag.add_task(Value(2));
|
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.
|
// Add task that depends on both x and y.
|
||||||
// Here, y could be reused as a dependency for other tasks, while x could not.
|
// 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));
|
let sum = dag.add_task(Add).depends_on((x, &y));
|
||||||
|
|
||||||
// Execute with true parallelism
|
// 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
|
// 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:
|
The `DagRunner` orchestrates task execution:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let handle = dag.add_task(MyTask::new());
|
let handle = dag.add_task(MyTask::new());
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
- **Compile-time type safety**: Dependencies are validated at compile time, preventing type errors
|
||||||
- **Cycle detection**: Runtime detection of circular dependencies
|
- **Cycle detection**: Runtime detection of circular dependencies
|
||||||
- **Memory safety**: Rust's ownership system prevents memory corruption
|
- **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
|
### What dagx Does NOT Provide
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub fn bench_dag_creation(c: &mut Criterion) {
|
|||||||
|
|
||||||
c.bench_function("add_100_tasks", |b| {
|
c.bench_function("add_100_tasks", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
black_box(dag.add_task(Value(i)));
|
black_box(dag.add_task(Value(i)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ pub fn bench_dag_scaling(c: &mut Criterion) {
|
|||||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
for i in 0..size {
|
for i in 0..size {
|
||||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
//! Long sequential chain: 100 tasks in sequence
|
//! Long sequential chain: 100 tasks in sequence
|
||||||
|
|
||||||
use criterion::Criterion;
|
use criterion::Criterion;
|
||||||
use dagx::TaskHandle;
|
|
||||||
|
|
||||||
pub fn bench_deep_chain(c: &mut Criterion) {
|
pub fn bench_deep_chain(c: &mut Criterion) {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
@@ -16,9 +15,9 @@ pub fn bench_deep_chain(c: &mut Criterion) {
|
|||||||
use dagx::DagRunner;
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
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
|
let mut prev = dag
|
||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||||
.depends_on(first);
|
.depends_on(first);
|
||||||
@@ -29,10 +28,11 @@ pub fn bench_deep_chain(c: &mut Criterion) {
|
|||||||
.depends_on(prev);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
dag.get(prev).unwrap()
|
output.get(prev).unwrap()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
//! Classic diamond: A → B,C → D
|
//! Classic diamond: A → B,C → D
|
||||||
|
|
||||||
use criterion::Criterion;
|
use criterion::Criterion;
|
||||||
use dagx::TaskHandle;
|
|
||||||
|
|
||||||
pub fn bench_diamond_pattern(c: &mut Criterion) {
|
pub fn bench_diamond_pattern(c: &mut Criterion) {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
@@ -16,9 +15,9 @@ pub fn bench_diamond_pattern(c: &mut Criterion) {
|
|||||||
use dagx::DagRunner;
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
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
|
let b = dag
|
||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||||
.depends_on(a);
|
.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))
|
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
|
||||||
.depends_on((&b, &c));
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
dag.get(d).unwrap()
|
output.get(d).unwrap()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
|||||||
use dagx::DagRunner;
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
|
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Extract from 3 sources
|
// Extract from 3 sources
|
||||||
let source1 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]));
|
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);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
dag.get(load).unwrap()
|
output.get(load).unwrap()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ pub fn bench_large_scale(c: &mut Criterion) {
|
|||||||
use dagx::DagRunner;
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
|
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
for i in 0..10_000 {
|
for i in 0..10_000 {
|
||||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2));
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub fn bench_linear_pipeline(c: &mut Criterion) {
|
|||||||
use dagx::DagRunner;
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
|
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||||
let b = dag
|
let b = dag
|
||||||
@@ -31,10 +31,11 @@ pub fn bench_linear_pipeline(c: &mut Criterion) {
|
|||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||||
.depends_on(d);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
dag.get(e).unwrap()
|
output.get(e).unwrap()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
//! One source task feeding 100 dependent tasks
|
//! One source task feeding 100 dependent tasks
|
||||||
|
|
||||||
use criterion::Criterion;
|
use criterion::Criterion;
|
||||||
use dagx::TaskHandle;
|
|
||||||
|
|
||||||
pub fn bench_wide_fanout(c: &mut Criterion) {
|
pub fn bench_wide_fanout(c: &mut Criterion) {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
@@ -16,8 +15,8 @@ pub fn bench_wide_fanout(c: &mut Criterion) {
|
|||||||
use dagx::DagRunner;
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
|
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||||
|
|
||||||
// Create 100 tasks that all depend on source
|
// Create 100 tasks that all depend on source
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
@@ -25,7 +24,8 @@ pub fn bench_wide_fanout(c: &mut Criterion) {
|
|||||||
.depends_on(source);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
|
|||||||
// a) Adding 10k tasks (construction only)
|
// a) Adding 10k tasks (construction only)
|
||||||
group.bench_function("construction", |b| {
|
group.bench_function("construction", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
for i in 0..10_000 {
|
for i in 0..10_000 {
|
||||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
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| {
|
group.bench_function("full_execution", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
for i in 0..10_000 {
|
for i in 0..10_000 {
|
||||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
@@ -38,7 +39,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
|
|||||||
group.bench_function("execution_only", |b| {
|
group.bench_function("execution_only", |b| {
|
||||||
b.iter_batched(
|
b.iter_batched(
|
||||||
|| {
|
|| {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
for i in 0..10_000 {
|
for i in 0..10_000 {
|
||||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||||
}
|
}
|
||||||
@@ -46,7 +47,8 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
|
|||||||
},
|
},
|
||||||
|dag| {
|
|dag| {
|
||||||
rt.block_on(async {
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Diamond pattern benchmarks (fan-out then fan-in)
|
//! Diamond pattern benchmarks (fan-out then fan-in)
|
||||||
|
|
||||||
use criterion::Criterion;
|
use criterion::Criterion;
|
||||||
use dagx::{DagRunner, TaskHandle};
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
pub fn bench_diamond(c: &mut Criterion) {
|
pub fn bench_diamond(c: &mut Criterion) {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
@@ -9,11 +9,10 @@ pub fn bench_diamond(c: &mut Criterion) {
|
|||||||
c.bench_function("diamond_pattern_50", |b| {
|
c.bench_function("diamond_pattern_50", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
for i in 0..50 {
|
for i in 0..50 {
|
||||||
let source: TaskHandle<_> =
|
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into();
|
|
||||||
|
|
||||||
let left = dag
|
let left = dag
|
||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||||
@@ -26,7 +25,8 @@ pub fn bench_diamond(c: &mut Criterion) {
|
|||||||
.depends_on((&left, &right));
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
|||||||
+12
-12
@@ -1,7 +1,7 @@
|
|||||||
//! Fan-out pattern benchmarks (1 → N dependencies)
|
//! Fan-out pattern benchmarks (1 → N dependencies)
|
||||||
|
|
||||||
use criterion::Criterion;
|
use criterion::Criterion;
|
||||||
use dagx::{DagRunner, TaskHandle};
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
pub fn bench_fanout(c: &mut Criterion) {
|
pub fn bench_fanout(c: &mut Criterion) {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
@@ -10,15 +10,16 @@ pub fn bench_fanout(c: &mut Criterion) {
|
|||||||
c.bench_function("fanout_1_to_100_i32", |b| {
|
c.bench_function("fanout_1_to_100_i32", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||||
|
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
||||||
.depends_on(source);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
@@ -29,13 +30,11 @@ pub fn bench_fanout(c: &mut Criterion) {
|
|||||||
c.bench_function("fanout_1_to_100_vec", |b| {
|
c.bench_function("fanout_1_to_100_vec", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let source: TaskHandle<_> = dag
|
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
// Create 1000 strings - framework wraps in Arc automatically
|
||||||
// Create 1000 strings - framework wraps in Arc automatically
|
(0..1000).map(|i| format!("Item {}", i)).collect::<Vec<_>>()
|
||||||
(0..1000).map(|i| format!("Item {}", i)).collect::<Vec<_>>()
|
}));
|
||||||
}))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
dag.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
|
dag.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
|
||||||
@@ -45,7 +44,8 @@ pub fn bench_fanout(c: &mut Criterion) {
|
|||||||
.depends_on(source);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
|||||||
+16
-17
@@ -1,7 +1,7 @@
|
|||||||
//! ETL (Extract, Transform, Load) pipeline benchmark
|
//! ETL (Extract, Transform, Load) pipeline benchmark
|
||||||
|
|
||||||
use criterion::{BenchmarkId, Criterion};
|
use criterion::{BenchmarkId, Criterion};
|
||||||
use dagx::{DagRunner, TaskHandle};
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
pub fn bench_etl_pipeline(c: &mut Criterion) {
|
pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
@@ -17,23 +17,21 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
|||||||
|b, &count| {
|
|b, &count| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Extract: Load JSON-like data (framework wraps in Arc automatically)
|
// Extract: Load JSON-like data (framework wraps in Arc automatically)
|
||||||
let extract: TaskHandle<_> = dag
|
let extract = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||||
.add_task(task_fn::<(), _, _>(move |_: ()| {
|
(0..count)
|
||||||
(0..count)
|
.map(|i| {
|
||||||
.map(|i| {
|
format!(
|
||||||
format!(
|
"{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}",
|
||||||
"{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}",
|
i,
|
||||||
i,
|
i,
|
||||||
i,
|
i * 10
|
||||||
i * 10
|
)
|
||||||
)
|
})
|
||||||
})
|
.collect::<Vec<_>>()
|
||||||
.collect::<Vec<_>>()
|
}));
|
||||||
}))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
// Transform: Three parallel transformations
|
// Transform: Three parallel transformations
|
||||||
|
|
||||||
@@ -92,7 +90,8 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
|||||||
))
|
))
|
||||||
.depends_on((&validate, &scores, &names));
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Memory efficiency benchmark with large data structures
|
//! Memory efficiency benchmark with large data structures
|
||||||
|
|
||||||
use criterion::{BenchmarkId, Criterion};
|
use criterion::{BenchmarkId, Criterion};
|
||||||
use dagx::{DagRunner, TaskHandle};
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
use std::hint::black_box;
|
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");
|
let mut group = c.benchmark_group("memory_efficiency");
|
||||||
|
|
||||||
// Test with different data sizes (in MB)
|
// 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(
|
group.bench_with_input(
|
||||||
BenchmarkId::new("large_data_20_consumers", format!("{}MB", mb_size)),
|
BenchmarkId::new("large_data_20_consumers", format!("{}MB", mb_size)),
|
||||||
mb_size,
|
mb_size,
|
||||||
|b, &mb| {
|
|b, &mb| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let source: TaskHandle<_> = dag
|
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||||
.add_task(task_fn::<(), _, _>(move |_: ()| {
|
// Create MB of data - framework wraps in Arc automatically
|
||||||
// Create MB of data - framework wraps in Arc automatically
|
vec![0u8; mb * 1024 * 1024]
|
||||||
vec![0u8; mb * 1024 * 1024]
|
}));
|
||||||
}))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
// 20 consumers share the data via automatic Arc wrapping
|
// 20 consumers share the data via automatic Arc wrapping
|
||||||
for i in 0..20 {
|
for i in 0..20 {
|
||||||
@@ -41,7 +39,8 @@ pub fn bench_memory_efficiency(c: &mut Criterion) {
|
|||||||
.depends_on(source);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Mixed realistic patterns - fan-out, fan-in, and processing
|
//! Mixed realistic patterns - fan-out, fan-in, and processing
|
||||||
|
|
||||||
use criterion::Criterion;
|
use criterion::Criterion;
|
||||||
use dagx::{DagRunner, TaskHandle};
|
use dagx::DagRunner;
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
pub fn bench_mixed_patterns(c: &mut Criterion) {
|
pub fn bench_mixed_patterns(c: &mut Criterion) {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
@@ -9,16 +9,14 @@ pub fn bench_mixed_patterns(c: &mut Criterion) {
|
|||||||
c.bench_function("mixed_patterns_realistic", |b| {
|
c.bench_function("mixed_patterns_realistic", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Stage 1: Load data (framework wraps in Arc automatically)
|
// Stage 1: Load data (framework wraps in Arc automatically)
|
||||||
let data: TaskHandle<_> = dag
|
let data = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
(0..1000)
|
||||||
(0..1000)
|
.map(|i| format!("Record-{:04}", i))
|
||||||
.map(|i| format!("Record-{:04}", i))
|
.collect::<Vec<_>>()
|
||||||
.collect::<Vec<_>>()
|
}));
|
||||||
}))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
// Stage 2: Fan-out for parallel analysis
|
// Stage 2: Fan-out for parallel analysis
|
||||||
let analysis1 = dag
|
let analysis1 = dag
|
||||||
@@ -54,7 +52,8 @@ pub fn bench_mixed_patterns(c: &mut Criterion) {
|
|||||||
.depends_on(summary);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -152,8 +152,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret
|
|||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// // Generated:
|
/// // Generated:
|
||||||
/// impl Task for Add {
|
/// impl Task<(i32, i32)> for Add {
|
||||||
/// type Input = (i32, i32);
|
|
||||||
/// type Output = i32;
|
/// type Output = i32;
|
||||||
///
|
///
|
||||||
/// async fn run(&mut self, input: Self::Input) -> Self::Output {
|
/// 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:
|
/// // Generated:
|
||||||
/// impl Task for Counter {
|
/// impl Task<(i32)> for Counter {
|
||||||
/// type Input = i32;
|
|
||||||
/// type Output = i32;
|
/// type Output = i32;
|
||||||
///
|
///
|
||||||
/// async fn run(&mut self, input: Self::Input) -> Self::Output {
|
/// async fn run(&mut self, input: Self::Input) -> Self::Output {
|
||||||
@@ -332,11 +330,10 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
|
|
||||||
// Generate the Task trait implementation based on whether we have self and async/sync
|
// Generate the Task trait implementation based on whether we have self and async/sync
|
||||||
quote! {
|
quote! {
|
||||||
impl ::dagx::Task for #struct_name {
|
impl ::dagx::Task<#input_type> for #struct_name {
|
||||||
type Input = #input_type;
|
|
||||||
type Output = #output_type;
|
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
|
#param_destructure
|
||||||
#run_call
|
#run_call
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#![cfg(not(tarpaulin_include))]
|
#![cfg(not(tarpaulin_include))]
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use crate::extract::ExtractInput;
|
use crate::extract::ExtractInput;
|
||||||
|
|
||||||
use dagx::{Task, TaskInput};
|
use dagx::{Task, TaskInput};
|
||||||
@@ -18,16 +20,16 @@ where
|
|||||||
O: Send,
|
O: Send,
|
||||||
{
|
{
|
||||||
f: F,
|
f: F,
|
||||||
_phantom: std::marker::PhantomData<fn(I) -> O>,
|
_phantom: PhantomData<fn(I) -> O>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I, O, F> Task for TaskFn<I, O, F>
|
impl<I, O, F> Task<I::Input> for TaskFn<I, O, F>
|
||||||
where
|
where
|
||||||
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
|
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
|
||||||
I: ExtractInput + Send + Sync + 'static,
|
I: ExtractInput + Send + Sync + 'static,
|
||||||
O: Send + Sync,
|
I::Input: Send + Sync + 'static,
|
||||||
|
O: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
type Input = I::Input;
|
|
||||||
type Output = O;
|
type Output = O;
|
||||||
|
|
||||||
async fn run(mut self, input: TaskInput<'_, I::Input>) -> O {
|
async fn run(mut self, input: TaskInput<'_, I::Input>) -> O {
|
||||||
@@ -45,6 +47,6 @@ where
|
|||||||
{
|
{
|
||||||
TaskFn {
|
TaskFn {
|
||||||
f,
|
f,
|
||||||
_phantom: std::marker::PhantomData,
|
_phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -15,7 +15,7 @@ impl Value { async fn run(&self) -> i32 { self.0 } }
|
|||||||
#[task]
|
#[task]
|
||||||
impl Add { async fn run(a: &i32, b: &i32) -> i32 { a + b } }
|
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 x = dag.add_task(Value(2));
|
||||||
let y = dag.add_task(Value(3));
|
let y = dag.add_task(Value(3));
|
||||||
let sum = dag.add_task(Add).depends_on((x, y));
|
let sum = dag.add_task(Add).depends_on((x, y));
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Most DAG libraries check for cycles when you execute the graph:
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Other libraries: cycle detected at RUNTIME
|
// Other libraries: cycle detected at RUNTIME
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let a = dag.add_task(TaskA);
|
let a = dag.add_task(TaskA);
|
||||||
let b = dag.add_task(TaskB);
|
let b = dag.add_task(TaskB);
|
||||||
a.depends_on(b);
|
a.depends_on(b);
|
||||||
@@ -20,7 +20,7 @@ dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
// dagx: cycle prevented at COMPILE TIME
|
// dagx: cycle prevented at COMPILE TIME
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let a_builder = dag.add_task(TaskA);
|
let a_builder = dag.add_task(TaskA);
|
||||||
let b_builder = dag.add_task(TaskB);
|
let b_builder = dag.add_task(TaskB);
|
||||||
|
|
||||||
|
|||||||
@@ -77,13 +77,13 @@ impl Add {
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Step 3: Create a new DAG
|
// Step 3: Create a new DAG
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Step 4: Add source tasks (tasks with no dependencies)
|
// Step 4: Add source tasks (tasks with no dependencies)
|
||||||
//
|
//
|
||||||
// add_task() returns a TaskHandle<T> where T is the task's output type
|
// add_task() returns a TaskHandle<T> where T is the task's output type
|
||||||
let x = dag.add_task(Value::new(2)).into(); // TaskHandle<i32>
|
let x = dag.add_task(Value::new(2)); // TaskHandle<i32>
|
||||||
let y = dag.add_task(Value::new(3)).into(); // TaskHandle<i32>
|
let y = dag.add_task(Value::new(3)); // TaskHandle<i32>
|
||||||
|
|
||||||
// Step 5: Add a task with dependencies
|
// 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)
|
// 1. Tasks with no dependencies run first (x and y in parallel)
|
||||||
// 2. Once x and y complete, sum runs (it depends on both)
|
// 2. Once x and y complete, sum runs (it depends on both)
|
||||||
println!("Running DAG...\n");
|
println!("Running DAG...\n");
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -113,9 +114,9 @@ async fn main() {
|
|||||||
//
|
//
|
||||||
// Use .get() with a TaskHandle to retrieve the output.
|
// Use .get() with a TaskHandle to retrieve the output.
|
||||||
// Results are not cloned, so you can only retrieve them once.
|
// 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
|
// Note: You can also retrieve intermediate results
|
||||||
assert_eq!(dag.get(x).unwrap(), 2);
|
assert_eq!(output.get(x).unwrap(), 2);
|
||||||
assert_eq!(dag.get(y).unwrap(), 3);
|
assert_eq!(output.get(y).unwrap(), 3);
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
//! Squared: 100
|
//! Squared: 100
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use dagx::{task, DagRunner, TaskHandle};
|
use dagx::{task, DagRunner};
|
||||||
|
|
||||||
// Source task with a value field
|
// Source task with a value field
|
||||||
struct Source {
|
struct Source {
|
||||||
@@ -127,10 +127,10 @@ impl Power {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Single source task - constructed with ::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
|
// Multiple downstream tasks consuming the same value
|
||||||
// Each constructed differently to show flexibility
|
// Each constructed differently to show flexibility
|
||||||
@@ -140,14 +140,15 @@ async fn main() {
|
|||||||
|
|
||||||
// Run the DAG
|
// Run the DAG
|
||||||
println!("Running fan-out DAG...\n");
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Print all results
|
// Print all results
|
||||||
println!("\nResults:");
|
println!("\nResults:");
|
||||||
println!(" Source: {}", dag.get(source).unwrap());
|
println!(" Source: {}", output.get(source).unwrap());
|
||||||
println!(" Plus One: {}", dag.get(plus_one).unwrap());
|
println!(" Plus One: {}", output.get(plus_one).unwrap());
|
||||||
println!(" Times Two: {}", dag.get(times_two).unwrap());
|
println!(" Times Two: {}", output.get(times_two).unwrap());
|
||||||
println!(" Squared: {}", dag.get(squared).unwrap());
|
println!(" Squared: {}", output.get(squared).unwrap());
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ impl BuildProfile {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Multiple independent source tasks
|
// Multiple independent source tasks
|
||||||
let name = dag.add_task(FetchName);
|
let name = dag.add_task(FetchName);
|
||||||
@@ -133,10 +133,11 @@ async fn main() {
|
|||||||
|
|
||||||
// Run the DAG
|
// Run the DAG
|
||||||
println!("Running fan-in DAG...\n");
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Print result
|
// Print result
|
||||||
println!("\n{}", dag.get(profile).unwrap());
|
println!("\n{}", output.get(profile).unwrap());
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ async fn main() -> DagResult<()> {
|
|||||||
println!("Running PARALLEL computation (4 workers on separate threads)...");
|
println!("Running PARALLEL computation (4 workers on separate threads)...");
|
||||||
let (parallel_time, parallel_result) = {
|
let (parallel_time, parallel_result) = {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Create 4 workers - dagx will run them in parallel
|
// Create 4 workers - dagx will run them in parallel
|
||||||
let sum1 = dag.add_task(ComputeSum::new(1, 251, WORK_DELAY));
|
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));
|
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?;
|
.await?;
|
||||||
|
|
||||||
let result = dag.get(total)?;
|
let result = output.get(total)?;
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
+28
-21
@@ -381,7 +381,7 @@ async fn main() {
|
|||||||
// Create shared circuit breaker: 3 failures trigger open, 3 second recovery timeout
|
// Create shared circuit breaker: 3 failures trigger open, 3 second recovery timeout
|
||||||
let circuit = Arc::new(CircuitBreaker::new(3, Duration::from_secs(3)));
|
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)
|
// Request 1: Success (circuit closed)
|
||||||
let call1 = dag.add_task(ProtectedServiceCall::new(
|
let call1 = dag.add_task(ProtectedServiceCall::new(
|
||||||
@@ -390,56 +390,60 @@ async fn main() {
|
|||||||
false, // success
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
println!("Result 1: {:?}\n", dag.get(call1).unwrap());
|
println!("Result 1: {:?}\n", output.get(call1).unwrap());
|
||||||
|
|
||||||
// Request 2: Failure (circuit closed, count = 1)
|
// Request 2: Failure (circuit closed, count = 1)
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let call2 = dag.add_task(ProtectedServiceCall::new(
|
let call2 = dag.add_task(ProtectedServiceCall::new(
|
||||||
"ServiceCall-2",
|
"ServiceCall-2",
|
||||||
circuit.clone(),
|
circuit.clone(),
|
||||||
true, // fail
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
println!("Result 2: {:?}\n", dag.get(call2).unwrap());
|
println!("Result 2: {:?}\n", output.get(call2).unwrap());
|
||||||
|
|
||||||
// Request 3: Failure (circuit closed, count = 2)
|
// Request 3: Failure (circuit closed, count = 2)
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let call3 = dag.add_task(ProtectedServiceCall::new(
|
let call3 = dag.add_task(ProtectedServiceCall::new(
|
||||||
"ServiceCall-3",
|
"ServiceCall-3",
|
||||||
circuit.clone(),
|
circuit.clone(),
|
||||||
true, // fail
|
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
|
.await
|
||||||
.unwrap();
|
.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!)
|
// Request 4: Failure (circuit closed, count = 3, opens!)
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let call4 = dag.add_task(ProtectedServiceCall::new(
|
let call4 = dag.add_task(ProtectedServiceCall::new(
|
||||||
"ServiceCall-4",
|
"ServiceCall-4",
|
||||||
circuit.clone(),
|
circuit.clone(),
|
||||||
true, // fail
|
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
|
.await
|
||||||
.unwrap();
|
.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
|
// 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(
|
let call5 = dag.add_task(ProtectedServiceCall::new(
|
||||||
"ServiceCall-5",
|
"ServiceCall-5",
|
||||||
circuit.clone(),
|
circuit.clone(),
|
||||||
@@ -450,11 +454,12 @@ async fn main() {
|
|||||||
.add_task(FallbackService::new("ServiceCall-5"))
|
.add_task(FallbackService::new("ServiceCall-5"))
|
||||||
.depends_on(call5);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
println!("Result 5: {:?}\n", dag.get(fallback5).unwrap());
|
println!("Result 5: {:?}\n", output.get(fallback5).unwrap());
|
||||||
|
|
||||||
// Example 2: Circuit recovery after timeout
|
// Example 2: Circuit recovery after timeout
|
||||||
println!("Example 2: Circuit recovery after timeout\n");
|
println!("Example 2: Circuit recovery after timeout\n");
|
||||||
@@ -462,32 +467,34 @@ async fn main() {
|
|||||||
sleep(Duration::from_secs(3)).await;
|
sleep(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
// Request 6: Circuit should be half-open, test succeeds, closes circuit
|
// 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(
|
let call6 = dag.add_task(ProtectedServiceCall::new(
|
||||||
"ServiceCall-6",
|
"ServiceCall-6",
|
||||||
circuit.clone(),
|
circuit.clone(),
|
||||||
false, // success
|
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
|
.await
|
||||||
.unwrap();
|
.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
|
// 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(
|
let call7 = dag.add_task(ProtectedServiceCall::new(
|
||||||
"ServiceCall-7",
|
"ServiceCall-7",
|
||||||
circuit.clone(),
|
circuit.clone(),
|
||||||
false, // success
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
println!("Result 7: {:?}\n", dag.get(call7).unwrap());
|
println!("Result 7: {:?}\n", output.get(call7).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("=== Circuit Breaker Example Complete ===");
|
println!("=== Circuit Breaker Example Complete ===");
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ impl GenerateReport {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
println!("Building data pipeline DAG...\n");
|
println!("Building data pipeline DAG...\n");
|
||||||
|
|
||||||
@@ -220,10 +220,11 @@ async fn main() {
|
|||||||
|
|
||||||
// Execute the pipeline
|
// Execute the pipeline
|
||||||
println!("Executing pipeline...\n");
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Print final report
|
// Print final report
|
||||||
println!("{}", dag.get(report).unwrap());
|
println!("{}", output.get(report).unwrap());
|
||||||
}
|
}
|
||||||
+16
-12
@@ -209,7 +209,7 @@ async fn main() {
|
|||||||
// Example 1: Successful path
|
// Example 1: Successful path
|
||||||
println!("Example 1: Valid input");
|
println!("Example 1: Valid input");
|
||||||
{
|
{
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let input = dag.add_task(StringSource("42"));
|
let input = dag.add_task(StringSource("42"));
|
||||||
|
|
||||||
@@ -222,11 +222,12 @@ async fn main() {
|
|||||||
let processed = dag.add_task(ProcessData).depends_on(validated);
|
let processed = dag.add_task(ProcessData).depends_on(validated);
|
||||||
let logged = dag.add_task(LogError).depends_on(processed);
|
let logged = dag.add_task(LogError).depends_on(processed);
|
||||||
|
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
match dag.get(logged).unwrap().as_ref() {
|
match output.get(logged).unwrap().as_ref() {
|
||||||
Ok(result) => println!("Final result: {}\n", result),
|
Ok(result) => println!("Final result: {}\n", result),
|
||||||
Err(e) => eprintln!("Final error: {}\n", e),
|
Err(e) => eprintln!("Final error: {}\n", e),
|
||||||
}
|
}
|
||||||
@@ -235,7 +236,7 @@ async fn main() {
|
|||||||
// Example 2: Parse error
|
// Example 2: Parse error
|
||||||
println!("Example 2: Invalid input (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"));
|
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 processed = dag.add_task(ProcessData).depends_on(validated);
|
||||||
let logged = dag.add_task(LogError).depends_on(processed);
|
let logged = dag.add_task(LogError).depends_on(processed);
|
||||||
|
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
match dag.get(logged).unwrap().as_ref() {
|
match output.get(logged).unwrap().as_ref() {
|
||||||
Ok(result) => println!("Final result: {}\n", result),
|
Ok(result) => println!("Final result: {}\n", result),
|
||||||
Err(e) => eprintln!("Final error: {}\n", e),
|
Err(e) => eprintln!("Final error: {}\n", e),
|
||||||
}
|
}
|
||||||
@@ -261,7 +263,7 @@ async fn main() {
|
|||||||
// Example 3: Validation error
|
// Example 3: Validation error
|
||||||
println!("Example 3: Out of range");
|
println!("Example 3: Out of range");
|
||||||
{
|
{
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let input = dag.add_task(StringSource("150"));
|
let input = dag.add_task(StringSource("150"));
|
||||||
|
|
||||||
@@ -274,11 +276,12 @@ async fn main() {
|
|||||||
let processed = dag.add_task(ProcessData).depends_on(validated);
|
let processed = dag.add_task(ProcessData).depends_on(validated);
|
||||||
let logged = dag.add_task(LogError).depends_on(processed);
|
let logged = dag.add_task(LogError).depends_on(processed);
|
||||||
|
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
match dag.get(logged).unwrap().as_ref() {
|
match output.get(logged).unwrap().as_ref() {
|
||||||
Ok(result) => println!("Final result: {}\n", result),
|
Ok(result) => println!("Final result: {}\n", result),
|
||||||
Err(e) => eprintln!("Final error: {}\n", e),
|
Err(e) => eprintln!("Final error: {}\n", e),
|
||||||
}
|
}
|
||||||
@@ -287,7 +290,7 @@ async fn main() {
|
|||||||
// Example 4: Error recovery with fallback
|
// Example 4: Error recovery with fallback
|
||||||
println!("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"));
|
let input = dag.add_task(StringSource("invalid"));
|
||||||
|
|
||||||
@@ -297,11 +300,12 @@ async fn main() {
|
|||||||
.add_task(WithFallback { fallback: 0 })
|
.add_task(WithFallback { fallback: 0 })
|
||||||
.depends_on(parsed);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = dag.get(with_fallback).unwrap();
|
let result = output.get(with_fallback).unwrap();
|
||||||
println!("Recovered with fallback: {}\n", result);
|
println!("Recovered with fallback: {}\n", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,14 +157,14 @@ section "TESTING"
|
|||||||
|
|
||||||
echo "→ Running library unit tests..."
|
echo "→ Running library unit tests..."
|
||||||
cargo test --lib || check_fail "Library unit tests failed"
|
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 ""
|
||||||
echo "→ Running integration tests with all feature combinations..."
|
echo "→ Running integration tests with all feature combinations..."
|
||||||
cargo test --test '*' --no-default-features || check_fail "Integration tests failed (no features)"
|
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 '*' --all-features || check_fail "Integration tests failed (all features)"
|
||||||
cargo test --test '*' || check_fail "Integration tests failed (default 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 ""
|
||||||
echo "→ Running doc tests with all features..."
|
echo "→ Running doc tests with all features..."
|
||||||
|
|||||||
+140
-16
@@ -3,10 +3,11 @@
|
|||||||
//! TaskBuilder uses compile-time type checking to ensure dependencies are wired correctly.
|
//! TaskBuilder uses compile-time type checking to ensure dependencies are wired correctly.
|
||||||
//! The type parameter tracks whether dependencies have been specified yet.
|
//! The type parameter tracks whether dependencies have been specified yet.
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use crate::deps::DepsTuple;
|
use crate::deps::DepsTuple;
|
||||||
use crate::runner::DagRunner;
|
use crate::runner::DagRunner;
|
||||||
use crate::task::Task;
|
use crate::task::Task;
|
||||||
use crate::types::{NodeId, TaskHandle};
|
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
@@ -57,17 +58,26 @@ use tracing::debug;
|
|||||||
/// let b = b.depends_on(a);
|
/// let b = b.depends_on(a);
|
||||||
/// // Now b is a TaskHandle<i32>
|
/// // Now b is a TaskHandle<i32>
|
||||||
///
|
///
|
||||||
/// dag.run(|fut| 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(b).unwrap(), 20);
|
/// 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>,
|
||||||
|
Input: Send + Sync + 'static,
|
||||||
|
{
|
||||||
pub(crate) id: NodeId,
|
pub(crate) id: NodeId,
|
||||||
pub(crate) dag: &'a DagRunner,
|
pub(crate) dag: &'a mut DagRunner,
|
||||||
pub(crate) _phantom: std::marker::PhantomData<(Tk, Deps)>,
|
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>,
|
||||||
|
Input: Send + Sync + 'static,
|
||||||
|
{
|
||||||
/// Provide all dependencies exactly once as a tuple.
|
/// Provide all dependencies exactly once as a tuple.
|
||||||
///
|
///
|
||||||
/// The dependencies must match the task's `Input` type exactly:
|
/// The dependencies must match the task's `Input` type exactly:
|
||||||
@@ -107,7 +117,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
|
|||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// # async {
|
/// # async {
|
||||||
/// let dag = DagRunner::new();
|
/// let mut dag = DagRunner::new();
|
||||||
///
|
///
|
||||||
/// let x = dag.add_task(Value(2)).into();
|
/// let x = dag.add_task(Value(2)).into();
|
||||||
/// let y = dag.add_task(Value(3));
|
/// let y = dag.add_task(Value(3));
|
||||||
@@ -118,13 +128,13 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
|
|||||||
/// // Multiple dependencies: tuple form
|
/// // Multiple dependencies: tuple form
|
||||||
/// let sum = dag.add_task(Add).depends_on((&x, y));
|
/// let sum = dag.add_task(Add).depends_on((&x, y));
|
||||||
///
|
///
|
||||||
/// dag.run(|fut| 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)]
|
#[allow(private_bounds)]
|
||||||
pub fn depends_on<D>(self, deps: D) -> TaskHandle<Tk::Output>
|
pub fn depends_on<D>(self, deps: D) -> TaskHandle<Tk::Output>
|
||||||
where
|
where
|
||||||
D: DepsTuple<Tk::Input>,
|
D: DepsTuple<Input>,
|
||||||
{
|
{
|
||||||
// Register dependencies in the DAG
|
// Register dependencies in the DAG
|
||||||
let dep_ids = deps.to_node_ids();
|
let dep_ids = deps.to_node_ids();
|
||||||
@@ -137,27 +147,141 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
|
|||||||
"wiring task dependencies"
|
"wiring task dependencies"
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut edges = self.dag.edges.lock();
|
|
||||||
let mut dependents = self.dag.dependents.lock();
|
|
||||||
|
|
||||||
for &dep_id in &dep_ids {
|
for &dep_id in &dep_ids {
|
||||||
// Add edge from this node to dependency
|
// 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);
|
node_edges.push(dep_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this node as dependent of the dependency
|
// 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);
|
node_dependents.push(self.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskHandle {
|
TaskHandle {
|
||||||
id: self.id,
|
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<T>` 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<T> {
|
||||||
|
pub(crate) id: NodeId,
|
||||||
|
pub(crate) _phantom: PhantomData<fn() -> T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for TaskHandle<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Copy for TaskHandle<T> {}
|
||||||
|
|
||||||
|
// TaskHandle can be converted from &TaskHandle (for .get() calls)
|
||||||
|
impl<T> From<&TaskHandle<T>> for TaskHandle<T> {
|
||||||
|
fn from(handle: &TaskHandle<T>) -> 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<Input>: Task<Input> + 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<Tk> TaskWire<()> for Tk
|
||||||
|
where
|
||||||
|
Tk: Task<()> + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Retval<'dag> = TaskHandle<Tk::Output>;
|
||||||
|
|
||||||
|
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<Tk, $($T: Send + Sync + 'static),+> 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)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod coverage_tests;
|
||||||
@@ -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
|
// Create task with initial state
|
||||||
let builder = dag.add_task(StatefulTask::new(21));
|
let builder = dag.add_task(StatefulTask::new(21));
|
||||||
|
|
||||||
// Explicit conversion - triggers line 71
|
// 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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(dag.get(handle).unwrap(), 42);
|
assert_eq!(output.get(handle).unwrap(), 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 = dag.add_task(TestTask);
|
||||||
let builder_id = builder.id;
|
let builder_id = builder.id;
|
||||||
|
|
||||||
// Convert without running the DAG
|
// Convert without running the DAG
|
||||||
let handle: TaskHandle<String> = builder.into();
|
let handle: TaskHandle<String> = builder;
|
||||||
|
|
||||||
// Verify the ID was transferred correctly
|
// Verify the ID was transferred correctly
|
||||||
assert_eq!(handle.id, builder_id);
|
assert_eq!(handle.id, builder_id);
|
||||||
+10
-11
@@ -1,7 +1,6 @@
|
|||||||
//! Unit tests for builder module
|
//! Unit tests for builder module
|
||||||
|
|
||||||
use crate::runner::DagRunner;
|
use crate::runner::DagRunner;
|
||||||
use crate::types::TaskHandle;
|
|
||||||
|
|
||||||
// Initialize tracing subscriber for tests (idempotent)
|
// Initialize tracing subscriber for tests (idempotent)
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
@@ -47,11 +46,11 @@ impl TestTaskWithInput {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_task_builder_from_conversion() {
|
fn test_task_builder_from_conversion() {
|
||||||
// Test line 165 in builder.rs - the From implementation
|
// 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 });
|
let builder = dag.add_task(TestTask { value: 42 });
|
||||||
|
|
||||||
// Convert builder to TaskHandle
|
// Convert builder to TaskHandle
|
||||||
let handle: TaskHandle<_> = builder.into();
|
let handle = builder;
|
||||||
|
|
||||||
// Check that the ID is preserved
|
// Check that the ID is preserved
|
||||||
assert_eq!(handle.id.0, 0); // First task should have ID 0
|
assert_eq!(handle.id.0, 0); // First task should have ID 0
|
||||||
@@ -59,17 +58,17 @@ fn test_task_builder_from_conversion() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_task_builder_chain() {
|
fn test_task_builder_chain() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Test chaining multiple tasks
|
// 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 t2 = dag.add_task(TestTaskWithInput).depends_on(t1);
|
||||||
let t3 = dag.add_task(TestTaskWithInput).depends_on(t2);
|
let t3 = dag.add_task(TestTaskWithInput).depends_on(t2);
|
||||||
|
|
||||||
// All should return TaskHandles
|
// All should return TaskHandles
|
||||||
let _: TaskHandle<_> = t1;
|
let _ = t1;
|
||||||
let _: TaskHandle<_> = t2;
|
let _ = t2;
|
||||||
let _: TaskHandle<_> = t3;
|
let _ = t3;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 a = dag.add_task(TestTask { value: 10 });
|
||||||
let b = dag.add_task(TestTask { value: 20 });
|
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));
|
let sum = dag.add_task(AddTask).depends_on((a, b));
|
||||||
|
|
||||||
// Should return a TaskHandle
|
// Should return a TaskHandle
|
||||||
let _: TaskHandle<_> = sum;
|
let _ = sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_task_builder_stores_correct_id() {
|
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 builder1 = dag.add_task(TestTask { value: 1 });
|
||||||
let builder2 = dag.add_task(TestTask { value: 2 });
|
let builder2 = dag.add_task(TestTask { value: 2 });
|
||||||
|
|||||||
+18
-46
@@ -7,36 +7,7 @@
|
|||||||
//! This trait is internal and not meant for external implementation. Users should only
|
//! This trait is internal and not meant for external implementation. Users should only
|
||||||
//! use the provided implementations.
|
//! use the provided implementations.
|
||||||
|
|
||||||
use crate::builder::TaskBuilder;
|
use crate::builder::{NodeId, TaskHandle};
|
||||||
use crate::task::Task;
|
|
||||||
use crate::types::{NodeId, TaskHandle};
|
|
||||||
|
|
||||||
trait IntoNodeRef {
|
|
||||||
type Output;
|
|
||||||
fn into_node_ref(self) -> NodeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> IntoNodeRef for &TaskHandle<T> {
|
|
||||||
type Output = T;
|
|
||||||
fn into_node_ref(self) -> NodeId {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> IntoNodeRef for TaskHandle<T> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait DepsTuple<Input> {
|
pub(crate) trait DepsTuple<Input> {
|
||||||
fn to_node_ids(self) -> Vec<NodeId>;
|
fn to_node_ids(self) -> Vec<NodeId>;
|
||||||
@@ -50,21 +21,21 @@ impl DepsTuple<()> for () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implementation for single dependency (all forms)
|
// Implementation for single dependency (all forms)
|
||||||
impl<T> DepsTuple<(T::Output,)> for T
|
impl<T, O> DepsTuple<(O,)> for T
|
||||||
where
|
where
|
||||||
T: IntoNodeRef,
|
T: Into<TaskHandle<O>>,
|
||||||
{
|
{
|
||||||
fn to_node_ids(self) -> Vec<NodeId> {
|
fn to_node_ids(self) -> Vec<NodeId> {
|
||||||
vec![self.into_node_ref()]
|
vec![self.into().id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> DepsTuple<T::Output> for (T,)
|
impl<T, O> DepsTuple<O> for (T,)
|
||||||
where
|
where
|
||||||
T: IntoNodeRef,
|
T: Into<TaskHandle<O>>,
|
||||||
{
|
{
|
||||||
fn to_node_ids(self) -> Vec<NodeId> {
|
fn to_node_ids(self) -> Vec<NodeId> {
|
||||||
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)))
|
/// task.depends_on((&dag.add_task(TaskA), &dag.add_task(TaskB)))
|
||||||
/// ```
|
/// ```
|
||||||
macro_rules! impl_deps_tuple {
|
macro_rules! impl_deps_tuple {
|
||||||
($($T:ident),+) => {
|
($($T:ident : $O:ident),+) => {
|
||||||
// For tuples of any combination of &TaskHandle, TaskHandle, and TaskBuilder
|
// For tuples of any combination of &TaskHandle, TaskHandle, and TaskBuilder
|
||||||
impl<$($T: IntoNodeRef),+> DepsTuple<($($T::Output,)+)> for ($($T,)+) {
|
impl<$($T: Into<TaskHandle<$O>>, $O),+> DepsTuple<($($O,)+)> for ($($T,)+)
|
||||||
|
{
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn to_node_ids(self) -> Vec<NodeId> {
|
fn to_node_ids(self) -> Vec<NodeId> {
|
||||||
let ($($T,)+) = self;
|
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.
|
// Generate DepsTuple implementations for tuples of size 2-8.
|
||||||
// Supporting up to 8 elements covers the vast majority of use cases.
|
// Supporting up to 8 elements covers the vast majority of use cases.
|
||||||
impl_deps_tuple!(T1, T2);
|
impl_deps_tuple!(T1:O1, T2:O2);
|
||||||
impl_deps_tuple!(T1, T2, T3);
|
impl_deps_tuple!(T1:O1, T2:O2, T3:O3);
|
||||||
impl_deps_tuple!(T1, T2, T3, T4);
|
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4);
|
||||||
impl_deps_tuple!(T1, T2, T3, T4, T5);
|
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5);
|
||||||
impl_deps_tuple!(T1, T2, T3, T4, T5, T6);
|
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5, T6:O6);
|
||||||
impl_deps_tuple!(T1, T2, T3, T4, T5, T6, T7);
|
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5, T6:O6, T7:O7);
|
||||||
impl_deps_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
|
impl_deps_tuple!(T1:O1, T2:O2, T3:O3, T4:O4, T5:O5, T6:O6, T7:O7, T8:O8);
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
//! Unit tests for deps module
|
//! Unit tests for deps module
|
||||||
|
|
||||||
|
use crate::builder::{NodeId, TaskHandle};
|
||||||
use crate::deps::DepsTuple;
|
use crate::deps::DepsTuple;
|
||||||
use crate::runner::DagRunner;
|
use crate::runner::DagRunner;
|
||||||
use crate::types::{NodeId, TaskHandle};
|
|
||||||
|
|
||||||
// Test task for dependency testing
|
// Test task for dependency testing
|
||||||
struct TestTask {
|
struct TestTask {
|
||||||
@@ -78,7 +78,7 @@ fn test_deps_tuple_single_handle_tuple_owned() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_deps_tuple_task_builder_ref() {
|
fn test_deps_tuple_task_builder_ref() {
|
||||||
// Test lines 51-55 in deps.rs - &TaskBuilder
|
// 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 = dag.add_task(TestTask { value: 42 });
|
||||||
let builder_id = builder.id;
|
let builder_id = builder.id;
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ fn test_deps_tuple_task_builder_ref() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_deps_tuple_task_builder_tuple() {
|
fn test_deps_tuple_task_builder_tuple() {
|
||||||
// Test lines 57-61 in deps.rs - (&TaskBuilder,)
|
// 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 = dag.add_task(TestTask { value: 100 });
|
||||||
let builder_id = builder.id;
|
let builder_id = builder.id;
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ fn test_deps_tuple_multiple_handles() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_deps_tuple_multiple_builders() {
|
fn test_deps_tuple_multiple_builders() {
|
||||||
// Test macro-generated implementations for 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 = dag.add_task(TestTask { value: 10 });
|
||||||
let builder1_id = builder1.id;
|
let builder1_id = builder1.id;
|
||||||
let builder2 = dag.add_task(TestTask { value: 20 });
|
let builder2 = dag.add_task(TestTask { value: 20 });
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ pub enum DagError {
|
|||||||
TaskPanicked { task_id: u32, panic_message: String },
|
TaskPanicked { task_id: u32, panic_message: String },
|
||||||
/// Result not found for task
|
/// Result not found for task
|
||||||
ResultNotFound { task_id: u32 },
|
ResultNotFound { task_id: u32 },
|
||||||
/// Attempted to run the DAG while it's already running
|
|
||||||
ConcurrentExecution,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for DagError {
|
impl std::fmt::Display for DagError {
|
||||||
@@ -65,14 +63,6 @@ impl std::fmt::Display for DagError {
|
|||||||
task_id
|
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."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,16 +54,6 @@ fn test_dag_error_display_result_not_found() {
|
|||||||
assert!(display.contains("Call 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]
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dag_error_std_error_impl() {
|
fn test_dag_error_std_error_impl() {
|
||||||
// Test that DagError implements std::error::Error
|
// Test that DagError implements std::error::Error
|
||||||
|
|||||||
+36
-35
@@ -42,18 +42,18 @@
|
|||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! # async {
|
//! # async {
|
||||||
//! let dag = DagRunner::new();
|
//! let mut dag = DagRunner::new();
|
||||||
//!
|
//!
|
||||||
//! // Add source tasks
|
//! // Add source tasks
|
||||||
//! let x = dag.add_task(Value(2));
|
//! 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
|
//! // Add task with dependencies
|
||||||
//! let sum = dag.add_task(Add).depends_on((x, &y));
|
//! let sum = dag.add_task(Add).depends_on((x, &y));
|
||||||
//!
|
//!
|
||||||
//! // Execute and retrieve results
|
//! // Execute and retrieve results
|
||||||
//! dag.run(|fut| 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);
|
||||||
//! # };
|
//! # };
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
//!
|
//!
|
||||||
//! A [`TaskHandle<T>`] is a typed, opaque reference to a task's output. Use it to:
|
//! A [`TaskHandle<T>`] is a typed, opaque reference to a task's output. Use it to:
|
||||||
//! - Wire dependencies between tasks
|
//! - Wire dependencies between tasks
|
||||||
//! - Retrieve results after execution with [`DagRunner::get`]
|
//! - Retrieve results after execution with [`DagOutput::get`]
|
||||||
//!
|
//!
|
||||||
//! ## Dependency Patterns
|
//! ## Dependency Patterns
|
||||||
//!
|
//!
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
//! # async fn run(&self) -> i32 { self.0 }
|
//! # async fn run(&self) -> i32 { self.0 }
|
||||||
//! # }
|
//! # }
|
||||||
//! # async {
|
//! # async {
|
||||||
//! # let dag = DagRunner::new();
|
//! # let mut dag = DagRunner::new();
|
||||||
//! let source = dag.add_task(Value(42));
|
//! let source = dag.add_task(Value(42));
|
||||||
//! # };
|
//! # };
|
||||||
//! ```
|
//! ```
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
//! async fn run(input: &i32) -> i32 { input * 2 }
|
//! async fn run(input: &i32) -> i32 { input * 2 }
|
||||||
//! }
|
//! }
|
||||||
//! # async {
|
//! # async {
|
||||||
//! # let dag = DagRunner::new();
|
//! # let mut dag = DagRunner::new();
|
||||||
//! # let source = dag.add_task(Value(42));
|
//! # let source = dag.add_task(Value(42));
|
||||||
//! let doubled = dag.add_task(Double).depends_on(source);
|
//! let doubled = dag.add_task(Double).depends_on(source);
|
||||||
//! # };
|
//! # };
|
||||||
@@ -275,7 +275,7 @@
|
|||||||
//! async fn run(a: &i32, b: &i32) -> i32 { a + b }
|
//! async fn run(a: &i32, b: &i32) -> i32 { a + b }
|
||||||
//! }
|
//! }
|
||||||
//! # async {
|
//! # async {
|
||||||
//! # let dag = DagRunner::new();
|
//! # let mut dag = DagRunner::new();
|
||||||
//! # let x = dag.add_task(Value(2));
|
//! # let x = dag.add_task(Value(2));
|
||||||
//! # let y = dag.add_task(Value(3));
|
//! # let y = dag.add_task(Value(3));
|
||||||
//! let sum = dag.add_task(Add).depends_on((x, y));
|
//! let sum = dag.add_task(Add).depends_on((x, y));
|
||||||
@@ -317,16 +317,16 @@
|
|||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! # async {
|
//! # async {
|
||||||
//! let dag = DagRunner::new();
|
//! let mut dag = DagRunner::new();
|
||||||
//!
|
//!
|
||||||
//! let base = dag.add_task(Value(10)).into();
|
//! let base = dag.add_task(Value(10)).into();
|
||||||
//! let plus1 = dag.add_task(Add(1)).depends_on(&base);
|
//! let plus1 = dag.add_task(Add(1)).depends_on(&base);
|
||||||
//! let times2 = dag.add_task(Scale(2)).depends_on(&base);
|
//! let times2 = dag.add_task(Scale(2)).depends_on(&base);
|
||||||
//!
|
//!
|
||||||
//! dag.run(|fut| 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!(output.get(plus1).unwrap(), 11);
|
||||||
//! assert_eq!(dag.get(times2).unwrap(), 20);
|
//! assert_eq!(output.get(times2).unwrap(), 20);
|
||||||
//! # };
|
//! # };
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
@@ -373,16 +373,16 @@
|
|||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! # async {
|
//! # async {
|
||||||
//! let dag = DagRunner::new();
|
//! let mut dag = DagRunner::new();
|
||||||
//!
|
//!
|
||||||
//! let name = dag.add_task(Name("Alice".to_string()));
|
//! let name = dag.add_task(Name("Alice".to_string()));
|
||||||
//! let age = dag.add_task(Age(30));
|
//! let age = dag.add_task(Age(30));
|
||||||
//! let active = dag.add_task(Active(true));
|
//! let active = dag.add_task(Active(true));
|
||||||
//! let result = dag.add_task(FormatUser).depends_on((name, age, active));
|
//! let result = dag.add_task(FormatUser).depends_on((name, age, active));
|
||||||
//!
|
//!
|
||||||
//! dag.run(|fut| 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 {
|
//! # async {
|
||||||
//! let dag = DagRunner::new();
|
//! let mut dag = DagRunner::new();
|
||||||
//!
|
//!
|
||||||
//! // Layer 1: Sources
|
//! // Layer 1: Sources
|
||||||
//! let x = dag.add_task(Value(2));
|
//! let x = dag.add_task(Value(2));
|
||||||
@@ -433,9 +433,9 @@
|
|||||||
//! // Layer 3: Final result
|
//! // Layer 3: Final result
|
||||||
//! let total = dag.add_task(Add).depends_on((&sum_xy, &prod_yz)); // 5 + 15 = 20
|
//! let total = dag.add_task(Add).depends_on((&sum_xy, &prod_yz)); // 5 + 15 = 20
|
||||||
//!
|
//!
|
||||||
//! dag.run(|fut| 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 {
|
//! # async {
|
||||||
//! let dag = DagRunner::new();
|
//! let mut dag = DagRunner::new();
|
||||||
//!
|
//!
|
||||||
//! let user = dag.add_task(CreateUser);
|
//! let user = dag.add_task(CreateUser);
|
||||||
//! let formatted = dag.add_task(FormatUser).depends_on(user);
|
//! let formatted = dag.add_task(FormatUser).depends_on(user);
|
||||||
//!
|
//!
|
||||||
//! dag.run(|fut| 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
|
//! // With Tokio
|
||||||
//! #[tokio::main]
|
//! #[tokio::main]
|
||||||
//! async fn main() {
|
//! async fn main() {
|
||||||
//! let dag = DagRunner::new();
|
//! let mut dag = DagRunner::new();
|
||||||
//! // ... build and run DAG
|
//! // ... build and run DAG
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! // With async-std
|
//! // With async-std
|
||||||
//! #[async_std::main]
|
//! #[async_std::main]
|
||||||
//! async fn main() {
|
//! async fn main() {
|
||||||
//! let dag = DagRunner::new();
|
//! let mut dag = DagRunner::new();
|
||||||
//! // ... build and run DAG
|
//! // ... build and run DAG
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! // With smol
|
//! // With smol
|
||||||
//! fn main() {
|
//! fn main() {
|
||||||
//! smol::block_on(async {
|
//! smol::block_on(async {
|
||||||
//! let dag = DagRunner::new();
|
//! let mut dag = DagRunner::new();
|
||||||
//! // ... build and run DAG
|
//! // ... build and run DAG
|
||||||
//! });
|
//! });
|
||||||
//! }
|
//! }
|
||||||
@@ -522,9 +522,9 @@
|
|||||||
//! dagx uses [`DagResult<T>`] (an alias for `Result<T, DagError>`) for operations that
|
//! dagx uses [`DagResult<T>`] (an alias for `Result<T, DagError>`) for operations that
|
||||||
//! can fail:
|
//! can fail:
|
||||||
//!
|
//!
|
||||||
//! - [`DagRunner::run`] returns `DagResult<()>` and can fail if tasks panic or if multiple
|
//! - [`DagRunner::run`] returns `DagResult<DagOutput>` and can fail if tasks panic or if multiple
|
||||||
//! concurrent runs are attempted
|
//! concurrent runs are attempted
|
||||||
//! - [`DagRunner::get`] returns `DagResult<T>` and can fail if the task hasn't executed or
|
//! - [`DagOutput::get`] returns `DagResult<T>` and can fail if the task hasn't executed or
|
||||||
//! the handle is invalid
|
//! the handle is invalid
|
||||||
//!
|
//!
|
||||||
//! You can handle errors explicitly or use `.unwrap()` for simple cases:
|
//! You can handle errors explicitly or use `.unwrap()` for simple cases:
|
||||||
@@ -538,11 +538,12 @@
|
|||||||
//! # async fn run(&self) -> i32 { self.0 }
|
//! # async fn run(&self) -> i32 { self.0 }
|
||||||
//! # }
|
//! # }
|
||||||
//! # async {
|
//! # async {
|
||||||
//! # let dag = DagRunner::new();
|
//! # let mut dag = DagRunner::new();
|
||||||
//! # let node = dag.add_task(Value(42));
|
//! # let node = dag.add_task(Value(42));
|
||||||
//! // Simple approach with .unwrap()
|
//! // Simple approach with .unwrap()
|
||||||
//! dag.run(|fut| 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 = dag.get(node).unwrap();
|
//! let result = output.get(node).unwrap();
|
||||||
|
//! # let mut dag = DagRunner::new();
|
||||||
//!
|
//!
|
||||||
//! // Or handle errors explicitly
|
//! // Or handle errors explicitly
|
||||||
//! match dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await {
|
//! match dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await {
|
||||||
@@ -651,7 +652,7 @@
|
|||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! # async {
|
//! # async {
|
||||||
//! let dag = DagRunner::new();
|
//! let mut dag = DagRunner::new();
|
||||||
//! let data = dag.add_task(FetchData).into();
|
//! let data = dag.add_task(FetchData).into();
|
||||||
//!
|
//!
|
||||||
//! // All three tasks get efficient Arc-wrapped sharing automatically
|
//! // All three tasks get efficient Arc-wrapped sharing automatically
|
||||||
@@ -659,7 +660,7 @@
|
|||||||
//! let task2 = dag.add_task(ProcessData).depends_on(&data);
|
//! let task2 = dag.add_task(ProcessData).depends_on(&data);
|
||||||
//! let task3 = dag.add_task(ProcessData).depends_on(&data);
|
//! let task3 = dag.add_task(ProcessData).depends_on(&data);
|
||||||
//!
|
//!
|
||||||
//! dag.run(|fut| 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 +669,7 @@
|
|||||||
//! - Framework wraps it in `Arc<T>` internally
|
//! - Framework wraps it in `Arc<T>` internally
|
||||||
//! - For fan-out (1→N), Arc is cloned N times (just atomic pointer increments - O(1))
|
//! - 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
|
//! - 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:**
|
//! **Performance characteristics:**
|
||||||
//! - **Heap types** (Vec, String, HashMap): Arc overhead is negligible (~few ns)
|
//! - **Heap types** (Vec, String, HashMap): Arc overhead is negligible (~few ns)
|
||||||
@@ -771,16 +772,16 @@ mod builder;
|
|||||||
mod deps;
|
mod deps;
|
||||||
mod error;
|
mod error;
|
||||||
mod node;
|
mod node;
|
||||||
|
mod output;
|
||||||
mod runner;
|
mod runner;
|
||||||
mod task;
|
mod task;
|
||||||
mod types;
|
|
||||||
|
|
||||||
// Public re-exports
|
// Public re-exports
|
||||||
pub use builder::TaskBuilder;
|
pub use builder::{TaskBuilder, TaskHandle};
|
||||||
pub use error::{DagError, DagResult};
|
pub use error::{DagError, DagResult};
|
||||||
|
pub use output::DagOutput;
|
||||||
pub use runner::DagRunner;
|
pub use runner::DagRunner;
|
||||||
pub use task::{Task, TaskInput};
|
pub use task::{Task, TaskInput};
|
||||||
pub use types::{Pending, TaskHandle};
|
|
||||||
|
|
||||||
// Re-export the procedural macro
|
// Re-export the procedural macro
|
||||||
pub use dagx_macros::task;
|
pub use dagx_macros::task;
|
||||||
+19
-6
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
use std::marker::PhantomData;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -50,20 +51,32 @@ pub(crate) trait ExecutableNode: Send {
|
|||||||
/// Each task executes exactly once, taking ownership and producing an output.
|
/// Each task executes exactly once, taking ownership and producing an output.
|
||||||
///
|
///
|
||||||
/// The output is also returned from execute_with_deps for central storage.
|
/// The output is also returned from execute_with_deps for central storage.
|
||||||
pub(crate) struct TypedNode<T: Task> {
|
pub(crate) struct TypedNode<Input, T>
|
||||||
|
where
|
||||||
|
Input: Send + Sync + 'static,
|
||||||
|
T: Task<Input> + 'static,
|
||||||
|
{
|
||||||
pub(crate) task: T,
|
pub(crate) task: T,
|
||||||
|
phantom: PhantomData<Input>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Task + 'static> TypedNode<T> {
|
impl<Input, T> TypedNode<Input, T>
|
||||||
|
where
|
||||||
|
T: Task<Input>,
|
||||||
|
Input: Send + Sync + 'static,
|
||||||
|
{
|
||||||
pub(crate) fn new(task: T) -> Self {
|
pub(crate) fn new(task: T) -> Self {
|
||||||
Self { task }
|
Self {
|
||||||
|
task,
|
||||||
|
phantom: PhantomData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Task + 'static> ExecutableNode for TypedNode<T>
|
impl<Input, T> ExecutableNode for TypedNode<Input, T>
|
||||||
where
|
where
|
||||||
T::Input: 'static,
|
T: Task<Input>,
|
||||||
T::Output: 'static,
|
Input: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
fn execute_with_deps(
|
fn execute_with_deps(
|
||||||
self: Box<Self>,
|
self: Box<Self>,
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use std::{any::Any, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{builder::NodeId, runner::PassThroughHashMap, DagError, DagResult, TaskHandle};
|
||||||
|
|
||||||
|
pub struct DagOutput {
|
||||||
|
outputs: PassThroughHashMap<NodeId, Arc<dyn Any + Send + Sync + 'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<T: 'static + Send + Sync, H>(&mut self, handle: H) -> DagResult<T>
|
||||||
|
where
|
||||||
|
H: Into<TaskHandle<T>>,
|
||||||
|
{
|
||||||
|
let handle: TaskHandle<T> = handle.into();
|
||||||
|
|
||||||
|
let arc_output = self
|
||||||
|
.outputs
|
||||||
|
.remove(&handle.id)
|
||||||
|
.ok_or(DagError::ResultNotFound {
|
||||||
|
task_id: handle.id.0,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Downcast Arc<dyn Any> to Arc<T>
|
||||||
|
let output = arc_output
|
||||||
|
.downcast::<T>()
|
||||||
|
.map_err(|_| DagError::TypeMismatch {
|
||||||
|
expected: std::any::type_name::<T>(),
|
||||||
|
found: "unknown",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Arc::into_inner(output).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn new(
|
||||||
|
outputs: PassThroughHashMap<NodeId, Arc<dyn Any + Send + Sync + 'static>>,
|
||||||
|
) -> Self {
|
||||||
|
Self { outputs }
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
-181
@@ -10,34 +10,18 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::hash::{BuildHasher, Hasher};
|
use std::hash::{BuildHasher, Hasher};
|
||||||
use std::panic::AssertUnwindSafe;
|
use std::panic::AssertUnwindSafe;
|
||||||
use std::sync::{
|
use std::sync::Arc;
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use futures_util::future::BoxFuture;
|
use futures_util::future::BoxFuture;
|
||||||
use futures_util::{stream::FuturesUnordered, FutureExt, StreamExt, TryFutureExt};
|
use futures_util::{stream::FuturesUnordered, FutureExt, StreamExt, TryFutureExt};
|
||||||
use parking_lot::Mutex;
|
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
use tracing::{debug, error, info, trace};
|
use tracing::{debug, error, info, trace};
|
||||||
|
|
||||||
use crate::builder::TaskBuilder;
|
use crate::builder::{NodeId, TaskWire};
|
||||||
use crate::error::{DagError, DagResult};
|
use crate::error::{DagError, DagResult};
|
||||||
use crate::node::{ExecutableNode, TypedNode};
|
use crate::node::{ExecutableNode, TypedNode};
|
||||||
use crate::task::Task;
|
use crate::DagOutput;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub(crate) struct PassThroughHasher {
|
pub(crate) struct PassThroughHasher {
|
||||||
@@ -76,10 +60,10 @@ pub(crate) type PassThroughHashMap<K, V> = HashMap<K, V, PassThroughHasher>;
|
|||||||
/// # Workflow
|
/// # Workflow
|
||||||
///
|
///
|
||||||
/// 1. Create a new DAG with [`DagRunner::new`]
|
/// 1. Create a new DAG with [`DagRunner::new`]
|
||||||
/// 2. Add tasks with [`DagRunner::add_task`] to get [`TaskBuilder`] builders
|
/// 2. Add tasks with [`DagRunner::add_task`] to get builders
|
||||||
/// 3. Wire dependencies with [`TaskBuilder::depends_on`]
|
/// 3. Wire dependencies with [`crate::TaskBuilder::depends_on`]
|
||||||
/// 4. Execute all tasks with [`DagRunner::run`]
|
/// 4. Execute all tasks with [`DagRunner::run`]
|
||||||
/// 5. Optionally retrieve outputs with [`DagRunner::get`]
|
/// 5. Optionally retrieve outputs with [`DagOutput::get`]
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -107,16 +91,16 @@ pub(crate) type PassThroughHashMap<K, V> = HashMap<K, V, PassThroughHasher>;
|
|||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// # async {
|
/// # async {
|
||||||
/// let dag = DagRunner::new();
|
/// let mut dag = DagRunner::new();
|
||||||
///
|
///
|
||||||
/// // Construct instances using ::new() pattern
|
/// // Construct instances using ::new() pattern
|
||||||
/// let x = dag.add_task(LoadValue::new(2));
|
/// let x = dag.add_task(LoadValue::new(2));
|
||||||
/// let y = dag.add_task(LoadValue::new(3));
|
/// let y = dag.add_task(LoadValue::new(3));
|
||||||
/// let sum = dag.add_task(Add).depends_on((x, y));
|
/// let sum = dag.add_task(Add).depends_on((x, y));
|
||||||
///
|
///
|
||||||
/// dag.run(|fut| 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<K, V> = HashMap<K, V, PassThroughHasher>;
|
|||||||
/// Outputs are Arc-wrapped and stored separately for retrieval via get().
|
/// Outputs are Arc-wrapped and stored separately for retrieval via get().
|
||||||
/// Arc enables efficient sharing during fanout without cloning data.
|
/// Arc enables efficient sharing during fanout without cloning data.
|
||||||
pub struct DagRunner {
|
pub struct DagRunner {
|
||||||
pub(crate) nodes: Mutex<Vec<Option<Box<dyn ExecutableNode + Sync>>>>,
|
pub(crate) nodes: Vec<Option<Box<dyn ExecutableNode + Sync>>>,
|
||||||
pub(crate) outputs:
|
pub(crate) edges: PassThroughHashMap<NodeId, Vec<NodeId>>, // node -> dependencies
|
||||||
Mutex<PassThroughHashMap<NodeId, std::sync::Arc<dyn std::any::Any + Send + Sync>>>,
|
pub(crate) dependents: PassThroughHashMap<NodeId, Vec<NodeId>>, // node -> tasks that depend on it
|
||||||
pub(crate) edges: Mutex<PassThroughHashMap<NodeId, Vec<NodeId>>>, // node -> dependencies
|
|
||||||
pub(crate) dependents: Mutex<PassThroughHashMap<NodeId, Vec<NodeId>>>, // node -> tasks that depend on it
|
|
||||||
pub(crate) next_id: Mutex<u32>,
|
|
||||||
pub(crate) run_lock: AtomicBool, // Ensures only one run() at a time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DagRunner {
|
impl Default for DagRunner {
|
||||||
@@ -150,31 +130,21 @@ impl DagRunner {
|
|||||||
/// ```
|
/// ```
|
||||||
/// use dagx::DagRunner;
|
/// use dagx::DagRunner;
|
||||||
///
|
///
|
||||||
/// let dag = DagRunner::new();
|
/// let mut dag = DagRunner::new();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
nodes: Mutex::new(Vec::new()),
|
nodes: Vec::new(),
|
||||||
outputs: Mutex::new(HashMap::default()),
|
edges: HashMap::default(),
|
||||||
edges: Mutex::new(HashMap::default()),
|
dependents: HashMap::default(),
|
||||||
dependents: Mutex::new(HashMap::default()),
|
|
||||||
next_id: Mutex::new(0),
|
|
||||||
run_lock: AtomicBool::new(false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
/// Add a task instance to the DAG, returning a node builder for wiring dependencies.
|
||||||
///
|
///
|
||||||
/// The returned [`TaskBuilder<Tk, Pending>`](TaskBuilder) can be used to:
|
/// If the task has no dependencies, a [`crate::TaskHandle`] will be returned.
|
||||||
/// - Specify dependencies via [`TaskBuilder::depends_on`]
|
/// If not, the returned [`crate::TaskBuilder`] can be used to
|
||||||
/// - Used directly as a [`TaskHandle`] to the task's output
|
/// specify dependencies via [`crate::TaskBuilder::depends_on`].
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -214,7 +184,7 @@ impl DagRunner {
|
|||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// # async {
|
/// # async {
|
||||||
/// let dag = DagRunner::new();
|
/// let mut dag = DagRunner::new();
|
||||||
///
|
///
|
||||||
/// // Construct task with initial value of 10
|
/// // Construct task with initial value of 10
|
||||||
/// let base = dag.add_task(LoadValue::new(10));
|
/// let base = dag.add_task(LoadValue::new(10));
|
||||||
@@ -222,17 +192,16 @@ impl DagRunner {
|
|||||||
/// // Construct task with offset of 1
|
/// // Construct task with offset of 1
|
||||||
/// let inc = dag.add_task(AddOffset::new(1)).depends_on(base);
|
/// let inc = dag.add_task(AddOffset::new(1)).depends_on(base);
|
||||||
///
|
///
|
||||||
/// dag.run(|fut| 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(inc).unwrap(), 11);
|
/// assert_eq!(output.get(inc).unwrap(), 11);
|
||||||
/// # };
|
/// # };
|
||||||
/// ```
|
/// ```
|
||||||
pub fn add_task<Tk>(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending>
|
pub fn add_task<'dag, Input, Tk>(&'dag mut self, task: Tk) -> Tk::Retval<'dag>
|
||||||
where
|
where
|
||||||
Tk: Task + Sync + 'static,
|
Tk: TaskWire<Input>,
|
||||||
Tk::Input: 'static,
|
Input: Send + Sync + 'static,
|
||||||
Tk::Output: 'static,
|
|
||||||
{
|
{
|
||||||
let id = self.alloc_id();
|
let id = NodeId(self.nodes.len() as u32);
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
debug!(
|
debug!(
|
||||||
@@ -242,15 +211,11 @@ impl DagRunner {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let node = TypedNode::new(task);
|
let node = TypedNode::new(task);
|
||||||
self.nodes.lock().push(Some(Box::new(node)));
|
self.nodes.push(Some(Box::new(node)));
|
||||||
self.edges.lock().insert(id, Vec::new());
|
self.edges.insert(id, Vec::new());
|
||||||
self.dependents.lock().insert(id, Vec::new());
|
self.dependents.insert(id, Vec::new());
|
||||||
|
|
||||||
TaskBuilder {
|
Tk::new_from_dag(id, self)
|
||||||
id,
|
|
||||||
dag: self,
|
|
||||||
_phantom: std::marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the entire DAG to completion using the provided spawner.
|
/// Run the entire DAG to completion using the provided spawner.
|
||||||
@@ -299,41 +264,24 @@ impl DagRunner {
|
|||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// # async {
|
/// # async {
|
||||||
/// let dag = DagRunner::new();
|
/// let mut dag = DagRunner::new();
|
||||||
///
|
///
|
||||||
/// let a = dag.add_task(Value(1));
|
/// let a = dag.add_task(Value(1));
|
||||||
/// let b = dag.add_task(Value(2));
|
/// let b = dag.add_task(Value(2));
|
||||||
/// let sum = dag.add_task(Add).depends_on((a, b));
|
/// let sum = dag.add_task(Add).depends_on((a, b));
|
||||||
///
|
///
|
||||||
/// dag.run(|fut| 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]
|
#[inline]
|
||||||
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))]
|
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))]
|
||||||
pub async fn run<S, F>(&self, spawner: S) -> DagResult<()>
|
pub async fn run<S, F>(mut self, spawner: S) -> DagResult<DagOutput>
|
||||||
where
|
where
|
||||||
S: Fn(BoxFuture<'static, DagResult<Arc<dyn Any + Send + Sync>>>) -> F,
|
S: Fn(BoxFuture<'static, DagResult<Arc<dyn Any + Send + Sync>>>) -> F,
|
||||||
F: Future<Output = DagResult<Arc<dyn Any + Send + Sync>>>,
|
F: Future<Output = DagResult<Arc<dyn Any + Send + Sync>>>,
|
||||||
{
|
{
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
info!("starting DAG execution");
|
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
|
// Build topological layers
|
||||||
let layers = self.compute_layers()?;
|
let layers = self.compute_layers()?;
|
||||||
@@ -346,8 +294,6 @@ impl DagRunner {
|
|||||||
total_tasks, "computed topological layers"
|
total_tasks, "computed topological layers"
|
||||||
);
|
);
|
||||||
|
|
||||||
let edges = self.edges.lock().clone();
|
|
||||||
|
|
||||||
let mut outputs: PassThroughHashMap<NodeId, Arc<dyn Any + Send + Sync>> =
|
let mut outputs: PassThroughHashMap<NodeId, Arc<dyn Any + Send + Sync>> =
|
||||||
HashMap::with_capacity_and_hasher(total_tasks, PassThroughHasher::default());
|
HashMap::with_capacity_and_hasher(total_tasks, PassThroughHasher::default());
|
||||||
let mut first_error = None;
|
let mut first_error = None;
|
||||||
@@ -392,13 +338,10 @@ impl DagRunner {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Take ownership of the node
|
// Take ownership of the node
|
||||||
let node = {
|
let node = self.nodes[node_id.0 as usize].take();
|
||||||
let mut nodes_lock = self.nodes.lock();
|
|
||||||
nodes_lock[node_id.0 as usize].take()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(node) = node {
|
if let Some(node) = node {
|
||||||
let dependencies: Vec<_> = edges[&node_id]
|
let dependencies: Vec<_> = self.edges[&node_id]
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|dep| outputs.get(dep))
|
.flat_map(|dep| outputs.get(dep))
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -456,12 +399,9 @@ impl DagRunner {
|
|||||||
trace!(task_id = node_id.0, "spawning task");
|
trace!(task_id = node_id.0, "spawning task");
|
||||||
|
|
||||||
// Take ownership of the node
|
// Take ownership of the node
|
||||||
let node = {
|
let node = self.nodes[node_id.0 as usize].take();
|
||||||
let mut nodes_lock = self.nodes.lock();
|
|
||||||
nodes_lock[node_id.0 as usize].take()
|
|
||||||
};
|
|
||||||
if let Some(node) = node {
|
if let Some(node) = node {
|
||||||
let dependencies: Vec<_> = edges[&node_id]
|
let dependencies: Vec<_> = self.edges[&node_id]
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|dep| outputs.get(dep))
|
.flat_map(|dep| outputs.get(dep))
|
||||||
.cloned()
|
.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
|
// Return first error if any
|
||||||
if let Some(err) = first_error {
|
if let Some(err) = first_error {
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
@@ -539,73 +472,7 @@ impl DagRunner {
|
|||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
info!("DAG execution completed successfully");
|
info!("DAG execution completed successfully");
|
||||||
|
|
||||||
Ok(())
|
Ok(DagOutput::new(outputs))
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<T: 'static + Send + Sync, H>(&self, handle: H) -> DagResult<T>
|
|
||||||
where
|
|
||||||
H: Into<TaskHandle<T>>,
|
|
||||||
{
|
|
||||||
let handle: TaskHandle<T> = 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<dyn Any> to Arc<T>
|
|
||||||
let output = arc_output
|
|
||||||
.downcast::<T>()
|
|
||||||
.map_err(|_| DagError::TypeMismatch {
|
|
||||||
expected: std::any::type_name::<T>(),
|
|
||||||
found: "unknown",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Arc::into_inner(output).unwrap())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -617,18 +484,11 @@ impl DagRunner {
|
|||||||
let mut layers = Vec::new();
|
let mut layers = Vec::new();
|
||||||
|
|
||||||
// Calculate in-degrees: for each node, count how many dependencies it has
|
// 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.
|
for (&node, deps) in self.edges.iter() {
|
||||||
// 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() {
|
|
||||||
let degree = deps.len();
|
let degree = deps.len();
|
||||||
in_degree.insert(node, degree);
|
in_degree.insert(node, degree);
|
||||||
}
|
}
|
||||||
drop(edges); // Release lock early
|
|
||||||
|
|
||||||
// Find all nodes with in-degree 0 (sources - nodes with no dependencies)
|
// Find all nodes with in-degree 0 (sources - nodes with no dependencies)
|
||||||
let mut queue: VecDeque<NodeId> = in_degree
|
let mut queue: VecDeque<NodeId> = in_degree
|
||||||
@@ -653,8 +513,7 @@ impl DagRunner {
|
|||||||
visited.insert(node);
|
visited.insert(node);
|
||||||
|
|
||||||
// For each node that depends on the current node
|
// For each node that depends on the current node
|
||||||
let dependents = self.dependents.lock();
|
if let Some(deps) = self.dependents.get(&node) {
|
||||||
if let Some(deps) = dependents.get(&node) {
|
|
||||||
for &dependent in deps {
|
for &dependent in deps {
|
||||||
if let Some(degree) = in_degree.get_mut(&dependent) {
|
if let Some(degree) = in_degree.get_mut(&dependent) {
|
||||||
*degree -= 1;
|
*degree -= 1;
|
||||||
|
|||||||
+23
-80
@@ -3,9 +3,10 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::hash::Hasher;
|
use std::hash::Hasher;
|
||||||
|
|
||||||
|
use crate::builder::TaskHandle;
|
||||||
use crate::error::DagError;
|
use crate::error::DagError;
|
||||||
use crate::runner::{DagRunner, PassThroughHasher};
|
use crate::runner::{DagRunner, PassThroughHasher};
|
||||||
use crate::types::TaskHandle;
|
use crate::DagOutput;
|
||||||
|
|
||||||
// Initialize tracing subscriber for tests (idempotent)
|
// Initialize tracing subscriber for tests (idempotent)
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
@@ -40,16 +41,16 @@ impl TestTask {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_dag_runner_new() {
|
fn test_dag_runner_new() {
|
||||||
// Test that DagRunner::new() creates a new instance
|
// Test that DagRunner::new() creates a new instance
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Initial state should be empty
|
// Initial state should be empty
|
||||||
assert_eq!(dag.nodes.lock().len(), 0);
|
assert_eq!(dag.nodes.len(), 0);
|
||||||
assert_eq!(dag.edges.lock().len(), 0);
|
assert_eq!(dag.edges.len(), 0);
|
||||||
assert_eq!(dag.dependents.lock().len(), 0);
|
assert_eq!(dag.dependents.len(), 0);
|
||||||
|
|
||||||
// Adding a task works
|
// Adding a task works
|
||||||
dag.add_task(TestTask { value: 42 });
|
dag.add_task(TestTask { value: 42 });
|
||||||
assert_eq!(dag.nodes.lock().len(), 1);
|
assert_eq!(dag.nodes.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -58,19 +59,20 @@ fn test_dag_runner_default() {
|
|||||||
let dag = DagRunner::default();
|
let dag = DagRunner::default();
|
||||||
|
|
||||||
// Should behave the same as new()
|
// Should behave the same as new()
|
||||||
assert_eq!(dag.nodes.lock().len(), 0);
|
assert_eq!(dag.nodes.len(), 0);
|
||||||
assert_eq!(dag.edges.lock().len(), 0);
|
assert_eq!(dag.edges.len(), 0);
|
||||||
assert_eq!(dag.dependents.lock().len(), 0);
|
assert_eq!(dag.dependents.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_wrong_type() {
|
async fn test_get_wrong_type() {
|
||||||
init_tracing();
|
init_tracing();
|
||||||
// Test getting with wrong type - this should return TypeMismatch
|
// 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 });
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -80,7 +82,7 @@ async fn test_get_wrong_type() {
|
|||||||
_phantom: std::marker::PhantomData,
|
_phantom: std::marker::PhantomData,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = dag.get(fake_handle);
|
let result = output.get(fake_handle);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
// When downcast fails, we get TypeMismatch
|
// When downcast fails, we get TypeMismatch
|
||||||
@@ -96,11 +98,12 @@ async fn test_get_wrong_type() {
|
|||||||
async fn test_get_result_not_found() {
|
async fn test_get_result_not_found() {
|
||||||
init_tracing();
|
init_tracing();
|
||||||
// Test getting a result before running the DAG
|
// 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 = dag.add_task(TestTask { value: 42 });
|
||||||
let handle_id = handle.id.0; // Save ID before moving handle
|
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());
|
assert!(result.is_err());
|
||||||
match result.unwrap_err() {
|
match result.unwrap_err() {
|
||||||
DagError::ResultNotFound { task_id } => {
|
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]
|
#[tokio::test]
|
||||||
async fn test_invalid_node_id_in_get() {
|
async fn test_invalid_node_id_in_get() {
|
||||||
init_tracing();
|
init_tracing();
|
||||||
@@ -179,15 +121,16 @@ async fn test_invalid_node_id_in_get() {
|
|||||||
|
|
||||||
// Create a handle with an ID that doesn't exist
|
// Create a handle with an ID that doesn't exist
|
||||||
let invalid_handle: TaskHandle<i32> = TaskHandle {
|
let invalid_handle: TaskHandle<i32> = TaskHandle {
|
||||||
id: crate::types::NodeId(999),
|
id: crate::builder::NodeId(999),
|
||||||
_phantom: std::marker::PhantomData,
|
_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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = dag.get(invalid_handle);
|
let result = output.get(invalid_handle);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
// With centralized output storage, we get ResultNotFound for invalid node IDs
|
// 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 mut dag = DagRunner::new();
|
||||||
let source: TaskHandle<_> = dag.add_task(Source).into();
|
let source = dag.add_task(Source);
|
||||||
|
|
||||||
// Create two tasks in the same layer - one panics, one doesn't
|
// Create two tasks in the same layer - one panics, one doesn't
|
||||||
let _panic_task = dag.add_task(PanicTask).depends_on(source);
|
let _panic_task = dag.add_task(PanicTask).depends_on(source);
|
||||||
|
|||||||
+6
-4
@@ -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
|
/// 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.
|
/// to your task code - you work with `T`, the framework handles Arc wrapping/unwrapping.
|
||||||
pub trait Task: Send {
|
pub trait Task<Input>: Send
|
||||||
type Input: Send;
|
where
|
||||||
type Output: Send + Sync; // Sync required for Arc-wrapping and cross-thread sharing
|
Input: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Output: Send + Sync + 'static; // Sync required for Arc-wrapping and cross-thread sharing
|
||||||
|
|
||||||
fn run(self, input: TaskInput<Self::Input>) -> impl Future<Output = Self::Output> + Send;
|
fn run(self, input: TaskInput<Input>) -> impl Future<Output = Self::Output> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TaskInput<'inputs, Input: Send> {
|
pub struct TaskInput<'inputs, Input: Send> {
|
||||||
|
|||||||
+2
-3
@@ -9,11 +9,10 @@ struct CustomTask {
|
|||||||
state: i32,
|
state: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Task for CustomTask {
|
impl Task<(i32,)> for CustomTask {
|
||||||
type Input = (i32,);
|
|
||||||
type Output = i32;
|
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 += input.next().0;
|
||||||
self.state
|
self.state
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<T>` 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<T> {
|
|
||||||
pub(crate) id: NodeId,
|
|
||||||
pub(crate) _phantom: std::marker::PhantomData<fn() -> T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Clone for TaskHandle<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
*self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Copy for TaskHandle<T> {}
|
|
||||||
|
|
||||||
/// Type-level marker for empty dependency list.
|
|
||||||
///
|
|
||||||
/// This type appears in the type signature of [`crate::TaskBuilder<Tk, Pending>`](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<Input = ()>, Deps> From<TaskBuilder<'a, Tk, Deps>> for TaskHandle<Tk::Output> {
|
|
||||||
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<T> From<&TaskHandle<T>> for TaskHandle<T> {
|
|
||||||
fn from(handle: &TaskHandle<T>) -> Self {
|
|
||||||
*handle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod coverage_tests;
|
|
||||||
@@ -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<String> = 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<T>> for TaskHandle<T> (line 90-93)
|
|
||||||
let handle: TaskHandle<String> = TaskHandle {
|
|
||||||
id: NodeId(123),
|
|
||||||
_phantom: PhantomData,
|
|
||||||
};
|
|
||||||
|
|
||||||
let handle_ref = &handle;
|
|
||||||
let converted: TaskHandle<String> = handle_ref.into();
|
|
||||||
|
|
||||||
assert_eq!(converted.id.0, 123);
|
|
||||||
assert_eq!(handle.id, converted.id);
|
|
||||||
}
|
|
||||||
@@ -9,13 +9,13 @@ use std::sync::Arc;
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
||||||
// Test 10 parallel chains of depth 500 each
|
// Test 10 parallel chains of depth 500 each
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let mut chain_ends = Vec::new();
|
let mut chain_ends = Vec::new();
|
||||||
|
|
||||||
for chain_id in 0..8 {
|
for chain_id in 0..8 {
|
||||||
let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| chain_id * 1000));
|
let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| chain_id * 1000));
|
||||||
let mut current: dagx::TaskHandle<i32> = first.into();
|
let mut current: dagx::TaskHandle<i32> = first;
|
||||||
|
|
||||||
for _ in 0..500 {
|
for _ in 0..500 {
|
||||||
current = dag
|
current = dag
|
||||||
@@ -51,7 +51,8 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
|||||||
&chain_ends[7],
|
&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?;
|
.await?;
|
||||||
|
|
||||||
// Each chain: start + 500
|
// Each chain: start + 500
|
||||||
@@ -59,7 +60,7 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
|||||||
// Chain 1: 1000 + 500 = 1500
|
// Chain 1: 1000 + 500 = 1500
|
||||||
// ...
|
// ...
|
||||||
// Sum = 500*8 + (0+1000+2000+...+7000) = 4000 + 28000 = 32000
|
// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -67,7 +68,7 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_deep_chain_execution_order() -> DagResult<()> {
|
async fn test_deep_chain_execution_order() -> DagResult<()> {
|
||||||
// Verify tasks in a deep chain execute in correct order
|
// 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 order = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
let first = dag.add_task(task_fn::<(), _, _>({
|
let first = dag.add_task(task_fn::<(), _, _>({
|
||||||
@@ -78,7 +79,7 @@ async fn test_deep_chain_execution_order() -> DagResult<()> {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
let mut current: dagx::TaskHandle<i32> = first.into();
|
let mut current: dagx::TaskHandle<i32> = first;
|
||||||
|
|
||||||
for expected in 1..100 {
|
for expected in 1..100 {
|
||||||
current = dag
|
current = dag
|
||||||
@@ -93,11 +94,12 @@ async fn test_deep_chain_execution_order() -> DagResult<()> {
|
|||||||
.depends_on(current);
|
.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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(order.load(Ordering::SeqCst), 100);
|
assert_eq!(order.load(Ordering::SeqCst), 100);
|
||||||
assert_eq!(dag.get(current)?, 99);
|
assert_eq!(output.get(current)?, 99);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -105,12 +107,12 @@ async fn test_deep_chain_execution_order() -> DagResult<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_deep_binary_tree() -> DagResult<()> {
|
async fn test_deep_binary_tree() -> DagResult<()> {
|
||||||
// Test a deep binary tree structure (depth 10 = 1023 nodes)
|
// 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<i32> {
|
fn build_tree(dag: &mut DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> {
|
||||||
if depth == 0 {
|
if depth == 0 {
|
||||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value));
|
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value));
|
||||||
task.into() // Convert TaskBuilder to TaskHandle
|
task // Convert TaskBuilder to TaskHandle
|
||||||
} else {
|
} else {
|
||||||
let left = build_tree(dag, depth - 1, value * 2);
|
let left = build_tree(dag, depth - 1, value * 2);
|
||||||
let right = build_tree(dag, depth - 1, value * 2 + 1);
|
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?;
|
.await?;
|
||||||
|
|
||||||
// Sum of all values in a complete binary tree with values 1024..2047
|
// Sum of all values in a complete binary tree with values 1024..2047
|
||||||
// This is sum from 2^10 to 2^11-1 = 1024 * 1023 / 2 + 1024 * 512 = 1047552
|
// This is sum from 2^10 to 2^11-1 = 1024 * 1023 / 2 + 1024 * 512 = 1047552
|
||||||
let result = dag.get(root)?;
|
let result = output.get(root)?;
|
||||||
assert!(result > 0);
|
assert!(result > 0);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -138,13 +141,13 @@ async fn test_deep_binary_tree() -> DagResult<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_fibonacci_chain() -> DagResult<()> {
|
async fn test_fibonacci_chain() -> DagResult<()> {
|
||||||
// Test a Fibonacci-like dependency pattern
|
// 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 f0_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64));
|
||||||
let f1_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 1i64));
|
let f1_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 1i64));
|
||||||
|
|
||||||
let mut prev2: dagx::TaskHandle<i64> = f0_builder.into();
|
let mut prev2: dagx::TaskHandle<i64> = f0_builder;
|
||||||
let mut prev1: dagx::TaskHandle<i64> = f1_builder.into();
|
let mut prev1: dagx::TaskHandle<i64> = f1_builder;
|
||||||
|
|
||||||
for _ in 2..50 {
|
for _ in 2..50 {
|
||||||
let next = dag
|
let next = dag
|
||||||
@@ -155,11 +158,12 @@ async fn test_fibonacci_chain() -> DagResult<()> {
|
|||||||
prev1 = next;
|
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?;
|
.await?;
|
||||||
|
|
||||||
// 50th Fibonacci number (0-indexed) = 7778742049
|
// 50th Fibonacci number (0-indexed) = 7778742049
|
||||||
assert_eq!(dag.get(prev1)?, 7778742049);
|
assert_eq!(output.get(prev1)?, 7778742049);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -167,13 +171,13 @@ async fn test_fibonacci_chain() -> DagResult<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_zigzag_dependencies() -> DagResult<()> {
|
async fn test_zigzag_dependencies() -> DagResult<()> {
|
||||||
// Test a zigzag pattern: two chains that cross-reference each other
|
// 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 a_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||||
let b_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
|
let b_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
|
||||||
|
|
||||||
let mut chain_a: dagx::TaskHandle<i32> = a_first.into();
|
let mut chain_a: dagx::TaskHandle<i32> = a_first;
|
||||||
let mut chain_b: dagx::TaskHandle<i32> = b_first.into();
|
let mut chain_b: dagx::TaskHandle<i32> = b_first;
|
||||||
|
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
if i % 2 == 0 {
|
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))
|
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||||
.depends_on((&chain_a, &chain_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?;
|
.await?;
|
||||||
|
|
||||||
let result = dag.get(final_task)?;
|
let result = output.get(final_task)?;
|
||||||
assert!(result > 100);
|
assert!(result > 100);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Tests for type system edge cases and limits
|
//! Tests for type system edge cases and limits
|
||||||
|
|
||||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
use dagx::{DagResult, DagRunner};
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
|
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
@@ -16,13 +16,13 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> {
|
|||||||
_phantom: PhantomData<T>,
|
_phantom: PhantomData<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Task producing ZST
|
// Task producing ZST
|
||||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Empty));
|
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Empty));
|
||||||
|
|
||||||
// Task that also produces ZST (independent since we can't extract custom types)
|
// 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
|
// Task with PhantomData
|
||||||
let _t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| PhantomWrapper::<i32> {
|
let _t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| PhantomWrapper::<i32> {
|
||||||
@@ -37,12 +37,13 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> {
|
|||||||
}))
|
}))
|
||||||
.depends_on(t2);
|
.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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(t1)?, Empty);
|
assert_eq!(output.get(t1)?, Empty);
|
||||||
assert_eq!(dag.get(t2)?, 42);
|
assert_eq!(output.get(t2)?, 42);
|
||||||
assert_eq!(dag.get(t4)?, "success");
|
assert_eq!(output.get(t4)?, "success");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,7 @@ async fn test_large_value_types() -> DagResult<()> {
|
|||||||
more_data: Vec<u64>,
|
more_data: Vec<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||||
let large = LargeStruct {
|
let large = LargeStruct {
|
||||||
@@ -73,37 +74,11 @@ async fn test_large_value_types() -> DagResult<()> {
|
|||||||
}))
|
}))
|
||||||
.depends_on(t1);
|
.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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(t2)?, 1042);
|
assert_eq!(output.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");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -113,7 +88,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
|
|||||||
// Test types that wrap references
|
// Test types that wrap references
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||||
let arc = Arc::new(Mutex::new(100));
|
let arc = Arc::new(Mutex::new(100));
|
||||||
@@ -126,10 +101,11 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
|
|||||||
.add_task(task_fn::<i32, _, _>(|&val: &i32| val * 2))
|
.add_task(task_fn::<i32, _, _>(|&val: &i32| val * 2))
|
||||||
.depends_on(t1);
|
.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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(t2)?, 200);
|
assert_eq!(output.get(t2)?, 200);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -8,17 +8,18 @@ use tokio::task::yield_now;
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_dag_with_only_source_tasks() -> DagResult<()> {
|
async fn test_dag_with_only_source_tasks() -> DagResult<()> {
|
||||||
// Test a DAG with multiple source tasks but no dependencies
|
// Test a DAG with multiple source tasks but no dependencies
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let tasks: Vec<_> = (0..100)
|
let tasks: Vec<_> = (0..100)
|
||||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||||
.collect();
|
.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?;
|
.await?;
|
||||||
|
|
||||||
for (i, task) in tasks.into_iter().enumerate() {
|
for (i, task) in tasks.into_iter().enumerate() {
|
||||||
assert_eq!(dag.get(task)?, i);
|
assert_eq!(output.get(task)?, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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 });
|
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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(task)?, 42);
|
assert_eq!(output.get(task)?, 42);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> {
|
async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> {
|
||||||
// Test a task that does the absolute minimum async work
|
// Test a task that does the absolute minimum async work
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
struct YieldTask;
|
struct YieldTask;
|
||||||
|
|
||||||
@@ -65,9 +67,10 @@ async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> {
|
|||||||
|
|
||||||
let task = dag.add_task(YieldTask);
|
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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(task)?, 1337);
|
assert_eq!(output.get(task)?, 1337);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
+18
-15
@@ -1,5 +1,5 @@
|
|||||||
// Test that custom types work with the #[task] macro
|
// Test that custom types work with the #[task] macro
|
||||||
use dagx::{task, DagRunner, TaskHandle};
|
use dagx::{task, DagRunner};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
struct Person {
|
struct Person {
|
||||||
@@ -51,33 +51,35 @@ impl PersonToCompany {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_custom_type_single_dependency() {
|
async fn test_custom_type_single_dependency() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let fetch = dag.add_task(FetchPerson);
|
let fetch = dag.add_task(FetchPerson);
|
||||||
let process = dag.add_task(ProcessPerson).depends_on(fetch);
|
let process = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||||
|
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = dag.get(process).unwrap();
|
let result = output.get(process).unwrap();
|
||||||
assert_eq!(result, "Alice is 30 years old");
|
assert_eq!(result, "Alice is 30 years old");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_custom_type_passthrough() {
|
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 transform = dag.add_task(PersonToCompany).depends_on(fetch);
|
||||||
let process = dag.add_task(ProcessPerson).depends_on(fetch);
|
let process = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||||
|
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let person_result = dag.get(process).unwrap();
|
let person_result = output.get(process).unwrap();
|
||||||
let company_result = dag.get(transform).unwrap();
|
let company_result = output.get(transform).unwrap();
|
||||||
|
|
||||||
assert_eq!(person_result.as_str(), "Alice is 30 years old");
|
assert_eq!(person_result.as_str(), "Alice is 30 years old");
|
||||||
assert_eq!(company_result.name, "Alice's Company");
|
assert_eq!(company_result.name, "Alice's Company");
|
||||||
@@ -86,20 +88,21 @@ async fn test_custom_type_passthrough() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_custom_type_fan_out() {
|
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 process1 = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||||
let process2 = dag.add_task(ProcessPerson).depends_on(fetch);
|
let process2 = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||||
let process3 = dag.add_task(ProcessPerson).depends_on(fetch);
|
let process3 = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||||
|
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result1 = dag.get(process1).unwrap();
|
let result1 = output.get(process1).unwrap();
|
||||||
let result2 = dag.get(process2).unwrap();
|
let result2 = output.get(process2).unwrap();
|
||||||
let result3 = dag.get(process3).unwrap();
|
let result3 = output.get(process3).unwrap();
|
||||||
|
|
||||||
assert_eq!(result1, "Alice is 30 years old");
|
assert_eq!(result1, "Alice is 30 years old");
|
||||||
assert_eq!(result2, "Alice is 30 years old");
|
assert_eq!(result2, "Alice is 30 years old");
|
||||||
|
|||||||
+15
-14
@@ -3,7 +3,7 @@
|
|||||||
//! These tests show that even if you try to create cycles, the Rust compiler
|
//! These tests show that even if you try to create cycles, the Rust compiler
|
||||||
//! prevents it through the type-state pattern.
|
//! prevents it through the type-state pattern.
|
||||||
|
|
||||||
use dagx::{task, DagRunner, TaskHandle};
|
use dagx::{task, DagRunner};
|
||||||
|
|
||||||
/// Demonstrates that TaskBuilder is consumed by depends_on,
|
/// Demonstrates that TaskBuilder is consumed by depends_on,
|
||||||
/// preventing you from wiring the same task multiple times
|
/// preventing you from wiring the same task multiple times
|
||||||
@@ -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 source = dag.add_task(Source);
|
||||||
let consumer_builder = dag.add_task(Consumer);
|
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);
|
let builder = dag.add_task(NoInput);
|
||||||
|
|
||||||
// Convert to handle (only works because Input = ())
|
// Convert to handle (only works because Input = ())
|
||||||
let handle: TaskHandle<_> = builder.into();
|
let handle = builder;
|
||||||
|
|
||||||
// TaskHandle is Copy/Clone - just an ID wrapper
|
// TaskHandle is Copy/Clone - just an ID wrapper
|
||||||
let _copy1 = handle;
|
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 a_builder = dag.add_task(TaskA);
|
||||||
let b_builder = dag.add_task(TaskB);
|
let b_builder = dag.add_task(TaskB);
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ fn test_ordering_prevents_cycles() {
|
|||||||
// So we can only create DAGs, never cycles!
|
// So we can only create DAGs, never cycles!
|
||||||
|
|
||||||
// Let's create a valid DAG instead: A→B (B depends on A)
|
// 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);
|
let _b_handle = b_builder.depends_on(a_handle);
|
||||||
// Now we could try: a_handle.depends_on(b_handle)?
|
// Now we could try: a_handle.depends_on(b_handle)?
|
||||||
// But TaskHandle has no such method!
|
// 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 task1 = dag.add_task(Task1);
|
||||||
let task2 = dag.add_task(Task2);
|
let task2 = dag.add_task(Task2);
|
||||||
|
|
||||||
// Both have unit input, so we can convert to handles
|
// Both have unit input, so we can convert to handles
|
||||||
let _handle1: TaskHandle<_> = task1.into();
|
let _handle1 = task1;
|
||||||
let _handle2: TaskHandle<_> = task2.into();
|
let _handle2 = task2;
|
||||||
|
|
||||||
// Now we have two handles, but...
|
// Now we have two handles, but...
|
||||||
// TaskHandle has no methods to add dependencies!
|
// 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 _a_builder = dag.add_task(A);
|
||||||
let _b_builder = dag.add_task(B);
|
let _b_builder = dag.add_task(B);
|
||||||
let _c_builder = dag.add_task(C);
|
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);
|
let _builder = dag.add_task(SelfReferential);
|
||||||
|
|
||||||
// To create A→A (self-loop):
|
// 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
|
// Build a valid DAG: two sources, two transforms, one aggregator
|
||||||
let s1 = dag.add_task(Source);
|
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));
|
let result = dag.add_task(Aggregate).depends_on((&t1, &t2));
|
||||||
|
|
||||||
// Execute and verify
|
// 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
|
.await
|
||||||
.unwrap();
|
.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:
|
// Now if we wanted to add a cycle, we'd need to:
|
||||||
// - Make one of the Source tasks depend on the Aggregate result
|
// - Make one of the Source tasks depend on the Aggregate result
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_dependency_resolution_order() -> DagResult<()> {
|
async fn test_dependency_resolution_order() -> DagResult<()> {
|
||||||
// Test that dependencies are resolved in the correct order
|
// 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()));
|
let execution_log = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
@@ -79,10 +79,11 @@ async fn test_dependency_resolution_order() -> DagResult<()> {
|
|||||||
.depends_on(d)
|
.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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(e)?, 5);
|
assert_eq!(output.get(e)?, 5);
|
||||||
|
|
||||||
// Verify execution order
|
// Verify execution order
|
||||||
let log = execution_log.lock().unwrap();
|
let log = execution_log.lock().unwrap();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use dagx_test::task_fn;
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_two_dependencies() -> DagResult<()> {
|
async fn test_two_dependencies() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||||
let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
|
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))
|
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
|
||||||
.depends_on((a, b));
|
.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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(sum)?, 30);
|
assert_eq!(output.get(sum)?, 30);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_three_dependencies() -> DagResult<()> {
|
async fn test_three_dependencies() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||||
let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
|
let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
|
||||||
@@ -33,19 +34,20 @@ async fn test_three_dependencies() -> DagResult<()> {
|
|||||||
))
|
))
|
||||||
.depends_on((a, b, c));
|
.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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(sum)?, 6);
|
assert_eq!(output.get(sum)?, 6);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_four_dependencies() -> DagResult<()> {
|
async fn test_four_dependencies() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let deps: Vec<_> = (1..=4)
|
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();
|
.collect();
|
||||||
|
|
||||||
let sum = dag
|
let sum = dag
|
||||||
@@ -54,19 +56,20 @@ async fn test_four_dependencies() -> DagResult<()> {
|
|||||||
))
|
))
|
||||||
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3]));
|
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3]));
|
||||||
|
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(sum)?, 10); // 1+2+3+4
|
assert_eq!(output.get(sum)?, 10); // 1+2+3+4
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_five_dependencies() -> DagResult<()> {
|
async fn test_five_dependencies() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let deps: Vec<_> = (1..=5)
|
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();
|
.collect();
|
||||||
|
|
||||||
let sum = dag
|
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]));
|
.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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(sum)?, 15); // 1+2+3+4+5
|
assert_eq!(output.get(sum)?, 15); // 1+2+3+4+5
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_six_dependencies() -> DagResult<()> {
|
async fn test_six_dependencies() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let deps: Vec<_> = (1..=6)
|
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();
|
.collect();
|
||||||
|
|
||||||
let sum = dag
|
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]));
|
.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?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_seven_dependencies() -> DagResult<()> {
|
async fn test_seven_dependencies() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let deps: Vec<_> = (1..=7)
|
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();
|
.collect();
|
||||||
|
|
||||||
let sum = dag
|
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],
|
&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?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_eight_dependencies() -> DagResult<()> {
|
async fn test_eight_dependencies() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let deps: Vec<_> = (1..=8)
|
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();
|
.collect();
|
||||||
|
|
||||||
let sum = dag
|
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],
|
&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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(sum)?, 36); // Sum of 1..=8
|
assert_eq!(output.get(sum)?, 36); // Sum of 1..=8
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
+48
-72
@@ -1,17 +1,17 @@
|
|||||||
//! Tests for error propagation through the DAG
|
//! Tests for error propagation through the DAG
|
||||||
|
|
||||||
use dagx::{task, DagRunner, TaskHandle};
|
use dagx::{task, DagError, DagRunner};
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_linear_error_propagation() {
|
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
|
let t2 = dag
|
||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| -> i32 {
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| -> i32 {
|
||||||
panic!("Error at t2 with input {}", x);
|
panic!("Error at t2 with input {}", x);
|
||||||
@@ -20,28 +20,19 @@ async fn test_linear_error_propagation() {
|
|||||||
let t3 = dag
|
let t3 = dag
|
||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||||
.depends_on(t2);
|
.depends_on(t2);
|
||||||
let t4 = dag
|
dag.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
|
||||||
.depends_on(t3);
|
.depends_on(t3);
|
||||||
|
|
||||||
let _ = dag
|
assert!(matches!(
|
||||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await;
|
.await,
|
||||||
|
Err(DagError::TaskPanicked { .. })
|
||||||
// t1 succeeds
|
));
|
||||||
assert_eq!(dag.get(t1).unwrap(), 1);
|
|
||||||
|
|
||||||
// t2 fails
|
|
||||||
assert!(dag.get(t2).is_err());
|
|
||||||
|
|
||||||
// t3 and t4 fail due to dependency
|
|
||||||
assert!(dag.get(t3).is_err());
|
|
||||||
assert!(dag.get(t4).is_err());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_diamond_error_propagation() {
|
async fn test_diamond_error_propagation() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// source
|
// source
|
||||||
// / \
|
// / \
|
||||||
@@ -49,7 +40,7 @@ async fn test_diamond_error_propagation() {
|
|||||||
// \ /
|
// \ /
|
||||||
// sink
|
// sink
|
||||||
|
|
||||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
|
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||||
|
|
||||||
let left = dag
|
let left = dag
|
||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x / 2))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x / 2))
|
||||||
@@ -61,61 +52,45 @@ async fn test_diamond_error_propagation() {
|
|||||||
}))
|
}))
|
||||||
.depends_on(source);
|
.depends_on(source);
|
||||||
|
|
||||||
let sink = dag
|
dag.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
|
||||||
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
|
|
||||||
.depends_on((&left, &right));
|
.depends_on((&left, &right));
|
||||||
|
|
||||||
let _ = dag
|
assert!(matches!(
|
||||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await;
|
.await,
|
||||||
|
Err(DagError::TaskPanicked { .. })
|
||||||
assert_eq!(dag.get(source).unwrap(), 100);
|
));
|
||||||
assert_eq!(dag.get(left).unwrap(), 50);
|
|
||||||
assert!(dag.get(right).is_err());
|
|
||||||
assert!(dag.get(sink).is_err()); // Fails due to right dependency
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_error_in_wide_fanout() {
|
async fn test_error_in_wide_fanout() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let source: TaskHandle<_> = dag
|
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| -> i32 {
|
||||||
.add_task(task_fn::<(), _, _>(|_: ()| -> i32 {
|
panic!("Source fails");
|
||||||
panic!("Source fails");
|
}));
|
||||||
}))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
// Create wide fanout from failing source
|
// Create wide fanout from failing source
|
||||||
let dependents: Vec<_> = (0..20)
|
for i in 0..20 {
|
||||||
.map(|i| {
|
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
||||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
.depends_on(source);
|
||||||
.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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
|
.await,
|
||||||
|
Err(DagError::TaskPanicked { .. })
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_selective_error_propagation() {
|
async fn test_selective_error_propagation() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Source that produces a Result
|
// Source that produces a Result
|
||||||
let source: TaskHandle<_> = dag
|
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
Result::<i32, String>::Err("Source error".to_string())
|
||||||
Result::<i32, String>::Err("Source error".to_string())
|
}));
|
||||||
}))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
// Handler that processes the Result
|
// Handler that processes the Result
|
||||||
let handler = dag
|
let handler = dag
|
||||||
@@ -140,34 +115,35 @@ async fn test_selective_error_propagation() {
|
|||||||
}))
|
}))
|
||||||
.depends_on(handler);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Source returns an Err variant (not a panic)
|
// Source returns an Err variant (not a panic)
|
||||||
assert_eq!(dag.get(source).unwrap(), Err("Source error".to_string()));
|
assert_eq!(output.get(source).unwrap(), Err("Source error".to_string()));
|
||||||
|
|
||||||
// Handler processes it
|
// Handler processes it
|
||||||
assert_eq!(dag.get(handler).unwrap(), -1);
|
assert_eq!(output.get(handler).unwrap(), -1);
|
||||||
|
|
||||||
// Final task handles the error case
|
// 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]
|
#[tokio::test]
|
||||||
async fn test_error_propagation_timing() {
|
async fn test_error_propagation_timing() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let propagation_order = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
let propagation_order = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
struct T1Task {
|
struct T1Task {
|
||||||
order: Arc<parking_lot::Mutex<Vec<&'static str>>>,
|
order: Arc<Mutex<Vec<&'static str>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[task]
|
#[task]
|
||||||
impl T1Task {
|
impl T1Task {
|
||||||
async fn run(&self) -> i32 {
|
async fn run(&self) -> i32 {
|
||||||
sleep(Duration::from_millis(10)).await;
|
sleep(Duration::from_millis(10)).await;
|
||||||
self.order.lock().push("t1_complete");
|
self.order.lock().unwrap().push("t1_complete");
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,13 +153,13 @@ async fn test_error_propagation_timing() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
struct T2Task {
|
struct T2Task {
|
||||||
order: Arc<parking_lot::Mutex<Vec<&'static str>>>,
|
order: Arc<Mutex<Vec<&'static str>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[task]
|
#[task]
|
||||||
impl T2Task {
|
impl T2Task {
|
||||||
async fn run(&self, x: &i32) -> i32 {
|
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;
|
sleep(Duration::from_millis(10)).await;
|
||||||
panic!("t2 fails with {x}");
|
panic!("t2 fails with {x}");
|
||||||
}
|
}
|
||||||
@@ -200,7 +176,7 @@ async fn test_error_propagation_timing() {
|
|||||||
let order = propagation_order.clone();
|
let order = propagation_order.clone();
|
||||||
move |x: &i32| {
|
move |x: &i32| {
|
||||||
let order = order.clone();
|
let order = order.clone();
|
||||||
order.lock().push("t3_should_not_run");
|
order.lock().unwrap().push("t3_should_not_run");
|
||||||
x + 1
|
x + 1
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -210,7 +186,7 @@ async fn test_error_propagation_timing() {
|
|||||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let order = propagation_order.lock().clone();
|
let order = propagation_order.lock().unwrap().clone();
|
||||||
|
|
||||||
// t1 completes, t2 starts and fails, t3 never runs
|
// t1 completes, t2 starts and fails, t3 never runs
|
||||||
assert!(order.contains(&"t1_complete"));
|
assert!(order.contains(&"t1_complete"));
|
||||||
|
|||||||
+26
-19
@@ -4,11 +4,11 @@ use dagx::{DagResult, DagRunner};
|
|||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_retry_pattern_simulation() -> DagResult<()> {
|
async fn test_retry_pattern_simulation() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let attempt_counter = Arc::new(AtomicUsize::new(0));
|
let attempt_counter = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
// Task that would fail on first attempts (simulated)
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Task completes with error on first "attempt"
|
// Task completes with error on first "attempt"
|
||||||
assert_eq!(dag.get(task)?, Err("Simulated failure"));
|
assert_eq!(output.get(task)?, Err("Simulated failure"));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_circuit_breaker_pattern() -> DagResult<()> {
|
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 failure_count = Arc::new(AtomicUsize::new(0));
|
||||||
let circuit_open = Arc::new(AtomicBool::new(false));
|
let circuit_open = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
@@ -71,12 +72,13 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.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?;
|
.await?;
|
||||||
|
|
||||||
// First few tasks fail and open circuit
|
// First few tasks fail and open circuit
|
||||||
for task in tasks.into_iter().take(4) {
|
for task in tasks.into_iter().take(4) {
|
||||||
assert!(dag.get(task)?.is_err());
|
assert!(output.get(task)?.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -84,8 +86,8 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_error_accumulation_pattern() -> DagResult<()> {
|
async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let errors = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
let errors = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
// Tasks that may produce errors
|
// Tasks that may produce errors
|
||||||
let mut tasks: Vec<_> = (0..10)
|
let mut tasks: Vec<_> = (0..10)
|
||||||
@@ -94,7 +96,10 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
|
|||||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||||
let errors = errors.clone();
|
let errors = errors.clone();
|
||||||
if i % 2 == 0 {
|
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));
|
return Err(format!("Task {} error", i));
|
||||||
}
|
}
|
||||||
Ok(i)
|
Ok(i)
|
||||||
@@ -102,11 +107,12 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.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?;
|
.await?;
|
||||||
|
|
||||||
// Check accumulated errors
|
// 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
|
assert_eq!(error_list.len(), 5); // Even numbered tasks
|
||||||
|
|
||||||
tasks.truncate(2);
|
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();
|
let [first, second] = <[_; 2]>::try_from(tasks).map_err(|_e| ()).unwrap();
|
||||||
|
|
||||||
// Verify specific results
|
// Verify specific results
|
||||||
assert!(dag.get(first)?.is_err());
|
assert!(output.get(first)?.is_err());
|
||||||
assert_eq!(dag.get(second)?, Ok(1));
|
assert_eq!(output.get(second)?, Ok(1));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_error_boundary_isolation() -> DagResult<()> {
|
async fn test_error_boundary_isolation() -> DagResult<()> {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Group A: contains an error - use Result
|
// Group A: contains an error - use Result
|
||||||
let a1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
let a1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||||
@@ -143,15 +149,16 @@ async fn test_error_boundary_isolation() -> DagResult<()> {
|
|||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||||
.depends_on(b2);
|
.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?;
|
.await?;
|
||||||
|
|
||||||
// Group A fails (returns Err)
|
// 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
|
// Groups B and C succeed
|
||||||
assert_eq!(dag.get(b2)?, 40);
|
assert_eq!(output.get(b2)?, 40);
|
||||||
assert_eq!(dag.get(c1)?, 45);
|
assert_eq!(output.get(c1)?, 45);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
+39
-31
@@ -6,43 +6,46 @@ use dagx::*;
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_empty_dag() {
|
async fn test_empty_dag() {
|
||||||
let dag = DagRunner::new();
|
let dag = DagRunner::new();
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let _ = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap(); // Should complete without error
|
.unwrap(); // Should complete without error
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_single_task() {
|
async fn test_single_task() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(dag.get(task).unwrap(), 42);
|
assert_eq!(output.get(task).unwrap(), 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_wide_parallel() {
|
async fn test_wide_parallel() {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let tasks: Vec<_> = (0..10)
|
let tasks: Vec<_> = (0..10)
|
||||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||||
.collect();
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
for (i, task) in tasks.into_iter().enumerate() {
|
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]
|
#[tokio::test]
|
||||||
async fn test_diamond_dependency() {
|
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
|
let b = dag
|
||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||||
.depends_on(a);
|
.depends_on(a);
|
||||||
@@ -53,13 +56,14 @@ async fn test_diamond_dependency() {
|
|||||||
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
|
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
|
||||||
.depends_on((&b, &c));
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Note: a, b, and c are not sinks (they have dependents)
|
// Note: a, b, and c are not sinks (they have dependents)
|
||||||
// Only d is a sink and can be retrieved
|
// Only d is a sink and can be retrieved
|
||||||
assert_eq!(dag.get(d).unwrap(), 35); // 15 + 20
|
assert_eq!(output.get(d).unwrap(), 35); // 15 + 20
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -70,7 +74,7 @@ struct User {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_different_output_types() {
|
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 string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
|
||||||
let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3]));
|
let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3]));
|
||||||
@@ -79,22 +83,23 @@ async fn test_different_output_types() {
|
|||||||
age: 30,
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(&dag.get(string_task).unwrap(), "hello");
|
assert_eq!(&output.get(string_task).unwrap(), "hello");
|
||||||
assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]);
|
assert_eq!(output.get(vec_task).unwrap(), vec![1, 2, 3]);
|
||||||
let user = dag.get(struct_task).unwrap();
|
let user = output.get(struct_task).unwrap();
|
||||||
assert_eq!(user.name, "Alice");
|
assert_eq!(user.name, "Alice");
|
||||||
assert_eq!(user.age, 30);
|
assert_eq!(user.age, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_multiple_sinks() {
|
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
|
// Branch 1
|
||||||
let branch1 = dag
|
let branch1 = dag
|
||||||
@@ -112,21 +117,22 @@ async fn test_multiple_sinks() {
|
|||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||||
.depends_on(branch2);
|
.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
|
.await
|
||||||
.unwrap(); // Should wait for both sinks
|
.unwrap(); // Should wait for both sinks
|
||||||
|
|
||||||
assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1
|
assert_eq!(output.get(sink1).unwrap(), 21); // (10 * 2) + 1
|
||||||
assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3
|
assert_eq!(output.get(sink2).unwrap(), 45); // (10 + 5) * 3
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_single_task_inline_path() {
|
async fn test_single_task_inline_path() {
|
||||||
// Test single-task layer inline execution path (line 367-410 in runner.rs)
|
// 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
|
// 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
|
let t2 = dag
|
||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||||
.depends_on(t1);
|
.depends_on(t1);
|
||||||
@@ -134,19 +140,20 @@ async fn test_single_task_inline_path() {
|
|||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||||
.depends_on(t2);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5
|
assert_eq!(output.get(t3).unwrap(), 25); // (10 * 2) + 5
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_multi_consumer_fanout() {
|
async fn test_multi_consumer_fanout() {
|
||||||
// Test fanout with multiple consumers from one producer
|
// 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
|
// Multiple consumers
|
||||||
let c1 = dag
|
let c1 = dag
|
||||||
@@ -159,11 +166,12 @@ async fn test_multi_consumer_fanout() {
|
|||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
|
||||||
.depends_on(producer);
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(dag.get(c1).unwrap(), 84);
|
assert_eq!(output.get(c1).unwrap(), 84);
|
||||||
assert_eq!(dag.get(c2).unwrap(), 52);
|
assert_eq!(output.get(c2).unwrap(), 52);
|
||||||
assert_eq!(dag.get(c3).unwrap(), 37);
|
assert_eq!(output.get(c3).unwrap(), 37);
|
||||||
}
|
}
|
||||||
@@ -4,19 +4,19 @@ use dagx::{task, DagResult, DagRunner};
|
|||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::sync::Barrier;
|
use tokio::sync::Barrier;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
||||||
// Force specific interleaving using barriers
|
// Force specific interleaving using barriers
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let barrier = Arc::new(Barrier::new(3));
|
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 {
|
struct InterleaveTask {
|
||||||
barrier: Arc<Barrier>,
|
barrier: Arc<Barrier>,
|
||||||
order: Arc<parking_lot::Mutex<Vec<String>>>,
|
order: Arc<Mutex<Vec<String>>>,
|
||||||
prefix: &'static str,
|
prefix: &'static str,
|
||||||
value: i32,
|
value: i32,
|
||||||
}
|
}
|
||||||
@@ -24,9 +24,15 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
|||||||
#[task]
|
#[task]
|
||||||
impl InterleaveTask {
|
impl InterleaveTask {
|
||||||
async fn run(&self) -> i32 {
|
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.barrier.wait().await;
|
||||||
self.order.lock().push(format!("{}_end", self.prefix));
|
self.order
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push(format!("{}_end", self.prefix));
|
||||||
self.value
|
self.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,13 +64,14 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
|||||||
))
|
))
|
||||||
.depends_on((t1, t2, t3));
|
.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?;
|
.await?;
|
||||||
|
|
||||||
assert_eq!(dag.get(collector)?, 6);
|
assert_eq!(output.get(collector)?, 6);
|
||||||
|
|
||||||
// Check interleaving pattern
|
// Check interleaving pattern
|
||||||
let events = order.lock().clone();
|
let events = order.lock().unwrap().clone();
|
||||||
assert_eq!(events.len(), 6);
|
assert_eq!(events.len(), 6);
|
||||||
|
|
||||||
// All starts should come before all ends due to barrier
|
// All starts should come before all ends due to barrier
|
||||||
@@ -80,14 +87,13 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_alternating_execution_pattern() -> DagResult<()> {
|
async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||||
// Create an alternating execution pattern
|
// Create an alternating execution pattern
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let turn = Arc::new(AtomicUsize::new(0));
|
let turn = Arc::new(AtomicUsize::new(0));
|
||||||
let events: Arc<parking_lot::Mutex<Vec<String>>> =
|
let events: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
Arc::new(parking_lot::Mutex::new(Vec::new()));
|
|
||||||
|
|
||||||
struct FirstA {
|
struct FirstA {
|
||||||
turn: Arc<AtomicUsize>,
|
turn: Arc<AtomicUsize>,
|
||||||
events: Arc<parking_lot::Mutex<Vec<String>>>,
|
events: Arc<Mutex<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[task]
|
#[task]
|
||||||
@@ -96,22 +102,20 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
|||||||
while self.turn.load(Ordering::SeqCst) != 0 {
|
while self.turn.load(Ordering::SeqCst) != 0 {
|
||||||
tokio::task::yield_now().await;
|
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);
|
self.turn.store(1, Ordering::SeqCst);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_a = dag.add_task(FirstA {
|
let mut chain_a = dag.add_task(FirstA {
|
||||||
turn: turn.clone(),
|
turn: turn.clone(),
|
||||||
events: events.clone(),
|
events: events.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut chain_a: dagx::TaskHandle<i32> = first_a.into();
|
|
||||||
|
|
||||||
struct FirstB {
|
struct FirstB {
|
||||||
turn: Arc<AtomicUsize>,
|
turn: Arc<AtomicUsize>,
|
||||||
events: Arc<parking_lot::Mutex<Vec<String>>>,
|
events: Arc<Mutex<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[task]
|
#[task]
|
||||||
@@ -120,22 +124,20 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
|||||||
while self.turn.load(Ordering::SeqCst) != 1 {
|
while self.turn.load(Ordering::SeqCst) != 1 {
|
||||||
tokio::task::yield_now().await;
|
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);
|
self.turn.store(2, Ordering::SeqCst);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_b = dag.add_task(FirstB {
|
let mut chain_b = dag.add_task(FirstB {
|
||||||
turn: turn.clone(),
|
turn: turn.clone(),
|
||||||
events: events.clone(),
|
events: events.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut chain_b: dagx::TaskHandle<i32> = first_b.into();
|
|
||||||
|
|
||||||
struct ChainA {
|
struct ChainA {
|
||||||
turn: Arc<AtomicUsize>,
|
turn: Arc<AtomicUsize>,
|
||||||
events: Arc<parking_lot::Mutex<Vec<String>>>,
|
events: Arc<Mutex<Vec<String>>>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +147,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
|||||||
while self.turn.load(Ordering::SeqCst) != self.idx * 2 {
|
while self.turn.load(Ordering::SeqCst) != self.idx * 2 {
|
||||||
tokio::task::yield_now().await;
|
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);
|
self.turn.store(self.idx * 2 + 1, Ordering::SeqCst);
|
||||||
x + 1
|
x + 1
|
||||||
}
|
}
|
||||||
@@ -153,7 +155,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
|||||||
|
|
||||||
struct ChainB {
|
struct ChainB {
|
||||||
turn: Arc<AtomicUsize>,
|
turn: Arc<AtomicUsize>,
|
||||||
events: Arc<parking_lot::Mutex<Vec<String>>>,
|
events: Arc<Mutex<Vec<String>>>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +165,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
|||||||
while self.turn.load(Ordering::SeqCst) != self.idx * 2 + 1 {
|
while self.turn.load(Ordering::SeqCst) != self.idx * 2 + 1 {
|
||||||
tokio::task::yield_now().await;
|
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);
|
self.turn.store(self.idx * 2 + 2, Ordering::SeqCst);
|
||||||
x + 1
|
x + 1
|
||||||
}
|
}
|
||||||
@@ -187,11 +189,12 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
|||||||
.depends_on(chain_b);
|
.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?;
|
.await?;
|
||||||
|
|
||||||
// Check alternating pattern
|
// Check alternating pattern
|
||||||
let event_list = events.lock().clone();
|
let event_list = events.lock().unwrap().clone();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
event_list,
|
event_list,
|
||||||
vec!["A0", "B0", "A1", "B1", "A2", "B2", "A3", "B3", "A4", "B4"]
|
vec!["A0", "B0", "A1", "B1", "A2", "B2", "A3", "B3", "A4", "B4"]
|
||||||
@@ -203,7 +206,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_layered_interleaving() -> DagResult<()> {
|
async fn test_layered_interleaving() -> DagResult<()> {
|
||||||
// Test interleaving across DAG layers
|
// Test interleaving across DAG layers
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
let layer_active = Arc::new(AtomicUsize::new(0));
|
let layer_active = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
struct Layer1Task {
|
struct Layer1Task {
|
||||||
@@ -228,7 +231,6 @@ async fn test_layered_interleaving() -> DagResult<()> {
|
|||||||
active: layer_active.clone(),
|
active: layer_active.clone(),
|
||||||
idx: i,
|
idx: i,
|
||||||
})
|
})
|
||||||
.into()
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -264,11 +266,12 @@ async fn test_layered_interleaving() -> DagResult<()> {
|
|||||||
})
|
})
|
||||||
.depends_on((&layer2[0], &layer2[1]));
|
.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?;
|
.await?;
|
||||||
|
|
||||||
// Sum of 0+1+2+3 = 6
|
// Sum of 0+1+2+3 = 6
|
||||||
assert_eq!(dag.get(layer3)?, 6);
|
assert_eq!(output.get(layer3)?, 6);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
+1
-1
@@ -8,7 +8,7 @@ mod boundaries;
|
|||||||
mod dependencies;
|
mod dependencies;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod execution;
|
mod execution;
|
||||||
|
mod interleaving;
|
||||||
mod parallelism;
|
mod parallelism;
|
||||||
mod runtimes;
|
mod runtimes;
|
||||||
mod timing;
|
|
||||||
mod tracing;
|
mod tracing;
|
||||||
@@ -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<i32> = 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::<i32, _, _>(move |&x: &i32| x * 10))
|
|
||||||
.depends_on(source);
|
|
||||||
|
|
||||||
let transform2 = dag
|
|
||||||
.add_task(task_fn::<i32, _, _>(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<i32> = 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<i32> = 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::<i32, _, _>(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::<i32, _, _>(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<i32> = 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::<i32, _, _>(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::<i32, _, _>(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();
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
//! Parallelism tests module
|
//! Parallelism tests module
|
||||||
|
|
||||||
pub mod concurrent;
|
|
||||||
pub mod spawning;
|
pub mod spawning;
|
||||||
pub mod threads;
|
pub mod threads;
|
||||||
pub mod timing;
|
pub mod timing;
|
||||||
@@ -9,7 +9,7 @@ use std::sync::Arc;
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
|
async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
|
||||||
// Verify that the spawner function is actually called for each task
|
// 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));
|
let spawn_count = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
// Create 10 independent tasks
|
// Create 10 independent tasks
|
||||||
@@ -27,12 +27,13 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Custom spawner that counts invocations
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Verify all tasks were spawned
|
// Verify all tasks were spawned
|
||||||
for (i, task) in tasks.into_iter().enumerate() {
|
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
|
// The spawner should have been called once for each task
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Tests for multi-threading and concurrent execution
|
//! Tests for multi-threading and concurrent execution
|
||||||
|
|
||||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
use dagx::{task, DagResult, DagRunner};
|
||||||
use dagx_test::task_fn;
|
use dagx_test::task_fn;
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
@@ -11,7 +11,7 @@ use tokio::time::sleep;
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||||
// Prove concurrent execution using atomic counters to track simultaneous tasks
|
// 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 concurrent_count = Arc::new(AtomicUsize::new(0));
|
||||||
let max_concurrent = 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();
|
.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?;
|
.await?;
|
||||||
|
|
||||||
// Verify all tasks completed
|
// Verify all tasks completed
|
||||||
for (i, task) in tasks.into_iter().enumerate() {
|
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
|
// Check that we achieved real parallelism
|
||||||
@@ -90,13 +91,13 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_massive_parallel_fanout() -> DagResult<()> {
|
async fn test_massive_parallel_fanout() -> DagResult<()> {
|
||||||
// Test extreme parallelism with 1000 parallel tasks
|
// Test extreme parallelism with 1000 parallel tasks
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let completed = Arc::new(AtomicUsize::new(0));
|
let completed = Arc::new(AtomicUsize::new(0));
|
||||||
let max_concurrent = Arc::new(AtomicUsize::new(0));
|
let max_concurrent = Arc::new(AtomicUsize::new(0));
|
||||||
let current = 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 {
|
struct FanoutTask {
|
||||||
comp: Arc<AtomicUsize>,
|
comp: Arc<AtomicUsize>,
|
||||||
@@ -147,7 +148,8 @@ async fn test_massive_parallel_fanout() -> DagResult<()> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let start = Instant::now();
|
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?;
|
.await?;
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
@@ -155,9 +157,9 @@ async fn test_massive_parallel_fanout() -> DagResult<()> {
|
|||||||
assert_eq!(completed.load(Ordering::SeqCst), 1000);
|
assert_eq!(completed.load(Ordering::SeqCst), 1000);
|
||||||
|
|
||||||
// Check some results
|
// Check some results
|
||||||
assert_eq!(dag.get(tasks[0])?, 1);
|
assert_eq!(output.get(tasks[0])?, 1);
|
||||||
assert_eq!(dag.get(tasks[500])?, 501);
|
assert_eq!(output.get(tasks[500])?, 501);
|
||||||
assert_eq!(dag.get(tasks[999])?, 1000);
|
assert_eq!(output.get(tasks[999])?, 1000);
|
||||||
|
|
||||||
let max_seen = max_concurrent.load(Ordering::SeqCst);
|
let max_seen = max_concurrent.load(Ordering::SeqCst);
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl SleepTask {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_parallel_execution_speedup() -> DagResult<()> {
|
async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||||
// Prove parallel execution by comparing timing
|
// Prove parallel execution by comparing timing
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let work_duration = Duration::from_millis(50);
|
let work_duration = Duration::from_millis(50);
|
||||||
let num_tasks = 10;
|
let num_tasks = 10;
|
||||||
@@ -33,13 +33,14 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let start = Instant::now();
|
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?;
|
.await?;
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
// Verify all tasks completed
|
// Verify all tasks completed
|
||||||
for (i, task) in tasks.into_iter().enumerate() {
|
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
|
// If tasks ran sequentially: 10 * 50ms = 500ms
|
||||||
@@ -53,7 +54,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
|||||||
|
|
||||||
// Parallel execution should be significantly faster
|
// Parallel execution should be significantly faster
|
||||||
assert!(
|
assert!(
|
||||||
elapsed < sequential_time / 4,
|
elapsed < sequential_time / 2,
|
||||||
"Parallel execution too slow: {:?} vs sequential {:?}",
|
"Parallel execution too slow: {:?} vs sequential {:?}",
|
||||||
elapsed,
|
elapsed,
|
||||||
sequential_time
|
sequential_time
|
||||||
@@ -65,7 +66,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||||
// Test that tasks in the same topological layer execute in parallel
|
// 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()));
|
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]))
|
.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?;
|
.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
|
// Analyze timing
|
||||||
let timing = layer_timing.lock().unwrap();
|
let timing = layer_timing.lock().unwrap();
|
||||||
|
|||||||
@@ -95,15 +95,15 @@ impl Add {
|
|||||||
#[test_case(AsyncExecutorRuntimeTest)]
|
#[test_case(AsyncExecutorRuntimeTest)]
|
||||||
fn test_basic_dag(runner: impl RuntimeTest) {
|
fn test_basic_dag(runner: impl RuntimeTest) {
|
||||||
runner.run_dagx_test(|spawner| async {
|
runner.run_dagx_test(|spawner| async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let x = dag.add_task(Value(2));
|
let x = dag.add_task(Value(2));
|
||||||
let y = dag.add_task(Value(3));
|
let y = dag.add_task(Value(3));
|
||||||
let sum = dag.add_task(Add).depends_on((x, y));
|
let sum = dag.add_task(Add).depends_on((x, y));
|
||||||
|
|
||||||
dag.run(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)]
|
#[test_case(AsyncExecutorRuntimeTest)]
|
||||||
fn test_parallel_execution(runner: impl RuntimeTest) {
|
fn test_parallel_execution(runner: impl RuntimeTest) {
|
||||||
runner.run_dagx_test(|spawner| async {
|
runner.run_dagx_test(|spawner| async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let tasks: Vec<_> = (0..10)
|
let tasks: Vec<_> = (0..10)
|
||||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
dag.run(spawner).await.unwrap();
|
let mut output = dag.run(spawner).await.unwrap();
|
||||||
|
|
||||||
for (i, task) in tasks.into_iter().enumerate() {
|
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)]
|
#[test_case(AsyncExecutorRuntimeTest)]
|
||||||
fn test_complex_dependencies(runner: impl RuntimeTest) {
|
fn test_complex_dependencies(runner: impl RuntimeTest) {
|
||||||
runner.run_dagx_test(|spawner| async {
|
runner.run_dagx_test(|spawner| async {
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let a = dag.add_task(Value(10));
|
let a = dag.add_task(Value(10));
|
||||||
let b = dag.add_task(Value(20));
|
let b = dag.add_task(Value(20));
|
||||||
@@ -142,8 +142,8 @@ fn test_complex_dependencies(runner: impl RuntimeTest) {
|
|||||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||||
.depends_on(sum);
|
.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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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<dagx::TaskHandle<usize>> = (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<dagx::TaskHandle<i32>> = 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::<i32, _, _>(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<RwLock<Vec<i32>>>,
|
|
||||||
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<RwLock<Vec<i32>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
//! Timing and race condition tests
|
|
||||||
//!
|
|
||||||
//! Tests for temporal edge cases, execution ordering, and race conditions.
|
|
||||||
|
|
||||||
mod concurrent_access;
|
|
||||||
mod interleaving;
|
|
||||||
@@ -39,17 +39,18 @@ async fn test_tracing_with_subscriber() {
|
|||||||
.with_test_writer()
|
.with_test_writer()
|
||||||
.try_init();
|
.try_init();
|
||||||
|
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
let a = dag.add_task(Value(2));
|
let a = dag.add_task(Value(2));
|
||||||
let b = dag.add_task(Value(3));
|
let b = dag.add_task(Value(3));
|
||||||
let sum = dag.add_task(Add).depends_on((a, b));
|
let sum = dag.add_task(Add).depends_on((a, b));
|
||||||
|
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
assert_eq!(output.get(sum).unwrap(), 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
@@ -60,19 +61,20 @@ async fn test_tracing_inline_execution() {
|
|||||||
.with_test_writer()
|
.with_test_writer()
|
||||||
.try_init();
|
.try_init();
|
||||||
|
|
||||||
let dag = DagRunner::new();
|
let mut dag = DagRunner::new();
|
||||||
|
|
||||||
// Create a linear chain (should trigger inline execution)
|
// 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 b = dag.add_task(Add).depends_on((&a, &a));
|
||||||
let c = dag.add_task(Multiply).depends_on((&b, &b));
|
let c = dag.add_task(Multiply).depends_on((&b, &b));
|
||||||
|
|
||||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
let mut output = dag
|
||||||
|
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// a = 1
|
// a = 1
|
||||||
// b = 1 + 1 = 2
|
// b = 1 + 1 = 2
|
||||||
// c = 2 * 2 = 4
|
// c = 2 * 2 = 4
|
||||||
assert_eq!(dag.get(c).unwrap(), 4);
|
assert_eq!(output.get(c).unwrap(), 4);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user