Merge pull request #11 from TechnoPorg/push-zvntumwxvoru
test: remove/consolidate redundant tests, examples, benchmarks, get tarpaulin coverage back to 80
This commit is contained in:
100 files changed
+763
-10238
No files matched your search
+11
-11
@@ -126,20 +126,20 @@ jobs:
|
||||
- name: Run all examples
|
||||
run: ./scripts/run_examples.sh
|
||||
|
||||
# coverage:
|
||||
# name: Code Coverage
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
coverage:
|
||||
name: Code Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# - name: Install Rust
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# - name: Install cargo-tarpaulin
|
||||
# - uses: taiki-e/install-action@cargo-tarpaulin
|
||||
- name: Install cargo-tarpaulin
|
||||
- uses: taiki-e/install-action@cargo-tarpaulin
|
||||
|
||||
# - name: Generate code coverage
|
||||
# run: cargo tarpaulin --lib --all-features --out Xml --out Lcov --output-dir coverage --fail-under 80
|
||||
- name: Generate code coverage
|
||||
run: cargo tarpaulin --out Xml --out Lcov --output-dir coverage --fail-under 80
|
||||
|
||||
security:
|
||||
name: Security Audit
|
||||
|
||||
Generated
+56
-138
@@ -38,27 +38,6 @@ version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "async-attributes"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener 2.5.3",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
@@ -96,21 +75,6 @@ dependencies = [
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-global-executor"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
@@ -135,7 +99,7 @@ version = "3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
|
||||
dependencies = [
|
||||
"event-listener 5.4.1",
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
@@ -157,14 +121,14 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener 5.4.1",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
]
|
||||
@@ -187,33 +151,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-std"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
|
||||
dependencies = [
|
||||
"async-attributes",
|
||||
"async-channel 1.9.0",
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"crossbeam-utils",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"gloo-timers",
|
||||
"kv-log-macro",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
@@ -228,7 +165,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -270,7 +207,7 @@ version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
@@ -437,16 +374,17 @@ name = "dagx"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"async-executor",
|
||||
"async-std",
|
||||
"async-trait",
|
||||
"criterion",
|
||||
"dagrs",
|
||||
"dagx-macros",
|
||||
"dagx-test",
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
"parking_lot",
|
||||
"pollster",
|
||||
"smol",
|
||||
"test-case",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -458,7 +396,14 @@ version = "0.3.1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dagx-test"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"dagx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -477,12 +422,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
@@ -500,7 +439,7 @@ version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener 5.4.1",
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -580,7 +519,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -619,18 +558,6 @@ version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.6.0"
|
||||
@@ -683,15 +610,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kv-log-macro"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -724,9 +642,6 @@ name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
@@ -1061,7 +976,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1113,7 +1028,7 @@ version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-channel",
|
||||
"async-executor",
|
||||
"async-fs",
|
||||
"async-io",
|
||||
@@ -1124,17 +1039,6 @@ dependencies = [
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
@@ -1146,6 +1050,39 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test-case"
|
||||
version = "3.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
|
||||
dependencies = [
|
||||
"test-case-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test-case-core"
|
||||
version = "3.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test-case-macros"
|
||||
version = "3.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
@@ -1188,7 +1125,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1210,7 +1147,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1264,12 +1201,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@@ -1309,23 +1240,10 @@ dependencies = [
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.104"
|
||||
@@ -1344,7 +1262,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
+7
-3
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["dagx-macros"]
|
||||
members = ["dagx-macros", "dagx-test"]
|
||||
|
||||
[package]
|
||||
name = "dagx"
|
||||
@@ -21,12 +21,15 @@ exclude = [".claude/", ".jj/", "dagx-spec.md"]
|
||||
default = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[lints.rust]
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin)',
|
||||
'cfg(tarpaulin_include)',
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12"
|
||||
@@ -34,13 +37,13 @@ dagx-macros = { version = "0.3.1", path = "./dagx-macros" }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dagx-test = { path = "./dagx-test" }
|
||||
tokio = { version = "1", features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
async-std = { version = "1", features = ["attributes"] }
|
||||
smol = "2"
|
||||
criterion = "0.7"
|
||||
# Quirky async runtimes for edge-case testing
|
||||
@@ -52,6 +55,7 @@ dagrs = { version = "0.5", default-features = false }
|
||||
async-trait = "0.1"
|
||||
# Tracing support for tests and examples
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
test-case = "3.3.1"
|
||||
|
||||
[[bench]]
|
||||
name = "dagx_bench"
|
||||
|
||||
@@ -214,8 +214,6 @@ impl ProcessUser {
|
||||
|
||||
**No trait implementations needed!** The `#[task]` macro generates type-specific extraction logic automatically. This includes nested structs, collections, enums, and any other type you define.
|
||||
|
||||
See [`custom_types.rs`](examples/custom_types.rs) for a complete example with nested custom types.
|
||||
|
||||
### Runtime Agnostic
|
||||
|
||||
dagx works with any async runtime. Provide a spawner function to `run()`:
|
||||
@@ -261,17 +259,8 @@ cargo run --example 04_parallel_computation
|
||||
Real-world patterns:
|
||||
|
||||
- [`circuit_breaker.rs`](examples/circuit_breaker.rs) - Circuit breaker pattern for resilient systems
|
||||
- [`complex_dag.rs`](examples/complex_dag.rs) - Multi-layer workflows with complex dependencies
|
||||
- [`conditional_workflow.rs`](examples/conditional_workflow.rs) - Conditional execution with Result types
|
||||
- [`custom_types.rs`](examples/custom_types.rs) - Using your own custom types (no trait implementations needed!)
|
||||
- [`data_pipeline.rs`](examples/data_pipeline.rs) - ETL data processing pipeline
|
||||
- [`debug_tracing.rs`](examples/debug_tracing.rs) - Debug metadata and task naming
|
||||
- [`error_handling.rs`](examples/error_handling.rs) - Error propagation and recovery
|
||||
- [`large_dag.rs`](examples/large_dag.rs) - Performance at 10,000+ nodes
|
||||
- [`parallelism_proof.rs`](examples/parallelism_proof.rs) - Proof of true parallel execution
|
||||
- [`retry_strategies.rs`](examples/retry_strategies.rs) - Retry patterns for transient failures
|
||||
- [`timeout.rs`](examples/timeout.rs) - Managing task timeouts
|
||||
- [`tracing_example.rs`](examples/tracing_example.rs) - Observability with tracing support
|
||||
|
||||
Run any example: `cargo run --example custom_types`
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
//! Basic DAG execution benchmarks
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner};
|
||||
|
||||
pub fn bench_dag_execution(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
c.bench_function("execute_10_independent_tasks", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..10 {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
}
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("execute_100_independent_tasks", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..100 {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
}
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("execute_deep_chain_20", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Build a manual chain (API doesn't support loop-based chaining due to type system)
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
let t0 = dag.add_task(Value(0));
|
||||
let t1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t0);
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t1);
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t2);
|
||||
let t4 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t3);
|
||||
let t5 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t4);
|
||||
let t6 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t5);
|
||||
let t7 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t6);
|
||||
let t8 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t7);
|
||||
let t9 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t8);
|
||||
let t10 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t9);
|
||||
let t11 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t10);
|
||||
let t12 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t11);
|
||||
let t13 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t12);
|
||||
let t14 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t13);
|
||||
let t15 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t14);
|
||||
let t16 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t15);
|
||||
let t17 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t16);
|
||||
let t18 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t17);
|
||||
let _t19 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(t18);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Basic DAG benchmarks - creation, execution, scaling
|
||||
|
||||
pub mod creation;
|
||||
pub mod execution;
|
||||
pub mod scaling;
|
||||
@@ -1,7 +1,8 @@
|
||||
//! DAG scaling benchmarks - how performance scales with task count
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
pub fn bench_dag_scaling(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
@@ -13,7 +13,8 @@ pub fn bench_deep_chain(c: &mut Criterion) {
|
||||
group.bench_function("dagx", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ pub fn bench_diamond_pattern(c: &mut Criterion) {
|
||||
group.bench_function("dagx", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
group.bench_function("dagx", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ pub fn bench_large_scale(c: &mut Criterion) {
|
||||
group.bench_function("dagx", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
|
||||
@@ -12,7 +12,8 @@ pub fn bench_linear_pipeline(c: &mut Criterion) {
|
||||
group.bench_function("dagx", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ pub fn bench_wide_fanout(c: &mut Criterion) {
|
||||
group.bench_function("dagx", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
@@ -28,12 +28,6 @@ criterion_group! {
|
||||
targets = basic::creation::bench_dag_creation
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = execution_benches;
|
||||
config = configure_criterion();
|
||||
targets = basic::execution::bench_dag_execution
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = scaling_benches;
|
||||
config = configure_criterion();
|
||||
@@ -54,7 +48,6 @@ criterion_group! {
|
||||
config = configure_criterion();
|
||||
targets =
|
||||
realistic::etl::bench_etl_pipeline,
|
||||
realistic::config_broadcast::bench_config_broadcast,
|
||||
realistic::memory_efficiency::bench_memory_efficiency,
|
||||
realistic::mixed::bench_mixed_patterns
|
||||
}
|
||||
@@ -73,7 +66,6 @@ criterion_group! {
|
||||
|
||||
criterion_main!(
|
||||
creation_benches,
|
||||
execution_benches,
|
||||
scaling_benches,
|
||||
pattern_benches,
|
||||
realistic_benches,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! 10,000 task breakdown benchmarks - construction vs execution
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use criterion::{BatchSize, Criterion};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
pub fn bench_10k_breakdown(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
@@ -35,19 +36,23 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
|
||||
|
||||
// c) Execution only (pre-built DAG, just run)
|
||||
group.bench_function("execution_only", |b| {
|
||||
// Pre-build the DAG outside the measurement
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
}
|
||||
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
}
|
||||
dag
|
||||
},
|
||||
|dag| {
|
||||
rt.block_on(async {
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
|
||||
group.finish();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Diamond pattern benchmarks (fan-out then fan-in)
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner, TaskHandle};
|
||||
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
pub fn bench_diamond(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Fan-out pattern benchmarks (1 → N dependencies)
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner, TaskHandle};
|
||||
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
pub fn bench_fanout(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
@@ -51,30 +51,4 @@ pub fn bench_fanout(c: &mut Criterion) {
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// Arc overhead benchmark - shows cost on Copy types
|
||||
// For Copy types like usize, Arc adds overhead (atomic refcounting + indirection)
|
||||
// For heap types like Vec, Arc overhead is negligible compared to clone cost
|
||||
c.bench_function("arc_overhead_copy_type", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Source produces a simple Copy type (usize)
|
||||
// Framework wraps in Arc<usize> internally
|
||||
let source: TaskHandle<_> =
|
||||
dag.add_task(task_fn::<(), _, _>(|_: ()| 42usize)).into();
|
||||
|
||||
// 100 dependents - Arc overhead visible on Copy types
|
||||
for i in 0..100 {
|
||||
dag.add_task(task_fn::<usize, _, _>(move |x: &usize| x + i))
|
||||
.depends_on(source);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
//! Configuration broadcast pattern benchmark - one config shared across many workers
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{task_fn, DagRunner, TaskHandle};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::hint::black_box;
|
||||
|
||||
pub fn bench_config_broadcast(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("config_broadcast");
|
||||
|
||||
for worker_count in [10, 50, 100, 200].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("config", worker_count),
|
||||
worker_count,
|
||||
|b, &workers| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Configuration source - framework wraps HashMap in Arc automatically
|
||||
let config: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
HashMap::from([
|
||||
("max_connections".to_string(), "100".to_string()),
|
||||
("timeout_ms".to_string(), "5000".to_string()),
|
||||
("batch_size".to_string(), "50".to_string()),
|
||||
("retry_count".to_string(), "3".to_string()),
|
||||
("worker_threads".to_string(), "4".to_string()),
|
||||
("buffer_size".to_string(), "8192".to_string()),
|
||||
("compression".to_string(), "true".to_string()),
|
||||
("log_level".to_string(), "info".to_string()),
|
||||
])
|
||||
}))
|
||||
.into();
|
||||
|
||||
// Workers receive and process based on shared config
|
||||
for worker_id in 0..workers {
|
||||
dag.add_task(task_fn::<HashMap<_, _>, _, _>(
|
||||
move |cfg: &HashMap<String, String>| {
|
||||
// Simulate work based on configuration
|
||||
let batch_size: usize = cfg
|
||||
.get("batch_size")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(10);
|
||||
let timeout: usize = cfg
|
||||
.get("timeout_ms")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1000);
|
||||
|
||||
// Simulate some computation
|
||||
black_box(worker_id * batch_size + timeout / 100)
|
||||
},
|
||||
))
|
||||
.depends_on(config);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
//! ETL (Extract, Transform, Load) pipeline benchmark
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{task_fn, DagRunner, TaskHandle};
|
||||
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("etl_scaling");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Memory efficiency benchmark with large data structures
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{task_fn, DagRunner, TaskHandle};
|
||||
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
use std::hint::black_box;
|
||||
|
||||
pub fn bench_memory_efficiency(c: &mut Criterion) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Mixed realistic patterns - fan-out, fan-in, and processing
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner, TaskHandle};
|
||||
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
pub fn bench_mixed_patterns(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Realistic workload benchmarks
|
||||
|
||||
pub mod config_broadcast;
|
||||
pub mod etl;
|
||||
pub mod memory_efficiency;
|
||||
pub mod mixed;
|
||||
@@ -98,7 +98,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret
|
||||
/// self.value
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
/// ```
|
||||
///
|
||||
/// ## Single Input
|
||||
///
|
||||
@@ -113,7 +113,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret
|
||||
/// input * 2
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
/// ```
|
||||
///
|
||||
/// ## Multiple Inputs (up to 8)
|
||||
///
|
||||
@@ -128,7 +128,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret
|
||||
/// format!("{}: {} ({})", b, a, c)
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
/// ```
|
||||
///
|
||||
/// # Requirements
|
||||
///
|
||||
@@ -168,7 +168,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret
|
||||
/// a + b
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
/// ```
|
||||
///
|
||||
/// For stateful tasks (with &mut self):
|
||||
///
|
||||
@@ -199,7 +199,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let impl_block = parse_macro_input!(item as ItemImpl);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "dagx-test"
|
||||
version = "0.3.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.81"
|
||||
authors = ["TechnoPorg"]
|
||||
license = "MIT"
|
||||
description = "Utilities for dagx's internal tests"
|
||||
repository = "https://github.com/swaits/dagx"
|
||||
homepage = "https://github.com/swaits/dagx"
|
||||
documentation = "https://docs.rs/dagx-test"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
dagx = { version = "0.3.1", path = "../" }
|
||||
@@ -1,46 +1,10 @@
|
||||
//! Input extraction for task dependencies via a list of dependencies.
|
||||
//!
|
||||
//! # Legacy Trait for Internal Use Only
|
||||
//!
|
||||
//! **NOTE**: This module is now primarily for internal use. The `ExtractInput` trait is only
|
||||
//! used by the internal `task_fn` test helper function.
|
||||
//!
|
||||
//! **For regular tasks**: The `#[task]` macro generates inline `run()` methods with
|
||||
//! type-specific extraction logic. This means **ANY type** implementing `Send + Sync`
|
||||
//! works automatically without needing `ExtractInput` implementations!
|
||||
//!
|
||||
//! # How it works
|
||||
//!
|
||||
//! When a task executes, it needs to extract its inputs from type-erased oneshot dependencies.
|
||||
//! Outputs are wrapped in Arc for efficient fanout (cheap Arc clones instead of data clones).
|
||||
//!
|
||||
//! **Modern approach (via `#[task]` macro)**:
|
||||
//! - The macro generates custom extraction logic inline in `run()`
|
||||
//! - Works with ANY type (just needs `Send + Sync + 'static`)
|
||||
//! - No trait implementations needed!
|
||||
//!
|
||||
//! **Legacy approach (via `ExtractInput` trait)**:
|
||||
//! - Used only by the internal `task_fn` test helper
|
||||
//! - Provides implementations for primitives, standard types, and tuples
|
||||
//! - Requires explicit trait implementations for custom types (not recommended)
|
||||
//!
|
||||
//! # Why macros?
|
||||
//!
|
||||
//! Rust lacks variadic generics, so we use macros to generate implementations for:
|
||||
//! - Common primitive/standard types (one impl per type)
|
||||
//! - Tuples of different sizes (one impl per size, up to 8 elements)
|
||||
//!
|
||||
//! This is standard practice in Rust for working with tuples of different arities.
|
||||
//! Input extraction for tasks from a list of dependencies.
|
||||
|
||||
use std::{collections::HashMap, hash::Hash, sync::Arc};
|
||||
|
||||
use crate::TaskInput;
|
||||
|
||||
/// Helper trait for input extraction from list of dependencies.
|
||||
///
|
||||
/// Type erasure occurs only at the ExecutableNode trait boundary - by the time
|
||||
/// we're in ExtractInput, we know the concrete type and can safely downcast.
|
||||
#[allow(dead_code)]
|
||||
pub trait ExtractInput: Sized + Clone {
|
||||
type Input: Send;
|
||||
type Retv<'input>;
|
||||
@@ -158,18 +122,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro to implement ExtractInput for tuple types.
|
||||
///
|
||||
/// We need separate implementations for each tuple size (2-8) because Rust doesn't support
|
||||
/// variadic generics. This is standard practice when working with tuples in Rust.
|
||||
///
|
||||
/// # Compile-time Safety
|
||||
///
|
||||
/// The number of dependencies is guaranteed to match the tuple size at compile time through:
|
||||
/// 1. The `DepsTuple<Tk::Input>` trait bound on `depends_on()` ensures type/arity matching
|
||||
/// 2. The `IsUnitType` constraint on `From<TaskBuilder>` prevents using tasks without calling `depends_on()`
|
||||
///
|
||||
/// Therefore, no runtime validation is needed - if it compiles, the dependency count is correct.
|
||||
macro_rules! impl_extract_tuple {
|
||||
($($T:ident),+) => {
|
||||
impl<$($T: 'static + Clone + Send + Sync),+> ExtractInput for ($($T,)+) {
|
||||
@@ -0,0 +1,50 @@
|
||||
//! DAG builder helpers for internal use in dagx tests and benchmarks.
|
||||
//!
|
||||
//! This crate is not meant for public use and offers no stability guarantees.
|
||||
//! The ergonomics are also such that you don't want to use it anyways. **Use the `#[task]` macro instead.**
|
||||
|
||||
#![cfg(not(tarpaulin_include))]
|
||||
|
||||
use crate::extract::ExtractInput;
|
||||
|
||||
use dagx::{Task, TaskInput};
|
||||
|
||||
mod extract;
|
||||
|
||||
pub struct TaskFn<I, O, F>
|
||||
where
|
||||
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
|
||||
I: ExtractInput + Send + Sync + 'static,
|
||||
O: Send,
|
||||
{
|
||||
f: F,
|
||||
_phantom: std::marker::PhantomData<fn(I) -> O>,
|
||||
}
|
||||
|
||||
impl<I, O, F> Task for TaskFn<I, O, F>
|
||||
where
|
||||
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
|
||||
I: ExtractInput + Send + Sync + 'static,
|
||||
O: Send + Sync,
|
||||
{
|
||||
type Input = I::Input;
|
||||
type Output = O;
|
||||
|
||||
async fn run(mut self, input: TaskInput<'_, I::Input>) -> O {
|
||||
(self.f)(I::extract_from_task_input(input).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to create a task from a closure.
|
||||
pub fn task_fn<I, O, F>(f: F) -> TaskFn<I, O, F>
|
||||
where
|
||||
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
|
||||
I: ExtractInput + Send + Sync + 'static,
|
||||
I::Input: Send + Sync + 'static,
|
||||
O: Send + Sync + 'static,
|
||||
{
|
||||
TaskFn {
|
||||
f,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,6 @@ RUST_LOG=dagx=debug cargo run # Task and layer details
|
||||
RUST_LOG=dagx=trace cargo run # All execution details
|
||||
```
|
||||
|
||||
See [`examples/tracing_example.rs`](../examples/tracing_example.rs) for a complete working example.
|
||||
|
||||
## Zero-Cost Guarantee
|
||||
|
||||
When the `tracing` feature is disabled (the default), there is **literally 0ns overhead**:
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
//! # Building Complex Workflows: Multi-Layer DAG
|
||||
//!
|
||||
//! This example demonstrates how to build complex, real-world DAGs by combining
|
||||
//! multiple dependency patterns into a cohesive workflow.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to combine fan-out, fan-in, and diamond patterns in one DAG
|
||||
//! - How to build multi-layer workflows with intermediate results
|
||||
//! - How to structure an ETL (Extract-Transform-Load) pipeline
|
||||
//! - How to pass results through multiple transformation stages
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! EXTRACT TRANSFORM COMBINE AGGREGATE
|
||||
//! ┌──────────┐ ┌──────────┐
|
||||
//! │ LoadData │────►│Transform │────┐
|
||||
//! │ A │ │ (×2) │ │
|
||||
//! └──────────┘ └──────────┘ │
|
||||
//! ├──►┌─────────┐───►┌─────────┐───►┌─────────┐
|
||||
//! │ │ Combine │ │ Average │ │ Report │
|
||||
//! └──►└─────────┘ └─────────┘ └─────────┘
|
||||
//! ┌──────────┐ ┌──────────┐ ┌──► │ │
|
||||
//! │ LoadData │────►│Transform │────┘ │ │
|
||||
//! │ B │ │ (×3) │ └──────────────┘
|
||||
//! └──────────┘ └──────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Multi-layer execution**: Tasks organized into logical stages (Extract → Transform → Combine)
|
||||
//! - **Many-to-many dependencies**: The Combine task depends on both Transform tasks
|
||||
//! - **Result reuse**: The combined result is used by both Average and Report tasks
|
||||
//! - **Diamond pattern**: Multiple paths from sources converge and then diverge again
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - ETL pipelines with multiple data sources
|
||||
//! - Multi-stage data processing workflows
|
||||
//! - Complex business logic with branching and merging
|
||||
//! - Data aggregation and reporting systems
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example complex_dag
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! Building ETL pipeline...
|
||||
//!
|
||||
//! Executing pipeline...
|
||||
//!
|
||||
//! Loading data from source A
|
||||
//! Loading data from source B
|
||||
//! Transforming: 6 * 2 = 12
|
||||
//! Transforming: 15 * 3 = 45
|
||||
//! Combining datasets: 12 + 45 = 57
|
||||
//! Computing average: 57 / 6 = 9.50
|
||||
//!
|
||||
//! ETL Pipeline Results:
|
||||
//! Items: 6
|
||||
//! Total: 57
|
||||
//! Average: 9.50
|
||||
//!
|
||||
//! Calculation:
|
||||
//! Source A: sum=6, transformed (x2) = 12
|
||||
//! Source B: sum=15, transformed (x3) = 45
|
||||
//! Combined: 6 items, sum=57, avg=9.50
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
|
||||
// Extract: Load data from different sources
|
||||
struct LoadData {
|
||||
source: &'static str,
|
||||
}
|
||||
|
||||
impl LoadData {
|
||||
fn new(source: &'static str) -> Self {
|
||||
Self { source }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl LoadData {
|
||||
async fn run(&mut self) -> i32 {
|
||||
// Simulate loading and summing data
|
||||
println!("Loading data from source {}", self.source);
|
||||
match self.source {
|
||||
"A" => 6, // sum of [1, 2, 3]
|
||||
"B" => 15, // sum of [4, 5, 6]
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform: Process data with a multiplier
|
||||
struct Transform {
|
||||
multiplier: i32,
|
||||
}
|
||||
|
||||
impl Transform {
|
||||
fn new(multiplier: i32) -> Self {
|
||||
Self { multiplier }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Transform {
|
||||
async fn run(&mut self, sum: &i32) -> i32 {
|
||||
let result = sum * self.multiplier;
|
||||
println!("Transforming: {} * {} = {}", sum, self.multiplier, result);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// Combine: Merge two datasets
|
||||
struct Combine;
|
||||
|
||||
#[task]
|
||||
impl Combine {
|
||||
async fn run(sum_a: &i32, sum_b: &i32) -> i32 {
|
||||
println!(
|
||||
"Combining datasets: {} + {} = {}",
|
||||
sum_a,
|
||||
sum_b,
|
||||
sum_a + sum_b
|
||||
);
|
||||
sum_a + sum_b
|
||||
}
|
||||
}
|
||||
|
||||
// Compute average (using a fixed count for simplicity)
|
||||
struct ComputeAverage {
|
||||
total_count: i32,
|
||||
}
|
||||
|
||||
impl ComputeAverage {
|
||||
fn new(total_count: i32) -> Self {
|
||||
Self { total_count }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ComputeAverage {
|
||||
async fn run(&mut self, sum: &i32) -> f64 {
|
||||
let avg = *sum as f64 / self.total_count as f64;
|
||||
println!(
|
||||
"Computing average: {} / {} = {:.2}",
|
||||
sum, self.total_count, avg
|
||||
);
|
||||
avg
|
||||
}
|
||||
}
|
||||
|
||||
// Format report
|
||||
struct FormatReport {
|
||||
count: i32,
|
||||
}
|
||||
|
||||
impl FormatReport {
|
||||
fn new(count: i32) -> Self {
|
||||
Self { count }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl FormatReport {
|
||||
async fn run(&mut self, sum: &i32, avg: &f64) -> String {
|
||||
format!(
|
||||
"ETL Pipeline Results:\n Items: {}\n Total: {}\n Average: {:.2}",
|
||||
self.count, sum, avg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
println!("Building ETL pipeline...\n");
|
||||
|
||||
// Extract: Load from two sources (parallel)
|
||||
let source_a = dag.add_task(LoadData::new("A"));
|
||||
let source_b = dag.add_task(LoadData::new("B"));
|
||||
|
||||
// Transform: Process each source (parallel)
|
||||
let transform_a = dag.add_task(Transform::new(2)).depends_on(source_a);
|
||||
let transform_b = dag.add_task(Transform::new(3)).depends_on(source_b);
|
||||
|
||||
// Combine: Merge the transformed datasets
|
||||
let combined = dag
|
||||
.add_task(Combine)
|
||||
.depends_on((&transform_a, &transform_b));
|
||||
|
||||
// Compute average on combined data (we know we have 6 items total)
|
||||
let avg = dag.add_task(ComputeAverage::new(6)).depends_on(combined);
|
||||
|
||||
// Format final report (needs both sum and average)
|
||||
let report = dag
|
||||
.add_task(FormatReport::new(6))
|
||||
.depends_on((&combined, &avg));
|
||||
|
||||
// Execute the entire ETL pipeline
|
||||
println!("Executing pipeline...\n");
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Display final report
|
||||
println!("\n{}", dag.get(report)?);
|
||||
println!("\nCalculation:");
|
||||
println!(" Source A: sum=6, transformed (x2) = 12");
|
||||
println!(" Source B: sum=15, transformed (x3) = 45");
|
||||
println!(" Combined: 6 items, sum=57, avg=9.50");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
//! # Pattern: Conditional Execution with Data Validation
|
||||
//!
|
||||
//! This example demonstrates how to implement conditional logic in DAGs by using
|
||||
//! simple data transformations rather than traditional Result types.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to implement validation logic in parallel tasks
|
||||
//! - How to use return values for conditional behavior (e.g., 0 for invalid)
|
||||
//! - How to categorize and branch based on computed values
|
||||
//! - How to aggregate results from multiple conditional branches
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! VALIDATE CATEGORIZE SUMMARIZE
|
||||
//! ┌─────────────┐ ┌──────────────┐
|
||||
//! │ Validate(10)│────20──►│Categorize(25)│────"low"────┐
|
||||
//! └─────────────┘ └──────────────┘ │
|
||||
//! │
|
||||
//! ┌─────────────┐ ┌──────────────┐ │
|
||||
//! │ Validate(-5)│────0───►│Categorize(25)│──"invalid"──┼──►┌────────────┐
|
||||
//! └─────────────┘ └──────────────┘ │ │ Summarize │
|
||||
//! │ └────────────┘
|
||||
//! ┌─────────────┐ ┌──────────────┐ │
|
||||
//! │ Validate(20)│────40──►│Categorize(25)│────"high"───┘
|
||||
//! └─────────────┘ └──────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Validation as transformation**: Invalid inputs become sentinel values (0)
|
||||
//! - **Parallel validation**: All inputs validated simultaneously
|
||||
//! - **Data-driven branching**: Categorization based on computed values
|
||||
//! - **Type-safe aggregation**: All branches produce the same type (String)
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - Input validation pipelines
|
||||
//! - Multi-stage data cleaning
|
||||
//! - Classification and categorization workflows
|
||||
//! - Quality control systems
|
||||
//! - Data preprocessing for ML pipelines
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example conditional_workflow
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! Processing and categorizing inputs in parallel...
|
||||
//!
|
||||
//! Valid input 10: doubling to 20
|
||||
//! Invalid input -5: returning 0
|
||||
//! Valid input 20: doubling to 40
|
||||
//!
|
||||
//! Final summary: Categories: [low, invalid, high]
|
||||
//! Expected: Categories: [low, invalid, high]
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
|
||||
// Task that validates and processes input
|
||||
struct ValidateAndDouble {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
impl ValidateAndDouble {
|
||||
fn new(value: i32) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ValidateAndDouble {
|
||||
async fn run(&mut self) -> i32 {
|
||||
if self.value > 0 {
|
||||
println!("Valid input {}: doubling to {}", self.value, self.value * 2);
|
||||
self.value * 2
|
||||
} else {
|
||||
println!("Invalid input {}: returning 0", self.value);
|
||||
0 // Return 0 for invalid inputs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task that categorizes a number
|
||||
struct Categorize {
|
||||
threshold: i32,
|
||||
}
|
||||
|
||||
impl Categorize {
|
||||
fn new(threshold: i32) -> Self {
|
||||
Self { threshold }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Categorize {
|
||||
async fn run(&mut self, value: &i32) -> String {
|
||||
if *value == 0 {
|
||||
"invalid".to_string()
|
||||
} else if *value >= self.threshold {
|
||||
"high".to_string()
|
||||
} else {
|
||||
"low".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task that combines categorization results
|
||||
struct SummarizeResults;
|
||||
|
||||
#[task]
|
||||
impl SummarizeResults {
|
||||
async fn run(cat1: &String, cat2: &String, cat3: &String) -> String {
|
||||
format!("Categories: [{}, {}, {}]", cat1, cat2, cat3)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
println!("Processing and categorizing inputs in parallel...\n");
|
||||
|
||||
// Validate and process multiple inputs in parallel
|
||||
let processed1 = dag.add_task(ValidateAndDouble::new(10)); // Valid: 20
|
||||
let processed2 = dag.add_task(ValidateAndDouble::new(-5)); // Invalid: 0
|
||||
let processed3 = dag.add_task(ValidateAndDouble::new(20)); // Valid: 40
|
||||
|
||||
// Categorize each result (also parallel, threshold = 25)
|
||||
let cat1 = dag.add_task(Categorize::new(25)).depends_on(processed1); // 20 < 25 = "low"
|
||||
let cat2 = dag.add_task(Categorize::new(25)).depends_on(processed2); // 0 = "invalid"
|
||||
let cat3 = dag.add_task(Categorize::new(25)).depends_on(processed3); // 40 >= 25 = "high"
|
||||
|
||||
// Combine results
|
||||
let summary = dag
|
||||
.add_task(SummarizeResults)
|
||||
.depends_on((&cat1, &cat2, &cat3));
|
||||
|
||||
// Execute
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Display results
|
||||
println!("\nFinal summary: {}", dag.get(summary)?);
|
||||
println!("Expected: Categories: [low, invalid, high]");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
//! # Custom Types: Using Your Own Structs
|
||||
//!
|
||||
//! This example demonstrates that **ANY custom type** works in dagx as long as it implements
|
||||
//! ` Send + Sync + 'static`. You don't need to implement any special traits!
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to use custom structs as task outputs
|
||||
//! - How custom types flow through the DAG automatically
|
||||
//! - How one task's output can be consumed by multiple downstream tasks
|
||||
//!
|
||||
//! ## Key Insight
|
||||
//!
|
||||
//! **dagx works with ANY type automatically!** The `#[task]` macro generates type-specific
|
||||
//! extraction logic, so you don't need to implement any special traits. Just make sure your
|
||||
//! type implements:
|
||||
//! - `Send + Sync` (required for async tasks)
|
||||
//! - `'static` (no lifetime constraints)
|
||||
//!
|
||||
//! That's it! No ExtractInput, no special annotations, no trait boilerplate.
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example custom_types
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! Creating custom types example...
|
||||
//!
|
||||
//! Creating user: Alice (age: 30)
|
||||
//! Processing user Alice for greeting
|
||||
//! Processing user Alice for validation
|
||||
//! Greeting: Hello, Alice!
|
||||
//! Validation: Alice is valid (age 30 >= 18)
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
// Task that produces a custom type
|
||||
struct CreateUser {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl CreateUser {
|
||||
async fn run(&self) -> User {
|
||||
println!("Creating user: {} (age: {})", self.name, self.age);
|
||||
User {
|
||||
name: self.name.clone(),
|
||||
age: self.age,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task that consumes the custom type and produces a String
|
||||
struct GreetUser;
|
||||
|
||||
#[task]
|
||||
impl GreetUser {
|
||||
async fn run(user: &User) -> String {
|
||||
println!("Processing user {} for greeting", user.name);
|
||||
format!("Hello, {}!", user.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Another task that also consumes the same custom type
|
||||
// This demonstrates fan-out: one output consumed by multiple tasks
|
||||
struct ValidateUser;
|
||||
|
||||
#[task]
|
||||
impl ValidateUser {
|
||||
async fn run(user: &User) -> String {
|
||||
println!("Processing user {} for validation", user.name);
|
||||
if user.age >= 18 {
|
||||
format!("{} is valid (age {} >= 18)", user.name, user.age)
|
||||
} else {
|
||||
format!("{} is too young (age {} < 18)", user.name, user.age)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("Creating custom types example...\n");
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a task that produces a custom User type
|
||||
let user_task: TaskHandle<_> = dag
|
||||
.add_task(CreateUser {
|
||||
name: "Alice".to_string(),
|
||||
age: 30,
|
||||
})
|
||||
.into();
|
||||
|
||||
// Two different tasks consume the same custom User type
|
||||
// dagx automatically handles Arc wrapping, so both tasks get &User
|
||||
let greeting = dag.add_task(GreetUser).depends_on(user_task);
|
||||
let validation = dag.add_task(ValidateUser).depends_on(user_task);
|
||||
|
||||
// Execute the DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Print results
|
||||
println!("Greeting: {}", dag.get(greeting).unwrap());
|
||||
println!("Validation: {}", dag.get(validation).unwrap());
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
//! # Debug Tracing and Task Naming
|
||||
//!
|
||||
//! This example demonstrates how to add debug metadata and tracing information to tasks
|
||||
//! for better observability in complex DAG workflows.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to add task names and IDs for debugging
|
||||
//! - How to implement correlation IDs for distributed tracing
|
||||
//! - How to track task execution timing and lifecycle events
|
||||
//! - How to build traceable task wrappers
|
||||
//!
|
||||
//! ## Debugging Patterns
|
||||
//!
|
||||
//! **1. Named Tasks** - Add human-readable names to tasks
|
||||
//! ```rust
|
||||
//! struct TaskMetadata {
|
||||
//! id: String,
|
||||
//! name: String,
|
||||
//! start_time: Option<Instant>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **2. Correlation IDs** - Track requests across task boundaries
|
||||
//! ```rust
|
||||
//! struct TraceContext {
|
||||
//! correlation_id: String,
|
||||
//! parent_task: Option<String>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **3. Execution Metrics** - Measure task performance
|
||||
//! ```rust
|
||||
//! struct TaskTrace {
|
||||
//! name: String,
|
||||
//! duration_ms: u64,
|
||||
//! status: &'static str,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! TRACE: order-12345
|
||||
//! ┌──────────────────┐
|
||||
//! │ ValidateOrder │ [order-12345] Start: 0ms
|
||||
//! │ ID: task-001 │──────► Status: Success
|
||||
//! └──────────────────┘ Duration: 10ms
|
||||
//! │
|
||||
//! ├──────────────────┬──────────────────┐
|
||||
//! ▼ ▼ ▼
|
||||
//! ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
//! │ CheckInventory │ │ ValidatePayment │ │ CalculateShipping│
|
||||
//! │ ID: task-002 │ │ ID: task-003 │ │ ID: task-004 │
|
||||
//! └──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
//! [order-12345] [order-12345] [order-12345]
|
||||
//! Duration: 45ms Duration: 120ms Duration: 15ms
|
||||
//! │ │ │
|
||||
//! └──────────────────┴──────────────────┘
|
||||
//! ▼
|
||||
//! ┌──────────────────┐
|
||||
//! │ FulfillOrder │ [order-12345]
|
||||
//! │ ID: task-005 │ Duration: 30ms
|
||||
//! └──────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Observability**: Make task execution visible for debugging
|
||||
//! - **Correlation IDs**: Track a request through multiple tasks
|
||||
//! - **Structured logging**: Consistent task metadata across logs
|
||||
//! - **Performance metrics**: Measure task execution time
|
||||
//! - **Task lifecycle**: Track start, completion, and errors
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - Debugging complex workflows in production
|
||||
//! - Performance analysis and bottleneck identification
|
||||
//! - Distributed tracing in microservices
|
||||
//! - SLA monitoring and alerting
|
||||
//! - Audit logging for compliance
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example debug_tracing
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Debug Tracing Example ===
|
||||
//!
|
||||
//! Starting order processing for order-12345
|
||||
//!
|
||||
//! [order-12345] [task-001] ValidateOrder: Starting
|
||||
//! [order-12345] [task-001] ValidateOrder: Completed in 10ms - Success
|
||||
//!
|
||||
//! [order-12345] [task-002] CheckInventory: Starting
|
||||
//! [order-12345] [task-003] ValidatePayment: Starting
|
||||
//! [order-12345] [task-004] CalculateShipping: Starting
|
||||
//! [order-12345] [task-002] CheckInventory: Completed in 45ms - Success
|
||||
//! [order-12345] [task-004] CalculateShipping: Completed in 15ms - Success
|
||||
//! [order-12345] [task-003] ValidatePayment: Completed in 120ms - Success
|
||||
//!
|
||||
//! [order-12345] [task-005] FulfillOrder: Starting
|
||||
//! [order-12345] [task-005] FulfillOrder: Completed in 30ms - Success
|
||||
//!
|
||||
//! === Execution Trace Summary ===
|
||||
//! Correlation ID: order-12345
|
||||
//! Total Tasks: 5
|
||||
//! Total Duration: 220ms
|
||||
//! Success: 5, Failed: 0
|
||||
//!
|
||||
//! Task Breakdown:
|
||||
//! ValidateOrder: 10ms
|
||||
//! CheckInventory: 45ms
|
||||
//! ValidatePayment: 120ms (slowest)
|
||||
//! CalculateShipping: 15ms
|
||||
//! FulfillOrder: 30ms
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
use std::time::Instant;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
// Stateful task that tracks execution metadata for debugging
|
||||
struct TracedTask {
|
||||
task_id: String,
|
||||
task_name: String,
|
||||
correlation_id: String,
|
||||
work_duration_ms: u64,
|
||||
start_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl TracedTask {
|
||||
fn new(
|
||||
task_id: impl Into<String>,
|
||||
task_name: impl Into<String>,
|
||||
correlation_id: impl Into<String>,
|
||||
work_duration_ms: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
task_name: task_name.into(),
|
||||
correlation_id: correlation_id.into(),
|
||||
work_duration_ms,
|
||||
start_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl TracedTask {
|
||||
async fn run(&mut self) -> Result<TaskTrace, String> {
|
||||
// Record start
|
||||
let start = Instant::now();
|
||||
self.start_time = Some(start);
|
||||
|
||||
println!(
|
||||
"[{}] [{}] {}: Starting",
|
||||
self.correlation_id, self.task_id, self.task_name
|
||||
);
|
||||
|
||||
// Simulate work
|
||||
sleep(Duration::from_millis(self.work_duration_ms)).await;
|
||||
|
||||
// Record completion
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
println!(
|
||||
"[{}] [{}] {}: Completed in {}ms - Success",
|
||||
self.correlation_id, self.task_id, self.task_name, duration_ms
|
||||
);
|
||||
|
||||
Ok(TaskTrace {
|
||||
task_id: self.task_id.clone(),
|
||||
task_name: self.task_name.clone(),
|
||||
correlation_id: self.correlation_id.clone(),
|
||||
duration_ms,
|
||||
status: "Success",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Task that can fail, demonstrating error tracing
|
||||
struct TracedTaskWithError {
|
||||
task_id: String,
|
||||
task_name: String,
|
||||
correlation_id: String,
|
||||
should_fail: bool,
|
||||
}
|
||||
|
||||
impl TracedTaskWithError {
|
||||
fn new(
|
||||
task_id: impl Into<String>,
|
||||
task_name: impl Into<String>,
|
||||
correlation_id: impl Into<String>,
|
||||
should_fail: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
task_name: task_name.into(),
|
||||
correlation_id: correlation_id.into(),
|
||||
should_fail,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl TracedTaskWithError {
|
||||
async fn run(&mut self) -> Result<TaskTrace, TaskTrace> {
|
||||
let start = Instant::now();
|
||||
|
||||
println!(
|
||||
"[{}] [{}] {}: Starting",
|
||||
self.correlation_id, self.task_id, self.task_name
|
||||
);
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
if self.should_fail {
|
||||
println!(
|
||||
"[{}] [{}] {}: Failed in {}ms - Validation Error",
|
||||
self.correlation_id, self.task_id, self.task_name, duration_ms
|
||||
);
|
||||
|
||||
Err(TaskTrace {
|
||||
task_id: self.task_id.clone(),
|
||||
task_name: self.task_name.clone(),
|
||||
correlation_id: self.correlation_id.clone(),
|
||||
duration_ms,
|
||||
status: "Failed",
|
||||
})
|
||||
} else {
|
||||
println!(
|
||||
"[{}] [{}] {}: Completed in {}ms - Success",
|
||||
self.correlation_id, self.task_id, self.task_name, duration_ms
|
||||
);
|
||||
|
||||
Ok(TaskTrace {
|
||||
task_id: self.task_id.clone(),
|
||||
task_name: self.task_name.clone(),
|
||||
correlation_id: self.correlation_id.clone(),
|
||||
duration_ms,
|
||||
status: "Success",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task trace data structure
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
struct TaskTrace {
|
||||
task_id: String,
|
||||
task_name: String,
|
||||
correlation_id: String,
|
||||
duration_ms: u64,
|
||||
status: &'static str,
|
||||
}
|
||||
|
||||
// Task that aggregates traces from multiple upstream tasks
|
||||
struct TraceAggregator {
|
||||
correlation_id: String,
|
||||
}
|
||||
|
||||
impl TraceAggregator {
|
||||
fn new(correlation_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
correlation_id: correlation_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl TraceAggregator {
|
||||
async fn run(
|
||||
&mut self,
|
||||
t1: &Result<TaskTrace, String>,
|
||||
t2: &Result<TaskTrace, String>,
|
||||
t3: &Result<TaskTrace, String>,
|
||||
) -> TraceSummary {
|
||||
let traces = vec![t1, t2, t3];
|
||||
|
||||
let mut summary = TraceSummary {
|
||||
correlation_id: self.correlation_id.clone(),
|
||||
task_count: traces.len(),
|
||||
total_duration_ms: 0,
|
||||
success_count: 0,
|
||||
failed_count: 0,
|
||||
task_details: Vec::new(),
|
||||
};
|
||||
|
||||
for trace_result in traces {
|
||||
match trace_result {
|
||||
Ok(trace) => {
|
||||
summary.success_count += 1;
|
||||
summary.total_duration_ms += trace.duration_ms;
|
||||
summary.task_details.push(trace.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
summary.failed_count += 1;
|
||||
eprintln!("[{}] Task failed: {}", self.correlation_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary
|
||||
}
|
||||
}
|
||||
|
||||
// Summary of traced execution
|
||||
#[derive(Clone, Debug)]
|
||||
struct TraceSummary {
|
||||
correlation_id: String,
|
||||
task_count: usize,
|
||||
total_duration_ms: u64,
|
||||
success_count: usize,
|
||||
failed_count: usize,
|
||||
task_details: Vec<TaskTrace>,
|
||||
}
|
||||
|
||||
impl TraceSummary {
|
||||
fn print(&self) {
|
||||
println!("\n=== Execution Trace Summary ===");
|
||||
println!("Correlation ID: {}", self.correlation_id);
|
||||
println!("Total Tasks: {}", self.task_count);
|
||||
println!("Total Duration: {}ms", self.total_duration_ms);
|
||||
println!(
|
||||
"Success: {}, Failed: {}",
|
||||
self.success_count, self.failed_count
|
||||
);
|
||||
|
||||
if !self.task_details.is_empty() {
|
||||
println!("\nTask Breakdown:");
|
||||
let max_duration = self
|
||||
.task_details
|
||||
.iter()
|
||||
.map(|t| t.duration_ms)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
for trace in &self.task_details {
|
||||
let slowest_tag = if trace.duration_ms == max_duration {
|
||||
" (slowest)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(
|
||||
" {}: {}ms{}",
|
||||
trace.task_name, trace.duration_ms, slowest_tag
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Debug Tracing Example ===\n");
|
||||
|
||||
// Example 1: Successful order processing with full tracing
|
||||
println!("Example 1: Successful order processing\n");
|
||||
{
|
||||
let correlation_id = "order-12345";
|
||||
println!("Starting order processing for {}\n", correlation_id);
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Root task: Validate order
|
||||
let _validate = dag.add_task(TracedTask::new(
|
||||
"task-001",
|
||||
"ValidateOrder",
|
||||
correlation_id,
|
||||
10,
|
||||
));
|
||||
|
||||
// Parallel validation tasks (independent, no dependencies)
|
||||
let inventory = dag.add_task(TracedTask::new(
|
||||
"task-002",
|
||||
"CheckInventory",
|
||||
correlation_id,
|
||||
45,
|
||||
));
|
||||
|
||||
let payment = dag.add_task(TracedTask::new(
|
||||
"task-003",
|
||||
"ValidatePayment",
|
||||
correlation_id,
|
||||
120,
|
||||
));
|
||||
|
||||
let shipping = dag.add_task(TracedTask::new(
|
||||
"task-004",
|
||||
"CalculateShipping",
|
||||
correlation_id,
|
||||
15,
|
||||
));
|
||||
|
||||
// Aggregate traces
|
||||
let summary = dag
|
||||
.add_task(TraceAggregator::new(correlation_id))
|
||||
.depends_on((inventory, payment, shipping));
|
||||
|
||||
// Run the DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Print summary
|
||||
dag.get(summary).unwrap().print();
|
||||
}
|
||||
|
||||
println!("\n");
|
||||
|
||||
// Example 2: Order with failed payment
|
||||
println!("Example 2: Order with failed payment\n");
|
||||
{
|
||||
let correlation_id = "order-67890";
|
||||
println!("Starting order processing for {}\n", correlation_id);
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let _validate = dag.add_task(TracedTaskWithError::new(
|
||||
"task-101",
|
||||
"ValidateOrder",
|
||||
correlation_id,
|
||||
false,
|
||||
));
|
||||
|
||||
let _inventory = dag.add_task(TracedTaskWithError::new(
|
||||
"task-102",
|
||||
"CheckInventory",
|
||||
correlation_id,
|
||||
false,
|
||||
));
|
||||
|
||||
// This task will fail
|
||||
let payment = dag.add_task(TracedTaskWithError::new(
|
||||
"task-103",
|
||||
"ValidatePayment",
|
||||
correlation_id,
|
||||
true, // will fail
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check results
|
||||
match dag.get(payment).unwrap().as_ref() {
|
||||
Ok(_) => println!("\nPayment validation succeeded"),
|
||||
Err(trace) => {
|
||||
println!("\n[{}] Payment validation failed:", correlation_id);
|
||||
println!(" Task ID: {}", trace.task_id);
|
||||
println!(" Duration: {}ms", trace.duration_ms);
|
||||
println!(" Status: {}", trace.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n=== Debug Tracing Example Complete ===");
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
//! # Working with Large DAGs: Performance and Scalability
|
||||
//!
|
||||
//! This example demonstrates dagx's performance characteristics when working
|
||||
//! with large-scale DAGs containing hundreds or thousands of tasks.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How dagx scales with increasing task counts
|
||||
//! - Performance characteristics of different DAG topologies
|
||||
//! - How to benchmark DAG construction and execution
|
||||
//! - Memory efficiency considerations
|
||||
//!
|
||||
//! ## DAG Topologies Tested
|
||||
//!
|
||||
//! **1. Wide DAG** - Maximum parallelism
|
||||
//! ```text
|
||||
//! ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
||||
//! │Task 1│ │Task 2│ │Task 3│ ... │Task N│
|
||||
//! └──────┘ └──────┘ └──────┘ └──────┘
|
||||
//! (1000 independent parallel tasks)
|
||||
//! ```
|
||||
//!
|
||||
//! **2. Deep DAG** - Sequential chain
|
||||
//! ```text
|
||||
//! ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
||||
//! │Task 1│───►│Task 2│───►│Task 3│───► ... ──►│Task N│
|
||||
//! └──────┘ └──────┘ └──────┘ └──────┘
|
||||
//! (100-level dependency chain)
|
||||
//! ```
|
||||
//!
|
||||
//! **3. Binary Tree** - Balanced aggregation
|
||||
//! ```text
|
||||
//! ┌────┐ ┌────┐ ┌────┐ ┌────┐
|
||||
//! │ T1 │ │ T2 │ │ T3 │ │ T4 │
|
||||
//! └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘
|
||||
//! └───┬───┘ └───┬───┘
|
||||
//! │ │
|
||||
//! ┌──▼──┐ ┌──▼──┐
|
||||
//! │ C1 │ │ C2 │
|
||||
//! └──┬──┘ └──┬──┘
|
||||
//! └───────┬──────┘
|
||||
//! │
|
||||
//! ┌──▼──┐
|
||||
//! │Root │
|
||||
//! └─────┘
|
||||
//! ```
|
||||
//!
|
||||
//! **4. Diamond Pattern** - Many-to-many
|
||||
//! ```text
|
||||
//! Layer 1: 50 sources
|
||||
//! Layer 2: 50 tasks (each depends on 2 sources)
|
||||
//! Layer 3: 4 aggregates
|
||||
//! Layer 4: 1 final result
|
||||
//! ```
|
||||
//!
|
||||
//! ## Performance Metrics
|
||||
//! - **Build time**: Time to construct the DAG structure
|
||||
//! - **Execution time**: Time to run all tasks
|
||||
//! - **Tasks/second**: Throughput metric
|
||||
//! - **Memory overhead**: Approximate per-task memory usage
|
||||
//!
|
||||
//! ## Key Observations
|
||||
//! - DAG construction is very fast (< 1ms for 1000s of tasks)
|
||||
//! - Parallel execution scales well with available CPU cores
|
||||
//! - Deep chains execute sequentially as expected
|
||||
//! - Memory overhead is minimal (~200 bytes per task)
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example large_dag --release # Use --release for accurate benchmarks
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Large DAG Performance Example ===
|
||||
//!
|
||||
//! Example 1: Wide DAG (1000 parallel tasks)
|
||||
//! Build time: 347µs
|
||||
//! Execution time: 12.5ms
|
||||
//! Tasks/second: 80000
|
||||
//! Sample sum (first 10): ...
|
||||
//!
|
||||
//! Example 2: Deep DAG (100-level chain)
|
||||
//! Build time: 156µs
|
||||
//! Execution time: 8.2ms
|
||||
//! Final result: ...
|
||||
//!
|
||||
//! Example 3: Binary Tree DAG (depth 10, 1023 tasks)
|
||||
//! Build time: 423µs
|
||||
//! Tree depth: 9
|
||||
//! Execution time: 15.3ms
|
||||
//! Root result: ...
|
||||
//!
|
||||
//! Example 4: Diamond Pattern (50x50 = 2500 edges)
|
||||
//! Build time: 234µs
|
||||
//! Execution time: 11.7ms
|
||||
//! Total tasks: 105
|
||||
//! Final result: ...
|
||||
//!
|
||||
//! Example 5: Memory Efficiency (100 tasks, measure overhead)
|
||||
//! i32 sum: ...
|
||||
//! String total length: ...
|
||||
//!
|
||||
//! === Large DAG Performance Example Complete ===
|
||||
//!
|
||||
//! Key Observations:
|
||||
//! - DAG construction is very fast (< 1ms for 1000s of tasks)
|
||||
//! - Parallel execution scales well with available CPU cores
|
||||
//! - Deep chains execute sequentially as expected
|
||||
//! - Memory overhead is minimal (~ 200 bytes per task)
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
// Simple computation task
|
||||
struct Compute {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Compute {
|
||||
async fn run(&mut self) -> i32 {
|
||||
// Simulate some work
|
||||
let mut result = self.value;
|
||||
for _ in 0..100 {
|
||||
result = result.wrapping_mul(31).wrapping_add(17);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// Task that combines two values
|
||||
struct Combine;
|
||||
|
||||
#[task]
|
||||
impl Combine {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a.wrapping_add(*b)
|
||||
}
|
||||
}
|
||||
|
||||
// Task that aggregates multiple values
|
||||
struct Aggregate;
|
||||
|
||||
#[task]
|
||||
impl Aggregate {
|
||||
async fn run(a: &i32, b: &i32, c: &i32, d: &i32) -> i32 {
|
||||
a.wrapping_add(*b).wrapping_add(*c).wrapping_add(*d)
|
||||
}
|
||||
}
|
||||
|
||||
// Task that formats a number as a string
|
||||
struct FormatNumber {
|
||||
id: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl FormatNumber {
|
||||
async fn run(&self) -> String {
|
||||
format!("Result {}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Large DAG Performance Example ===\n");
|
||||
|
||||
// Example 1: Wide DAG (many parallel tasks)
|
||||
println!("Example 1: Wide DAG (1000 parallel tasks)");
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 1000 independent tasks
|
||||
let tasks: Vec<_> = (0..1000)
|
||||
.map(|i| dag.add_task(Compute { value: i }))
|
||||
.collect();
|
||||
|
||||
let build_time = start.elapsed();
|
||||
println!(" Build time: {:?}", build_time);
|
||||
|
||||
let exec_start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = exec_start.elapsed();
|
||||
|
||||
println!(" Execution time: {:?}", exec_time);
|
||||
println!(" Tasks/second: {:.0}", 1000.0 / exec_time.as_secs_f64());
|
||||
|
||||
// Verify a few results
|
||||
let sum: i32 = tasks
|
||||
.into_iter()
|
||||
.take(10)
|
||||
.map(|t| dag.get(t).unwrap())
|
||||
.fold(0i32, |acc, x| acc.wrapping_add(x));
|
||||
println!(" Sample sum (first 10): {}\n", sum);
|
||||
}
|
||||
|
||||
// Example 2: Deep DAG (long dependency chain)
|
||||
println!("Example 2: Deep DAG (100-level chain)");
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a deep chain by combining adjacent task pairs
|
||||
let tasks: Vec<_> = (0..100)
|
||||
.map(|i| dag.add_task(Compute { value: i }).into())
|
||||
.collect();
|
||||
let mut chain_tasks = vec![];
|
||||
for i in 0..99 {
|
||||
let combined = dag.add_task(Combine).depends_on((&tasks[i], &tasks[i + 1]));
|
||||
chain_tasks.push(combined);
|
||||
}
|
||||
|
||||
let prev = chain_tasks.last().unwrap();
|
||||
|
||||
let build_time = start.elapsed();
|
||||
println!(" Build time: {:?}", build_time);
|
||||
|
||||
let exec_start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = exec_start.elapsed();
|
||||
|
||||
println!(" Execution time: {:?}", exec_time);
|
||||
println!(" Final result: {}\n", dag.get(prev).unwrap());
|
||||
}
|
||||
|
||||
// Example 3: Binary tree DAG
|
||||
println!("Example 3: Binary Tree DAG (depth 10, 1023 tasks)");
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Build binary tree: each level combines pairs from previous level
|
||||
let current_level: Vec<_> = (0..512)
|
||||
.map(|i| dag.add_task(Compute { value: i }))
|
||||
.collect();
|
||||
|
||||
// Process all levels reducing by combining pairs
|
||||
let mut level = 0;
|
||||
let mut layer: Vec<dagx::TaskHandle<i32>> =
|
||||
current_level.into_iter().map(|b| b.into()).collect();
|
||||
|
||||
while layer.len() > 1 {
|
||||
level += 1;
|
||||
let mut next_layer = Vec::new();
|
||||
|
||||
for i in (0..layer.len()).step_by(2) {
|
||||
if i + 1 < layer.len() {
|
||||
// We have a pair - combine them
|
||||
let combined = dag.add_task(Combine).depends_on((&layer[i], &layer[i + 1]));
|
||||
next_layer.push(combined);
|
||||
} else {
|
||||
// Odd one out - just pass it through
|
||||
next_layer.push(layer[i]);
|
||||
}
|
||||
}
|
||||
|
||||
layer = next_layer;
|
||||
}
|
||||
|
||||
let root = &layer[0];
|
||||
|
||||
let build_time = start.elapsed();
|
||||
println!(" Build time: {:?}", build_time);
|
||||
println!(" Tree depth: {}", level);
|
||||
|
||||
let exec_start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = exec_start.elapsed();
|
||||
|
||||
println!(" Execution time: {:?}", exec_time);
|
||||
println!(" Root result: {}\n", dag.get(root).unwrap());
|
||||
}
|
||||
|
||||
// Example 4: Diamond pattern (many-to-many)
|
||||
println!("Example 4: Diamond Pattern (50x50 = 2500 edges)");
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Layer 1: 50 source tasks
|
||||
let sources: Vec<_> = (0..50)
|
||||
.map(|i| dag.add_task(Compute { value: i }).into())
|
||||
.collect();
|
||||
|
||||
// Layer 2: 50 tasks, each depends on all sources
|
||||
let middle: Vec<_> = (0..50)
|
||||
.map(|i| {
|
||||
let combine = dag.add_task(Combine);
|
||||
// Each middle task depends on two sources
|
||||
combine.depends_on((
|
||||
&sources[i % sources.len()],
|
||||
&sources[(i + 1) % sources.len()],
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 3: Aggregate to 4 groups
|
||||
let aggregates: Vec<_> = (0..4)
|
||||
.map(|i| {
|
||||
let start_idx = i * 12;
|
||||
dag.add_task(Aggregate).depends_on((
|
||||
&middle[start_idx % middle.len()],
|
||||
&middle[(start_idx + 1) % middle.len()],
|
||||
&middle[(start_idx + 2) % middle.len()],
|
||||
&middle[(start_idx + 3) % middle.len()],
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 4: Final aggregate
|
||||
let final_result = dag.add_task(Aggregate).depends_on((
|
||||
&aggregates[0],
|
||||
&aggregates[1],
|
||||
&aggregates[2],
|
||||
&aggregates[3],
|
||||
));
|
||||
|
||||
let build_time = start.elapsed();
|
||||
println!(" Build time: {:?}", build_time);
|
||||
|
||||
let exec_start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = exec_start.elapsed();
|
||||
|
||||
println!(" Execution time: {:?}", exec_time);
|
||||
println!(" Total tasks: {}", 50 + 50 + 4 + 1);
|
||||
println!(" Final result: {}\n", dag.get(final_result).unwrap());
|
||||
}
|
||||
|
||||
// Example 5: Memory efficiency test
|
||||
println!("Example 5: Memory Efficiency (100 tasks, measure overhead)");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create tasks with various output sizes
|
||||
let small_tasks: Vec<_> = (0..50)
|
||||
.map(|i| dag.add_task(Compute { value: i }))
|
||||
.collect();
|
||||
|
||||
let string_tasks: Vec<_> = (0..50)
|
||||
.map(|i| dag.add_task(FormatNumber { id: i }))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify results are accessible
|
||||
let i32_sum: i32 = small_tasks
|
||||
.into_iter()
|
||||
.map(|t| dag.get(t).unwrap())
|
||||
.fold(0i32, |acc, x| acc.wrapping_add(x));
|
||||
let string_count = string_tasks
|
||||
.into_iter()
|
||||
.map(|t| dag.get(t).unwrap().len())
|
||||
.sum::<usize>();
|
||||
|
||||
println!(" i32 sum: {}", i32_sum);
|
||||
println!(" String total length: {}", string_count);
|
||||
}
|
||||
|
||||
println!("\n=== Large DAG Performance Example Complete ===");
|
||||
println!("\nKey Observations:");
|
||||
println!(" - DAG construction is very fast (< 1ms for 1000s of tasks)");
|
||||
println!(" - Parallel execution scales well with available CPU cores");
|
||||
println!(" - Deep chains execute sequentially as expected");
|
||||
println!(" - Memory overhead is minimal (~ 200 bytes per task)");
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
//! # Retry Strategies for Resilient Workflows
|
||||
//!
|
||||
//! This example demonstrates various retry patterns for handling transient failures
|
||||
//! in DAG workflows, such as network errors, rate limits, or temporary service unavailability.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to implement immediate, linear, and exponential backoff retry strategies
|
||||
//! - How to distinguish retriable vs non-retriable errors
|
||||
//! - How to track retry attempts and provide diagnostics
|
||||
//! - How to implement jitter to prevent thundering herd problems
|
||||
//!
|
||||
//! ## Retry Patterns
|
||||
//!
|
||||
//! **1. Immediate Retry** - Retry without delay (for quick transient failures)
|
||||
//! ```text
|
||||
//! Attempt 1: ✗ Error
|
||||
//! Attempt 2: ✗ Error (no delay)
|
||||
//! Attempt 3: ✓ Success
|
||||
//! ```
|
||||
//!
|
||||
//! **2. Linear Backoff** - Fixed delay between retries
|
||||
//! ```text
|
||||
//! Attempt 1: ✗ Error
|
||||
//! Wait 100ms
|
||||
//! Attempt 2: ✗ Error
|
||||
//! Wait 100ms
|
||||
//! Attempt 3: ✓ Success
|
||||
//! ```
|
||||
//!
|
||||
//! **3. Exponential Backoff** - Increasing delays between retries
|
||||
//! ```text
|
||||
//! Attempt 1: ✗ Error
|
||||
//! Wait 100ms
|
||||
//! Attempt 2: ✗ Error
|
||||
//! Wait 200ms
|
||||
//! Attempt 3: ✗ Error
|
||||
//! Wait 400ms
|
||||
//! Attempt 4: ✓ Success
|
||||
//! ```
|
||||
//!
|
||||
//! **4. Exponential Backoff with Jitter** - Randomized delays to prevent coordinated retries
|
||||
//! ```text
|
||||
//! Attempt 1: ✗ Error
|
||||
//! Wait 87ms (50-150ms range)
|
||||
//! Attempt 2: ✗ Error
|
||||
//! Wait 234ms (100-300ms range)
|
||||
//! Attempt 3: ✓ Success
|
||||
//! ```
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! ┌──────────────────────┐
|
||||
//! │ ApiCallWithRetry │
|
||||
//! │ Max attempts: 3 │
|
||||
//! │ Strategy: Exp │
|
||||
//! └──────────────────────┘
|
||||
//! │
|
||||
//! ├─► Attempt 1: ✗ (Connection timeout)
|
||||
//! │ Wait 100ms
|
||||
//! │
|
||||
//! ├─► Attempt 2: ✗ (Connection timeout)
|
||||
//! │ Wait 200ms
|
||||
//! │
|
||||
//! └─► Attempt 3: ✓ Success
|
||||
//!
|
||||
//! ┌──────────────────────┐
|
||||
//! │ ApiCallWithRetry │
|
||||
//! │ Max attempts: 3 │
|
||||
//! │ Strategy: Linear │
|
||||
//! └──────────────────────┘
|
||||
//! │
|
||||
//! ├─► Attempt 1: ✗ (Rate limit)
|
||||
//! │ Wait 500ms
|
||||
//! │
|
||||
//! ├─► Attempt 2: ✗ (Rate limit)
|
||||
//! │ Wait 500ms
|
||||
//! │
|
||||
//! ├─► Attempt 3: ✗ (Rate limit)
|
||||
//! │
|
||||
//! └─► Final: ✗ Retry exhausted
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Idempotency**: Retried operations should be safe to repeat
|
||||
//! - **Retriable errors**: Network timeouts, rate limits, temporary service errors
|
||||
//! - **Non-retriable errors**: Authentication failures, invalid input, logical errors
|
||||
//! - **Backoff strategies**: Balance between recovery speed and system load
|
||||
//! - **Jitter**: Randomization to avoid thundering herd when many clients retry simultaneously
|
||||
//! - **Retry budgets**: Limit total retries to prevent cascading failures
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - External API calls with transient network failures
|
||||
//! - Database operations with temporary connection issues
|
||||
//! - Distributed systems with eventual consistency
|
||||
//! - Rate-limited API integrations
|
||||
//! - Cloud service calls with intermittent availability
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example retry_strategies
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Retry Strategies Example ===
|
||||
//!
|
||||
//! Example 1: Immediate retry strategy
|
||||
//! [ApiCall-1] Attempt 1/3: Calling external API...
|
||||
//! [ApiCall-1] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-1] Retrying immediately (attempt 2/3)
|
||||
//! [ApiCall-1] Attempt 2/3: Calling external API...
|
||||
//! [ApiCall-1] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-1] Retrying immediately (attempt 3/3)
|
||||
//! [ApiCall-1] Attempt 3/3: Calling external API...
|
||||
//! [ApiCall-1] ✓ Success: API response data
|
||||
//! Result: Ok(RetryResult { attempts: 3, success: true })
|
||||
//!
|
||||
//! Example 2: Linear backoff (500ms delay)
|
||||
//! [ApiCall-2] Attempt 1/4: Calling external API...
|
||||
//! [ApiCall-2] ✗ Transient error: Rate limit exceeded
|
||||
//! [ApiCall-2] Waiting 500ms before retry (attempt 2/4)
|
||||
//! [ApiCall-2] Attempt 2/4: Calling external API...
|
||||
//! [ApiCall-2] ✓ Success: API response data
|
||||
//! Result: Ok(RetryResult { attempts: 2, success: true })
|
||||
//!
|
||||
//! Example 3: Exponential backoff
|
||||
//! [ApiCall-3] Attempt 1/5: Calling external API...
|
||||
//! [ApiCall-3] ✗ Transient error: Service unavailable
|
||||
//! [ApiCall-3] Waiting 100ms before retry (attempt 2/5)
|
||||
//! [ApiCall-3] Attempt 2/5: Calling external API...
|
||||
//! [ApiCall-3] ✗ Transient error: Service unavailable
|
||||
//! [ApiCall-3] Waiting 200ms before retry (attempt 3/5)
|
||||
//! [ApiCall-3] Attempt 3/5: Calling external API...
|
||||
//! [ApiCall-3] ✓ Success: API response data
|
||||
//! Result: Ok(RetryResult { attempts: 3, success: true })
|
||||
//!
|
||||
//! Example 4: Non-retriable error (fails immediately)
|
||||
//! [ApiCall-4] Attempt 1/3: Calling external API...
|
||||
//! [ApiCall-4] ✗ Non-retriable error: Authentication failed
|
||||
//! Result: Err("Authentication failed - not retrying")
|
||||
//!
|
||||
//! Example 5: Retry exhausted
|
||||
//! [ApiCall-5] Attempt 1/3: Calling external API...
|
||||
//! [ApiCall-5] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-5] Waiting 100ms before retry (attempt 2/3)
|
||||
//! [ApiCall-5] Attempt 2/3: Calling external API...
|
||||
//! [ApiCall-5] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-5] Waiting 200ms before retry (attempt 3/3)
|
||||
//! [ApiCall-5] Attempt 3/3: Calling external API...
|
||||
//! [ApiCall-5] ✗ Transient error: Connection timeout
|
||||
//! [ApiCall-5] All retry attempts exhausted
|
||||
//! Result: Err("Retry exhausted after 3 attempts: Connection timeout")
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
// Retry strategy configuration
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum RetryStrategy {
|
||||
Immediate,
|
||||
Linear { delay_ms: u64 },
|
||||
Exponential { base_ms: u64, factor: f64 },
|
||||
}
|
||||
|
||||
impl RetryStrategy {
|
||||
fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
|
||||
match self {
|
||||
RetryStrategy::Immediate => None,
|
||||
RetryStrategy::Linear { delay_ms } => Some(Duration::from_millis(*delay_ms)),
|
||||
RetryStrategy::Exponential { base_ms, factor } => {
|
||||
let delay = *base_ms as f64 * factor.powi(attempt as i32 - 1);
|
||||
Some(Duration::from_millis(delay as u64))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simulated error types
|
||||
#[derive(Clone, Debug)]
|
||||
enum ApiError {
|
||||
// Retriable errors
|
||||
ConnectionTimeout,
|
||||
ServiceUnavailable,
|
||||
RateLimitExceeded,
|
||||
|
||||
// Non-retriable errors
|
||||
AuthenticationFailed,
|
||||
#[allow(dead_code)]
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
fn is_retriable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ApiError::ConnectionTimeout
|
||||
| ApiError::ServiceUnavailable
|
||||
| ApiError::RateLimitExceeded
|
||||
)
|
||||
}
|
||||
|
||||
fn message(&self) -> &'static str {
|
||||
match self {
|
||||
ApiError::ConnectionTimeout => "Connection timeout",
|
||||
ApiError::ServiceUnavailable => "Service unavailable",
|
||||
ApiError::RateLimitExceeded => "Rate limit exceeded",
|
||||
ApiError::AuthenticationFailed => "Authentication failed",
|
||||
ApiError::InvalidInput => "Invalid input",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Result of retry operation
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
struct RetryResult {
|
||||
attempts: u32,
|
||||
success: bool,
|
||||
final_error: Option<String>,
|
||||
}
|
||||
|
||||
// Task that simulates an API call with configurable failure pattern
|
||||
struct ApiCallWithRetry {
|
||||
task_id: String,
|
||||
max_attempts: u32,
|
||||
strategy: RetryStrategy,
|
||||
// Simulated failure pattern: errors to return before success
|
||||
failure_sequence: Vec<ApiError>,
|
||||
current_attempt: u32,
|
||||
}
|
||||
|
||||
impl ApiCallWithRetry {
|
||||
fn new(
|
||||
task_id: impl Into<String>,
|
||||
max_attempts: u32,
|
||||
strategy: RetryStrategy,
|
||||
failure_sequence: Vec<ApiError>,
|
||||
) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
max_attempts,
|
||||
strategy,
|
||||
failure_sequence,
|
||||
current_attempt: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn attempt_api_call(&mut self) -> Result<String, ApiError> {
|
||||
println!(
|
||||
"[{}] Attempt {}/{}: Calling external API...",
|
||||
self.task_id,
|
||||
self.current_attempt + 1,
|
||||
self.max_attempts
|
||||
);
|
||||
|
||||
// Simulate network delay
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Check if we should fail this attempt
|
||||
if (self.current_attempt as usize) < self.failure_sequence.len() {
|
||||
let error = self.failure_sequence[self.current_attempt as usize].clone();
|
||||
Err(error)
|
||||
} else {
|
||||
Ok("API response data".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ApiCallWithRetry {
|
||||
async fn run(&mut self) -> Result<RetryResult, String> {
|
||||
loop {
|
||||
self.current_attempt += 1;
|
||||
|
||||
match self.attempt_api_call().await {
|
||||
Ok(data) => {
|
||||
println!("[{}] ✓ Success: {}", self.task_id, data);
|
||||
return Ok(RetryResult {
|
||||
attempts: self.current_attempt,
|
||||
success: true,
|
||||
final_error: None,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
println!(
|
||||
"[{}] ✗ {} error: {}",
|
||||
self.task_id,
|
||||
if error.is_retriable() {
|
||||
"Transient"
|
||||
} else {
|
||||
"Non-retriable"
|
||||
},
|
||||
error.message()
|
||||
);
|
||||
|
||||
// Check if error is retriable
|
||||
if !error.is_retriable() {
|
||||
return Err(format!("{} - not retrying", error.message()));
|
||||
}
|
||||
|
||||
// Check if we have attempts remaining
|
||||
if self.current_attempt >= self.max_attempts {
|
||||
println!("[{}] All retry attempts exhausted", self.task_id);
|
||||
return Err(format!(
|
||||
"Retry exhausted after {} attempts: {}",
|
||||
self.current_attempt,
|
||||
error.message()
|
||||
));
|
||||
}
|
||||
|
||||
// Calculate and apply backoff delay
|
||||
if let Some(delay) = self.strategy.calculate_delay(self.current_attempt) {
|
||||
println!(
|
||||
"[{}] Waiting {}ms before retry (attempt {}/{})",
|
||||
self.task_id,
|
||||
delay.as_millis(),
|
||||
self.current_attempt + 1,
|
||||
self.max_attempts
|
||||
);
|
||||
sleep(delay).await;
|
||||
} else {
|
||||
println!(
|
||||
"[{}] Retrying immediately (attempt {}/{})",
|
||||
self.task_id,
|
||||
self.current_attempt + 1,
|
||||
self.max_attempts
|
||||
);
|
||||
}
|
||||
|
||||
// Continue loop to retry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Retry Strategies Example ===\n");
|
||||
|
||||
// Example 1: Immediate retry - succeeds on 3rd attempt
|
||||
println!("Example 1: Immediate retry strategy");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-1",
|
||||
3,
|
||||
RetryStrategy::Immediate,
|
||||
vec![
|
||||
ApiError::ConnectionTimeout,
|
||||
ApiError::ConnectionTimeout,
|
||||
// 3rd attempt will succeed
|
||||
],
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
// Example 2: Linear backoff - succeeds on 2nd attempt
|
||||
println!("Example 2: Linear backoff (500ms delay)");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-2",
|
||||
4,
|
||||
RetryStrategy::Linear { delay_ms: 500 },
|
||||
vec![
|
||||
ApiError::RateLimitExceeded,
|
||||
// 2nd attempt will succeed
|
||||
],
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
// Example 3: Exponential backoff - succeeds on 3rd attempt
|
||||
println!("Example 3: Exponential backoff");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-3",
|
||||
5,
|
||||
RetryStrategy::Exponential {
|
||||
base_ms: 100,
|
||||
factor: 2.0,
|
||||
},
|
||||
vec![
|
||||
ApiError::ServiceUnavailable,
|
||||
ApiError::ServiceUnavailable,
|
||||
// 3rd attempt will succeed
|
||||
],
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
// Example 4: Non-retriable error - fails immediately
|
||||
println!("Example 4: Non-retriable error (fails immediately)");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-4",
|
||||
3,
|
||||
RetryStrategy::Immediate,
|
||||
vec![ApiError::AuthenticationFailed],
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
// Example 5: Retry exhausted - all attempts fail
|
||||
println!("Example 5: Retry exhausted");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let api_call = dag.add_task(ApiCallWithRetry::new(
|
||||
"ApiCall-5",
|
||||
3,
|
||||
RetryStrategy::Exponential {
|
||||
base_ms: 100,
|
||||
factor: 2.0,
|
||||
},
|
||||
vec![
|
||||
ApiError::ConnectionTimeout,
|
||||
ApiError::ConnectionTimeout,
|
||||
ApiError::ConnectionTimeout,
|
||||
// All attempts will fail
|
||||
],
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {:?}\n", dag.get(api_call).unwrap());
|
||||
}
|
||||
|
||||
println!("=== Retry Strategies Example Complete ===");
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
//! # Managing Timeouts in Tasks
|
||||
//!
|
||||
//! This example demonstrates how to implement timeout patterns in DAG tasks using
|
||||
//! tokio's timeout utilities for time-bounded execution.
|
||||
//!
|
||||
//! ## What You'll Learn
|
||||
//! - How to implement timeouts using `tokio::time::timeout`
|
||||
//! - How to build tasks with built-in timeout logic
|
||||
//! - How to handle timeout errors as Result types
|
||||
//! - How to apply timeouts at both task-level and DAG-level
|
||||
//!
|
||||
//! ## Timeout Patterns
|
||||
//!
|
||||
//! **1. Task-Level Timeout** - Individual task with timeout
|
||||
//! ```rust
|
||||
//! async fn run(&mut self) -> Result<String, String> {
|
||||
//! let work = async { /* ... */ };
|
||||
//! match timeout(Duration::from_millis(self.timeout_ms), work).await {
|
||||
//! Ok(result) => Ok(result),
|
||||
//! Err(_) => Err("Task timed out".to_string()),
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **2. DAG-Level Timeout** - Entire workflow with timeout
|
||||
//! ```rust
|
||||
//! match timeout(dag_timeout, dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await {
|
||||
//! Ok(result) => { /* DAG completed */ },
|
||||
//! Err(_) => { /* DAG timed out */ },
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Workflow Diagram
|
||||
//! ```text
|
||||
//! ┌─────────────┐
|
||||
//! │ FastTask │ (completes in 100ms)
|
||||
//! │ (timeout │ ───► Ok("Fast task completed")
|
||||
//! │ 500ms) │
|
||||
//! └─────────────┘
|
||||
//!
|
||||
//! ┌─────────────┐
|
||||
//! │ SlowTask │ (takes 1000ms)
|
||||
//! │ (timeout │ ───► Err("Task timed out after 200ms")
|
||||
//! │ 200ms) │
|
||||
//! └─────────────┘
|
||||
//! │
|
||||
//! └──────►┌──────────────┐
|
||||
//! │ProcessResult │ ───► "✗ Task timed out..."
|
||||
//! └──────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//! - **Timeout as error**: Timeouts produce `Err` results that propagate through the DAG
|
||||
//! - **Error as data**: Use Result<T, E> to represent timeout states
|
||||
//! - **Graceful degradation**: Downstream tasks can handle timeout errors
|
||||
//! - **Time-bounded execution**: Enforce SLAs and prevent runaway tasks
|
||||
//!
|
||||
//! ## Use Cases
|
||||
//! - API calls with timeout limits
|
||||
//! - SLA enforcement in workflows
|
||||
//! - Preventing resource exhaustion from slow tasks
|
||||
//! - Circuit breaker patterns
|
||||
//! - External service integration with timeouts
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//! ```bash
|
||||
//! cargo run --example timeout
|
||||
//! ```
|
||||
//!
|
||||
//! ## Expected Output
|
||||
//! ```text
|
||||
//! === Timeout Example ===
|
||||
//!
|
||||
//! Example 1: All tasks complete successfully
|
||||
//! Fast task: Ok("Fast task completed")
|
||||
//! Timed task: Ok("Work completed in 100ms")
|
||||
//!
|
||||
//! Example 2: Task exceeds timeout
|
||||
//! Result: ✗ Task timed out after 200ms (work would take 1000ms)
|
||||
//!
|
||||
//! Example 3: Parallel tasks with different speeds
|
||||
//! Fast task 1: Ok("Fast task completed")
|
||||
//! Fast task 2: Ok("Fast task completed")
|
||||
//! Slow task: Ok("Slow task completed after 500ms")
|
||||
//! Timed task 1: Ok("Work completed in 100ms")
|
||||
//! Timed task 2: Err("Task timed out after 200ms...")
|
||||
//!
|
||||
//! Example 4: Downstream tasks handling timeout errors
|
||||
//! Processed 1: ✗ Task timed out after 100ms (work would take 500ms)
|
||||
//! Processed 2: ✗ Task timed out after 100ms (work would take 500ms)
|
||||
//!
|
||||
//! Example 5: Whole-DAG timeout
|
||||
//! DAG completed within timeout
|
||||
//! Task 1: Ok("Slow task completed after 200ms")
|
||||
//! Task 2: Ok("Slow task completed after 200ms")
|
||||
//! Task 3: Ok("Slow task completed after 200ms")
|
||||
//!
|
||||
//! === Timeout Example Complete ===
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
|
||||
use tokio::time::{sleep, timeout, Duration};
|
||||
|
||||
// Fast task that completes quickly
|
||||
struct FastTask;
|
||||
|
||||
#[task]
|
||||
impl FastTask {
|
||||
async fn run() -> Result<String, String> {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
Ok("Fast task completed".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Slow task that takes a long time
|
||||
struct SlowTask {
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl SlowTask {
|
||||
async fn run(&mut self) -> Result<String, String> {
|
||||
sleep(Duration::from_millis(self.duration_ms)).await;
|
||||
Ok(format!("Slow task completed after {}ms", self.duration_ms))
|
||||
}
|
||||
}
|
||||
|
||||
// Task with built-in timeout
|
||||
struct TaskWithTimeout {
|
||||
work_duration_ms: u64,
|
||||
timeout_ms: u64,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl TaskWithTimeout {
|
||||
async fn run(&mut self) -> Result<String, String> {
|
||||
let work = async {
|
||||
sleep(Duration::from_millis(self.work_duration_ms)).await;
|
||||
format!("Work completed in {}ms", self.work_duration_ms)
|
||||
};
|
||||
|
||||
match timeout(Duration::from_millis(self.timeout_ms), work).await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err(format!(
|
||||
"Task timed out after {}ms (work would take {}ms)",
|
||||
self.timeout_ms, self.work_duration_ms
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task that processes results (including timeouts)
|
||||
struct ProcessResult;
|
||||
|
||||
#[task]
|
||||
impl ProcessResult {
|
||||
async fn run(input: &Result<String, String>) -> String {
|
||||
match input {
|
||||
Ok(msg) => format!("✓ {}", msg),
|
||||
Err(err) => format!("✗ {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Timeout Example ===\n");
|
||||
|
||||
// Example 1: All tasks complete within timeout
|
||||
println!("Example 1: All tasks complete successfully");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let fast = dag.add_task(FastTask);
|
||||
let timed = dag.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 100,
|
||||
timeout_ms: 500,
|
||||
});
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Fast task: {:?}", dag.get(fast).unwrap());
|
||||
println!("Timed task: {:?}\n", dag.get(timed).unwrap());
|
||||
}
|
||||
|
||||
// Example 2: Task exceeds timeout
|
||||
println!("Example 2: Task exceeds timeout");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let timed = dag.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 1000,
|
||||
timeout_ms: 200,
|
||||
});
|
||||
|
||||
let processed = dag.add_task(ProcessResult).depends_on(timed);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result: {}\n", dag.get(processed).unwrap());
|
||||
}
|
||||
|
||||
// Example 3: Mixed fast and slow tasks
|
||||
println!("Example 3: Parallel tasks with different speeds");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let fast1 = dag.add_task(FastTask);
|
||||
let fast2 = dag.add_task(FastTask);
|
||||
|
||||
let slow = dag.add_task(SlowTask { duration_ms: 500 });
|
||||
|
||||
let timed1 = dag.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 100,
|
||||
timeout_ms: 200,
|
||||
});
|
||||
|
||||
let timed2 = dag.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 300,
|
||||
timeout_ms: 200,
|
||||
});
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Fast task 1: {:?}", dag.get(fast1).unwrap());
|
||||
println!("Fast task 2: {:?}", dag.get(fast2).unwrap());
|
||||
println!("Slow task: {:?}", dag.get(slow).unwrap());
|
||||
println!("Timed task 1: {:?}", dag.get(timed1).unwrap());
|
||||
println!("Timed task 2: {:?}\n", dag.get(timed2).unwrap());
|
||||
}
|
||||
|
||||
// Example 4: Cascading timeout errors
|
||||
println!("Example 4: Downstream tasks handling timeout errors");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(TaskWithTimeout {
|
||||
work_duration_ms: 500,
|
||||
timeout_ms: 100,
|
||||
})
|
||||
.into();
|
||||
|
||||
let processed1 = dag.add_task(ProcessResult).depends_on(source);
|
||||
let processed2 = dag.add_task(ProcessResult).depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Processed 1: {}", dag.get(processed1).unwrap());
|
||||
println!("Processed 2: {}\n", dag.get(processed2).unwrap());
|
||||
}
|
||||
|
||||
// Example 5: Implementing whole-DAG timeout
|
||||
println!("Example 5: Whole-DAG timeout");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(SlowTask { duration_ms: 200 });
|
||||
let t2 = dag.add_task(SlowTask { duration_ms: 200 });
|
||||
let t3 = dag.add_task(SlowTask { duration_ms: 200 });
|
||||
|
||||
// Run the entire DAG with a timeout
|
||||
let dag_timeout = Duration::from_millis(500);
|
||||
match timeout(
|
||||
dag_timeout,
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
result.unwrap();
|
||||
println!("DAG completed within timeout");
|
||||
println!("Task 1: {:?}", dag.get(t1).unwrap());
|
||||
println!("Task 2: {:?}", dag.get(t2).unwrap());
|
||||
println!("Task 3: {:?}", dag.get(t3).unwrap());
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!(
|
||||
"DAG execution timed out after {}ms",
|
||||
dag_timeout.as_millis()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n=== Timeout Example Complete ===");
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
//! # Tracing Example
|
||||
//!
|
||||
//! This example demonstrates how to use the optional tracing feature to gain
|
||||
//! visibility into DAG execution for debugging and performance analysis.
|
||||
//!
|
||||
//! ## Running This Example
|
||||
//!
|
||||
//! ```bash
|
||||
//! # With default log level (info)
|
||||
//! cargo run --example tracing_example --features tracing
|
||||
//!
|
||||
//! # With debug level (shows more details)
|
||||
//! RUST_LOG=dagx=debug cargo run --example tracing_example --features tracing
|
||||
//!
|
||||
//! # With trace level (shows all execution details)
|
||||
//! RUST_LOG=dagx=trace cargo run --example tracing_example --features tracing
|
||||
//! ```
|
||||
//!
|
||||
//! ## What You'll See
|
||||
//!
|
||||
//! - Task additions to the DAG
|
||||
//! - Dependency wiring between tasks
|
||||
//! - Topological layer computation
|
||||
//! - Layer-by-layer execution
|
||||
//! - Inline vs spawned task execution
|
||||
//! - Completion status
|
||||
//!
|
||||
//! ## Log Levels
|
||||
//!
|
||||
//! - **INFO**: High-level DAG execution start/completion
|
||||
//! - **DEBUG**: Task additions, dependency wiring, layer computation
|
||||
//! - **TRACE**: Individual task execution details (inline vs spawned)
|
||||
//! - **ERROR**: Errors, panics, cycle detection
|
||||
//!
|
||||
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
// Simple source task
|
||||
struct Value(i32);
|
||||
|
||||
#[task]
|
||||
impl Value {
|
||||
async fn run(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Computation task
|
||||
struct Add;
|
||||
|
||||
#[task]
|
||||
impl Add {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
// Another computation task
|
||||
struct Multiply;
|
||||
|
||||
#[task]
|
||||
impl Multiply {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a * b
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize tracing subscriber with environment filter
|
||||
// Use RUST_LOG environment variable to control log level
|
||||
fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("dagx=info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
println!("=== DAG Execution with Tracing ===\n");
|
||||
println!("Run with RUST_LOG=dagx=debug or RUST_LOG=dagx=trace for more details\n");
|
||||
|
||||
// Example 1: Simple diamond pattern
|
||||
println!("Example 1: Diamond Pattern");
|
||||
println!("---------------------------");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Layer 0: Source
|
||||
let source = dag.add_task(Value(10)).into();
|
||||
|
||||
// Layer 1: Two parallel computations
|
||||
let left = dag.add_task(Add).depends_on((&source, &source));
|
||||
let right = dag.add_task(Multiply).depends_on((&source, &source));
|
||||
|
||||
// Layer 2: Combine results
|
||||
let result = dag.add_task(Add).depends_on((&left, &right));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("\nResult: {}", dag.get(result).unwrap());
|
||||
println!("(Expected: 10+10 = 20, 10*10 = 100, 20+100 = 120)\n");
|
||||
}
|
||||
|
||||
// Example 2: Linear chain (demonstrates inline execution)
|
||||
println!("\nExample 2: Linear Chain (Inline Execution)");
|
||||
println!("-------------------------------------------");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a linear chain - each layer has only one task
|
||||
// This should trigger inline execution optimization
|
||||
let a = dag.add_task(Value(1)).into();
|
||||
let b = dag.add_task(Add).depends_on((&a, &a));
|
||||
let c = dag.add_task(Multiply).depends_on((&b, &b));
|
||||
let d = dag.add_task(Add).depends_on((&c, &c));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("\nResult: {}", dag.get(d).unwrap());
|
||||
println!("(Expected: 1+1 = 2, 2*2 = 4, 4+4 = 8)\n");
|
||||
}
|
||||
|
||||
// Example 3: Wide pattern (demonstrates spawned execution)
|
||||
println!("\nExample 3: Wide Pattern (Spawned Execution)");
|
||||
println!("--------------------------------------------");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Layer 0: Multiple sources
|
||||
let v1 = dag.add_task(Value(1)).into();
|
||||
let v2 = dag.add_task(Value(2)).into();
|
||||
let v3 = dag.add_task(Value(3)).into();
|
||||
let v4 = dag.add_task(Value(4)).into();
|
||||
|
||||
// Layer 1: Multiple parallel computations
|
||||
let sum12 = dag.add_task(Add).depends_on((&v1, &v2));
|
||||
let sum23 = dag.add_task(Add).depends_on((&v2, &v3));
|
||||
let sum34 = dag.add_task(Add).depends_on((&v3, &v4));
|
||||
|
||||
// Layer 2: Final combination
|
||||
let sum_all = dag.add_task(Add).depends_on((&sum12, &sum23));
|
||||
let final_result = dag.add_task(Add).depends_on((&sum_all, &sum34));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("\nResult: {}", dag.get(final_result).unwrap());
|
||||
println!("(Expected: (1+2)+(2+3) = 8, 8+(3+4) = 15)\n");
|
||||
}
|
||||
|
||||
println!("\n=== Tracing Example Complete ===");
|
||||
println!("\nTry running with different log levels:");
|
||||
println!(" RUST_LOG=dagx=info - High-level execution info");
|
||||
println!(" RUST_LOG=dagx=debug - Task additions and layer details");
|
||||
println!(" RUST_LOG=dagx=trace - All execution details including inline/spawned");
|
||||
}
|
||||
+35
-35
@@ -22,23 +22,23 @@ BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
check_pass() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
check_fail() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
exit 1
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_warn() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
section() {
|
||||
echo ""
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
@@ -57,18 +57,18 @@ check_pass "Rust version compatible"
|
||||
echo ""
|
||||
echo "→ Checking for uncommitted changes..."
|
||||
if jj status 2>&1 | grep -q "Working copy changes:"; then
|
||||
check_warn "Uncommitted changes found - you'll need to commit before publishing"
|
||||
check_warn "Uncommitted changes found - you'll need to commit before publishing"
|
||||
else
|
||||
check_pass "No uncommitted changes"
|
||||
check_pass "No uncommitted changes"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "→ Checking for old 'dagrunner' references..."
|
||||
OLD_REFS=$(grep -ri "dagrunner" --exclude-dir=target --exclude-dir=.git --exclude-dir=.jj --exclude="*.lock" --exclude="release_check.sh" . 2>/dev/null | grep -v "DagRunner" | grep -v "dag-runner" | wc -l)
|
||||
if [ "$OLD_REFS" -gt 0 ]; then
|
||||
check_fail "Found $OLD_REFS old 'dagrunner' references (excluding DagRunner struct)"
|
||||
check_fail "Found $OLD_REFS old 'dagrunner' references (excluding DagRunner struct)"
|
||||
else
|
||||
check_pass "No old 'dagrunner' references found"
|
||||
check_pass "No old 'dagrunner' references found"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
@@ -90,9 +90,9 @@ echo ""
|
||||
echo "→ Checking for TODO/FIXME/XXX/HACK in source..."
|
||||
TODO_COUNT=$(grep -r "TODO\|FIXME\|XXX\|HACK" src/ dagx-macros/src/ 2>/dev/null | wc -l)
|
||||
if [ "$TODO_COUNT" -gt 0 ]; then
|
||||
check_warn "Found $TODO_COUNT TODO/FIXME/HACK comments in source"
|
||||
check_warn "Found $TODO_COUNT TODO/FIXME/HACK comments in source"
|
||||
else
|
||||
check_pass "No TODO/FIXME/HACK comments in source"
|
||||
check_pass "No TODO/FIXME/HACK comments in source"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
@@ -218,16 +218,16 @@ check_pass "LICENSE files present in both crates"
|
||||
section "COVERAGE CHECK"
|
||||
|
||||
echo "→ Running coverage analysis..."
|
||||
COVERAGE=$(cargo tarpaulin --lib --all-features --out Stdout 2>&1 | grep -oP '^\d+\.\d+%' | head -1 | sed 's/%//')
|
||||
COVERAGE=$(cargo tarpaulin --out Stdout 2>&1 | grep -oP '^\d+\.\d+%' | head -1 | sed 's/%//')
|
||||
if [ -z "$COVERAGE" ]; then
|
||||
check_warn "Could not determine coverage percentage"
|
||||
check_warn "Could not determine coverage percentage"
|
||||
else
|
||||
echo " Current coverage: ${COVERAGE}%"
|
||||
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
|
||||
check_warn "Coverage is ${COVERAGE}% (below 80% target)"
|
||||
else
|
||||
check_pass "Coverage is ${COVERAGE}% (meets 80% target)"
|
||||
fi
|
||||
echo " Current coverage: ${COVERAGE}%"
|
||||
if (($(echo "$COVERAGE < 80" | bc -l))); then
|
||||
check_warn "Coverage is ${COVERAGE}% (below 80% target)"
|
||||
else
|
||||
check_pass "Coverage is ${COVERAGE}% (meets 80% target)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
@@ -248,10 +248,10 @@ check_pass "All examples build successfully"
|
||||
echo ""
|
||||
echo "→ Checking examples run without panics..."
|
||||
if [ -f ./scripts/run_examples.sh ]; then
|
||||
./scripts/run_examples.sh || check_fail "Some examples failed to run"
|
||||
check_pass "All examples run successfully"
|
||||
./scripts/run_examples.sh || check_fail "Some examples failed to run"
|
||||
check_pass "All examples run successfully"
|
||||
else
|
||||
check_warn "run_examples.sh not found, skipping runtime check"
|
||||
check_warn "run_examples.sh not found, skipping runtime check"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -297,9 +297,9 @@ echo "→ Verifying version consistency..."
|
||||
DAGX_VERSION=$(grep '^version = ' Cargo.toml | head -1 | cut -d'"' -f2)
|
||||
MACROS_VERSION=$(grep '^version = ' dagx-macros/Cargo.toml | head -1 | cut -d'"' -f2)
|
||||
if [ "$DAGX_VERSION" != "$MACROS_VERSION" ]; then
|
||||
check_fail "Version mismatch: dagx=$DAGX_VERSION, dagx-macros=$MACROS_VERSION"
|
||||
check_fail "Version mismatch: dagx=$DAGX_VERSION, dagx-macros=$MACROS_VERSION"
|
||||
else
|
||||
check_pass "Versions match: both are $DAGX_VERSION"
|
||||
check_pass "Versions match: both are $DAGX_VERSION"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
@@ -316,10 +316,10 @@ echo ""
|
||||
echo "→ Testing dagx package (dry-run with --allow-dirty)..."
|
||||
echo " Note: This will fail until dagx-macros is published to crates.io"
|
||||
if cargo publish --dry-run --allow-dirty 2>&1 | grep -q "failed to select a version for the requirement.*dagx-macros"; then
|
||||
check_warn "dagx package check shows dependency on unpublished dagx-macros v$VERSION (expected)"
|
||||
check_warn "dagx package check shows dependency on unpublished dagx-macros v$VERSION (expected)"
|
||||
else
|
||||
cargo publish --dry-run --allow-dirty || check_fail "dagx package check failed unexpectedly"
|
||||
check_pass "dagx package is ready (dagx-macros must be published first)"
|
||||
cargo publish --dry-run --allow-dirty || check_fail "dagx package check failed unexpectedly"
|
||||
check_pass "dagx package is ready (dagx-macros must be published first)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -332,9 +332,9 @@ echo "→ Checking package size..."
|
||||
PACKAGE_SIZE=$(cargo package --list --allow-dirty 2>/dev/null | wc -l)
|
||||
echo " Files to be packaged: $PACKAGE_SIZE"
|
||||
if [ "$PACKAGE_SIZE" -lt 5 ]; then
|
||||
check_fail "Suspiciously few files to package ($PACKAGE_SIZE)"
|
||||
check_fail "Suspiciously few files to package ($PACKAGE_SIZE)"
|
||||
else
|
||||
check_pass "Package contains $PACKAGE_SIZE files"
|
||||
check_pass "Package contains $PACKAGE_SIZE files"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
@@ -345,16 +345,16 @@ section "SECURITY CHECKS"
|
||||
|
||||
echo "→ Checking for sensitive data..."
|
||||
if grep -r "api[_-]key\|secret\|password\|token" src/ dagx-macros/src/ 2>/dev/null | grep -v "^Binary" | grep -v "token for a node" | grep -q .; then
|
||||
check_fail "Potential sensitive data found in source"
|
||||
check_fail "Potential sensitive data found in source"
|
||||
else
|
||||
check_pass "No sensitive data found"
|
||||
check_pass "No sensitive data found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "→ Verifying .gitignore..."
|
||||
[ -f .gitignore ] || check_fail ".gitignore missing"
|
||||
if ! grep -qE "^/?target(/)?$" .gitignore; then
|
||||
check_fail ".gitignore doesn't exclude target/"
|
||||
check_fail ".gitignore doesn't exclude target/"
|
||||
fi
|
||||
check_pass ".gitignore looks good"
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
|
||||
/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
#[allow(private_bounds)]
|
||||
pub fn depends_on<D>(self, deps: D) -> TaskHandle<Tk::Output>
|
||||
where
|
||||
D: DepsTuple<Tk::Input>,
|
||||
|
||||
@@ -57,18 +57,6 @@ fn test_task_builder_from_conversion() {
|
||||
assert_eq!(handle.id.0, 0); // First task should have ID 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_builder_depends_on_returns_handle() {
|
||||
init_tracing();
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(TestTask { value: 10 });
|
||||
let dependent = dag.add_task(TestTaskWithInput).depends_on(source);
|
||||
|
||||
// depends_on should return a TaskHandle
|
||||
let _handle: TaskHandle<_> = dependent;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_builder_chain() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
//! # Compile-Time Cycle Prevention
|
||||
//!
|
||||
//! This module documents how the type system prevents cycles in the DAG.
|
||||
//! **Cycles are impossible via the public API** due to the type-state pattern.
|
||||
//!
|
||||
//! ## Why Cycles Can't Happen
|
||||
//!
|
||||
//! 1. [`crate::builder::TaskBuilder::depends_on`] **consumes** the builder (takes `self` by value)
|
||||
//! 2. It returns a [`crate::types::TaskHandle`] - an immutable, opaque reference
|
||||
//! 3. [`crate::types::TaskHandle`] has **no methods** to add or modify dependencies
|
||||
//! 4. You can only wire dependencies **once** per task
|
||||
//!
|
||||
//! ## The Catch-22
|
||||
//!
|
||||
//! To create a cycle A→B→A, you would need:
|
||||
//! - Task A to depend on Task B's handle (requires B finalized)
|
||||
//! - Task B to depend on Task A's handle (requires A finalized)
|
||||
//!
|
||||
//! But you can't finalize both before wiring them together!
|
||||
//!
|
||||
//! ## Proof by Compilation Failure
|
||||
//!
|
||||
//! The following examples demonstrate that cycles fail to compile:
|
||||
//!
|
||||
//! ### Proof 1: Self-Loop Prevention via Move Semantics
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use dagx::*;
|
||||
//!
|
||||
//! struct TaskA;
|
||||
//! #[dagx::task]
|
||||
//! impl TaskA {
|
||||
//! async fn run(&self, input: &i32) -> i32 {
|
||||
//! input + 1
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! async fn self_loop() {
|
||||
//! let dag = DagRunner::new();
|
||||
//! let a_builder = dag.add_task(TaskA);
|
||||
//!
|
||||
//! // ERROR: a_builder moved in the call to depends_on
|
||||
//! // Can't use 'a_builder' as both the builder and the dependency
|
||||
//! let a = a_builder.depends_on(&a_builder);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Proof 2: TaskHandle is Immutable
|
||||
//!
|
||||
//! ```compile_fail,E0599
|
||||
//! use dagx::*;
|
||||
//!
|
||||
//! struct NoInput;
|
||||
//! #[dagx::task]
|
||||
//! impl NoInput {
|
||||
//! async fn run(&self) -> i32 { 42 }
|
||||
//! }
|
||||
//!
|
||||
//! async fn unit_cycle() {
|
||||
//! let dag = DagRunner::new();
|
||||
//! let a_builder = dag.add_task(NoInput);
|
||||
//! let b_builder = dag.add_task(NoInput);
|
||||
//!
|
||||
//! // We can convert to handles (unit input = no dependencies required)
|
||||
//! let a_handle: TaskHandle<i32> = a_builder.into();
|
||||
//! let b_handle: TaskHandle<i32> = b_builder.into();
|
||||
//!
|
||||
//! // But TaskHandle has no methods to add dependencies!
|
||||
//! // ERROR: no method named `depends_on` found for type `TaskHandle<i32>`
|
||||
//! let _ = a_handle.depends_on(&b_handle);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Implications
|
||||
//!
|
||||
//! Because cycles are impossible at compile-time:
|
||||
//! - No runtime cycle detection is needed
|
||||
//! - Topological sort will always succeed
|
||||
//! - The DAG structure is guaranteed by the type system
|
||||
//!
|
||||
//! This is an example of **zero-cost abstraction** - the type system
|
||||
//! provides safety guarantees without runtime overhead.
|
||||
@@ -17,17 +17,6 @@ impl TestTask {
|
||||
}
|
||||
}
|
||||
|
||||
struct TestTaskWithInput;
|
||||
|
||||
#[crate::task]
|
||||
impl TestTaskWithInput {
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
async fn run(input: &i32) -> i32 {
|
||||
input * 2
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deps_tuple_unit() {
|
||||
// Test lines 20-22 in deps.rs - unit type implementation
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
/// Errors that can occur during DAG construction and execution
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum DagError {
|
||||
/// Invalid dependency: task does not exist
|
||||
InvalidDependency { task_id: u32 },
|
||||
|
||||
+1
-50
@@ -1,6 +1,6 @@
|
||||
//! Unit tests for error module
|
||||
|
||||
use crate::error::{DagError, DagResult};
|
||||
use crate::error::DagError;
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_display_invalid_dependency() {
|
||||
@@ -74,52 +74,3 @@ fn test_dag_error_std_error_impl() {
|
||||
let _ = err_ref.to_string();
|
||||
assert!(err_ref.source().is_none()); // DagError doesn't chain errors
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_equality() {
|
||||
let err1 = DagError::InvalidDependency { task_id: 42 };
|
||||
let err2 = DagError::InvalidDependency { task_id: 42 };
|
||||
let err3 = DagError::InvalidDependency { task_id: 99 };
|
||||
|
||||
assert_eq!(err1, err2);
|
||||
assert_ne!(err1, err3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_clone() {
|
||||
let err = DagError::TaskPanicked {
|
||||
task_id: 10,
|
||||
panic_message: "test panic".to_string(),
|
||||
};
|
||||
|
||||
let cloned = err.clone();
|
||||
assert_eq!(err, cloned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_debug() {
|
||||
let err = DagError::TypeMismatch {
|
||||
expected: "i32",
|
||||
found: "bool",
|
||||
};
|
||||
|
||||
let debug_str = format!("{:?}", err);
|
||||
assert!(debug_str.contains("TypeMismatch"));
|
||||
assert!(debug_str.contains("expected"));
|
||||
assert!(debug_str.contains("found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_result_type_alias() {
|
||||
// Test that DagResult works as expected
|
||||
fn returns_dag_result() -> DagResult<i32> {
|
||||
Ok(42)
|
||||
}
|
||||
|
||||
fn returns_dag_error() -> DagResult<String> {
|
||||
Err(DagError::ResultNotFound { task_id: 1 })
|
||||
}
|
||||
|
||||
assert!(returns_dag_result().is_ok());
|
||||
assert!(returns_dag_error().is_err());
|
||||
}
|
||||
+2
-15
@@ -6,7 +6,7 @@
|
||||
//! # Features
|
||||
//!
|
||||
//! - **Compile-time cycle prevention**: The type system makes cycles **impossible**—no runtime
|
||||
//! cycle detection needed! See [`cycle_prevention`] module for detailed explanation and proof.
|
||||
//! cycle detection needed! See [the repository documentation](https://github.com/swaits/dagx/blob/main/docs/CYCLE_PREVENTION.md) for a detailed explanation.
|
||||
//! - **Compile-time type safety**: Dependencies are validated at compile time through the type
|
||||
//! system. The public API is fully type-safe with no runtime type errors. Internal execution
|
||||
//! uses type erasure for heterogeneous task storage, but this is never exposed to users.
|
||||
@@ -482,8 +482,7 @@
|
||||
//! ```
|
||||
//!
|
||||
//! **No trait implementations needed!** Works with nested structs, collections, enums, and any
|
||||
//! other custom type. See [`examples/custom_types.rs`](https://github.com/swaits/dagx/blob/main/examples/custom_types.rs)
|
||||
//! for a complete example with complex nested types.
|
||||
//! other custom type.
|
||||
//!
|
||||
//! # Runtime Agnostic
|
||||
//!
|
||||
@@ -764,21 +763,13 @@
|
||||
//!
|
||||
//! This follows the same zero-cost pattern used by Tokio, Hyper, and other performance-critical
|
||||
//! Rust async libraries.
|
||||
//!
|
||||
//! See [`examples/tracing_example.rs`](https://github.com/swaits/dagx/blob/main/examples/tracing_example.rs)
|
||||
//! for a complete working example.
|
||||
//!
|
||||
|
||||
#![allow(private_bounds, private_interfaces)]
|
||||
|
||||
extern crate self as dagx;
|
||||
|
||||
// Module declarations
|
||||
mod builder;
|
||||
pub mod cycle_prevention;
|
||||
mod deps;
|
||||
mod error;
|
||||
mod extract;
|
||||
mod node;
|
||||
mod runner;
|
||||
mod task;
|
||||
@@ -793,7 +784,3 @@ pub use types::{Pending, TaskHandle};
|
||||
|
||||
// Re-export the procedural macro
|
||||
pub use dagx_macros::task;
|
||||
|
||||
// Internal testing support - not part of the public API
|
||||
#[doc(hidden)]
|
||||
pub use task::task_fn;
|
||||
+1
-1
@@ -68,7 +68,7 @@ async fn test_execute_with_deps_sink_node() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_execute_with_deps_non_sink_node() {
|
||||
// Test that non-sink nodes send output via channels
|
||||
// Test that non-sink nodes return their output
|
||||
let node = TypedNode::new(TestTask { value: 42 });
|
||||
|
||||
// Execute with senders (non-sink node)
|
||||
|
||||
+6
-14
@@ -10,12 +10,12 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::future::Future;
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::{stream::FuturesUnordered, FutureExt, StreamExt, TryFutureExt};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
@@ -28,8 +28,6 @@ use crate::node::{ExecutableNode, TypedNode};
|
||||
use crate::task::Task;
|
||||
use crate::types::{NodeId, Pending, TaskHandle};
|
||||
|
||||
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
|
||||
// Guard to ensure run_lock is released even on early return or panic
|
||||
struct RunGuard<'a> {
|
||||
lock: &'a AtomicBool,
|
||||
@@ -279,11 +277,6 @@ impl DagRunner {
|
||||
/// - Returns `DagError::ConcurrentExecution` if the DAG is already executing
|
||||
/// - Returns `DagError::TaskPanicked` if any task panics during execution
|
||||
///
|
||||
/// # Note on Cycles
|
||||
///
|
||||
/// Cycles are impossible via the public API due to compile-time prevention.
|
||||
/// See the [`crate::cycle_prevention`] module for details.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
@@ -319,8 +312,8 @@ impl DagRunner {
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))]
|
||||
pub async fn run<S, F>(&self, spawner: S) -> DagResult<()>
|
||||
where
|
||||
S: Fn(BoxFuture<'static, DagResult<(NodeId, Arc<dyn Any + Send + Sync>)>>) -> F,
|
||||
F: Future<Output = DagResult<(NodeId, Arc<dyn Any + Send + Sync>)>>,
|
||||
S: Fn(BoxFuture<'static, DagResult<Arc<dyn Any + Send + Sync>>>) -> F,
|
||||
F: Future<Output = DagResult<Arc<dyn Any + Send + Sync>>>,
|
||||
{
|
||||
#[cfg(feature = "tracing")]
|
||||
info!("starting DAG execution");
|
||||
@@ -474,14 +467,15 @@ impl DagRunner {
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let inner_future = spawner(node.execute_with_deps(dependencies));
|
||||
// Spawn the task using the provided spawner
|
||||
let inner_future = async move {
|
||||
let result = node.execute_with_deps(dependencies).await?;
|
||||
let result = inner_future.await?;
|
||||
Ok((node_id, result))
|
||||
};
|
||||
|
||||
Some(
|
||||
AssertUnwindSafe(spawner(Box::pin(inner_future)))
|
||||
AssertUnwindSafe(inner_future)
|
||||
.catch_unwind()
|
||||
.unwrap_or_else(move |panic_payload| {
|
||||
// Convert panic to error
|
||||
@@ -675,13 +669,11 @@ impl DagRunner {
|
||||
}
|
||||
|
||||
if !current_layer.is_empty() {
|
||||
current_layer.sort(); // Deterministic ordering
|
||||
layers.push(current_layer);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle detection removed: cycles are impossible via the public API.
|
||||
// See src/cycle_prevention.rs for proof that the type system prevents cycles.
|
||||
//
|
||||
// The type-state pattern enforces acyclic structure at compile time through:
|
||||
// 1. TaskBuilder::depends_on() consumes the builder (move semantics)
|
||||
|
||||
+18
-21
@@ -1,7 +1,10 @@
|
||||
//! Unit tests for runner module
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hasher;
|
||||
|
||||
use crate::error::DagError;
|
||||
use crate::runner::DagRunner;
|
||||
use crate::runner::{DagRunner, PassThroughHasher};
|
||||
use crate::types::TaskHandle;
|
||||
|
||||
// Initialize tracing subscriber for tests (idempotent)
|
||||
@@ -168,23 +171,6 @@ async fn test_concurrent_run_protection() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_task_increments_id() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let handle1 = dag.add_task(TestTask { value: 1 });
|
||||
let handle2 = dag.add_task(TestTask { value: 2 });
|
||||
let handle3 = dag.add_task(TestTask { value: 3 });
|
||||
|
||||
// Node IDs should be sequential
|
||||
assert_eq!(handle1.id.0, 0);
|
||||
assert_eq!(handle2.id.0, 1);
|
||||
assert_eq!(handle3.id.0, 2);
|
||||
|
||||
// Check that nodes were actually added
|
||||
assert_eq!(dag.nodes.lock().len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_invalid_node_id_in_get() {
|
||||
init_tracing();
|
||||
@@ -255,7 +241,18 @@ async fn test_task_panic_in_multi_task_layer() {
|
||||
.await;
|
||||
|
||||
// The run should fail due to the panic
|
||||
// Note: Tokio catches panics in spawned tasks, so the behavior depends on the runtime
|
||||
// This test mainly ensures we don't crash
|
||||
assert!(result.is_ok() || result.is_err());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_passthrough_hasher() {
|
||||
let mut passthrough_hashmap = HashMap::with_hasher(PassThroughHasher::default());
|
||||
passthrough_hashmap.insert(64u32, "test string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_passthrough_hasher_only_u32() {
|
||||
let mut passthrough_hasher = PassThroughHasher::default();
|
||||
passthrough_hasher.write(&[0; 4]);
|
||||
}
|
||||
-47
@@ -5,8 +5,6 @@
|
||||
|
||||
use std::{any::Any, future::Future, marker::PhantomData, slice::Iter, sync::Arc};
|
||||
|
||||
use crate::extract::ExtractInput;
|
||||
|
||||
/// A unit of async work with typed inputs and outputs.
|
||||
///
|
||||
/// Use the [`task`](crate::task) macro to implement this trait. The macro automatically
|
||||
@@ -149,50 +147,5 @@ impl TaskInput<'static, ()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// **INTERNAL USE ONLY**: This struct is for testing purposes only and should
|
||||
/// not be used in production code. Use the `#[task]` macro instead.
|
||||
#[doc(hidden)]
|
||||
pub struct TaskFn<I, O, F>
|
||||
where
|
||||
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
|
||||
I: ExtractInput + Send + Sync + 'static,
|
||||
O: Send,
|
||||
{
|
||||
f: F,
|
||||
_phantom: std::marker::PhantomData<fn(I) -> O>,
|
||||
}
|
||||
|
||||
impl<I, O, F> Task for TaskFn<I, O, F>
|
||||
where
|
||||
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
|
||||
I: ExtractInput + Send + Sync + 'static,
|
||||
O: Send + Sync,
|
||||
{
|
||||
type Input = I::Input;
|
||||
type Output = O;
|
||||
|
||||
async fn run(mut self, input: TaskInput<'_, I::Input>) -> O {
|
||||
(self.f)(I::extract_from_task_input(input).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to create a task from a closure.
|
||||
///
|
||||
/// **INTERNAL USE ONLY**: This function is for testing purposes only and should
|
||||
/// not be used in production code. Use the `#[task]` macro instead.
|
||||
#[doc(hidden)]
|
||||
pub fn task_fn<I, O, F>(f: F) -> TaskFn<I, O, F>
|
||||
where
|
||||
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
|
||||
I: ExtractInput + Send + Sync + 'static,
|
||||
I::Input: Send + Sync + 'static,
|
||||
O: Send + Sync + 'static,
|
||||
{
|
||||
TaskFn {
|
||||
f,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+7
-180
@@ -1,99 +1,9 @@
|
||||
//! Unit tests for task module
|
||||
|
||||
use crate::task::{task_fn, Task, TaskInput};
|
||||
use crate::task::{Task, TaskInput};
|
||||
use std::any::Any;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Test the task_fn function and TaskFn struct
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_creation_and_execution() {
|
||||
// Test line 111 in task.rs - Task implementation for TaskFn
|
||||
|
||||
// Create a simple task function
|
||||
let task = task_fn::<i32, _, _>(|input: &i32| input * 2);
|
||||
|
||||
// Test the Task trait implementation
|
||||
let result = task
|
||||
.run(TaskInput::new(
|
||||
[Arc::new(42) as Arc<dyn Any + Send + Sync + 'static>].iter(),
|
||||
))
|
||||
.await;
|
||||
assert_eq!(result, 84);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_unit_input() {
|
||||
// Test task_fn with unit input
|
||||
let task = task_fn::<(), _, _>(|_: ()| 100);
|
||||
|
||||
let result = task
|
||||
.run(TaskInput::new(
|
||||
[Arc::new(()) as Arc<dyn Any + Send + Sync + 'static>].iter(),
|
||||
))
|
||||
.await;
|
||||
assert_eq!(result, 100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_tuple_input() {
|
||||
// Test task_fn with tuple input
|
||||
let task = task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b);
|
||||
|
||||
let result = task
|
||||
.run(TaskInput::new(
|
||||
[
|
||||
Arc::new(10) as Arc<dyn Any + Send + Sync + 'static>,
|
||||
Arc::new(20) as Arc<dyn Any + Send + Sync + 'static>,
|
||||
]
|
||||
.iter(),
|
||||
))
|
||||
.await;
|
||||
assert_eq!(result, 30);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_string_input() {
|
||||
// Test task_fn with String input
|
||||
let task = task_fn::<String, _, _>(|input: &String| input.len());
|
||||
|
||||
let test_string = String::from("hello");
|
||||
let result = task
|
||||
.run(TaskInput::new(
|
||||
[Arc::new(test_string) as Arc<dyn Any + Send + Sync + 'static>].iter(),
|
||||
))
|
||||
.await;
|
||||
assert_eq!(result, 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_complex_output() {
|
||||
// Test task_fn with complex output type
|
||||
let task = task_fn::<i32, _, _>(|&input: &i32| vec![input, input * 2, input * 3]);
|
||||
|
||||
let result = task
|
||||
.run(TaskInput::new(
|
||||
[Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter(),
|
||||
))
|
||||
.await;
|
||||
assert_eq!(result, vec![5, 10, 15]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_stateful_closure() {
|
||||
// Test that task_fn can capture state
|
||||
let multiplier = 3;
|
||||
let task = task_fn::<i32, _, _>(move |input: &i32| input * multiplier);
|
||||
|
||||
let result = task
|
||||
.run(TaskInput::new(
|
||||
[Arc::new(7) as Arc<dyn Any + Send + Sync + 'static>].iter(),
|
||||
))
|
||||
.await;
|
||||
assert_eq!(result, 21);
|
||||
}
|
||||
|
||||
// Test custom Task implementation
|
||||
struct CustomTask {
|
||||
state: i32,
|
||||
@@ -103,15 +13,9 @@ impl Task for CustomTask {
|
||||
type Input = (i32,);
|
||||
type Output = i32;
|
||||
|
||||
#[allow(refining_impl_trait)]
|
||||
fn run(
|
||||
mut self,
|
||||
input: TaskInput<'_, Self::Input>,
|
||||
) -> Pin<Box<dyn Future<Output = Self::Output> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
self.state += input.next().0;
|
||||
self.state
|
||||
})
|
||||
async fn run(mut self, input: TaskInput<'_, Self::Input>) -> i32 {
|
||||
self.state += input.next().0;
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,85 +34,8 @@ async fn test_custom_task_implementation() {
|
||||
// This demonstrates the ownership model
|
||||
}
|
||||
|
||||
// Test that Task trait bounds are correct
|
||||
fn assert_task_bounds<T: Task>() {
|
||||
// This function just checks that T implements Task with the right bounds
|
||||
fn assert_send<T: Send>() {}
|
||||
|
||||
// These should compile if bounds are correct
|
||||
assert_send::<T>();
|
||||
assert_send::<T::Input>();
|
||||
assert_send::<T::Output>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_trait_bounds() {
|
||||
// Verify that our test tasks meet the Task trait bounds
|
||||
assert_task_bounds::<CustomTask>();
|
||||
|
||||
// We can't directly test TaskFn because it's a private type and needs a concrete closure type,
|
||||
// but the fact that task_fn works in the tests above proves it's correct
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_single_call() {
|
||||
// Test that task_fn works (can only call once due to ownership)
|
||||
let task = task_fn::<i32, _, _>(|x: &i32| x + 1);
|
||||
|
||||
assert_eq!(
|
||||
task.run(TaskInput::new(
|
||||
[Arc::new(1) as Arc<dyn Any + Send + Sync + 'static>].iter()
|
||||
))
|
||||
.await,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_result_type() {
|
||||
// Test task_fn returning Result
|
||||
let task = task_fn::<i32, _, _>(|&x: &i32| {
|
||||
if x > 0 {
|
||||
Ok(x * 2)
|
||||
} else {
|
||||
Err("negative input")
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
task.run(TaskInput::new(
|
||||
[Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter()
|
||||
))
|
||||
.await,
|
||||
Ok(10)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_option_type() {
|
||||
// Test task_fn returning Option
|
||||
let task = task_fn::<i32, _, _>(|&x: &i32| if x > 0 { Some(x * 2) } else { None });
|
||||
|
||||
assert_eq!(
|
||||
task.run(TaskInput::new(
|
||||
[Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter()
|
||||
))
|
||||
.await,
|
||||
Some(10)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_explicit_trait_call() {
|
||||
// Test explicit Task trait method call to cover line 121
|
||||
|
||||
let task = task_fn::<i32, _, _>(|x: &i32| x * 3);
|
||||
|
||||
// Explicitly call the Task trait's run method
|
||||
let result = Task::run(
|
||||
task,
|
||||
TaskInput::new([Arc::new(10) as Arc<dyn Any + Send + Sync + 'static>].iter()),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(result, 30);
|
||||
fn test_empty_task_input() {
|
||||
let empty = TaskInput::empty();
|
||||
assert_eq!(empty.inputs.len(), 0);
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ use crate::builder::TaskBuilder;
|
||||
use crate::task::Task;
|
||||
|
||||
/// Opaque node identifier
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct NodeId(pub u32);
|
||||
|
||||
/// Opaque, typed token for a node's output.
|
||||
|
||||
@@ -4,90 +4,6 @@
|
||||
|
||||
use crate::{task, DagRunner, TaskHandle};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unit_type_taskbuilder_to_handle_conversion() {
|
||||
// Test that explicitly uses the From<TaskBuilder> conversion for unit-type tasks
|
||||
// This covers line 71 in types.rs
|
||||
|
||||
// Unit struct (no fields) - its Input type is ()
|
||||
struct UnitTask;
|
||||
|
||||
#[task]
|
||||
impl UnitTask {
|
||||
async fn run(&self) -> i32 {
|
||||
42
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// This creates a TaskBuilder<UnitTask, Pending>
|
||||
// Since UnitTask has Input = (), it can be converted to TaskHandle
|
||||
let builder = dag.add_task(UnitTask);
|
||||
|
||||
// Explicitly convert to TaskHandle using From implementation
|
||||
// This triggers the From<TaskBuilder> implementation at line 69-74,
|
||||
// specifically covering line 71
|
||||
let handle: TaskHandle<_> = builder.into();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify the task ran and we can get its output
|
||||
assert_eq!(dag.get(handle).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unit_type_conversion_with_multiple_tasks() {
|
||||
// Test conversion with multiple unit-type tasks
|
||||
|
||||
struct FirstTask;
|
||||
#[task]
|
||||
impl FirstTask {
|
||||
async fn run(&self) -> String {
|
||||
"first".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct SecondTask;
|
||||
#[task]
|
||||
impl SecondTask {
|
||||
async fn run(&self) -> i32 {
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
struct ThirdTask;
|
||||
#[task]
|
||||
impl ThirdTask {
|
||||
async fn run(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create builders for all tasks
|
||||
let first_builder = dag.add_task(FirstTask);
|
||||
let second_builder = dag.add_task(SecondTask);
|
||||
let third_builder = dag.add_task(ThirdTask);
|
||||
|
||||
// Convert each builder to handle explicitly
|
||||
let first_handle: TaskHandle<String> = first_builder.into();
|
||||
let second_handle: TaskHandle<_> = second_builder.into();
|
||||
let third_handle: TaskHandle<bool> = third_builder.into();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify all outputs
|
||||
assert_eq!(dag.get(first_handle).unwrap(), "first");
|
||||
assert_eq!(dag.get(second_handle).unwrap(), 100);
|
||||
assert!(dag.get(third_handle).unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unit_struct_with_state() {
|
||||
// Even structs with fields can have unit input type
|
||||
@@ -124,75 +40,9 @@ async fn test_unit_struct_with_state() {
|
||||
assert_eq!(dag.get(handle).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_taskbuilder_conversion_assignment() {
|
||||
// Test the conversion through assignment (different from .into())
|
||||
|
||||
struct SimpleTask;
|
||||
#[task]
|
||||
impl SimpleTask {
|
||||
async fn run(&self) -> Vec<i32> {
|
||||
vec![1, 2, 3]
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let builder = dag.add_task(SimpleTask);
|
||||
|
||||
// Conversion through type annotation and assignment
|
||||
// This also uses From<TaskBuilder> implementation
|
||||
let handle: TaskHandle<Vec<i32>> = builder.into();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = dag.get(handle).unwrap();
|
||||
assert_eq!(result, vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mixed_unit_and_dependent_tasks() {
|
||||
// Test that unit-type tasks work alongside tasks with dependencies
|
||||
|
||||
struct Source;
|
||||
#[task]
|
||||
impl Source {
|
||||
async fn run(&self) -> i32 {
|
||||
10
|
||||
}
|
||||
}
|
||||
|
||||
struct Doubler;
|
||||
#[task]
|
||||
impl Doubler {
|
||||
async fn run(&self, input: &i32) -> i32 {
|
||||
input * 2
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Source is unit-type, can be converted
|
||||
let source_builder = dag.add_task(Source);
|
||||
let source_handle: TaskHandle<_> = source_builder.into(); // Covers line 71
|
||||
|
||||
// Doubler needs dependencies, so we use it differently
|
||||
let doubler = dag.add_task(Doubler).depends_on(source_handle);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(source_handle).unwrap(), 10);
|
||||
assert_eq!(dag.get(doubler).unwrap(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taskbuilder_conversion_without_running() {
|
||||
// Test that conversion works even without running the DAG
|
||||
// This ensures the conversion is purely a type-level operation
|
||||
|
||||
struct TestTask;
|
||||
#[task]
|
||||
impl TestTask {
|
||||
@@ -211,54 +61,3 @@ fn test_taskbuilder_conversion_without_running() {
|
||||
// Verify the ID was transferred correctly
|
||||
assert_eq!(handle.id, builder_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_conversions_preserve_ids() {
|
||||
// Ensure that multiple conversions maintain correct IDs
|
||||
|
||||
struct Task1;
|
||||
#[task]
|
||||
impl Task1 {
|
||||
async fn run(&self) -> i32 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
struct Task2;
|
||||
#[task]
|
||||
impl Task2 {
|
||||
async fn run(&self) -> i32 {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
struct Task3;
|
||||
#[task]
|
||||
impl Task3 {
|
||||
async fn run(&self) -> i32 {
|
||||
3
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let builder1 = dag.add_task(Task1);
|
||||
let builder2 = dag.add_task(Task2);
|
||||
let builder3 = dag.add_task(Task3);
|
||||
|
||||
let id1 = builder1.id;
|
||||
let id2 = builder2.id;
|
||||
let id3 = builder3.id;
|
||||
|
||||
// Convert all builders
|
||||
let handle1: TaskHandle<_> = builder1.into();
|
||||
let handle2: TaskHandle<_> = builder2.into();
|
||||
let handle3: TaskHandle<_> = builder3.into();
|
||||
|
||||
// Verify IDs are preserved and sequential
|
||||
assert_eq!(handle1.id, id1);
|
||||
assert_eq!(handle2.id, id2);
|
||||
assert_eq!(handle3.id, id3);
|
||||
assert_eq!(handle1.id.0 + 1, handle2.id.0);
|
||||
assert_eq!(handle2.id.0 + 1, handle3.id.0);
|
||||
}
|
||||
+5
-104
@@ -4,54 +4,19 @@ use crate::types::{NodeId, TaskHandle};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[test]
|
||||
fn test_node_id_operations() {
|
||||
let id1 = NodeId(1);
|
||||
let id2 = NodeId(2);
|
||||
let id3 = NodeId(1);
|
||||
|
||||
// Test equality
|
||||
assert_eq!(id1, id3);
|
||||
assert_ne!(id1, id2);
|
||||
|
||||
// Test ordering
|
||||
assert!(id1 < id2);
|
||||
assert!(id2 > id1);
|
||||
|
||||
// Test Debug
|
||||
let debug_str = format!("{:?}", id1);
|
||||
assert!(debug_str.contains("NodeId"));
|
||||
assert!(debug_str.contains("1"));
|
||||
|
||||
// Test Clone
|
||||
let cloned = id1;
|
||||
assert_eq!(id1, cloned);
|
||||
|
||||
// Test Copy
|
||||
let copied = id1;
|
||||
assert_eq!(id1, copied);
|
||||
|
||||
// Test Hash (compile-time check that it implements Hash)
|
||||
use std::collections::HashSet;
|
||||
let mut set = HashSet::new();
|
||||
set.insert(id1);
|
||||
set.insert(id2);
|
||||
assert_eq!(set.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::clone_on_copy)]
|
||||
fn test_task_handle_clone() {
|
||||
let handle: TaskHandle<(i32,)> = TaskHandle {
|
||||
id: NodeId(42),
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
// Test Clone implementation (lines 44-46 in types.rs)
|
||||
let cloned = handle;
|
||||
// Test Clone implementation
|
||||
let cloned = handle.clone();
|
||||
assert_eq!(handle.id.0, cloned.id.0);
|
||||
|
||||
// Test that clone is actually a copy (same memory semantics)
|
||||
let handle2 = handle;
|
||||
let handle3 = handle2;
|
||||
let handle2 = handle.clone();
|
||||
let handle3 = handle2.clone();
|
||||
assert_eq!(handle3.id.0, 42);
|
||||
}
|
||||
|
||||
@@ -69,70 +34,6 @@ fn test_task_handle_copy() {
|
||||
assert_eq!(copied2.id.0, 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_handle_with_different_types() {
|
||||
// Test that TaskHandle works with various types
|
||||
let int_handle: TaskHandle<i32> = TaskHandle {
|
||||
id: NodeId(1),
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
let string_handle: TaskHandle<String> = TaskHandle {
|
||||
id: NodeId(2),
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
let vec_handle: TaskHandle<Vec<u8>> = TaskHandle {
|
||||
id: NodeId(3),
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
// Clone and copy should work for all
|
||||
let _ = int_handle;
|
||||
let _ = string_handle;
|
||||
let _ = vec_handle;
|
||||
|
||||
let _ = int_handle;
|
||||
let _ = string_handle;
|
||||
let _ = vec_handle;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_handle_explicit_clone() {
|
||||
// Test explicit .clone() call to cover line 47-48
|
||||
let handle: TaskHandle<i32> = TaskHandle {
|
||||
id: NodeId(42),
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
#[allow(clippy::clone_on_copy)]
|
||||
let cloned = handle.clone();
|
||||
assert_eq!(handle.id.0, cloned.id.0);
|
||||
assert_eq!(cloned.id.0, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_builder_from_conversion() {
|
||||
use crate::runner::DagRunner;
|
||||
|
||||
// Test From<TaskBuilder> for TaskHandle (line 69-74)
|
||||
struct SimpleTask;
|
||||
#[crate::task]
|
||||
impl SimpleTask {
|
||||
async fn run(&self) -> i32 {
|
||||
42
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let builder = dag.add_task(SimpleTask);
|
||||
let builder_id = builder.id;
|
||||
|
||||
// This conversion uses the From implementation
|
||||
let handle: TaskHandle<_> = builder.into();
|
||||
assert_eq!(handle.id, builder_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_handle_ref_from_conversion() {
|
||||
// Test From<&TaskHandle<T>> for TaskHandle<T> (line 90-93)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
[test_config]
|
||||
manifest-path = "./Cargo.toml"
|
||||
skip-clean = true
|
||||
engine = "Llvm"
|
||||
# cargo-tarpaulin already reports tracing lines incorrectly when the feature is turned off,
|
||||
# and this is exacerbated when building with all-features.
|
||||
all-features = false
|
||||
run-types = ["Tests"]
|
||||
command = "Test"
|
||||
locked = true
|
||||
frozen = true
|
||||
fail-under = 80.0
|
||||
-1348
File diff suppressed because it is too large.
Load diff
@@ -1,70 +1,11 @@
|
||||
//! Tests for extremely deep dependency chains
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_1000_level_deep_chain() -> DagResult<()> {
|
||||
// Test a linear chain of 1000 tasks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
|
||||
let mut current = first.into(); // Convert to TaskHandle
|
||||
|
||||
for _i in 1..1000 {
|
||||
current = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&prev: &i32| prev + 1))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(current)?, 999);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_with_branches() -> DagResult<()> {
|
||||
// Test a deep chain where each node has a side branch
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
|
||||
let mut main_chain: dagx::TaskHandle<i32> = first.into();
|
||||
let mut branches = Vec::new();
|
||||
|
||||
for i in 1..500 {
|
||||
// Main chain continues
|
||||
main_chain = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&prev: &i32| prev + 1))
|
||||
.depends_on(main_chain);
|
||||
|
||||
// Create a branch at every 10th node
|
||||
if i % 10 == 0 {
|
||||
let branch = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |main_val: &i32| main_val * 2))
|
||||
.depends_on(main_chain);
|
||||
branches.push(branch);
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(main_chain)?, 499);
|
||||
|
||||
// Check some branches
|
||||
if !branches.is_empty() {
|
||||
assert_eq!(dag.get(branches[0])?, 20); // 10 * 2
|
||||
assert_eq!(dag.get(branches[4])?, 100); // 50 * 2
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
||||
// Test 10 parallel chains of depth 500 each
|
||||
@@ -223,30 +164,6 @@ async fn test_fibonacci_chain() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_with_accumulator() -> DagResult<()> {
|
||||
// Test a deep chain where each node accumulates a value
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Use a simpler accumulation pattern - just sum
|
||||
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64));
|
||||
let mut current: dagx::TaskHandle<i64> = first.into();
|
||||
|
||||
for i in 1..=200 {
|
||||
current = dag
|
||||
.add_task(task_fn::<i64, _, _>(move |sum: &i64| sum + i as i64))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(current)?;
|
||||
assert_eq!(result, 20100); // sum of 1..=200
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_zigzag_dependencies() -> DagResult<()> {
|
||||
// Test a zigzag pattern: two chains that cross-reference each other
|
||||
@@ -285,25 +202,3 @@ async fn test_zigzag_dependencies() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(tarpaulin, ignore)] // This test is extremely resource intensive
|
||||
async fn test_10000_level_chain_stress() -> DagResult<()> {
|
||||
// Ultimate deep chain test
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0u64));
|
||||
let mut current: dagx::TaskHandle<u64> = first.into();
|
||||
|
||||
for _ in 1..10_000 {
|
||||
current = dag
|
||||
.add_task(task_fn::<u64, _, _>(|prev: &u64| prev + 1))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(current)?, 9_999);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
//! Large scale stress tests with thousands of nodes and edges
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_10000_independent_tasks() -> DagResult<()> {
|
||||
// Test with 10,000 independent tasks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut tasks: Vec<_> = (0..10_000)
|
||||
.map(|i| {
|
||||
let counter = counter.clone();
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
i
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all tasks completed
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 10_000);
|
||||
|
||||
// Spot check some results
|
||||
assert_eq!(dag.get(tasks.swap_remove(9_999))?, 9_999);
|
||||
assert_eq!(dag.get(tasks.swap_remove(5_000))?, 5_000);
|
||||
assert_eq!(dag.get(tasks.swap_remove(0))?, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_5000_node_pyramid() -> DagResult<()> {
|
||||
// Create a pyramid structure with ~5000 nodes
|
||||
// Layer sizes: 2048 -> 512 -> 128 -> 32 -> 8 -> 2 -> 1
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Bottom layer: 2048 nodes - convert to TaskHandles
|
||||
let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048)
|
||||
.map(|i| {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(
|
||||
move |_: ()| {
|
||||
if i < 1024 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
},
|
||||
));
|
||||
task.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let layer_sizes = vec![512, 128, 32, 8, 2];
|
||||
|
||||
for layer_size in layer_sizes {
|
||||
let mut next_layer = Vec::with_capacity(layer_size);
|
||||
let group_size = current_layer.len() / layer_size;
|
||||
|
||||
for i in 0..layer_size {
|
||||
let start = i * group_size;
|
||||
let deps: Vec<_> = current_layer[start..start + group_size.min(4)]
|
||||
.iter()
|
||||
.take(4)
|
||||
.collect();
|
||||
|
||||
let task = match deps.len() {
|
||||
1 => dag
|
||||
.add_task(task_fn::<i32, _, _>(|&a: &i32| a))
|
||||
.depends_on(deps[0]),
|
||||
2 => dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((deps[0], deps[1])),
|
||||
3 => dag
|
||||
.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
|(a, b, c): (&i32, &i32, &i32)| a + b + c,
|
||||
))
|
||||
.depends_on((deps[0], deps[1], deps[2])),
|
||||
4 => dag
|
||||
.add_task(task_fn::<(i32, i32, i32, i32), _, _>(
|
||||
|(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d,
|
||||
))
|
||||
.depends_on((deps[0], deps[1], deps[2], deps[3])),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
next_layer.push(task);
|
||||
}
|
||||
current_layer = next_layer;
|
||||
}
|
||||
|
||||
// Final layer combines the last 2
|
||||
let root = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((¤t_layer[0], ¤t_layer[1]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// We created 1024 ones and 1024 zeros at the bottom
|
||||
// The sum should propagate up to 1024
|
||||
let result = dag.get(root)?;
|
||||
assert!(result > 0, "Expected positive result, got {}", result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> {
|
||||
// Create a pyramid structure with ~5000 nodes
|
||||
// Layer sizes: 2048 -> 512 -> 128 -> 32 -> 8 -> 2 -> 1
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Bottom layer: 2048 nodes - convert to TaskHandles
|
||||
let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048)
|
||||
.map(|i| {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(
|
||||
move |_: ()| {
|
||||
if i < 1024 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
},
|
||||
));
|
||||
task.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let layer_sizes = vec![512, 128, 32, 8, 2];
|
||||
|
||||
for layer_size in layer_sizes {
|
||||
let mut next_layer = Vec::with_capacity(layer_size);
|
||||
let group_size = current_layer.len() / layer_size;
|
||||
|
||||
for i in 0..layer_size {
|
||||
let start = i * group_size;
|
||||
let deps: Vec<_> = current_layer[start..start + group_size.min(4)]
|
||||
.iter()
|
||||
.take(4)
|
||||
.collect();
|
||||
|
||||
let task = match deps.len() {
|
||||
1 => dag
|
||||
.add_task(task_fn::<i32, _, _>(|&a: &i32| a))
|
||||
.depends_on(deps[0]),
|
||||
2 => dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((deps[0], deps[1])),
|
||||
3 => dag
|
||||
.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
|(a, b, c): (&i32, &i32, &i32)| a + b + c,
|
||||
))
|
||||
.depends_on((deps[0], deps[1], deps[2])),
|
||||
4 => dag
|
||||
.add_task(task_fn::<(i32, i32, i32, i32), _, _>(
|
||||
|(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d,
|
||||
))
|
||||
.depends_on((deps[0], deps[1], deps[2], deps[3])),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
next_layer.push(task);
|
||||
}
|
||||
current_layer = next_layer;
|
||||
}
|
||||
|
||||
// Final layer combines the last 2
|
||||
let root = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((¤t_layer[0], ¤t_layer[1]));
|
||||
|
||||
dag.run(|fut| fut).await?;
|
||||
|
||||
// We created 1024 ones and 1024 zeros at the bottom
|
||||
// The sum should propagate up to 1024
|
||||
let result = dag.get(root)?;
|
||||
assert!(result > 0, "Expected positive result, got {}", result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> {
|
||||
// Test a very wide DAG with 1000 sources and 1000 sinks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 1000 source tasks
|
||||
let sources: Vec<_> = (0..1000)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i % 10))
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create 1000 sink tasks, each depending on 3 random sources
|
||||
let sinks: Vec<_> = (0..1000)
|
||||
.map(|i| {
|
||||
let idx1 = (i * 7) % 1000;
|
||||
let idx2 = (i * 13) % 1000;
|
||||
let idx3 = (i * 17) % 1000;
|
||||
|
||||
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
move |(a, b, c): (&i32, &i32, &i32)| a + b + c + i as i32,
|
||||
))
|
||||
.depends_on((&sources[idx1], &sources[idx2], &sources[idx3]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify some sinks completed correctly
|
||||
let result0 = dag.get(sinks[0])?;
|
||||
let result500 = dag.get(sinks[500])?;
|
||||
let result999 = dag.get(sinks[999])?;
|
||||
|
||||
assert!(result0 >= 0);
|
||||
assert!(result500 >= 500);
|
||||
assert!(result999 >= 999);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_10000_node_linear_chain_segments() -> DagResult<()> {
|
||||
// Test 10000 nodes arranged as 100 parallel chains of 100 nodes each
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut chain_ends = Vec::new();
|
||||
|
||||
for chain_id in 0..100 {
|
||||
let first = dag.add_task(task_fn::<(), _, _>({
|
||||
let counter = counter.clone();
|
||||
move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
chain_id * 100
|
||||
}
|
||||
}));
|
||||
let mut current: dagx::TaskHandle<i32> = first.into();
|
||||
|
||||
for _i in 1..100 {
|
||||
current = dag
|
||||
.add_task(task_fn::<i32, _, _>({
|
||||
let counter = counter.clone();
|
||||
move |&prev: &i32| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
prev + 1
|
||||
}
|
||||
}))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
chain_ends.push(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// All 10,000 tasks should have executed
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 10_000);
|
||||
|
||||
// Each chain should end with start + 99
|
||||
for (chain_id, &chain_end) in chain_ends.iter().enumerate() {
|
||||
let expected = chain_id as i32 * 100 + 99;
|
||||
assert_eq!(dag.get(chain_end)?, expected);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(tarpaulin, ignore)] // This test is very resource intensive
|
||||
async fn test_100000_nodes_stress() -> DagResult<()> {
|
||||
// Ultimate stress test: 100,000 nodes
|
||||
let dag = DagRunner::new();
|
||||
let completed = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create 100,000 tasks in batches
|
||||
let batch_size = 1000;
|
||||
let num_batches = 100;
|
||||
|
||||
for batch in 0..num_batches {
|
||||
let _: Vec<_> = (0..batch_size)
|
||||
.map(|i| {
|
||||
let completed = completed.clone();
|
||||
let task_id = batch * batch_size + i;
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let completed = completed.clone();
|
||||
completed.fetch_add(1, Ordering::Relaxed);
|
||||
task_id
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(completed.load(Ordering::Relaxed), 100_000);
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,6 +4,5 @@
|
||||
//! by normal usage patterns.
|
||||
|
||||
mod deep_chains;
|
||||
mod large_scale;
|
||||
mod type_limits;
|
||||
mod zero_tasks;
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for type system edge cases and limits
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
@@ -108,89 +108,6 @@ async fn test_unit_type_chains() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_string_types_variety() -> DagResult<()> {
|
||||
// Test various string types
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| "static str"));
|
||||
let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| String::from("owned string")));
|
||||
let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
std::borrow::Cow::Borrowed("cow str")
|
||||
}));
|
||||
let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| Box::new("boxed str")));
|
||||
let t5 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
std::sync::Arc::new("arc str 2")
|
||||
}));
|
||||
let t6 = dag.add_task(task_fn::<(), _, _>(|_: ()| std::sync::Arc::new("arc str")));
|
||||
|
||||
let combined = dag
|
||||
.add_task(task_fn::<
|
||||
(
|
||||
&str,
|
||||
String,
|
||||
std::borrow::Cow<str>,
|
||||
Box<&str>,
|
||||
std::sync::Arc<&str>,
|
||||
std::sync::Arc<&str>,
|
||||
),
|
||||
_,
|
||||
_,
|
||||
>(
|
||||
|(s1, s2, s3, s4, s5, s6): (
|
||||
&&str,
|
||||
&String,
|
||||
&std::borrow::Cow<str>,
|
||||
&Box<&str>,
|
||||
&std::sync::Arc<&str>,
|
||||
&std::sync::Arc<&str>,
|
||||
)| { format!("{} {} {} {} {} {}", s1, s2, s3, s4, s5, s6) },
|
||||
))
|
||||
.depends_on((t1, t2, t3, t4, t5, t6));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(combined)?;
|
||||
assert!(result.contains("static str"));
|
||||
assert!(result.contains("owned string"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generic_type_constraints() -> DagResult<()> {
|
||||
// Test with types that have trait bounds
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct Wrapper<T: Clone + Send + 'static>(T);
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(42)));
|
||||
let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper("hello")));
|
||||
let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(vec![1, 2, 3])));
|
||||
|
||||
let combined = dag
|
||||
.add_task(task_fn::<
|
||||
(Wrapper<i32>, Wrapper<&str>, Wrapper<Vec<i32>>),
|
||||
_,
|
||||
_,
|
||||
>(
|
||||
|(w1, w2, w3): (&Wrapper<i32>, &Wrapper<&str>, &Wrapper<Vec<i32>>)| {
|
||||
format!("{:?} {:?} {:?}", w1, w2, w3)
|
||||
},
|
||||
))
|
||||
.depends_on((t1, t2, t3));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(combined)?;
|
||||
assert!(result.contains("Wrapper(42)"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reference_wrapper_types() -> DagResult<()> {
|
||||
// Test types that wrap references
|
||||
@@ -216,71 +133,3 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_array_types() -> DagResult<()> {
|
||||
// Test with fixed-size arrays
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| [1, 2, 3, 4, 5]));
|
||||
let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| [6, 7, 8, 9, 10]));
|
||||
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<([i32; 5], [i32; 5]), _, _>(
|
||||
|(a1, a2): (&[i32; 5], &[i32; 5])| {
|
||||
let mut sum = 0;
|
||||
for i in 0..5 {
|
||||
sum += a1[i] + a2[i];
|
||||
}
|
||||
sum
|
||||
},
|
||||
))
|
||||
.depends_on((t1, t2));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t3)?, 55);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_enum_variants() -> DagResult<()> {
|
||||
// Test with various enum types
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum MyEnum {
|
||||
Unit,
|
||||
Tuple(i32, String),
|
||||
Struct { x: i32, y: i32 },
|
||||
LargeVariant([u8; 1024]),
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Unit));
|
||||
let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
MyEnum::Tuple(42, "test".to_string())
|
||||
}));
|
||||
let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Struct { x: 10, y: 20 }));
|
||||
let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::LargeVariant([0; 1024])));
|
||||
|
||||
let combined = dag
|
||||
.add_task(task_fn::<(MyEnum, MyEnum, MyEnum, MyEnum), _, _>(
|
||||
|(e1, e2, e3, e4): (&MyEnum, &MyEnum, &MyEnum, &MyEnum)| {
|
||||
matches!(e1, MyEnum::Unit)
|
||||
&& matches!(e2, MyEnum::Tuple(42, _))
|
||||
&& matches!(e3, MyEnum::Struct { x: 10, y: 20 })
|
||||
&& matches!(e4, MyEnum::LargeVariant(_))
|
||||
},
|
||||
))
|
||||
.depends_on((t1, t2, t3, t4));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert!(dag.get(combined)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,89 +1,10 @@
|
||||
//! Tests for empty DAGs and minimal task configurations
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::task::yield_now;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_dag_execution() -> DagResult<()> {
|
||||
// Test that an empty DAG can be created and run without issues
|
||||
let dag = DagRunner::new();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Should complete successfully with no tasks
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task_no_deps() -> DagResult<()> {
|
||||
// Test the absolute minimum: one task with no dependencies
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, 42);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task_unit_input_output() -> DagResult<()> {
|
||||
// Test a task that takes unit and returns unit
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let executed = Arc::new(AtomicUsize::new(0));
|
||||
let exec_clone = executed.clone();
|
||||
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let exec = exec_clone.clone();
|
||||
exec.fetch_add(1, Ordering::SeqCst);
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
dag.get(task)?; // Just verify it doesn't error
|
||||
assert_eq!(executed.load(Ordering::SeqCst), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_zero_sized_type_task() -> DagResult<()> {
|
||||
// Test with zero-sized types
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct ZeroSized;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| ZeroSized));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, ZeroSized);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_dag_multiple_runs() -> DagResult<()> {
|
||||
// Test that an empty DAG can be run multiple times
|
||||
let dag = DagRunner::new();
|
||||
|
||||
for _ in 0..10 {
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dag_with_only_source_tasks() -> DagResult<()> {
|
||||
// Test a DAG with multiple source tasks but no dependencies
|
||||
@@ -127,47 +48,6 @@ async fn test_single_self_contained_stateful_task() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_alternating_empty_and_filled_dag_runs() -> DagResult<()> {
|
||||
// Test alternating between empty DAG and DAG with tasks
|
||||
for i in 0..5 {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
if i % 2 == 0 {
|
||||
// Empty DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
} else {
|
||||
// DAG with a task
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dag_with_never_type_simulation() -> DagResult<()> {
|
||||
// Test handling of tasks that conceptually "never" complete
|
||||
// (simulated by immediate completion with special marker)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum Never {}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// This task completes immediately but returns a Result containing "never" type
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::<i32, Never>::Ok(42)));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, Ok(42));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> {
|
||||
// Test a task that does the absolute minimum async work
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
// Common test utilities and helpers for the test suite
|
||||
|
||||
// Re-export task_fn for internal testing
|
||||
// Note: task_fn is #[doc(hidden)] and not part of the documented public API
|
||||
pub use dagx::task_fn;
|
||||
|
||||
// Re-export commonly used test structs
|
||||
#[allow(dead_code)]
|
||||
pub mod tasks {
|
||||
use dagx::task;
|
||||
|
||||
// Simple seed task with mutable state
|
||||
pub struct Seed {
|
||||
pub value: i32,
|
||||
}
|
||||
|
||||
impl Seed {
|
||||
pub fn new(v: i32) -> Self {
|
||||
Self { value: v }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Seed {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
// Stateless increment task
|
||||
pub struct Inc;
|
||||
|
||||
impl Inc {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Inc {
|
||||
async fn run(x: &i32) -> i32 {
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Stateless add task
|
||||
pub struct Add;
|
||||
|
||||
impl Add {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Add {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
// Stateless multiply task
|
||||
pub struct Mul;
|
||||
|
||||
impl Mul {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Mul {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a * b
|
||||
}
|
||||
}
|
||||
|
||||
// Stateful counter task
|
||||
pub struct Counter {
|
||||
pub count: i32,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
pub fn new(initial: i32) -> Self {
|
||||
Self { count: initial }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Counter {
|
||||
async fn run(&mut self, input: &i32) -> i32 {
|
||||
self.count += input;
|
||||
self.count
|
||||
}
|
||||
}
|
||||
|
||||
// Task that outputs multiple types combined as string
|
||||
pub struct OutputResult;
|
||||
|
||||
impl OutputResult {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl OutputResult {
|
||||
async fn run(s: &String, n: &usize, f: &bool) -> String {
|
||||
format!("{s}:{n}:{f}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Test that custom types work without any trait implementations
|
||||
// Test that custom types work with the #[task] macro
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -84,28 +84,6 @@ async fn test_custom_type_passthrough() {
|
||||
assert_eq!(company_result.employees.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_type_diamond_pattern() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Source
|
||||
let fetch: TaskHandle<_> = dag.add_task(FetchPerson).into();
|
||||
|
||||
// Two paths from source
|
||||
let path_a = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||
let path_b = dag.add_task(PersonToCompany).depends_on(fetch);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result_a = dag.get(path_a).unwrap();
|
||||
let result_b = dag.get(path_b).unwrap();
|
||||
|
||||
assert_eq!(result_a.as_str(), "Alice is 30 years old");
|
||||
assert_eq!(result_b.name, "Alice's Company");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_type_fan_out() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Dependency tests module
|
||||
|
||||
pub mod resolution;
|
||||
pub mod single;
|
||||
pub mod tuples;
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for dependency resolution order and transitive dependencies
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -99,68 +99,3 @@ async fn test_dependency_resolution_order() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_transitive_dependencies() -> DagResult<()> {
|
||||
// Test that transitive dependencies work correctly
|
||||
// A -> B -> C (creates transitive A -> C relationship)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(b);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(a)?, 10);
|
||||
assert_eq!(dag.get(b)?, 20);
|
||||
assert_eq!(dag.get(c)?, 25); // 20 + 5
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_50_levels() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
|
||||
let mut handles = vec![first.into()];
|
||||
|
||||
for _i in 1..=50 {
|
||||
let next = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + 1))
|
||||
.depends_on(handles.last().unwrap());
|
||||
handles.push(next);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(handles.last().unwrap())?, 50);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_200_levels() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
let mut current = first.into();
|
||||
|
||||
for _ in 0..200 {
|
||||
current = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(current)?, 201);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
//! Tests for single dependencies and basic dependency data flow
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_dependency() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
let dependent = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(dependent)?, 43);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dependency_data_flow() -> DagResult<()> {
|
||||
// Test that data flows correctly through dependencies
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]))
|
||||
.into();
|
||||
|
||||
let sum = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|v: &Vec<i32>| {
|
||||
v.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(source);
|
||||
|
||||
let product = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|v: &Vec<i32>| {
|
||||
v.iter().product::<i32>()
|
||||
}))
|
||||
.depends_on(source);
|
||||
|
||||
let final_result = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(s, p): (&i32, &i32)| s + p))
|
||||
.depends_on((&sum, &product));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Note: sum and product are not sinks (final_result depends on them)
|
||||
// Only final_result is a sink and can be retrieved
|
||||
assert_eq!(dag.get(final_result)?, 135); // 15+120
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dependencies_with_different_types() -> DagResult<()> {
|
||||
// Test that dependencies with different types work correctly
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let int_source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
let string_source = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
|
||||
let bool_source = dag.add_task(task_fn::<(), _, _>(|_: ()| true));
|
||||
|
||||
let combined = dag
|
||||
.add_task(task_fn::<(i32, String, bool), _, _>(
|
||||
|(i, s, b): (&i32, &String, &bool)| format!("{}: {} ({})", s, i, b),
|
||||
))
|
||||
.depends_on((int_source, string_source, bool_source));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(combined)?, "hello: 42 (true)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_shared_dependencies() -> DagResult<()> {
|
||||
// Test that a single node can be a dependency for multiple downstream nodes
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let shared: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
// Create 10 tasks that all depend on the shared node
|
||||
let dependents: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * (i + 1)))
|
||||
.depends_on(shared)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all dependents got the correct value from shared
|
||||
for (i, handle) in dependents.iter().enumerate() {
|
||||
assert_eq!(dag.get(handle)?, 42 * (i as i32 + 1));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_source_nodes() -> DagResult<()> {
|
||||
// Test DAG with multiple source nodes (no dependencies)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let sources: Vec<_> = (0..20)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
for (i, source) in sources.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(source)?, i as i32 * 10);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for tuple dependencies (2-8 dependencies)
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_two_dependencies() -> DagResult<()> {
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
//! Tests for different error types and error handling
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagError, DagRunner, TaskHandle};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_result_not_found_error() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
// Try to get result before running
|
||||
let result = dag.get(t1);
|
||||
assert!(matches!(result, Err(DagError::ResultNotFound { .. })));
|
||||
|
||||
// Run and verify it works after
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(t1).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_nested_result_error_handling() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Task that returns a Result
|
||||
let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Result::<i32, String>::Err("Internal error".to_string())
|
||||
}));
|
||||
|
||||
let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::<i32, String>::Ok(42)));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Both tasks complete successfully (they return Results)
|
||||
assert_eq!(dag.get(task1).unwrap(), Err("Internal error".to_string()));
|
||||
assert_eq!(dag.get(task2).unwrap(), Ok(42));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_option_none_not_error() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| None::<i32>));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// None is a valid result, not an error
|
||||
assert_eq!(dag.get(task).unwrap(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_error_types() {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum CustomError {
|
||||
ValidationError(String),
|
||||
ProcessingError { code: i32, details: String },
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Result::<i32, CustomError>::Err(CustomError::ValidationError("Invalid input".to_string()))
|
||||
}));
|
||||
|
||||
let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Result::<i32, CustomError>::Err(CustomError::ProcessingError {
|
||||
code: 500,
|
||||
details: "Processing failed".to_string(),
|
||||
})
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check custom errors are preserved
|
||||
match dag.get(task1).unwrap().as_ref() {
|
||||
Err(CustomError::ValidationError(msg)) => assert_eq!(msg, "Invalid input"),
|
||||
_ => panic!("Wrong error type"),
|
||||
}
|
||||
|
||||
match dag.get(task2).unwrap().as_ref() {
|
||||
Err(CustomError::ProcessingError { code, .. }) => assert_eq!(*code, 500),
|
||||
_ => panic!("Wrong error type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_in_tuple_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let ok_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
let err_task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("Error in dependency");
|
||||
}));
|
||||
|
||||
let dependent = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((ok_task, err_task));
|
||||
|
||||
let result = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(dag.get(dependent).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_string_panic_vs_structured_panic() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let string_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("String panic message");
|
||||
}));
|
||||
|
||||
let formatted_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("Formatted panic: value={}, code={}", 42, "ERROR");
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// Both should result in errors
|
||||
assert!(dag.get(string_panic).is_err());
|
||||
assert!(dag.get(formatted_panic).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_anyhow_like_error_chain() {
|
||||
// Test error chains with simple string-based errors
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
// Simulate an error chain with nested Results
|
||||
let _inner: Result<i32, String> = Err("Inner error".to_string());
|
||||
let outer: Result<Result<i32, String>, String> = Err("Outer error".to_string());
|
||||
|
||||
outer
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get the chained error
|
||||
let result = dag.get(task).unwrap();
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(e) = result.as_ref() {
|
||||
assert_eq!(e, "Outer error");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,5 @@
|
||||
//!
|
||||
//! Tests for error scenarios and recovery patterns.
|
||||
|
||||
mod error_types;
|
||||
mod propagation;
|
||||
mod recovery;
|
||||
+1
-178
@@ -1,9 +1,8 @@
|
||||
//! Tests for error propagation through the DAG
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
@@ -76,100 +75,6 @@ async fn test_diamond_error_propagation() {
|
||||
assert!(dag.get(sink).is_err()); // Fails due to right dependency
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_stops_at_boundary() {
|
||||
let dag = DagRunner::new();
|
||||
let execution_tracker = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Branch with error
|
||||
let error_source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>({
|
||||
let tracker = execution_tracker.clone();
|
||||
move |_: ()| -> i32 {
|
||||
let tracker = tracker.clone();
|
||||
tracker.fetch_add(1, Ordering::SeqCst);
|
||||
panic!("Error source");
|
||||
}
|
||||
}))
|
||||
.into();
|
||||
|
||||
let error_dependent = dag
|
||||
.add_task(task_fn::<i32, _, _>({
|
||||
let tracker = execution_tracker.clone();
|
||||
move |_: &i32| {
|
||||
let tracker = tracker.clone();
|
||||
tracker.fetch_add(100, Ordering::SeqCst); // Should not execute
|
||||
42
|
||||
}
|
||||
}))
|
||||
.depends_on(error_source);
|
||||
|
||||
// Independent branch
|
||||
let independent = dag.add_task(task_fn::<(), _, _>({
|
||||
let tracker = execution_tracker.clone();
|
||||
move |_: ()| {
|
||||
let tracker = tracker.clone();
|
||||
tracker.fetch_add(10, Ordering::SeqCst);
|
||||
10
|
||||
}
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// Error branch: source runs but dependent doesn't
|
||||
assert!(dag.get(error_source).is_err());
|
||||
assert!(dag.get(error_dependent).is_err());
|
||||
|
||||
// Independent branch runs
|
||||
assert_eq!(dag.get(independent).unwrap(), 10);
|
||||
|
||||
// Check execution: 1 (error_source) + 10 (independent) = 11
|
||||
assert_eq!(execution_tracker.load(Ordering::SeqCst), 11);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_error_sources_convergence() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Multiple error sources
|
||||
let error1 = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("Error source 1");
|
||||
}))
|
||||
.into();
|
||||
|
||||
let error2 = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("Error source 2");
|
||||
}))
|
||||
.into();
|
||||
|
||||
let success = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
// Convergence point depends on all three
|
||||
let convergence = dag
|
||||
.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
|(_, _, _): (&i32, &i32, &i32)| 100,
|
||||
))
|
||||
.depends_on((&error1, &error2, &success));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// All error sources fail
|
||||
assert!(dag.get(error1).is_err());
|
||||
assert!(dag.get(error2).is_err());
|
||||
|
||||
// Success task works
|
||||
assert_eq!(dag.get(success).unwrap(), 42);
|
||||
|
||||
// Convergence fails due to any error dependency
|
||||
assert!(dag.get(convergence).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_in_wide_fanout() {
|
||||
let dag = DagRunner::new();
|
||||
@@ -312,85 +217,3 @@ async fn test_error_propagation_timing() {
|
||||
assert!(order.contains(&"t2_start"));
|
||||
assert!(!order.contains(&"t3_should_not_run"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_partial_branch_failure_propagation() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// root
|
||||
// / | \
|
||||
// a1 b1 c1
|
||||
// | | |
|
||||
// a2 b2 c2
|
||||
// | (err) |
|
||||
// a3 b3 c3
|
||||
// \ | /
|
||||
// sink
|
||||
|
||||
let root: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
|
||||
|
||||
// Branch A
|
||||
let a1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(root);
|
||||
let a2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
|
||||
.depends_on(a1);
|
||||
let a3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 3))
|
||||
.depends_on(a2);
|
||||
|
||||
// Branch B (with error) - use Result instead of panic
|
||||
let b1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| Ok::<i32, &str>(x * 2)))
|
||||
.depends_on(root);
|
||||
let b2 = dag
|
||||
.add_task(task_fn::<Result<_, _>, _, _>(|_x: &Result<i32, &str>| {
|
||||
Err::<i32, &str>("Branch B fails at b2")
|
||||
}))
|
||||
.depends_on(b1);
|
||||
let b3 = dag
|
||||
.add_task(task_fn::<Result<_, _>, _, _>(|x: &Result<i32, &str>| {
|
||||
x.map(|v| v * 3)
|
||||
}))
|
||||
.depends_on(b2);
|
||||
|
||||
// Branch C
|
||||
let c1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 1))
|
||||
.depends_on(root);
|
||||
let c2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 2))
|
||||
.depends_on(c1);
|
||||
let c3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 3))
|
||||
.depends_on(c2);
|
||||
|
||||
// Sink depends on all branches - handle Result from B
|
||||
let sink = dag
|
||||
.add_task(task_fn::<(i32, Result<_, _>, _), _, _>(
|
||||
|(a, b, c): (&i32, &Result<i32, &str>, &i32)| match b {
|
||||
Ok(b_val) => Ok(a + b_val + c),
|
||||
Err(e) => Err(*e),
|
||||
},
|
||||
))
|
||||
.depends_on((&a3, &b3, &c3));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Branch A succeeds
|
||||
assert_eq!(dag.get(a3).unwrap(), 106);
|
||||
|
||||
// Branch B fails at b2
|
||||
assert_eq!(dag.get(b1).unwrap(), Ok(200));
|
||||
assert_eq!(dag.get(b2).unwrap(), Err("Branch B fails at b2"));
|
||||
assert_eq!(dag.get(b3).unwrap(), Err("Branch B fails at b2")); // Propagated error
|
||||
|
||||
// Branch C succeeds
|
||||
assert_eq!(dag.get(c3).unwrap(), 94);
|
||||
|
||||
// Sink fails due to branch B
|
||||
assert_eq!(dag.get(sink).unwrap(), Err("Branch B fails at b2"));
|
||||
}
|
||||
+1
-129
@@ -1,42 +1,11 @@
|
||||
//! Tests for error recovery patterns
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fallback_on_error() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Primary task that fails
|
||||
let primary = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("Primary task failed");
|
||||
#[allow(unreachable_code)]
|
||||
42
|
||||
}));
|
||||
|
||||
// Fallback task
|
||||
let fallback = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
|
||||
// Selector that uses fallback if primary fails
|
||||
let _selector = dag.add_task(task_fn::<(), _, _>(|_: ()| {}));
|
||||
|
||||
// Run DAG
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// Primary should fail
|
||||
assert!(dag.get(primary).is_err());
|
||||
|
||||
// Fallback should succeed
|
||||
assert_eq!(dag.get(fallback)?, 100);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_retry_pattern_simulation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
@@ -113,38 +82,6 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_partial_recovery_with_defaults() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Some tasks that might fail
|
||||
let _tasks: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
if i % 3 == 0 {
|
||||
panic!("Task {} failed", i);
|
||||
}
|
||||
i * 10
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Aggregator that handles failures gracefully
|
||||
let aggregator = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
// In real scenario, would check task results and use defaults
|
||||
vec![0, 10, 20, 0, 40, 50, 0, 70, 80, 0] // 0 for failed tasks
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
let result = dag.get(aggregator)?;
|
||||
assert_eq!(result.len(), 10);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
@@ -183,36 +120,6 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_graceful_degradation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Critical task
|
||||
let critical = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]));
|
||||
|
||||
// Optional enhancement that fails - returns Result
|
||||
let _enhancement = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Err::<Vec<i32>, &str>("Enhancement failed")
|
||||
}));
|
||||
|
||||
// Final task that works with what's available
|
||||
let final_task = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|base: &Vec<i32>| {
|
||||
// Would combine with enhancement if available
|
||||
// Here just uses base
|
||||
base.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(critical);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Critical path works
|
||||
assert_eq!(dag.get(final_task)?, 15);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_boundary_isolation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
@@ -248,38 +155,3 @@ async fn test_error_boundary_isolation() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compensation_action() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let compensation_triggered = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Action that fails
|
||||
let action = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
// Perform some action that fails
|
||||
panic!("Action failed");
|
||||
}));
|
||||
|
||||
// Compensation task (runs independently)
|
||||
let compensation = dag.add_task(task_fn::<(), _, _>({
|
||||
let triggered = compensation_triggered.clone();
|
||||
move |_: ()| {
|
||||
let triggered = triggered.clone();
|
||||
triggered.store(true, Ordering::SeqCst);
|
||||
"Compensation executed"
|
||||
}
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// Action failed
|
||||
assert!(dag.get(action).is_err());
|
||||
|
||||
// Compensation ran successfully
|
||||
assert_eq!(dag.get(compensation)?, "Compensation executed");
|
||||
assert!(compensation_triggered.load(Ordering::SeqCst));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
// Basic execution tests for DagRunner
|
||||
use crate::common::task_fn;
|
||||
|
||||
use dagx::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_dag() {
|
||||
let dag = DagRunner::new();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should complete without error
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(task).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(b);
|
||||
let d = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(c);
|
||||
let e = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(d);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Note: a, b, c, d are not sinks (they have dependents in the chain)
|
||||
// Only e is a sink and can be retrieved
|
||||
assert_eq!(dag.get(e).unwrap(), 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wide_parallel() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(a);
|
||||
let d = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
|
||||
.depends_on((&b, &c));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Note: a, b, and c are not sinks (they have dependents)
|
||||
// Only d is a sink and can be retrieved
|
||||
assert_eq!(dag.get(d).unwrap(), 35); // 15 + 20
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_different_output_types() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
|
||||
let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3]));
|
||||
let struct_task = dag.add_task(task_fn::<(), _, _>(|_: ()| User {
|
||||
name: "Alice".to_string(),
|
||||
age: 30,
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&dag.get(string_task).unwrap(), "hello");
|
||||
assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]);
|
||||
let user = dag.get(struct_task).unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
assert_eq!(user.age, 30);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_sinks() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
|
||||
// Branch 1
|
||||
let branch1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
let sink1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(branch1);
|
||||
|
||||
// Branch 2
|
||||
let branch2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(source);
|
||||
let sink2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(branch2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should wait for both sinks
|
||||
|
||||
assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1
|
||||
assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task_inline_path() {
|
||||
// Test single-task layer inline execution path (line 367-410 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Single task in its own layer
|
||||
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(t1);
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(t2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multi_task_spawn_path() {
|
||||
// Test multi-task layer spawn path (line 411-456 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
|
||||
|
||||
// Create multiple tasks in the same layer (all depend on source)
|
||||
let t1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(source);
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
|
||||
.depends_on(source);
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 3))
|
||||
.depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(t1).unwrap(), 101);
|
||||
assert_eq!(dag.get(t2).unwrap(), 102);
|
||||
assert_eq!(dag.get(t3).unwrap(), 103);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_producer_consumer_channels() {
|
||||
// Test channel creation and management (lines 301-337 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Producer
|
||||
let producer: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]))
|
||||
.into();
|
||||
|
||||
// Consumer
|
||||
let consumer = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
|
||||
data.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(producer);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(consumer).unwrap(), 15);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multi_consumer_fanout() {
|
||||
// Test fanout with multiple consumers from one producer
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let producer: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
// Multiple consumers
|
||||
let c1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(producer);
|
||||
let c2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 10))
|
||||
.depends_on(producer);
|
||||
let c3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
|
||||
.depends_on(producer);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(c1).unwrap(), 84);
|
||||
assert_eq!(dag.get(c2).unwrap(), 52);
|
||||
assert_eq!(dag.get(c3).unwrap(), 37);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compute_layers_with_dependents_tracking() {
|
||||
// Test layer computation with dependent tracking (lines 590-600 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create diamond pattern to ensure dependents are tracked correctly
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
|
||||
|
||||
let left = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(source);
|
||||
let right = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
|
||||
.depends_on(source);
|
||||
|
||||
let sink = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
|
||||
.depends_on((&left, &right));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sink).unwrap(), 5); // (1+1) + (1+2) = 5
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layer_execution_order() {
|
||||
// Test that layers execute in correct topological order
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
let execution_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let order1 = execution_order.clone();
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let order = order1.clone();
|
||||
order.lock().push(1);
|
||||
1
|
||||
}));
|
||||
|
||||
let order2 = execution_order.clone();
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order2.clone();
|
||||
order.lock().push(2);
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(t1);
|
||||
|
||||
let order3 = execution_order.clone();
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order3.clone();
|
||||
order.lock().push(3);
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(t2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let order = execution_order.lock().clone();
|
||||
assert_eq!(order, vec![1, 2, 3]);
|
||||
assert_eq!(dag.get(t3).unwrap(), 3);
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
//! Integration tests for end-to-end functionality
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, Task};
|
||||
|
||||
use std::time::Instant;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
// === Shared Test Tasks ===
|
||||
|
||||
struct LoadValue {
|
||||
value: i32,
|
||||
}
|
||||
impl LoadValue {
|
||||
fn new(value: i32) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
#[dagx::task]
|
||||
impl LoadValue {
|
||||
async fn run(&self) -> i32 {
|
||||
self.value // Read-only state: use &self
|
||||
}
|
||||
}
|
||||
|
||||
struct Multiply {
|
||||
factor: i32,
|
||||
}
|
||||
impl Multiply {
|
||||
fn new(factor: i32) -> Self {
|
||||
Self { factor }
|
||||
}
|
||||
}
|
||||
#[dagx::task]
|
||||
impl Multiply {
|
||||
async fn run(&self, input: &i32) -> i32 {
|
||||
input * self.factor // Read-only state: use &self
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b // Stateless: no self needed
|
||||
}
|
||||
}
|
||||
|
||||
// === Integration Tests ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_etl_pipeline() -> DagResult<()> {
|
||||
// Simulate an Extract-Transform-Load pipeline
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Extract: Load data from sources
|
||||
let source1 = dag.add_task(LoadValue::new(10));
|
||||
let source2 = dag.add_task(LoadValue::new(20));
|
||||
let source3 = dag.add_task(LoadValue::new(30));
|
||||
|
||||
// Transform: Process each source
|
||||
let transform1 = dag.add_task(Multiply::new(2)).depends_on(source1);
|
||||
let transform2 = dag.add_task(Multiply::new(3)).depends_on(source2);
|
||||
let transform3 = dag.add_task(Multiply::new(4)).depends_on(source3);
|
||||
|
||||
// Load: Aggregate results
|
||||
let combine12 = dag.add_task(Add).depends_on((&transform1, &transform2));
|
||||
let final_result = dag.add_task(Add).depends_on((&combine12, &transform3));
|
||||
|
||||
// Execute pipeline
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify
|
||||
let result = dag.get(final_result)?;
|
||||
assert_eq!(result, (10 * 2) + (20 * 3) + (30 * 4)); // 20 + 60 + 120 = 200
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_multi_layer_dag() -> DagResult<()> {
|
||||
// Complex multi-layer DAG
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Layer 1: Sources
|
||||
let a = dag.add_task(LoadValue::new(1));
|
||||
let b = dag.add_task(LoadValue::new(2));
|
||||
let c = dag.add_task(LoadValue::new(3));
|
||||
|
||||
// Layer 2: First transformations
|
||||
let ab = dag.add_task(Add).depends_on((&a, &b));
|
||||
let bc = dag.add_task(Add).depends_on((&b, &c));
|
||||
|
||||
// Layer 3: Second transformations
|
||||
let double_ab = dag.add_task(Multiply::new(2)).depends_on(ab);
|
||||
let double_bc = dag.add_task(Multiply::new(2)).depends_on(bc);
|
||||
|
||||
// Layer 4: Final result
|
||||
let final_result = dag.add_task(Add).depends_on((&double_ab, &double_bc));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(final_result)?;
|
||||
// (1+2)*2 + (2+3)*2 = 6 + 10 = 16
|
||||
assert_eq!(result, 16);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_multiple_sinks() -> DagResult<()> {
|
||||
// DAG with multiple output sinks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(LoadValue::new(10));
|
||||
|
||||
// Multiple independent branches
|
||||
let branch1 = dag.add_task(Multiply::new(2)).depends_on(source);
|
||||
let branch2 = dag.add_task(Multiply::new(3)).depends_on(source);
|
||||
let branch3 = dag.add_task(Multiply::new(4)).depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// All branches should complete
|
||||
assert_eq!(dag.get(branch1)?, 20);
|
||||
assert_eq!(dag.get(branch2)?, 30);
|
||||
assert_eq!(dag.get(branch3)?, 40);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_diamond_cascade() -> DagResult<()> {
|
||||
// Multiple diamond patterns in sequence
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Diamond 1
|
||||
let source1 = dag.add_task(LoadValue::new(10));
|
||||
let left1 = dag.add_task(Multiply::new(2)).depends_on(source1);
|
||||
let right1 = dag.add_task(Multiply::new(3)).depends_on(source1);
|
||||
let sink1 = dag.add_task(Add).depends_on((&left1, &right1));
|
||||
|
||||
// Diamond 2 (depends on Diamond 1)
|
||||
let left2 = dag.add_task(Multiply::new(2)).depends_on(sink1);
|
||||
let right2 = dag.add_task(Multiply::new(3)).depends_on(sink1);
|
||||
let sink2 = dag.add_task(Add).depends_on((&left2, &right2));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(sink2)?;
|
||||
// Diamond 1: (10*2) + (10*3) = 50
|
||||
// Diamond 2: (50*2) + (50*3) = 250
|
||||
assert_eq!(result, 250);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_wide_and_deep() -> DagResult<()> {
|
||||
// Combination of wide (parallel) and deep (sequential) patterns
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Wide: 5 parallel sources
|
||||
let sources: Vec<_> = (0..5)
|
||||
.map(|i| dag.add_task(LoadValue::new(i * 10)))
|
||||
.collect();
|
||||
|
||||
// Deep: Chain each source through 3 transformations
|
||||
let mut final_nodes = Vec::new();
|
||||
for source in &sources {
|
||||
let step1 = dag.add_task(Multiply::new(2)).depends_on(source);
|
||||
let step2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(step1);
|
||||
let step3 = dag.add_task(Multiply::new(3)).depends_on(step2);
|
||||
final_nodes.push(step3);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify each chain
|
||||
for (i, node) in final_nodes.iter().enumerate() {
|
||||
let expected = ((i as i32 * 10) * 2 + 5) * 3;
|
||||
assert_eq!(dag.get(node)?, expected);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_error_recovery() -> DagResult<()> {
|
||||
// Test that DAG handles normal execution gracefully
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(LoadValue::new(42));
|
||||
let transform = dag.add_task(Multiply::new(2)).depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Should be able to access results
|
||||
assert_eq!(dag.get(transform)?, 84);
|
||||
|
||||
// Getting valid task should return ok
|
||||
let result = dag.get(transform);
|
||||
assert!(result.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_large_scale() -> DagResult<()> {
|
||||
// Large-scale integration test
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 100 sources
|
||||
let sources: Vec<_> = (0..100).map(|i| dag.add_task(LoadValue::new(i))).collect();
|
||||
|
||||
// Create 50 aggregation tasks (each combines 2 sources)
|
||||
let aggregations: Vec<_> = (0..50)
|
||||
.map(|i| {
|
||||
dag.add_task(Add)
|
||||
.depends_on((&sources[i * 2], &sources[i * 2 + 1]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create 25 second-level aggregations
|
||||
let second_level: Vec<_> = (0..25)
|
||||
.map(|i| {
|
||||
dag.add_task(Add)
|
||||
.depends_on((&aggregations[i * 2], &aggregations[i * 2 + 1]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify some results
|
||||
assert_eq!(dag.get(aggregations[0])?, 1); // First two
|
||||
assert_eq!(dag.get(second_level[0])?, 1 + (2 + 3)); // First four
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_reused_values() -> DagResult<()> {
|
||||
// Test that a single source can be used by multiple consumers
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(LoadValue::new(100));
|
||||
|
||||
// Use source in multiple different ways
|
||||
let double = dag.add_task(Multiply::new(2)).depends_on(source);
|
||||
let triple = dag.add_task(Multiply::new(3)).depends_on(source);
|
||||
let quadruple = dag.add_task(Multiply::new(4)).depends_on(source);
|
||||
|
||||
// Combine some of the results
|
||||
let combined = dag.add_task(Add).depends_on((&double, &triple));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(double)?, 200);
|
||||
assert_eq!(dag.get(triple)?, 300);
|
||||
assert_eq!(dag.get(quadruple)?, 400);
|
||||
assert_eq!(dag.get(combined)?, 500);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_parallel_execution_timing() -> DagResult<()> {
|
||||
// Test that independent tasks actually run in parallel by measuring execution time
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let sleep_duration = Duration::from_millis(100);
|
||||
let num_parallel_tasks = 5;
|
||||
|
||||
// Create 5 independent tasks that each sleep for 100ms
|
||||
let tasks: Vec<_> = (0..num_parallel_tasks)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
sleep(sleep_duration).await;
|
||||
i
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Verify all tasks completed
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
}
|
||||
|
||||
// If tasks ran sequentially, it would take 5 * 100ms = 500ms
|
||||
// With parallelism, it should take ~100ms (plus overhead)
|
||||
// We'll check it's less than 300ms (generous margin for CI environments)
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(300),
|
||||
"Parallel execution took {:?}, expected < 300ms (sequential would be 500ms)",
|
||||
elapsed
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> {
|
||||
// Test mixed parallel and sequential execution patterns
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let sleep_duration = Duration::from_millis(50);
|
||||
|
||||
// Layer 1: 4 parallel tasks (should take ~50ms)
|
||||
let parallel_tasks: Vec<_> = (0..4)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
sleep(sleep_duration).await;
|
||||
i * 10
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: Sequential chain of 3 tasks depending on the first parallel task
|
||||
// (should add ~150ms)
|
||||
let seq1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
sleep(sleep_duration).await;
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(parallel_tasks[0]);
|
||||
|
||||
let seq2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
sleep(sleep_duration).await;
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(seq1);
|
||||
|
||||
let seq3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
sleep(sleep_duration).await;
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(seq2);
|
||||
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Verify results
|
||||
assert_eq!(dag.get(seq3)?, 3); // 0 + 1 + 1 + 1
|
||||
|
||||
// Expected time: ~200ms (4 layers of 50ms each: parallel layer + 3 sequential)
|
||||
// If everything ran sequentially: 7 * 50ms = 350ms
|
||||
// We'll check it's less than 300ms
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(300),
|
||||
"Execution took {:?}, expected < 300ms (fully sequential would be 350ms)",
|
||||
elapsed
|
||||
);
|
||||
|
||||
// Also verify it took more than just the parallel layer
|
||||
assert!(
|
||||
elapsed >= Duration::from_millis(150),
|
||||
"Execution took {:?}, expected >= 150ms (should include sequential chain)",
|
||||
elapsed
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+168
-3
@@ -1,4 +1,169 @@
|
||||
// Test module for execution tests
|
||||
// Basic execution tests for DagRunner
|
||||
use dagx_test::task_fn;
|
||||
|
||||
mod basic;
|
||||
mod results;
|
||||
use dagx::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_dag() {
|
||||
let dag = DagRunner::new();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should complete without error
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(task).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wide_parallel() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(a);
|
||||
let d = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
|
||||
.depends_on((&b, &c));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Note: a, b, and c are not sinks (they have dependents)
|
||||
// Only d is a sink and can be retrieved
|
||||
assert_eq!(dag.get(d).unwrap(), 35); // 15 + 20
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_different_output_types() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
|
||||
let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3]));
|
||||
let struct_task = dag.add_task(task_fn::<(), _, _>(|_: ()| User {
|
||||
name: "Alice".to_string(),
|
||||
age: 30,
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&dag.get(string_task).unwrap(), "hello");
|
||||
assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]);
|
||||
let user = dag.get(struct_task).unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
assert_eq!(user.age, 30);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_sinks() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
|
||||
// Branch 1
|
||||
let branch1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
let sink1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(branch1);
|
||||
|
||||
// Branch 2
|
||||
let branch2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(source);
|
||||
let sink2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(branch2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should wait for both sinks
|
||||
|
||||
assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1
|
||||
assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task_inline_path() {
|
||||
// Test single-task layer inline execution path (line 367-410 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Single task in its own layer
|
||||
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(t1);
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(t2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multi_consumer_fanout() {
|
||||
// Test fanout with multiple consumers from one producer
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let producer: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
// Multiple consumers
|
||||
let c1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(producer);
|
||||
let c2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 10))
|
||||
.depends_on(producer);
|
||||
let c3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
|
||||
.depends_on(producer);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(c1).unwrap(), 84);
|
||||
assert_eq!(dag.get(c2).unwrap(), 52);
|
||||
assert_eq!(dag.get(c3).unwrap(), 37);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// Result and error handling tests for DagRunner
|
||||
use crate::common::task_fn;
|
||||
|
||||
use crate::common::tasks::Counter;
|
||||
use dagx::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_result_type() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let success_task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::<i32, String>(42)));
|
||||
let error_task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Err::<i32, String>("error".to_string())
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(success_task).unwrap(), Ok(42));
|
||||
assert_eq!(dag.get(error_task).unwrap(), Err("error".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dag_result_type_works() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
|
||||
// Test that DagResult<()> works with ? operator pattern
|
||||
async fn run_dag(dag: &DagRunner) -> DagResult<()> {
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let result = run_dag(&dag).await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(dag.get(task).unwrap(), 100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_not_executed_error() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
// Try to get result before running - should error
|
||||
let result = dag.get(task);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(DagError::ResultNotFound { task_id }) => {
|
||||
assert_eq!(task_id, 0); // First task has ID 0
|
||||
}
|
||||
_ => panic!("Expected ResultNotFound error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_messages_are_helpful() {
|
||||
// Test ResultNotFound error message
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
let result = dag.get(task);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{}", result.unwrap_err());
|
||||
assert!(err_msg.contains("Result not found"));
|
||||
assert!(err_msg.contains("task #0")); // Task ID
|
||||
assert!(err_msg.contains("Call dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_successful_execution_returns_ok() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
// Should return Ok(())
|
||||
let result = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Should be able to get value
|
||||
assert_eq!(dag.get(task).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateful_tasks() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let counter = dag.add_task(Counter::new(10)).depends_on(input);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(counter).unwrap(), 15); // 10 + 5
|
||||
}
|
||||
|
||||
// Cycle detection test removed - it required access to private fields
|
||||
// The public API correctly prevents creating cycles, so this is tested implicitly
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_captured_state() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let multiplier = 7;
|
||||
let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let scaled = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * multiplier))
|
||||
.depends_on(base);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(scaled).unwrap(), 35);
|
||||
}
|
||||
@@ -4,18 +4,11 @@
|
||||
#![allow(clippy::needless_borrow)]
|
||||
#![allow(clippy::redundant_clone)]
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
mod boundaries;
|
||||
mod dependencies;
|
||||
mod errors;
|
||||
mod execution;
|
||||
mod parallelism;
|
||||
mod patterns;
|
||||
mod performance;
|
||||
mod runtimes;
|
||||
mod task_patterns;
|
||||
mod timing;
|
||||
mod tracing;
|
||||
mod type_safety;
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Concurrent access tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
//! Tests for execution correctness and parallelism
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||
// Test that tasks in the same topological layer execute in parallel
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let layer_timing = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct Layer0Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer0Task {
|
||||
async fn run(&self) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L0_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
self.idx * 10
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 0: 3 independent sources
|
||||
let sources: Vec<_> = (0..3)
|
||||
.map(|i| {
|
||||
dag.add_task(Layer0Task {
|
||||
timing: layer_timing.clone(),
|
||||
idx: i,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
struct Layer1Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer1Task {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L1_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 1: Tasks depending on layer 0
|
||||
let layer1: Vec<_> = sources
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, source)| {
|
||||
dag.add_task(Layer1Task {
|
||||
idx: i,
|
||||
timing: layer_timing.clone(),
|
||||
})
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: Final aggregation
|
||||
let final_task = {
|
||||
let timing = layer_timing.clone();
|
||||
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
move |(a, b, c): (&i32, &i32, &i32)| {
|
||||
let timing = timing.clone();
|
||||
let start = Instant::now();
|
||||
timing.lock().unwrap().push(("L2_final".to_string(), start));
|
||||
a + b + c
|
||||
},
|
||||
))
|
||||
.depends_on((&layer1[0], &layer1[1], &layer1[2]))
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1)
|
||||
|
||||
// Analyze timing
|
||||
let timing = layer_timing.lock().unwrap();
|
||||
|
||||
// Get layer 0 start times
|
||||
let l0_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L0_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Get layer 1 start times
|
||||
let l1_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L1_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 0 tasks should all start nearly simultaneously
|
||||
let l0_spread = l0_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l0_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l0_spread < Duration::from_millis(5),
|
||||
"Layer 0 tasks should start together, spread was {:?}",
|
||||
l0_spread
|
||||
);
|
||||
|
||||
// Layer 1 tasks should also start nearly simultaneously (after layer 0 completes)
|
||||
let l1_spread = l1_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l1_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l1_spread < Duration::from_millis(5),
|
||||
"Layer 1 tasks should start together, spread was {:?}",
|
||||
l1_spread
|
||||
);
|
||||
|
||||
// Layer 1 should start after layer 0
|
||||
let earliest_l1 = *l1_times.iter().min().unwrap();
|
||||
let earliest_l0 = *l0_times.iter().min().unwrap();
|
||||
assert!(
|
||||
earliest_l1 >= earliest_l0 + Duration::from_millis(15),
|
||||
"Layer 1 should start after layer 0 completes"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_completion_order_matches_dependencies() -> DagResult<()> {
|
||||
// Verify that tasks complete in dependency order
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let completion_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct ATask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ATask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
self.order.lock().unwrap().push("A");
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
// Create a complex DAG with clear ordering requirements
|
||||
let a = dag.add_task(ATask {
|
||||
order: completion_order.clone(),
|
||||
});
|
||||
|
||||
struct BTask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl BTask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
self.order.lock().unwrap().push("B");
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
let b = dag.add_task(BTask {
|
||||
order: completion_order.clone(),
|
||||
});
|
||||
|
||||
let c = {
|
||||
let order = completion_order.clone();
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| {
|
||||
let order = order.clone();
|
||||
order.lock().unwrap().push("C");
|
||||
x + y
|
||||
}))
|
||||
.depends_on((a, b))
|
||||
};
|
||||
|
||||
let d = {
|
||||
let order = completion_order.clone();
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order.clone();
|
||||
order.lock().unwrap().push("D");
|
||||
x * 2
|
||||
}))
|
||||
.depends_on(c)
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(d)?, 6); // (1 + 2) * 2
|
||||
|
||||
// Check completion order
|
||||
let order = completion_order.lock().unwrap();
|
||||
|
||||
// A and B can complete in any order (parallel)
|
||||
assert!(order.contains(&"A"));
|
||||
assert!(order.contains(&"B"));
|
||||
|
||||
// C must come after both A and B
|
||||
let c_pos = order.iter().position(|name| *name == "C").unwrap();
|
||||
let a_pos = order.iter().position(|name| *name == "A").unwrap();
|
||||
let b_pos = order.iter().position(|name| *name == "B").unwrap();
|
||||
assert!(c_pos > a_pos, "C should complete after A");
|
||||
assert!(c_pos > b_pos, "C should complete after B");
|
||||
|
||||
// D must come after C
|
||||
let d_pos = order.iter().position(|name| *name == "D").unwrap();
|
||||
assert!(d_pos > c_pos, "D should complete after C");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
|
||||
// Prove that dependent tasks wait for their dependencies
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let execution_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct ATask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ATask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
self.order.lock().unwrap().push("A");
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
// Create a chain: A -> B -> C -> D
|
||||
let a: TaskHandle<_> = dag
|
||||
.add_task(ATask {
|
||||
order: execution_order.clone(),
|
||||
})
|
||||
.into();
|
||||
|
||||
struct BTask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl BTask {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.order.lock().unwrap().push("B");
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let b = dag
|
||||
.add_task(BTask {
|
||||
order: execution_order.clone(),
|
||||
})
|
||||
.depends_on(a);
|
||||
|
||||
struct CTask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl CTask {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.order.lock().unwrap().push("C");
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let c = dag
|
||||
.add_task(CTask {
|
||||
order: execution_order.clone(),
|
||||
})
|
||||
.depends_on(b);
|
||||
|
||||
let d = {
|
||||
let order = execution_order.clone();
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order.clone();
|
||||
order.lock().unwrap().push("D");
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(c)
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify results
|
||||
assert_eq!(dag.get(a)?, 1);
|
||||
assert_eq!(dag.get(b)?, 2);
|
||||
assert_eq!(dag.get(c)?, 3);
|
||||
assert_eq!(dag.get(d)?, 4);
|
||||
|
||||
// Verify execution order
|
||||
let order = execution_order.lock().unwrap();
|
||||
let expected = vec!["A", "B", "C", "D"];
|
||||
assert_eq!(order.as_slice(), expected.as_slice());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Parallelism tests module
|
||||
|
||||
pub mod concurrent;
|
||||
pub mod execution;
|
||||
pub mod spawning;
|
||||
pub mod threads;
|
||||
pub mod timing;
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for spawner functionality
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,81 +1,13 @@
|
||||
//! Tests for multi-threading and concurrent execution
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::ThreadId;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tasks_run_on_different_threads() -> DagResult<()> {
|
||||
// Prove that tasks CAN run on multiple different threads (true parallelism capability)
|
||||
// Note: Tokio may choose to run fast tasks on the same thread for efficiency
|
||||
use std::thread;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let thread_ids = Arc::new(Mutex::new(std::collections::HashSet::new()));
|
||||
|
||||
struct LayerTask {
|
||||
ids: Arc<Mutex<HashSet<ThreadId>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl LayerTask {
|
||||
async fn run(&self) -> i32 {
|
||||
let tid = thread::current().id();
|
||||
self.ids.lock().unwrap().insert(tid);
|
||||
|
||||
// Yield to give tokio reason to distribute
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
|
||||
// Some CPU work too
|
||||
let mut sum = 0;
|
||||
for j in 0..1000 {
|
||||
sum += j;
|
||||
}
|
||||
self.idx + sum
|
||||
}
|
||||
}
|
||||
|
||||
// Create 50 tasks with actual work to encourage multi-threading
|
||||
let tasks: Vec<_> = (0..50)
|
||||
.map(|i| {
|
||||
dag.add_task(LayerTask {
|
||||
ids: thread_ids.clone(),
|
||||
idx: i,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all tasks completed
|
||||
for task in tasks.into_iter() {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
// With a multi-threaded runtime and sufficient work, we typically see multiple threads
|
||||
// But we can't guarantee it (scheduler's choice), so we just verify the capability exists
|
||||
let unique_threads = thread_ids.lock().unwrap().len();
|
||||
|
||||
// On a multi-core system with sufficient work, tokio should use multiple threads
|
||||
// This is a soft assertion - if it fails, it means tasks CAN'T use multiple threads
|
||||
if unique_threads == 1 {
|
||||
eprintln!(
|
||||
"Warning: All tasks ran on 1 thread. This may indicate parallelism isn't working, \
|
||||
or tokio chose not to distribute work (valid for small/fast tasks)"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||
// Prove concurrent execution using atomic counters to track simultaneous tasks
|
||||
@@ -155,73 +87,6 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_parallel_execution() -> DagResult<()> {
|
||||
// Test diamond pattern: source -> (parallel1, parallel2) -> sink
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let concurrent_middle = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
|
||||
|
||||
struct ParallelTask1 {
|
||||
counter: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ParallelTask1 {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.counter.fetch_add(1, Ordering::SeqCst);
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
let count = self.counter.load(Ordering::SeqCst);
|
||||
self.counter.fetch_sub(1, Ordering::SeqCst);
|
||||
assert!(count > 0, "Should see concurrent execution");
|
||||
x * 2
|
||||
}
|
||||
}
|
||||
|
||||
// These should run in parallel
|
||||
let parallel1 = dag
|
||||
.add_task(ParallelTask1 {
|
||||
counter: concurrent_middle.clone(),
|
||||
})
|
||||
.depends_on(source);
|
||||
|
||||
struct ParallelTask2 {
|
||||
counter: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ParallelTask2 {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.counter.fetch_add(1, Ordering::SeqCst);
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
let count = self.counter.load(Ordering::SeqCst);
|
||||
self.counter.fetch_sub(1, Ordering::SeqCst);
|
||||
// Both should be running
|
||||
assert!(count > 0, "Should see concurrent execution");
|
||||
x * 3
|
||||
}
|
||||
}
|
||||
|
||||
let parallel2 = dag
|
||||
.add_task(ParallelTask2 {
|
||||
counter: concurrent_middle.clone(),
|
||||
})
|
||||
.depends_on(source);
|
||||
|
||||
let sink = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((¶llel1, ¶llel2));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sink)?, 500); // (100 * 2) + (100 * 3)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_massive_parallel_fanout() -> DagResult<()> {
|
||||
// Test extreme parallelism with 1000 parallel tasks
|
||||
|
||||
+151
-2
@@ -1,8 +1,12 @@
|
||||
//! Tests for timing and speedup verification
|
||||
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::time::sleep;
|
||||
|
||||
struct SleepTask(usize, Duration);
|
||||
@@ -49,7 +53,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
|
||||
// Parallel execution should be significantly faster
|
||||
assert!(
|
||||
elapsed < sequential_time / 2,
|
||||
elapsed < sequential_time / 4,
|
||||
"Parallel execution too slow: {:?} vs sequential {:?}",
|
||||
elapsed,
|
||||
sequential_time
|
||||
@@ -57,3 +61,148 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||
// Test that tasks in the same topological layer execute in parallel
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let layer_timing = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct Layer0Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer0Task {
|
||||
async fn run(&self) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L0_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
self.idx * 10
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 0: 3 independent sources
|
||||
let sources: Vec<_> = (0..3)
|
||||
.map(|i| {
|
||||
dag.add_task(Layer0Task {
|
||||
timing: layer_timing.clone(),
|
||||
idx: i,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
struct Layer1Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer1Task {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L1_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 1: Tasks depending on layer 0
|
||||
let layer1: Vec<_> = sources
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, source)| {
|
||||
dag.add_task(Layer1Task {
|
||||
idx: i,
|
||||
timing: layer_timing.clone(),
|
||||
})
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: Final aggregation
|
||||
let final_task = {
|
||||
let timing = layer_timing.clone();
|
||||
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
move |(a, b, c): (&i32, &i32, &i32)| {
|
||||
let timing = timing.clone();
|
||||
let start = Instant::now();
|
||||
timing.lock().unwrap().push(("L2_final".to_string(), start));
|
||||
a + b + c
|
||||
},
|
||||
))
|
||||
.depends_on((&layer1[0], &layer1[1], &layer1[2]))
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1)
|
||||
|
||||
// Analyze timing
|
||||
let timing = layer_timing.lock().unwrap();
|
||||
|
||||
// Get layer 0 start times
|
||||
let l0_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L0_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Get layer 1 start times
|
||||
let l1_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L1_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 0 tasks should all start nearly simultaneously
|
||||
let l0_spread = l0_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l0_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l0_spread < Duration::from_millis(5),
|
||||
"Layer 0 tasks should start together, spread was {:?}",
|
||||
l0_spread
|
||||
);
|
||||
|
||||
// Layer 1 tasks should also start nearly simultaneously (after layer 0 completes)
|
||||
let l1_spread = l1_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l1_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l1_spread < Duration::from_millis(5),
|
||||
"Layer 1 tasks should start together, spread was {:?}",
|
||||
l1_spread
|
||||
);
|
||||
|
||||
// Layer 1 should start after layer 0
|
||||
let earliest_l1 = *l1_times.iter().min().unwrap();
|
||||
let earliest_l0 = *l0_times.iter().min().unwrap();
|
||||
assert!(
|
||||
earliest_l1 >= earliest_l0 + Duration::from_millis(15),
|
||||
"Layer 1 should start after layer 0 completes"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
//! Butterfly pattern DAG tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_butterfly_network() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Simplified butterfly network - avoid tuples entirely
|
||||
// Layer 0: inputs
|
||||
let inputs: Vec<_> = (0..8)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i as f64))
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 1: butterfly operations (compute pairs separately)
|
||||
let mut layer1 = Vec::new();
|
||||
for i in 0..4 {
|
||||
let idx1 = i * 2;
|
||||
let idx2 = i * 2 + 1;
|
||||
// Sum operation
|
||||
layer1.push(
|
||||
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| {
|
||||
(a + b) / 2.0
|
||||
}))
|
||||
.depends_on((&inputs[idx1], &inputs[idx2])),
|
||||
);
|
||||
// Difference operation
|
||||
layer1.push(
|
||||
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| {
|
||||
(a - b) / 2.0
|
||||
}))
|
||||
.depends_on((&inputs[idx1], &inputs[idx2])),
|
||||
);
|
||||
}
|
||||
|
||||
// Layer 2: crossover connections
|
||||
let layer2: Vec<_> = vec![
|
||||
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b))
|
||||
.depends_on((&layer1[0], &layer1[4])),
|
||||
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b))
|
||||
.depends_on((&layer1[1], &layer1[5])),
|
||||
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b))
|
||||
.depends_on((&layer1[2], &layer1[6])),
|
||||
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b))
|
||||
.depends_on((&layer1[3], &layer1[7])),
|
||||
];
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify some outputs exist
|
||||
for task in &layer2 {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recursive_butterfly() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
fn build_butterfly(
|
||||
dag: &DagRunner,
|
||||
inputs: Vec<dagx::TaskHandle<i32>>,
|
||||
) -> Vec<dagx::TaskHandle<i32>> {
|
||||
if inputs.len() <= 2 {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
let mid = inputs.len() / 2;
|
||||
let mut outputs = Vec::new();
|
||||
|
||||
// Butterfly connections
|
||||
for i in 0..mid {
|
||||
let sum = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((&inputs[i], &inputs[i + mid]));
|
||||
|
||||
let diff = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| {
|
||||
(a - b).abs()
|
||||
}))
|
||||
.depends_on((&inputs[i], &inputs[i + mid]));
|
||||
|
||||
outputs.push(sum);
|
||||
outputs.push(diff);
|
||||
}
|
||||
|
||||
outputs
|
||||
}
|
||||
|
||||
// Create 4 inputs
|
||||
let inputs: Vec<dagx::TaskHandle<i32>> = vec![10, 20, 30, 40]
|
||||
.into_iter()
|
||||
.map(|val| {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| val));
|
||||
task.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let outputs = build_butterfly(&dag, inputs);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(outputs.len(), 4);
|
||||
for output in outputs {
|
||||
let _ = dag.get(output)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_benes_network() -> DagResult<()> {
|
||||
// Simplified Benes network - avoid tuples
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// 4 inputs
|
||||
let inputs: Vec<_> = (0..4)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i))
|
||||
.into()
|
||||
}) // Powers of 2
|
||||
.collect();
|
||||
|
||||
// First stage: sort pairs
|
||||
let stage1: Vec<_> = vec![
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
|
||||
a.min(b)
|
||||
}))
|
||||
.depends_on((&inputs[0], &inputs[1])),
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
|
||||
a.max(b)
|
||||
}))
|
||||
.depends_on((&inputs[0], &inputs[1])),
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
|
||||
a.min(b)
|
||||
}))
|
||||
.depends_on((&inputs[2], &inputs[3])),
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
|
||||
a.max(b)
|
||||
}))
|
||||
.depends_on((&inputs[2], &inputs[3])),
|
||||
];
|
||||
|
||||
// Second stage with crossover
|
||||
let outputs: Vec<_> = vec![
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b))
|
||||
.depends_on((&stage1[0], &stage1[2])),
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b))
|
||||
.depends_on((&stage1[1], &stage1[3])),
|
||||
];
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
for output in outputs {
|
||||
let _ = dag.get(output)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_shuffle_exchange_network() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create 8 inputs
|
||||
let inputs: Vec<TaskHandle<_>> = (0..8)
|
||||
.map(|i| {
|
||||
let counter = counter.clone();
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
i
|
||||
}))
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Shuffle stage: bit rotation
|
||||
let shuffled: Vec<_> = (0..8)
|
||||
.map(|i| {
|
||||
// Shuffle pattern: rotate bits
|
||||
let source = ((i << 1) | (i >> 2)) & 0x7;
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x))
|
||||
.depends_on(inputs[source])
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Exchange stage: adjacent pairs
|
||||
let exchanged: Vec<_> = (0..4)
|
||||
.flat_map(|i| {
|
||||
let idx = i * 2;
|
||||
vec![
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(
|
||||
|(&a, &b): (&i32, &i32)| if a > b { b } else { a },
|
||||
))
|
||||
.depends_on((&shuffled[idx], &shuffled[idx + 1])),
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(
|
||||
|(&a, &b): (&i32, &i32)| if a > b { a } else { b },
|
||||
))
|
||||
.depends_on((&shuffled[idx], &shuffled[idx + 1])),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 8);
|
||||
|
||||
for task in exchanged {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
//! Complex DAG pattern tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workflow_with_conditional_paths() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Input validation
|
||||
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
let validate = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| {
|
||||
if x > 0 && x < 100 {
|
||||
Ok(x)
|
||||
} else {
|
||||
Err("Invalid input")
|
||||
}
|
||||
}))
|
||||
.depends_on(input);
|
||||
|
||||
// Process valid input
|
||||
let process = dag
|
||||
.add_task(task_fn::<Result<_, _>, _, _>(
|
||||
|result: &Result<i32, &str>| match result {
|
||||
Ok(x) => *x * 2,
|
||||
Err(_) => -1,
|
||||
},
|
||||
))
|
||||
.depends_on(validate);
|
||||
|
||||
// Parallel enhancement steps
|
||||
let enhance1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 10))
|
||||
.depends_on(process);
|
||||
|
||||
let enhance2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(process);
|
||||
|
||||
let enhance3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
|
||||
.depends_on(process);
|
||||
|
||||
// Combine enhancements
|
||||
let combine = dag
|
||||
.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
|(&e1, &e2, &e3): (&i32, &i32, &i32)| vec![e1, e2, e3],
|
||||
))
|
||||
.depends_on((&enhance1, &enhance2, &enhance3));
|
||||
|
||||
// Final aggregation
|
||||
let final_result = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|values: &Vec<i32>| {
|
||||
values.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(combine);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(process)?, 84);
|
||||
let result = dag.get(final_result)?;
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_map_reduce_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Map phase: process 16 inputs in parallel
|
||||
let mappers: Vec<dagx::TaskHandle<Vec<(&str, usize)>>> = (0..16)
|
||||
.map(|i| {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
// Simulate word count
|
||||
vec![("word1", i % 3), ("word2", i % 5), ("word3", i % 7)]
|
||||
}));
|
||||
task.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Shuffle phase: group by key (simplified - just 4 reducers)
|
||||
let shuffled: Vec<_> = (0..4)
|
||||
.map(|reducer_id| {
|
||||
// Each reducer gets data from 4 mappers
|
||||
let mapper_indices: Vec<usize> = (0..4).map(|i| reducer_id * 4 + i).collect();
|
||||
|
||||
let deps: Vec<_> = mapper_indices.iter().map(|&i| mappers[i]).collect();
|
||||
|
||||
match deps.len() {
|
||||
4 => dag
|
||||
.add_task(task_fn::<
|
||||
(
|
||||
Vec<(&str, usize)>,
|
||||
Vec<(&str, usize)>,
|
||||
Vec<(&str, usize)>,
|
||||
Vec<(&str, usize)>,
|
||||
),
|
||||
_,
|
||||
_,
|
||||
>(
|
||||
move |inputs: (
|
||||
&Vec<(&str, usize)>,
|
||||
&Vec<(&str, usize)>,
|
||||
&Vec<(&str, usize)>,
|
||||
&Vec<(&str, usize)>,
|
||||
)| {
|
||||
let mut combined = Vec::<(&str, usize)>::new();
|
||||
combined.extend(inputs.0);
|
||||
combined.extend(inputs.1);
|
||||
combined.extend(inputs.2);
|
||||
combined.extend(inputs.3);
|
||||
combined
|
||||
},
|
||||
))
|
||||
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3])),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Reduce phase
|
||||
let reducers: Vec<_> = shuffled
|
||||
.iter()
|
||||
.map(|shuffled_data| {
|
||||
dag.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<(&str, usize)>| {
|
||||
// Sum values by key
|
||||
let mut totals = std::collections::HashMap::new();
|
||||
for (key, value) in data {
|
||||
*totals.entry(*key).or_insert(0) += value;
|
||||
}
|
||||
totals.len()
|
||||
}))
|
||||
.depends_on(shuffled_data)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Final aggregation
|
||||
let final_task = dag
|
||||
.add_task(task_fn::<(_, _, _, _), _, _>(
|
||||
|(r1, r2, r3, r4): (&usize, &usize, &usize, &usize)| r1 + r2 + r3 + r4,
|
||||
))
|
||||
.depends_on((&reducers[0], &reducers[1], &reducers[2], &reducers[3]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(final_task)?;
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_pipeline_with_feedback() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let iteration_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Initial value
|
||||
let init = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
|
||||
// Stage 1: Transform
|
||||
let stage1 = dag
|
||||
.add_task(task_fn::<i32, _, _>({
|
||||
let counter = iteration_count.clone();
|
||||
move |x: &i32| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
x * 2
|
||||
}
|
||||
}))
|
||||
.depends_on(init);
|
||||
|
||||
// Stage 2: Filter
|
||||
let stage2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(
|
||||
|&x: &i32| if x < 100 { Some(x) } else { None },
|
||||
))
|
||||
.depends_on(stage1);
|
||||
|
||||
// Stage 3: Accumulate
|
||||
let stage3 = dag
|
||||
.add_task(task_fn::<Option<_>, _, _>(|opt: &Option<i32>| {
|
||||
opt.map(|x| x + 10).unwrap_or(0)
|
||||
}))
|
||||
.depends_on(stage2);
|
||||
|
||||
// Note: Can't create actual cycles in DAG, but we simulate feedback
|
||||
// by having a separate "feedback" task that would feed back in a real system
|
||||
let _feedback = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| {
|
||||
// In a real feedback loop, this would feed back to stage1
|
||||
x / 2
|
||||
}))
|
||||
.depends_on(stage3);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(stage1)?, 2);
|
||||
assert_eq!(dag.get(stage3)?, 12);
|
||||
assert_eq!(iteration_count.load(Ordering::SeqCst), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_scatter_gather_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Scatter: distribute work to multiple workers
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
vec![10, 20, 30, 40, 50, 60, 70, 80]
|
||||
}))
|
||||
.into();
|
||||
|
||||
// Workers process chunks
|
||||
let workers: Vec<_> = (0..4)
|
||||
.map(|worker_id| {
|
||||
dag.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<i32>| {
|
||||
// Each worker processes a chunk
|
||||
let chunk_size = data.len() / 4;
|
||||
let start = worker_id * chunk_size;
|
||||
let end = if worker_id == 3 {
|
||||
data.len()
|
||||
} else {
|
||||
start + chunk_size
|
||||
};
|
||||
|
||||
data[start..end].iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Gather: collect results from all workers
|
||||
let gather = dag
|
||||
.add_task(task_fn::<(i32, i32, i32, i32), _, _>(
|
||||
|(w0, w1, w2, w3): (&i32, &i32, &i32, &i32)| w0 + w1 + w2 + w3,
|
||||
))
|
||||
.depends_on((&workers[0], &workers[1], &workers[2], &workers[3]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Sum of 10+20+30+40+50+60+70+80 = 360
|
||||
assert_eq!(dag.get(gather)?, 360);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fork_join_with_barriers() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let phase_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Fork phase
|
||||
let parent = dag.add_task(task_fn::<(), _, _>({
|
||||
let counter = phase_counter.clone();
|
||||
move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
counter.store(1, Ordering::SeqCst);
|
||||
100
|
||||
}
|
||||
}));
|
||||
|
||||
// Multiple parallel paths
|
||||
let mut path1 = Vec::new();
|
||||
let parent_handle: dagx::TaskHandle<i32> = parent.into();
|
||||
let mut prev1 = parent_handle;
|
||||
for i in 0..3 {
|
||||
let counter = phase_counter.clone();
|
||||
let task = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let counter = counter.clone();
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
||||
x + i
|
||||
}))
|
||||
.depends_on(prev1);
|
||||
path1.push(task);
|
||||
prev1 = task;
|
||||
}
|
||||
|
||||
let mut path2 = Vec::new();
|
||||
let mut prev2 = parent_handle;
|
||||
for i in 0..3 {
|
||||
let counter = phase_counter.clone();
|
||||
let task = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let counter = counter.clone();
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
||||
x * 2 + i
|
||||
}))
|
||||
.depends_on(prev2);
|
||||
path2.push(task);
|
||||
prev2 = task;
|
||||
}
|
||||
|
||||
// Barrier: wait for all paths
|
||||
let barrier = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>({
|
||||
let counter = phase_counter.clone();
|
||||
move |(p1, p2): (&i32, &i32)| {
|
||||
let counter = counter.clone();
|
||||
counter.store(2, Ordering::SeqCst);
|
||||
p1 + p2
|
||||
}
|
||||
}))
|
||||
.depends_on((&path1[2], &path2[2]));
|
||||
|
||||
// Join phase
|
||||
let join = dag
|
||||
.add_task(task_fn::<i32, _, _>({
|
||||
let counter = phase_counter.clone();
|
||||
move |&x: &i32| {
|
||||
let counter = counter.clone();
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 2);
|
||||
x
|
||||
}
|
||||
}))
|
||||
.depends_on(barrier);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let _ = dag.get(join)?;
|
||||
assert_eq!(phase_counter.load(Ordering::SeqCst), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
//! Grid pattern DAG tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_2d_grid_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a 4x4 grid where each cell depends on the cell above and to the left
|
||||
let mut grid: Vec<Vec<Option<dagx::TaskHandle<i32>>>> = vec![vec![None; 4]; 4];
|
||||
|
||||
// Initialize first row and column
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..4 {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i as i32));
|
||||
grid[0][i] = Some(task.into());
|
||||
if i > 0 {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i as i32 * 10));
|
||||
grid[i][0] = Some(task.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the grid with dependencies
|
||||
for i in 1..4 {
|
||||
for j in 1..4 {
|
||||
let above = grid[i - 1][j].as_ref().unwrap();
|
||||
let left = grid[i][j - 1].as_ref().unwrap();
|
||||
|
||||
let task = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(move |(a, l): (&i32, &i32)| {
|
||||
a + l
|
||||
}))
|
||||
.depends_on((above, left));
|
||||
grid[i][j] = Some(task);
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Check corner values
|
||||
let bottom_right = dag.get(grid[3][3].as_ref().unwrap())?;
|
||||
assert!(bottom_right > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_3d_grid_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a 3x3x3 grid
|
||||
let size = 3;
|
||||
let mut grid: Vec<Vec<Vec<Option<dagx::TaskHandle<usize>>>>> =
|
||||
vec![vec![vec![None; size]; size]; size];
|
||||
|
||||
// Initialize origin
|
||||
let origin = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
grid[0][0][0] = Some(origin.into());
|
||||
|
||||
// Fill the 3D grid
|
||||
for x in 0..size {
|
||||
for y in 0..size {
|
||||
for z in 0..size {
|
||||
if x == 0 && y == 0 && z == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let deps: Vec<dagx::TaskHandle<usize>> = vec![
|
||||
if x > 0 { grid[x - 1][y][z] } else { None },
|
||||
if y > 0 { grid[x][y - 1][z] } else { None },
|
||||
if z > 0 { grid[x][y][z - 1] } else { None },
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let task_handle: dagx::TaskHandle<usize> = match deps.len() {
|
||||
0 => {
|
||||
let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| x + y + z));
|
||||
t.into()
|
||||
}
|
||||
1 => dag
|
||||
.add_task(task_fn::<usize, _, _>(move |prev: &usize| prev + 1))
|
||||
.depends_on(deps[0]),
|
||||
2 => dag
|
||||
.add_task(task_fn::<(usize, usize), _, _>(
|
||||
move |(a, b): (&usize, &usize)| a + b,
|
||||
))
|
||||
.depends_on((&deps[0], &deps[1])),
|
||||
3 => dag
|
||||
.add_task(task_fn::<(usize, usize, usize), _, _>(
|
||||
move |(a, b, c): (&usize, &usize, &usize)| a + b + c,
|
||||
))
|
||||
.depends_on((&deps[0], &deps[1], &deps[2])),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
grid[x][y][z] = Some(task_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Check that all cells have values
|
||||
for row in &grid {
|
||||
for col in row {
|
||||
for task in col.iter().flatten() {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hexagonal_grid() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Hexagonal grid with axial coordinates
|
||||
let mut hex_grid = std::collections::HashMap::new();
|
||||
|
||||
// Center hex
|
||||
let center_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
let center: dagx::TaskHandle<i32> = center_builder.into();
|
||||
hex_grid.insert((0, 0), center);
|
||||
|
||||
// Ring 1 (6 hexes around center)
|
||||
let directions = [(1, 0), (0, 1), (-1, 1), (-1, 0), (0, -1), (1, -1)];
|
||||
|
||||
for (i, &(dq, dr)) in directions.iter().enumerate() {
|
||||
let neighbor: dagx::TaskHandle<i32> = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |c: &i32| c + (i as i32 + 1) * 10))
|
||||
.depends_on(center);
|
||||
hex_grid.insert((dq, dr), neighbor);
|
||||
}
|
||||
|
||||
// Ring 2 (12 hexes)
|
||||
for &(q, r) in &[
|
||||
(2, 0),
|
||||
(1, 1),
|
||||
(0, 2),
|
||||
(-1, 2),
|
||||
(-2, 2),
|
||||
(-2, 1),
|
||||
(-2, 0),
|
||||
(-1, -1),
|
||||
(0, -2),
|
||||
(1, -2),
|
||||
(2, -2),
|
||||
(2, -1),
|
||||
] {
|
||||
// Find neighbors that exist
|
||||
let neighbors: Vec<_> = directions
|
||||
.iter()
|
||||
.filter_map(|&(dq, dr)| hex_grid.get(&(q - dq, r - dr)).cloned())
|
||||
.take(2) // Just use first 2 neighbors for simplicity
|
||||
.collect();
|
||||
|
||||
let task: dagx::TaskHandle<i32> = if neighbors.len() == 2 {
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(move |(a, b): (&i32, &i32)| {
|
||||
(a + b) / 2 + q + r
|
||||
}))
|
||||
.depends_on((&neighbors[0], &neighbors[1]))
|
||||
} else if neighbors.len() == 1 {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |n: &i32| n + q + r))
|
||||
.depends_on(neighbors[0])
|
||||
} else {
|
||||
let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| q + r));
|
||||
t.into()
|
||||
};
|
||||
|
||||
hex_grid.insert((q, r), task);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify center and some ring values
|
||||
assert_eq!(dag.get(hex_grid.get(&(0, 0)).unwrap())?, 100);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_toroidal_grid() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
const SIZE: usize = 4;
|
||||
let mut grid: Vec<Vec<Option<dagx::TaskHandle<i32>>>> = vec![vec![None; SIZE]; SIZE];
|
||||
|
||||
// Initialize all cells
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..SIZE {
|
||||
for j in 0..SIZE {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| (i * SIZE + j) as i32));
|
||||
grid[i][j] = Some(task.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Create toroidal connections (wrapping edges)
|
||||
let mut torus_tasks = Vec::new();
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..SIZE {
|
||||
for j in 0..SIZE {
|
||||
let curr = grid[i][j].as_ref().unwrap();
|
||||
let up = grid[(i + SIZE - 1) % SIZE][j].as_ref().unwrap();
|
||||
let down = grid[(i + 1) % SIZE][j].as_ref().unwrap();
|
||||
let left = grid[i][(j + SIZE - 1) % SIZE].as_ref().unwrap();
|
||||
let right = grid[i][(j + 1) % SIZE].as_ref().unwrap();
|
||||
|
||||
let task = dag
|
||||
.add_task(task_fn::<(_, _, _, _, _), _, _>(
|
||||
move |(c, u, d, l, r): (&i32, &i32, &i32, &i32, &i32)| {
|
||||
// Average of self and 4 neighbors
|
||||
(c + u + d + l + r) / 5
|
||||
},
|
||||
))
|
||||
.depends_on((curr, up, down, left, right));
|
||||
|
||||
torus_tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all torus tasks completed
|
||||
for task in torus_tasks {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
//! DAG pattern tests
|
||||
//!
|
||||
//! Tests for various DAG topologies and patterns.
|
||||
|
||||
mod butterfly;
|
||||
mod complex;
|
||||
mod grid;
|
||||
mod tree;
|
||||
@@ -1,282 +0,0 @@
|
||||
//! Tree pattern DAG tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_binary_tree_reduction() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 8 leaf nodes
|
||||
let leaves: Vec<_> = (0..8)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i)))
|
||||
.collect();
|
||||
|
||||
// Build binary tree reduction
|
||||
let mut current_level: Vec<dagx::TaskHandle<i32>> =
|
||||
leaves.into_iter().map(|t| t.into()).collect();
|
||||
|
||||
while current_level.len() > 1 {
|
||||
let mut next_level = Vec::new();
|
||||
|
||||
for chunk in current_level.chunks(2) {
|
||||
if chunk.len() == 2 {
|
||||
let task = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((&chunk[0], &chunk[1]));
|
||||
next_level.push(task);
|
||||
} else {
|
||||
next_level.push(chunk[0]);
|
||||
}
|
||||
}
|
||||
|
||||
current_level = next_level;
|
||||
}
|
||||
|
||||
let root = current_level[0];
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Sum of powers of 2 from 0 to 7: 1+2+4+8+16+32+64+128 = 255
|
||||
assert_eq!(dag.get(root)?, 255);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_n_ary_tree() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Build a 3-ary tree (each node has 3 children)
|
||||
fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> {
|
||||
if depth == 0 {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value));
|
||||
return task.into();
|
||||
}
|
||||
|
||||
let children: Vec<_> = (0..3)
|
||||
.map(|i| build_tree(dag, depth - 1, value * 3 + i))
|
||||
.collect();
|
||||
|
||||
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
move |(a, b, c): (&i32, &i32, &i32)| value + a + b + c,
|
||||
))
|
||||
.depends_on((&children[0], &children[1], &children[2]))
|
||||
}
|
||||
|
||||
let root = build_tree(&dag, 3, 1);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(root)?;
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unbalanced_tree() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create an unbalanced tree
|
||||
// root
|
||||
// / \
|
||||
// a b
|
||||
// / \ \
|
||||
// c d e
|
||||
// / \
|
||||
// f g
|
||||
|
||||
let f: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
|
||||
let g = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
|
||||
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(f);
|
||||
|
||||
let d_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
|
||||
let d: dagx::TaskHandle<i32> = d_builder.into();
|
||||
|
||||
let e = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(g);
|
||||
|
||||
let a = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(c, d): (&i32, &i32)| c + d))
|
||||
.depends_on((&c, &d));
|
||||
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|e: &i32| e + 10))
|
||||
.depends_on(e);
|
||||
|
||||
let root = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b))
|
||||
.depends_on((&a, &b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(f)?, 1);
|
||||
assert_eq!(dag.get(c)?, 2);
|
||||
assert_eq!(dag.get(a)?, 5); // 2 + 3
|
||||
assert_eq!(dag.get(b)?, 16); // 6 + 10
|
||||
assert_eq!(dag.get(root)?, 80); // 5 * 16
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_trie_like_structure() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Build a trie-like structure for paths
|
||||
let root: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| "root".to_string()))
|
||||
.into();
|
||||
|
||||
// First level
|
||||
let usr = dag
|
||||
.add_task(task_fn::<String, _, _>(|r: &String| format!("{}/usr", r)))
|
||||
.depends_on(root);
|
||||
|
||||
let etc = dag
|
||||
.add_task(task_fn::<String, _, _>(|r: &String| format!("{}/etc", r)))
|
||||
.depends_on(root);
|
||||
|
||||
let var = dag
|
||||
.add_task(task_fn::<String, _, _>(|r: &String| format!("{}/var", r)))
|
||||
.depends_on(root);
|
||||
|
||||
// Second level
|
||||
let usr_bin = dag
|
||||
.add_task(task_fn::<String, _, _>(|u: &String| format!("{}/bin", u)))
|
||||
.depends_on(usr);
|
||||
|
||||
let usr_lib = dag
|
||||
.add_task(task_fn::<String, _, _>(|u: &String| format!("{}/lib", u)))
|
||||
.depends_on(usr);
|
||||
|
||||
let etc_config = dag
|
||||
.add_task(task_fn::<String, _, _>(|e: &String| {
|
||||
format!("{}/config", e)
|
||||
}))
|
||||
.depends_on(etc);
|
||||
|
||||
let var_log = dag
|
||||
.add_task(task_fn::<String, _, _>(|v: &String| format!("{}/log", v)))
|
||||
.depends_on(var);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(usr_bin)?, "root/usr/bin");
|
||||
assert_eq!(dag.get(usr_lib)?, "root/usr/lib");
|
||||
assert_eq!(dag.get(etc_config)?, "root/etc/config");
|
||||
assert_eq!(dag.get(var_log)?, "root/var/log");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_balanced_k_ary_tree() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a perfectly balanced 4-ary tree of depth 3
|
||||
const K: usize = 4;
|
||||
const DEPTH: usize = 3;
|
||||
|
||||
fn create_balanced_tree(
|
||||
dag: &DagRunner,
|
||||
depth: usize,
|
||||
k: usize,
|
||||
node_id: usize,
|
||||
) -> dagx::TaskHandle<usize> {
|
||||
if depth == 0 {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| node_id));
|
||||
return task.into();
|
||||
}
|
||||
|
||||
let children: Vec<_> = (0..k)
|
||||
.map(|i| create_balanced_tree(dag, depth - 1, k, node_id * k + i))
|
||||
.collect();
|
||||
|
||||
match k {
|
||||
2 => dag
|
||||
.add_task(task_fn::<(usize, usize), _, _>(
|
||||
move |(a, b): (&usize, &usize)| node_id + a + b,
|
||||
))
|
||||
.depends_on((&children[0], &children[1])),
|
||||
3 => dag
|
||||
.add_task(task_fn::<(usize, usize, usize), _, _>(
|
||||
move |(a, b, c): (&usize, &usize, &usize)| node_id + a + b + c,
|
||||
))
|
||||
.depends_on((&children[0], &children[1], &children[2])),
|
||||
4 => dag
|
||||
.add_task(task_fn::<(usize, usize, usize, usize), _, _>(
|
||||
move |(a, b, c, d): (&usize, &usize, &usize, &usize)| node_id + a + b + c + d,
|
||||
))
|
||||
.depends_on((&children[0], &children[1], &children[2], &children[3])),
|
||||
_ => panic!("Unsupported k value"),
|
||||
}
|
||||
}
|
||||
|
||||
let root = create_balanced_tree(&dag, DEPTH, K, 0);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(root)?;
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_merkle_tree_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Simulate a Merkle tree for 8 data blocks
|
||||
let data_blocks: Vec<_> = (0..8)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
// Simulate hash of data block
|
||||
format!("hash_{}", i)
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Convert to TaskHandles
|
||||
let mut current_level: Vec<dagx::TaskHandle<String>> =
|
||||
data_blocks.into_iter().map(|t| t.into()).collect();
|
||||
|
||||
while current_level.len() > 1 {
|
||||
let mut next_level = Vec::new();
|
||||
|
||||
for pair in current_level.chunks(2) {
|
||||
if pair.len() == 2 {
|
||||
let hash_task = dag
|
||||
.add_task(task_fn::<(String, String), _, _>(
|
||||
|(left, right): (&String, &String)| format!("hash({}_{})", left, right),
|
||||
))
|
||||
.depends_on((&pair[0], &pair[1]));
|
||||
next_level.push(hash_task);
|
||||
} else {
|
||||
next_level.push(pair[0]);
|
||||
}
|
||||
}
|
||||
|
||||
current_level = next_level;
|
||||
}
|
||||
|
||||
let root_hash = current_level[0];
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(root_hash)?;
|
||||
assert!(result.starts_with("hash"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
//! Stress tests and benchmarks
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::DagRunner;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_100_levels() {
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
|
||||
// Create a chain 100 levels deep
|
||||
let start = Instant::now();
|
||||
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
|
||||
let mut handles = vec![first.into()];
|
||||
|
||||
for _ in 1..=100 {
|
||||
let next = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + 1))
|
||||
.depends_on(handles.last().unwrap());
|
||||
handles.push(next);
|
||||
}
|
||||
let build_time = start.elapsed();
|
||||
|
||||
println!("Built 100-level chain in {:?}", build_time);
|
||||
|
||||
// Execute
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = start.elapsed();
|
||||
|
||||
println!("Executed 100-level chain in {:?}", exec_time);
|
||||
assert!(
|
||||
exec_time < Duration::from_secs(5),
|
||||
"Execution took too long"
|
||||
);
|
||||
|
||||
// Verify result
|
||||
assert_eq!(dag.get(handles.last().unwrap()).unwrap(), 100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stress_mixed_patterns() {
|
||||
// Stress test with mixed DAG patterns
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
let mut all_tasks = vec![];
|
||||
|
||||
// Add various patterns
|
||||
for i in 0..10 {
|
||||
// Linear chains
|
||||
let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 1000));
|
||||
let mut chain_handles = vec![first.into()];
|
||||
for j in 1..10 {
|
||||
let next = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + j))
|
||||
.depends_on(chain_handles.last().unwrap());
|
||||
chain_handles.push(next);
|
||||
}
|
||||
all_tasks.push(*chain_handles.last().unwrap());
|
||||
|
||||
// Diamonds
|
||||
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 100));
|
||||
let source_handle: dagx::TaskHandle<i32> = source.into();
|
||||
let left = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source_handle);
|
||||
let right = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(source_handle);
|
||||
let merge = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
|
||||
.depends_on((&left, &right));
|
||||
all_tasks.push(merge);
|
||||
|
||||
// Fan-outs
|
||||
let hub = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
let hub_handle: dagx::TaskHandle<i32> = hub.into();
|
||||
for j in 0..5 {
|
||||
let fan = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + j * 10))
|
||||
.depends_on(hub_handle);
|
||||
all_tasks.push(fan);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
let exec_time = start.elapsed();
|
||||
|
||||
println!("Executed mixed pattern stress test in {:?}", exec_time);
|
||||
|
||||
// Verify some results
|
||||
for task in all_tasks.iter().take(10) {
|
||||
assert!(dag.get(task).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
//! Runtime compatibility tests for async-std
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::DagRunner;
|
||||
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(&mut self, a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_basic_dag_async_std() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(Value(2));
|
||||
let y = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
|
||||
dag.run(async_std::task::spawn).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parallel_execution_async_std() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(async_std::task::spawn).await.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_complex_dependencies_async_std() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(10));
|
||||
let b = dag.add_task(Value(20));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
let double = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(sum);
|
||||
|
||||
dag.run(async_std::task::spawn).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(double).unwrap().clone(), 60);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//! Runtime integration tests - verify dagx works correctly across different runtimes
|
||||
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(&mut self, a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_integration_tokio() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(10));
|
||||
let b = dag.add_task(Value(20));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sum)?, 30);
|
||||
Ok(())
|
||||
}
|
||||
+146
-4
@@ -1,7 +1,149 @@
|
||||
//! Runtime tests module
|
||||
|
||||
pub mod compatibility;
|
||||
pub mod quirky;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
mod async_std;
|
||||
mod smol;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
use futures_util::{future::BoxFuture, task::SpawnExt, Future};
|
||||
use test_case::test_case;
|
||||
|
||||
type BoxFunc = Box<
|
||||
dyn Fn(
|
||||
BoxFuture<'static, DagResult<Arc<dyn Any + Send + Sync>>>,
|
||||
) -> BoxFuture<'static, DagResult<Arc<dyn Any + Send + Sync>>>,
|
||||
>;
|
||||
|
||||
trait RuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>;
|
||||
}
|
||||
|
||||
struct SmolRuntimeTest;
|
||||
|
||||
impl RuntimeTest for SmolRuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
{
|
||||
smol::block_on(f(Box::new(|fut| Box::pin(smol::spawn(fut)))));
|
||||
}
|
||||
}
|
||||
|
||||
struct FuturesExecutorRuntimeTest;
|
||||
|
||||
impl RuntimeTest for FuturesExecutorRuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
{
|
||||
let spawner = futures_executor::ThreadPool::new().unwrap();
|
||||
|
||||
futures_executor::block_on(f(Box::new(move |fut| {
|
||||
Box::pin(spawner.spawn_with_handle(fut).unwrap())
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
struct PollsterRuntimeTest;
|
||||
|
||||
impl RuntimeTest for PollsterRuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
{
|
||||
pollster::block_on(f(Box::new(|fut| {
|
||||
Box::pin(async { pollster::block_on(fut) })
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
static ASYNC_EXECUTOR: async_executor::Executor = async_executor::Executor::new();
|
||||
|
||||
struct AsyncExecutorRuntimeTest;
|
||||
|
||||
impl RuntimeTest for AsyncExecutorRuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
{
|
||||
smol::future::block_on(
|
||||
ASYNC_EXECUTOR.run(f(Box::new(|fut| Box::pin(ASYNC_EXECUTOR.spawn(fut))))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(&mut self, a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(SmolRuntimeTest)]
|
||||
#[test_case(FuturesExecutorRuntimeTest)]
|
||||
#[test_case(PollsterRuntimeTest)]
|
||||
#[test_case(AsyncExecutorRuntimeTest)]
|
||||
fn test_basic_dag(runner: impl RuntimeTest) {
|
||||
runner.run_dagx_test(|spawner| async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(Value(2));
|
||||
let y = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
|
||||
dag.run(spawner).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
})
|
||||
}
|
||||
|
||||
#[test_case(SmolRuntimeTest)]
|
||||
#[test_case(FuturesExecutorRuntimeTest)]
|
||||
#[test_case(PollsterRuntimeTest)]
|
||||
#[test_case(AsyncExecutorRuntimeTest)]
|
||||
fn test_parallel_execution(runner: impl RuntimeTest) {
|
||||
runner.run_dagx_test(|spawner| async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(spawner).await.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test_case(SmolRuntimeTest)]
|
||||
#[test_case(FuturesExecutorRuntimeTest)]
|
||||
#[test_case(PollsterRuntimeTest)]
|
||||
#[test_case(AsyncExecutorRuntimeTest)]
|
||||
fn test_complex_dependencies(runner: impl RuntimeTest) {
|
||||
runner.run_dagx_test(|spawner| async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(10));
|
||||
let b = dag.add_task(Value(20));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
let double = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(sum);
|
||||
|
||||
dag.run(spawner).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(double).unwrap().clone(), 60);
|
||||
});
|
||||
}
|
||||
@@ -1,493 +0,0 @@
|
||||
//! Tests for quirky/edge-case async runtimes
|
||||
//!
|
||||
//! This file tests dagx with unusual async runtimes to ensure
|
||||
//! the spawner parameter works correctly across different executor types.
|
||||
//!
|
||||
//! Tested runtimes:
|
||||
//! - async-executor: Minimal executor from smol ecosystem
|
||||
//! - pollster: Blocking/synchronous executor
|
||||
//! - futures-executor: Basic futures crate executor
|
||||
|
||||
// Note: executor.run() synchronously executes futures to completion.
|
||||
// We don't need the return value in these tests, so we allow unused results.
|
||||
#![allow(clippy::let_underscore_future)]
|
||||
#![allow(unused_must_use)]
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use futures_executor::ThreadPool;
|
||||
use futures_util::task::SpawnExt;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
// =======================
|
||||
// async-executor Tests
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_basic() {
|
||||
// async-executor is a minimal executor from the smol ecosystem
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
|
||||
let sum = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((x, y));
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 30);
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_complex_dag() {
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a diamond pattern
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)).into();
|
||||
|
||||
let left = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
|
||||
let right = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(source);
|
||||
|
||||
let sink = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
|
||||
.depends_on((&left, &right));
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sink).unwrap(), 25); // (5*2) + (5*3) = 25
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_parallel_tasks() {
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create 10 independent tasks
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
let c = counter.clone();
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let c = c.clone();
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
i
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
// All tasks should have run
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
||||
|
||||
// Verify results
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i as i32);
|
||||
}
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_with_local_spawn() {
|
||||
// Test with LocalExecutor (single-threaded)
|
||||
let executor = async_executor::LocalExecutor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(x).unwrap(), 42);
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_deep_chain() {
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Build a chain of 50 tasks
|
||||
let start = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
|
||||
let mut current = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(start);
|
||||
|
||||
for _ in 1..50 {
|
||||
current = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(current).unwrap(), 50);
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
// =======================
|
||||
// pollster Tests
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_pollster_basic() {
|
||||
// pollster is a blocking executor - it blocks the thread
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
let doubled = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(x);
|
||||
|
||||
dag.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(doubled).unwrap(), 200);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pollster_complex() {
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Fan-out pattern
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 7)).into();
|
||||
|
||||
let tasks: Vec<_> = (0..5)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * i))
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), 7 * i as i32);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pollster_error_handling() {
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Task that returns Result
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::<i32, String>(42)));
|
||||
|
||||
dag.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(task).unwrap(), Ok(42));
|
||||
});
|
||||
}
|
||||
|
||||
// =======================
|
||||
// futures-executor Tests
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_futures_executor_basic() {
|
||||
futures_executor::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 15));
|
||||
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 25));
|
||||
let product = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b))
|
||||
.depends_on((x, y));
|
||||
|
||||
let spawner = ThreadPool::new().unwrap();
|
||||
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(product).unwrap(), 375); // 15 * 25
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_futures_executor_threadpool() {
|
||||
let pool = futures_executor::ThreadPool::new().unwrap();
|
||||
let spawner = pool.clone();
|
||||
|
||||
pool.spawn_ok(async move {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create multiple independent tasks
|
||||
let tasks: Vec<_> = (0..20)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * i)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), (i * i) as i32);
|
||||
}
|
||||
});
|
||||
|
||||
// Give tasks time to complete
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// Note: LocalPool test removed because it cannot run within another executor context
|
||||
// (cargo test framework may use tokio, causing EnterError)
|
||||
// LocalPool is single-threaded anyway, so futures-executor ThreadPool is more representative
|
||||
|
||||
#[test]
|
||||
fn test_futures_executor_diamond_pattern() {
|
||||
futures_executor::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 8)).into();
|
||||
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
|
||||
.depends_on(a);
|
||||
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 2))
|
||||
.depends_on(a);
|
||||
|
||||
let d = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x * y))
|
||||
.depends_on((&b, &c));
|
||||
|
||||
let spawner = ThreadPool::new().unwrap();
|
||||
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(d).unwrap(), 60); // (8+2) * (8-2) = 10 * 6
|
||||
});
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Cross-Runtime Tests
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_all_runtimes_compatibility() {
|
||||
// This test verifies the same DAG works across all runtimes
|
||||
|
||||
// Test 1: async-executor
|
||||
{
|
||||
let executor = async_executor::Executor::new();
|
||||
executor.run(async {
|
||||
let dag = DagRunner::new();
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123));
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
assert_eq!(dag.get(x).unwrap(), 123);
|
||||
});
|
||||
}
|
||||
|
||||
// Test 2: pollster
|
||||
{
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123));
|
||||
dag.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(x).unwrap(), 123);
|
||||
});
|
||||
}
|
||||
|
||||
// Test 3: futures-executor
|
||||
{
|
||||
futures_executor::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123));
|
||||
let spawner = ThreadPool::new().unwrap();
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
assert_eq!(dag.get(x).unwrap(), 123);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_runtime_edge_cases() {
|
||||
// Test with LocalExecutor (no Send requirement)
|
||||
let executor = async_executor::LocalExecutor::new();
|
||||
|
||||
executor.run(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// This tests that our API works even with !Send futures
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
// Local state
|
||||
|
||||
42
|
||||
}));
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(x).unwrap(), 42);
|
||||
});
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Stress Tests for Quirky Runtimes
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_stress() {
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
executor.run(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 100 tasks
|
||||
let tasks: Vec<_> = (0..100)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
// Small delay to test executor scheduling
|
||||
i
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i as i32);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pollster_nested_blocks() {
|
||||
// Test nested blocking - edge case
|
||||
pollster::block_on(async {
|
||||
let dag1 = DagRunner::new();
|
||||
let x = dag1.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
|
||||
dag1.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag1.get(x).unwrap(), 10);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_futures_executor_multi_pool() {
|
||||
// Test with multiple thread pools
|
||||
let pool1 = futures_executor::ThreadPool::new().unwrap();
|
||||
let spawner = pool1.clone();
|
||||
|
||||
pool1.spawn_ok(async move {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 50));
|
||||
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
assert_eq!(dag.get(x).unwrap(), 50);
|
||||
});
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Edge Case: Sync Tasks with Quirky Runtimes
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_sync_tasks_on_async_executor() {
|
||||
struct SyncAdd;
|
||||
|
||||
#[dagx::task]
|
||||
impl SyncAdd {
|
||||
fn run(a: &i32, b: &i32) -> i32 {
|
||||
// Synchronous task
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
executor.run(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 7));
|
||||
let sum = dag.add_task(SyncAdd).depends_on((x, y));
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 12);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_tasks_on_pollster() {
|
||||
struct SyncDouble;
|
||||
|
||||
#[dagx::task]
|
||||
impl SyncDouble {
|
||||
fn run(x: &i32) -> i32 {
|
||||
x * 2
|
||||
}
|
||||
}
|
||||
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 21));
|
||||
let doubled = dag.add_task(SyncDouble).depends_on(x);
|
||||
|
||||
dag.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(doubled).unwrap(), 42);
|
||||
});
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
//! Runtime compatibility tests for smol
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::DagRunner;
|
||||
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(&mut self, a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_dag_smol() {
|
||||
smol::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(Value(2));
|
||||
let y = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
|
||||
dag.run(smol::spawn).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_execution_smol() {
|
||||
smol::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(smol::spawn).await.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_dependencies_smol() {
|
||||
smol::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(10));
|
||||
let b = dag.add_task(Value(20));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
let double = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(sum);
|
||||
|
||||
dag.run(smol::spawn).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(double).unwrap().clone(), 60);
|
||||
});
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// Combined task pattern tests
|
||||
use crate::common::task_fn;
|
||||
// Comprehensive test showing all three patterns working together
|
||||
|
||||
use dagx::*;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
// Pattern 1: Stateless - Pure computation, no state
|
||||
struct StatelessAdd;
|
||||
#[task]
|
||||
impl StatelessAdd {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Read-only state - Configuration data
|
||||
struct ReadOnlyMultiplier(i32);
|
||||
#[task]
|
||||
impl ReadOnlyMultiplier {
|
||||
async fn run(&self, input: &i32) -> i32 {
|
||||
input * self.0 // Read-only access
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: Mutable state - Accumulates state
|
||||
struct MutableAccumulator(i32);
|
||||
#[task]
|
||||
impl MutableAccumulator {
|
||||
async fn run(&mut self, input: &i32) -> i32 {
|
||||
self.0 += input; // Modifies state
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_all_three_task_patterns() {
|
||||
// Comprehensive test showing all three patterns working together
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create initial values
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
|
||||
|
||||
// Pattern 1: Stateless addition
|
||||
let sum = dag.add_task(StatelessAdd).depends_on((x, y)); // 5 + 3 = 8
|
||||
|
||||
// Pattern 2: Read-only multiplication
|
||||
let doubled = dag.add_task(ReadOnlyMultiplier(2)).depends_on(sum); // 8 * 2 = 16
|
||||
|
||||
// Pattern 3: Mutable accumulation
|
||||
let accumulated = dag.add_task(MutableAccumulator(10)).depends_on(doubled); // 10 + 16 = 26
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 8);
|
||||
assert_eq!(dag.get(doubled).unwrap(), 16);
|
||||
assert_eq!(dag.get(accumulated).unwrap(), 26);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_parallel_execution() {
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create 3 independent tasks that should run in parallel
|
||||
let c1 = counter.clone();
|
||||
let c2 = counter.clone();
|
||||
let c3 = counter.clone();
|
||||
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let c = c1.clone();
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
1
|
||||
}));
|
||||
|
||||
let t2 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let c = c2.clone();
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
2
|
||||
}));
|
||||
|
||||
let t3 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let c = c3.clone();
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
3
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// All three tasks should have run
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 3);
|
||||
assert_eq!(dag.get(t1).unwrap(), 1);
|
||||
assert_eq!(dag.get(t2).unwrap(), 2);
|
||||
assert_eq!(dag.get(t3).unwrap(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_captured_state() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let multiplier = 7;
|
||||
let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let scaled = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * multiplier))
|
||||
.depends_on(base);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(scaled).unwrap(), 35);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Test module for task pattern tests
|
||||
|
||||
mod combined;
|
||||
mod readonly;
|
||||
mod stateless;
|
||||
@@ -1,91 +0,0 @@
|
||||
// Read-only state task pattern tests
|
||||
use crate::common::task_fn;
|
||||
// These tests verify the read-only state pattern using &self
|
||||
|
||||
use dagx::*;
|
||||
|
||||
// Read-only state tasks
|
||||
struct ReadOnlyValue(i32);
|
||||
|
||||
#[task]
|
||||
impl ReadOnlyValue {
|
||||
async fn run(&self) -> i32 {
|
||||
self.0 // Read-only access
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadOnlyMultiplier(i32);
|
||||
|
||||
#[task]
|
||||
impl ReadOnlyMultiplier {
|
||||
async fn run(&self, input: &i32) -> i32 {
|
||||
input * self.0 // Read-only access to state
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadOnlyConfig {
|
||||
multiplier: i32,
|
||||
offset: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ReadOnlyConfig {
|
||||
async fn run(&self, input: &i32) -> i32 {
|
||||
(input * self.multiplier) + self.offset
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readonly_state_basic() {
|
||||
// Test basic read-only state task using &self
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let value = dag.add_task(ReadOnlyValue(42));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(value).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readonly_state_with_input() {
|
||||
// Test read-only state task with input using &self
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let result = dag.add_task(ReadOnlyMultiplier(7)).depends_on(input);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(result).unwrap(), 35);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readonly_state_multiple_uses() {
|
||||
// Test that read-only state can be used multiple times
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
let input2 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
|
||||
|
||||
let config = ReadOnlyConfig {
|
||||
multiplier: 3,
|
||||
offset: 5,
|
||||
};
|
||||
|
||||
let result1 = dag.add_task(config).depends_on(input1);
|
||||
// Note: Can't reuse config because it was moved, so create another
|
||||
let config2 = ReadOnlyConfig {
|
||||
multiplier: 3,
|
||||
offset: 5,
|
||||
};
|
||||
let result2 = dag.add_task(config2).depends_on(input2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(result1).unwrap(), 35); // (10 * 3) + 5
|
||||
assert_eq!(dag.get(result2).unwrap(), 65); // (20 * 3) + 5
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
// Stateless task pattern tests
|
||||
use crate::common::task_fn;
|
||||
|
||||
use dagx::*;
|
||||
|
||||
// Stateless task structs
|
||||
struct StatelessAdd;
|
||||
|
||||
#[task]
|
||||
impl StatelessAdd {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessDouble;
|
||||
|
||||
#[task]
|
||||
impl StatelessDouble {
|
||||
async fn run(x: &i32) -> i32 {
|
||||
x * 2
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessSource;
|
||||
|
||||
#[task]
|
||||
impl StatelessSource {
|
||||
async fn run() -> i32 {
|
||||
42
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessFormat;
|
||||
|
||||
#[task]
|
||||
impl StatelessFormat {
|
||||
async fn run(n: &i32, s: &String, b: &bool) -> String {
|
||||
format!("{}: {} ({})", s, n, b)
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessMultiply;
|
||||
|
||||
#[task]
|
||||
impl StatelessMultiply {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a * b
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessInc;
|
||||
|
||||
#[task]
|
||||
impl StatelessInc {
|
||||
async fn run(x: &i32) -> i32 {
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessSquare;
|
||||
|
||||
#[task]
|
||||
impl StatelessSquare {
|
||||
async fn run(x: &i32) -> i32 {
|
||||
x * x
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessConcat;
|
||||
|
||||
#[task]
|
||||
impl StatelessConcat {
|
||||
async fn run(a: &String, b: &String) -> String {
|
||||
format!("{}{}", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_basic() {
|
||||
// Test basic stateless task with two inputs
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
|
||||
let sum = dag.add_task(StatelessAdd).depends_on((x, y));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(sum).unwrap(), 30);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_single_input() {
|
||||
// Test stateless task with single input
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 21));
|
||||
let doubled = dag.add_task(StatelessDouble).depends_on(input);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(doubled).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_no_input() {
|
||||
// Test stateless source task with no inputs
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(StatelessSource);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(source).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_multiple_inputs() {
|
||||
// Test stateless task with multiple inputs of different types
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let num = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
let text = dag.add_task(task_fn::<(), _, _>(|_: ()| "test".to_string()));
|
||||
let flag = dag.add_task(task_fn::<(), _, _>(|_: ()| true));
|
||||
|
||||
let result = dag.add_task(StatelessFormat).depends_on((num, text, flag));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(result).unwrap(), "test: 42 (true)");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_mixed_with_stateful() {
|
||||
// Test mixing stateless and stateful tasks
|
||||
// Define Counter inline for this test
|
||||
struct Counter {
|
||||
count: i32,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
fn new(initial: i32) -> Self {
|
||||
Self { count: initial }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Counter {
|
||||
async fn run(&mut self, input: &i32) -> i32 {
|
||||
self.count += input;
|
||||
self.count
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
|
||||
|
||||
// Stateless multiplication
|
||||
let product = dag.add_task(StatelessMultiply).depends_on((x, y));
|
||||
|
||||
// Stateful counter
|
||||
let counted = dag.add_task(Counter::new(10)).depends_on(product);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(product).unwrap(), 15);
|
||||
assert_eq!(dag.get(counted).unwrap(), 25); // 10 + 15
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_chain() {
|
||||
// Test chaining stateless tasks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let start = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
|
||||
let step1 = dag.add_task(StatelessInc).depends_on(start);
|
||||
let step2 = dag.add_task(StatelessInc).depends_on(step1);
|
||||
let step3 = dag.add_task(StatelessInc).depends_on(step2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(step1).unwrap(), 1);
|
||||
assert_eq!(dag.get(step2).unwrap(), 2);
|
||||
assert_eq!(dag.get(step3).unwrap(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_parallel() {
|
||||
// Test parallel execution of stateless tasks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (1..=5)
|
||||
.map(|i| {
|
||||
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
dag.add_task(StatelessSquare).depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(tasks[0]).unwrap(), 1);
|
||||
assert_eq!(dag.get(tasks[1]).unwrap(), 4);
|
||||
assert_eq!(dag.get(tasks[2]).unwrap(), 9);
|
||||
assert_eq!(dag.get(tasks[3]).unwrap(), 16);
|
||||
assert_eq!(dag.get(tasks[4]).unwrap(), 25);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_with_string_types() {
|
||||
// Test stateless tasks with complex types
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let hello = dag.add_task(task_fn::<(), _, _>(|_: ()| "Hello, ".to_string()));
|
||||
let world = dag.add_task(task_fn::<(), _, _>(|_: ()| "World!".to_string()));
|
||||
|
||||
let greeting = dag.add_task(StatelessConcat).depends_on((hello, world));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(greeting).unwrap(), "Hello, World!");
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for concurrent access patterns and race conditions
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for forced execution interleaving patterns
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -272,113 +272,3 @@ async fn test_layered_interleaving() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cross_branch_synchronization() -> DagResult<()> {
|
||||
// Test synchronization between parallel branches
|
||||
let dag = DagRunner::new();
|
||||
let sync_point = Arc::new(AtomicUsize::new(0));
|
||||
let events = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
// Branch A
|
||||
let a1: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
events.lock().push("A1");
|
||||
sync.fetch_add(1, Ordering::SeqCst);
|
||||
1
|
||||
}
|
||||
}))
|
||||
.into();
|
||||
|
||||
struct A2Task {
|
||||
sync: Arc<AtomicUsize>,
|
||||
events: Arc<parking_lot::Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl A2Task {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
// Wait for B1
|
||||
while self.sync.load(Ordering::SeqCst) < 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push("A2");
|
||||
self.sync.fetch_add(1, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let a2 = dag
|
||||
.add_task(A2Task {
|
||||
sync: sync_point.clone(),
|
||||
events: events.clone(),
|
||||
})
|
||||
.depends_on(a1);
|
||||
|
||||
struct B1Task {
|
||||
sync: Arc<AtomicUsize>,
|
||||
events: Arc<parking_lot::Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl B1Task {
|
||||
async fn run(&self) -> i32 {
|
||||
// Wait for A1
|
||||
while self.sync.load(Ordering::SeqCst) < 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push("B1");
|
||||
self.sync.fetch_add(1, Ordering::SeqCst);
|
||||
10
|
||||
}
|
||||
}
|
||||
|
||||
// Branch B
|
||||
let b1 = dag.add_task(B1Task {
|
||||
events: events.clone(),
|
||||
sync: sync_point.clone(),
|
||||
});
|
||||
|
||||
struct B2Task {
|
||||
sync: Arc<AtomicUsize>,
|
||||
events: Arc<parking_lot::Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl B2Task {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
// Wait for A2
|
||||
while self.sync.load(Ordering::SeqCst) < 3 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push("B2");
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let b2 = dag
|
||||
.add_task(B2Task {
|
||||
events: events.clone(),
|
||||
sync: sync_point.clone(),
|
||||
})
|
||||
.depends_on(b1);
|
||||
|
||||
// Merge
|
||||
let merge = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((&a2, &b2));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(merge)?, 13); // 2 + 11
|
||||
|
||||
// Check forced interleaving
|
||||
let event_list = events.lock().clone();
|
||||
assert_eq!(event_list, vec!["A1", "B1", "A2", "B2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,6 +5,3 @@
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
mod with_tracing;
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
mod without_tracing;
|
||||
@@ -52,33 +52,6 @@ async fn test_tracing_with_subscriber() {
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_tracing_with_complex_dag() {
|
||||
// Initialize tracing subscriber
|
||||
let _ = fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a diamond pattern
|
||||
let source = dag.add_task(Value(10)).into();
|
||||
let left = dag.add_task(Add).depends_on((&source, &source));
|
||||
let right = dag.add_task(Multiply).depends_on((&source, &source));
|
||||
let sink = dag.add_task(Add).depends_on((&left, &right));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// source = 10
|
||||
// left = 10 + 10 = 20
|
||||
// right = 10 * 10 = 100
|
||||
// sink = 20 + 100 = 120
|
||||
assert_eq!(dag.get(sink).unwrap(), 120);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_tracing_inline_execution() {
|
||||
// Initialize tracing subscriber
|
||||
@@ -103,38 +76,3 @@ async fn test_tracing_inline_execution() {
|
||||
// c = 2 * 2 = 4
|
||||
assert_eq!(dag.get(c).unwrap(), 4);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_tracing_multiple_layers() {
|
||||
// Initialize tracing subscriber
|
||||
let _ = fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")),
|
||||
)
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Layer 0
|
||||
let v1 = dag.add_task(Value(1));
|
||||
let v2 = dag.add_task(Value(2)).into();
|
||||
let v3 = dag.add_task(Value(3));
|
||||
|
||||
// Layer 1
|
||||
let sum12 = dag.add_task(Add).depends_on((v1, &v2));
|
||||
let mul23 = dag.add_task(Multiply).depends_on((&v2, v3));
|
||||
|
||||
// Layer 2
|
||||
let final_sum = dag.add_task(Add).depends_on((&sum12, &mul23));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// v1 = 1, v2 = 2, v3 = 3
|
||||
// sum12 = 1 + 2 = 3
|
||||
// mul23 = 2 * 3 = 6
|
||||
// final_sum = 3 + 6 = 9
|
||||
assert_eq!(dag.get(final_sum).unwrap(), 9);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//! Tests that the library works correctly without tracing feature
|
||||
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
struct Value(i32);
|
||||
|
||||
#[task]
|
||||
impl Value {
|
||||
async fn run(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
|
||||
#[task]
|
||||
impl Add {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_basic_dag_without_tracing() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(2));
|
||||
let b = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_complex_dag_without_tracing() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create multiple layers
|
||||
let v1 = dag.add_task(Value(1));
|
||||
let v2 = dag.add_task(Value(2));
|
||||
let v3 = dag.add_task(Value(3));
|
||||
let v4 = dag.add_task(Value(4));
|
||||
|
||||
let sum12 = dag.add_task(Add).depends_on((v1, v2));
|
||||
let sum34 = dag.add_task(Add).depends_on((v3, v4));
|
||||
let final_sum = dag.add_task(Add).depends_on((&sum12, &sum34));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(final_sum).unwrap(), 10);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Type safety tests for TaskHandle
|
||||
use crate::common::task_fn;
|
||||
|
||||
// These tests verify dagx's compile-time type safety guarantees.
|
||||
// Tests cover both runtime behavior (wrong types return None) and
|
||||
// compile-time checking (type mismatches fail to compile).
|
||||
|
||||
use dagx::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_with_wrong_type_returns_none() {
|
||||
// Verify that accessing with wrong type returns error, not panic
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Add task that returns i32
|
||||
let int_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42i32));
|
||||
|
||||
// Run DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify correct type works
|
||||
assert_eq!(dag.get(int_task), Ok(42i32));
|
||||
|
||||
// The type safety is enforced at compile time
|
||||
// We cannot create fake handles with wrong types without accessing private fields
|
||||
// This is actually better - it means type safety is compile-time enforced!
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_preserves_type() {
|
||||
// Verify TaskHandle<T> preserves type information correctly
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create typed handles
|
||||
let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
|
||||
let int_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42i32));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get with correct types
|
||||
assert_eq!(dag.get(string_task).unwrap(), "hello".to_string());
|
||||
assert_eq!(dag.get(int_task).unwrap(), 42);
|
||||
|
||||
// Type safety is enforced at compile time - we cannot create wrong-typed handles
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_type_safety_handle_reuse() {
|
||||
// Verify that handles maintain type safety across reuse
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100i32)).into();
|
||||
|
||||
// Use the same source handle multiple times
|
||||
let double = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
let triple = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(double).unwrap(), 200);
|
||||
assert_eq!(dag.get(triple).unwrap(), 300);
|
||||
}
|
||||
Reference in new issue
Block a user