Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb794361e | ||
|
|
9fbe4c2fd8 | ||
|
|
c8efb00175 | ||
|
|
766555bef4 | ||
|
|
a13f148778 | ||
|
|
827041c1b6 | ||
|
|
c002161216 | ||
|
|
72a83e29dc | ||
|
|
e4fc0b0d44 | ||
|
|
d83a815ed0 |
No files matched your search
@@ -136,10 +136,11 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install cargo-tarpaulin
|
||||
- uses: taiki-e/install-action@cargo-tarpaulin
|
||||
uses: taiki-e/install-action@cargo-tarpaulin
|
||||
|
||||
- name: Generate code coverage
|
||||
run: cargo tarpaulin --out Xml --out Lcov --output-dir coverage --fail-under 80
|
||||
run: |
|
||||
cargo tarpaulin --out Xml --out Lcov --output-dir coverage --fail-under 80
|
||||
|
||||
security:
|
||||
name: Security Audit
|
||||
|
||||
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now able to return bare tuples as task outputs
|
||||
- No longer able to create a DAG with cyclic dependencies ([#3](https://github.com/swaits/dagx/pull/3))
|
||||
- The `#[task]` macro no longer requires an explicit return type ([#2](https://github.com/swaits/dagx/pull/2))
|
||||
|
||||
### Changed
|
||||
|
||||
- Task inputs no longer need to be `Clone` ([#7](https://github.com/swaits/dagx/pull/7))
|
||||
- Building a DAG is now a single-threaded operation ([#12](https://github.com/swaits/dagx/pull/12))
|
||||
- Running a DAG now consumes the `DagRunner` and returns a `DagOutput` from which to get results ([#12](https://github.com/swaits/dagx/pull/12))
|
||||
- `#[task]` macro support is now gated behind the `derive` default feature
|
||||
- Tasks now receive input via the `TaskInput` type, which guarantees via the public API that dependencies can be extracted ([#7](https://github.com/swaits/dagx/pull/7))
|
||||
- Task outputs are now extracted as owned values, not clones of an Arc value ([#7](https://github.com/swaits/dagx/pull/7))
|
||||
|
||||
### Internal
|
||||
|
||||
- The `task_fn` macro has been extracted to the dagx-test crate
|
||||
- Task inputs are passed to tasks directly from the runner, not through channels ([#1](https://github.com/swaits/dagx/pull/1))
|
||||
- Redundant tests/examples/benchmarks removed to lower maintenance and refactor burden ([#11](https://github.com/swaits/dagx/pull/11))
|
||||
- parking_lot dependency removed ([#12](https://github.com/swaits/dagx/pull/12))
|
||||
- futures dependency replaced with futures-util ([#8](https://github.com/swaits/dagx/pull/8))
|
||||
|
||||
## [0.3.1] - 2025-10-10
|
||||
|
||||
[View changes](https://github.com/swaits/dagx/compare/v0.3.0...v0.3.1)
|
||||
|
||||
+14
-7
@@ -21,14 +21,16 @@ cargo clippy -- -D warnings
|
||||
## Development Setup
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Rust 1.81+ (edition 2021)
|
||||
- Git
|
||||
|
||||
**Optional tools:**
|
||||
|
||||
```bash
|
||||
rustup component add rustfmt clippy
|
||||
cargo install cargo-llvm-cov # For coverage
|
||||
cargo install cargo-audit # For security checks
|
||||
cargo binstall cargo-tarpaulin # For coverage
|
||||
cargo binstall cargo-audit # For security checks
|
||||
```
|
||||
|
||||
## Making Changes
|
||||
@@ -67,7 +69,8 @@ cargo build --examples # Examples compile
|
||||
- All doc examples must compile
|
||||
|
||||
Example:
|
||||
```rust
|
||||
|
||||
````rust
|
||||
/// Adds a task to the DAG.
|
||||
///
|
||||
/// # Examples
|
||||
@@ -75,30 +78,31 @@ Example:
|
||||
/// ```
|
||||
/// use dagx::DagRunner;
|
||||
///
|
||||
/// let dag = DagRunner::new();
|
||||
/// let mut dag = DagRunner::new();
|
||||
/// let task = dag.add_task(MyTask::new());
|
||||
/// ```
|
||||
pub fn add_task<T: Task>(&self, task: T) -> TaskBuilder<T> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
## Testing
|
||||
|
||||
**Test organization:**
|
||||
|
||||
- Unit tests: `#[cfg(test)]` modules in source files
|
||||
- Integration tests: `tests/` directory
|
||||
- Examples: `examples/` directory (must compile)
|
||||
- Doctests: In `///` comments (must pass)
|
||||
|
||||
**Runtime testing:**
|
||||
|
||||
```bash
|
||||
cargo test # Default (tokio)
|
||||
cargo test --test async_std_runtime # async-std
|
||||
cargo test --test smol_runtime # smol
|
||||
```
|
||||
|
||||
**Benchmarks:**
|
||||
|
||||
```bash
|
||||
cargo bench
|
||||
# View results: open target/criterion/report/index.html
|
||||
@@ -130,6 +134,7 @@ Closes #123
|
||||
**Types**: `feat`, `fix`, `docs`, `test`, `refactor`, `perf`, `chore`
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `feat: add timeout support for tasks`
|
||||
- `fix: correct cycle detection in diamond DAG`
|
||||
- `docs: clarify TaskHandle usage in README`
|
||||
@@ -137,12 +142,14 @@ Closes #123
|
||||
## Pull Requests
|
||||
|
||||
**PR description should include:**
|
||||
|
||||
- What changed and why
|
||||
- How it was tested
|
||||
- Any breaking changes
|
||||
- Link to related issue (if applicable)
|
||||
|
||||
**Review process:**
|
||||
|
||||
- Maintainer reviews within 1 week
|
||||
- Address feedback
|
||||
- Once approved, maintainer merges
|
||||
|
||||
Generated
+122
-355
@@ -2,26 +2,11 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -52,9 +37,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.3"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
|
||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
@@ -90,14 +75,14 @@ dependencies = [
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.1"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
@@ -148,7 +133,7 @@ dependencies = [
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -180,26 +165,11 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.4"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
@@ -216,9 +186,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
version = "3.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
@@ -228,9 +198,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
@@ -261,18 +231,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.48"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.48"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
@@ -280,9 +250,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.5"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
@@ -359,9 +329,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "dagrs"
|
||||
version = "0.5.0"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4bb104a09802f2db14e50ba926d8bca3a96f50eac8b4798ca8a010e6807ae63"
|
||||
checksum = "377ed242839af227cbb674dc4c6eb44866e1fa922e962c7159c938e1f62797dd"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
@@ -381,7 +351,6 @@ dependencies = [
|
||||
"dagx-test",
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
"parking_lot",
|
||||
"pollster",
|
||||
"smol",
|
||||
"test-case",
|
||||
@@ -419,7 +388,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -451,9 +420,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -466,9 +435,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -476,27 +445,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -513,9 +481,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -524,21 +492,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -548,24 +516,18 @@ dependencies = [
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.6.0"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -574,17 +536,6 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -596,15 +547,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -618,9 +569,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.176"
|
||||
version = "0.2.182"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
@@ -628,20 +579,11 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
@@ -654,37 +596,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.1"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -696,25 +618,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -733,41 +636,12 @@ version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.4"
|
||||
@@ -818,7 +692,7 @@ dependencies = [
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -829,18 +703,18 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -865,20 +739,11 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.3"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -888,9 +753,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.11"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -899,27 +764,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.6"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -928,12 +787,6 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -943,12 +796,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -981,15 +828,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1003,18 +850,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.6"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
@@ -1041,9 +889,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
version = "2.0.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1104,24 +952,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
version = "1.49.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1130,9 +973,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
@@ -1141,9 +984,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1152,9 +995,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
@@ -1173,9 +1016,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.20"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
@@ -1191,9 +1034,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
@@ -1211,17 +1054,11 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.104"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -1230,25 +1067,11 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.104"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1256,31 +1079,31 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.104"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.104"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.81"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
|
||||
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -1292,7 +1115,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1301,24 +1124,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -1329,65 +1134,27 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
name = "zerocopy"
|
||||
version = "0.8.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
+3
-3
@@ -18,7 +18,8 @@ categories = ["asynchronous", "concurrency", "data-structures"]
|
||||
exclude = [".claude/", ".jj/", "dagx-spec.md"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["derive"]
|
||||
derive = ["dep:dagx-macros"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[workspace.lints.rust]
|
||||
@@ -32,8 +33,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12"
|
||||
dagx-macros = { version = "0.3.1", path = "./dagx-macros" }
|
||||
dagx-macros = { version = "0.3.1", path = "./dagx-macros", optional = true }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -10,34 +10,20 @@ A minimal, type-safe, runtime-agnostic async DAG (Directed Acyclic Graph) execut
|
||||
|
||||
## Why dagx?
|
||||
|
||||
### 🚀 Blazing Fast: 1.04-129x faster than dagrs
|
||||
### Blazing Fast: 1-100x faster than dagrs
|
||||
|
||||
| Workload | Tasks | dagx | dagrs | Speedup |
|
||||
| ----------------- | ------ | ------- | ------- | ------------------ |
|
||||
| Sequential chain | 5 | 3.0 µs | 385 µs | **129x faster** 🚀 |
|
||||
| Sequential chain | 100 | 79 µs | 703 µs | **8.9x faster** |
|
||||
| Diamond pattern | 4 | 11 µs | 387 µs | **34x faster** |
|
||||
| Fan-out (1→100) | 101 | 155 µs | 595 µs | **3.85x faster** |
|
||||
| Independent tasks | 10,000 | 12.7 ms | 13.3 ms | **1.04x faster** |
|
||||
| Workload | Tasks | dagx | dagrs | Speedup |
|
||||
| ----------------- | ------ | --------- | --------- | ------------------ |
|
||||
| Sequential chain | 5 | 1.02 µs | 770.42 µs | **755x faster** 🚀 |
|
||||
| Diamond pattern | 4 | 5.16 µs | 770.87 µs | **149x faster** |
|
||||
| Sequential chain | 100 | 25.09 µs | 1.19 ms | **47.4x faster** |
|
||||
| Fan-out (1→100) | 101 | 100.75 µs | 1.02 ms | **10.1x faster** |
|
||||
| Independent tasks | 10,000 | 8.61 ms | 15.37 ms | **1.79x faster** |
|
||||
|
||||
**Per-task overhead:**
|
||||
|
||||
- Construction: ~100 ns/task
|
||||
- Inline execution (sequential): ~790 ns/task
|
||||
- Parallel execution: ~1.3 µs/task
|
||||
|
||||
### 🛡️ Compile-Time Safety
|
||||
|
||||
- **Cycles are impossible** — the type system prevents them at compile time, zero runtime overhead
|
||||
- **No runtime type errors** — dependencies validated at compile time
|
||||
- **Compiler-verified correctness** — no surprise failures in production
|
||||
|
||||
See [how it works](docs/CYCLE_PREVENTION.md).
|
||||
|
||||
### ✨ Simple API
|
||||
### Simple API
|
||||
|
||||
```rust
|
||||
let sum = dag.add_task(Add).depends_on((&x, &y));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await?;
|
||||
```
|
||||
|
||||
@@ -80,51 +66,49 @@ impl Add {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Add source tasks with no dependencies
|
||||
let x = dag.add_task(Value(2));
|
||||
let y: TaskHandle<_> = dag.add_task(Value(3)).into();
|
||||
let y = dag.add_task(Value(3));
|
||||
|
||||
// Add task that depends on both x and y.
|
||||
// Here, y could be reused as a dependency for other tasks, while x could not.
|
||||
let sum = dag.add_task(Add).depends_on((x, &y));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
|
||||
// Execute with true parallelism
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
|
||||
// Retrieve results
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
assert_eq!(output.get(sum), 5);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
dagx provides true parallel execution with sub-microsecond overhead per task.
|
||||
|
||||
**Why is dagx so fast?**
|
||||
|
||||
- **Inline fast-path**: Sequential chains execute inline without spawning (8.9-129x faster)
|
||||
- **Adaptive execution**: Inline for sequential work, true parallelism for concurrent work
|
||||
- **Zero-cost abstractions**: Generics and monomorphization eliminate overhead
|
||||
|
||||
See [design philosophy](docs/DESIGN_PHILOSOPHY.md) for details.
|
||||
|
||||
## Features
|
||||
|
||||
- **Compile-time cycle prevention**: Type system makes cycles impossible — no runtime checks
|
||||
- **Compile-time type safety**: Dependencies validated at compile time, no runtime type errors
|
||||
- **Works with ANY type**: Custom types work automatically — just `Send + Sync + 'static`, no trait implementations needed
|
||||
- **Runtime-agnostic**: Works with Tokio, async-std, smol, or any async runtime
|
||||
- **True parallelism**: Tasks spawn to multiple threads for genuine parallel execution
|
||||
- **Type-state pattern**: API prevents incorrect wiring through compile-time errors
|
||||
- **Zero-cost abstractions**: Leverages generics and monomorphization for minimal overhead
|
||||
- **Flexible task patterns**: Supports stateless, read-only, and mutable state tasks
|
||||
- **Simple API**: Just `#[task]`, `DagRunner`, `TaskHandle`, and `TaskBuilder`
|
||||
- **Comprehensive error handling**: Result-based errors with actionable messages
|
||||
- **Optional tracing**: Zero-cost observability via optional `tracing` feature flag
|
||||
### Compile-Time Safety
|
||||
|
||||
## Core Concepts
|
||||
- **Cycles are impossible** — the type system prevents them at compile time, zero runtime overhead
|
||||
- **No runtime type errors** — dependencies validated at compile time
|
||||
- **Compiler-verified correctness** — no surprise failures in production
|
||||
|
||||
See [how it works](docs/CYCLE_PREVENTION.md).
|
||||
|
||||
### Runtime Agnostic
|
||||
|
||||
dagx works with any async runtime. Provide a spawner function to `run()`:
|
||||
|
||||
```rust
|
||||
// With Tokio
|
||||
// The join handle result can be unwrapped because dagx catches panics internally
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
|
||||
// With smol
|
||||
dag.run(|fut| smol::spawn(fut)).await.unwrap();
|
||||
|
||||
// Single-threaded concurrency on the invoking runtime
|
||||
// Can be faster in situations where waiting time dominates
|
||||
dag.run(|fut| fut).await.unwrap()
|
||||
```
|
||||
|
||||
### Task Patterns
|
||||
|
||||
@@ -166,73 +150,41 @@ impl Counter {
|
||||
}
|
||||
```
|
||||
|
||||
### DagRunner
|
||||
### Tracing
|
||||
|
||||
The `DagRunner` orchestrates task execution:
|
||||
dagx provides optional observability using the `tracing` crate, controlled by the `tracing` feature flag.
|
||||
|
||||
```rust
|
||||
let dag = DagRunner::new();
|
||||
let handle = dag.add_task(MyTask::new());
|
||||
**Enabling Tracing**
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dagx = { version = "0.3", features = ["tracing"] }
|
||||
tracing-subscriber = "0.3"
|
||||
```
|
||||
|
||||
### TaskHandle
|
||||
**Log Levels**
|
||||
|
||||
A `TaskHandle<T>` is a typed reference to a task's output. Use it to wire dependencies and retrieve results:
|
||||
- **INFO**: DAG execution start/completion
|
||||
- **DEBUG**: Task additions, dependency wiring, layer computation
|
||||
- **TRACE**: Individual task execution (inline vs spawned), detailed execution flow
|
||||
- **ERROR**: Task panics, concurrent execution attempts
|
||||
|
||||
```rust
|
||||
// Single dependency
|
||||
let task = dag.add_task(my_task).depends_on(&upstream);
|
||||
### Other
|
||||
|
||||
// Multiple dependencies (order matters!)
|
||||
let task = dag.add_task(my_task).depends_on((&upstream1, &upstream2));
|
||||
```
|
||||
- **True parallelism**: Chosen runtime executes tasks concurrently and/or in parallel
|
||||
- **No boilerplate**: The `derive` feature and the `#[task]` macro are enabled by default to simplify task implementation.
|
||||
|
||||
### Custom Types
|
||||
## Performance
|
||||
|
||||
**dagx works with ANY type automatically!** As long as your type implements `Send + Sync + 'static`, it works seamlessly:
|
||||
dagx provides true parallel execution with sub-microsecond overhead per task.
|
||||
|
||||
```rust
|
||||
struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
**How is dagx so fast?**
|
||||
|
||||
#[task]
|
||||
impl CreateUser {
|
||||
async fn run(&self) -> User {
|
||||
User { name: "Alice".to_string(), age: 30 }
|
||||
}
|
||||
}
|
||||
- **Inline fast-path**: Sequential chains execute inline without spawning
|
||||
- **Adaptive execution**: Inline for sequential work, executor-agnostic parallelism for concurrent work
|
||||
- **Zero-cost abstractions**: Compile-time graph validation eliminates overhead
|
||||
|
||||
#[task]
|
||||
impl ProcessUser {
|
||||
async fn run(user: &User) -> String {
|
||||
format!("{} is {} years old", user.name, user.age)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
### Runtime Agnostic
|
||||
|
||||
dagx works with any async runtime. Provide a spawner function to `run()`:
|
||||
|
||||
```rust
|
||||
// With Tokio
|
||||
// The join handle result can be unwrapped because dagx catches panics internally
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
|
||||
// With async-std
|
||||
dag.run(|fut| async_std::task::spawn(fut)).await.unwrap();
|
||||
|
||||
// With smol
|
||||
dag.run(|fut| smol::spawn(fut)).await.unwrap();
|
||||
|
||||
// Single-threaded on the invoking runtime
|
||||
// Faster for situations with many computationally light tasks
|
||||
dag.run(|fut| fut).await.unwrap()
|
||||
```
|
||||
See [design philosophy](docs/DESIGN_PHILOSOPHY.md) for details.
|
||||
|
||||
## Tutorials & Examples
|
||||
|
||||
@@ -262,59 +214,18 @@ Real-world patterns:
|
||||
- [`data_pipeline.rs`](examples/data_pipeline.rs) - ETL data processing pipeline
|
||||
- [`error_handling.rs`](examples/error_handling.rs) - Error propagation and recovery
|
||||
|
||||
Run any example: `cargo run --example custom_types`
|
||||
Run any example: `cargo run --example circuit_breaker`
|
||||
|
||||
## Advanced Topics
|
||||
## Documentation
|
||||
|
||||
Detailed documentation on dagx internals and advanced features:
|
||||
Full API documentation is available at [docs.rs/dagx](https://docs.rs/dagx).
|
||||
|
||||
Detailed documentation on dagx's internals and advanced features:
|
||||
|
||||
- [**Compile-Time Cycle Prevention**](docs/CYCLE_PREVENTION.md) - How the type system prevents cycles
|
||||
- [**Design Philosophy**](docs/DESIGN_PHILOSOPHY.md) - Primitives as scheduler, inline fast-path optimization
|
||||
- [**Tracing Support**](docs/TRACING.md) - Zero-cost observability with the `tracing` crate
|
||||
- [**Library Comparisons**](docs/COMPARISONS.md) - Detailed comparison with dagrs, async_dag, and others
|
||||
|
||||
## Important Limitations
|
||||
|
||||
### Tasks Cannot Return Bare Tuples
|
||||
|
||||
Tasks cannot return bare tuples as output types. If you need to return multiple values, use one of these workarounds:
|
||||
|
||||
**Option 1: Use a struct (recommended)**
|
||||
|
||||
```rust
|
||||
struct UserData {
|
||||
name: String,
|
||||
age: i32,
|
||||
}
|
||||
|
||||
struct FetchUser;
|
||||
|
||||
#[task]
|
||||
impl FetchUser {
|
||||
async fn run(id: &i32) -> UserData {
|
||||
UserData {
|
||||
name: "Alice".to_string(),
|
||||
age: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Option 2: Wrap in Result**
|
||||
|
||||
```rust
|
||||
struct FetchData;
|
||||
|
||||
#[task]
|
||||
impl FetchData {
|
||||
async fn run(id: &i32) -> Result<(String, i32), String> {
|
||||
Ok(("Alice".to_string(), 30))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Structs are preferred because they're self-documenting and easier to refactor.
|
||||
|
||||
## When to Use dagx
|
||||
|
||||
dagx is ideal for:
|
||||
@@ -346,16 +257,12 @@ xdg-open target/criterion/report/index.html
|
||||
start target/criterion/report/index.html
|
||||
```
|
||||
|
||||
_Benchmarks run on AMD Ryzen 7 7840U (Zen 4) @ 3.3GHz._
|
||||
_Benchmarks run on Intel i9-13950HX @ 5.5GHz._
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project follows the [Builder's Code of Conduct](https://builderscode.org).
|
||||
|
||||
## Documentation
|
||||
|
||||
Full API documentation is available at [docs.rs/dagx](https://docs.rs/dagx).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
+9
-13
@@ -8,7 +8,7 @@ dagx is a task execution library designed for safety and predictability. This do
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.1.x | :white_check_mark: |
|
||||
| 0.3.x | :white_check_mark: |
|
||||
|
||||
## Security Model
|
||||
|
||||
@@ -17,11 +17,10 @@ dagx is a task execution library designed for safety and predictability. This do
|
||||
- **Compile-time type safety**: Dependencies are validated at compile time, preventing type errors
|
||||
- **Cycle detection**: Runtime detection of circular dependencies
|
||||
- **Memory safety**: Rust's ownership system prevents memory corruption
|
||||
- **Thread safety**: Interior mutability uses `parking_lot::Mutex` for safe concurrent access
|
||||
- **Panic isolation**: Panics in any task will be caught and not propagated through the executor
|
||||
|
||||
### What dagx Does NOT Provide
|
||||
|
||||
- **Panic isolation**: Panics in one task will propagate and may affect other tasks (see Panic Handling below)
|
||||
- **Resource limits**: No built-in timeout, memory limits, or CPU throttling
|
||||
- **Sandboxing**: Tasks run with full process privileges
|
||||
- **Authentication/Authorization**: No access control between tasks
|
||||
@@ -31,15 +30,14 @@ dagx is a task execution library designed for safety and predictability. This do
|
||||
|
||||
### 1. Panic Handling
|
||||
|
||||
**Current Behavior**: dagx does NOT isolate panics between tasks. If a task panics:
|
||||
**Current Behavior**: dagx isolates panics between tasks. If a task panics:
|
||||
|
||||
- The panic will propagate through the executor
|
||||
- Other tasks in the same execution layer may or may not complete (timing-dependent)
|
||||
- The entire DAG execution will terminate
|
||||
- The panic will be caught and converted to a `TaskPanicked` error
|
||||
- Other tasks in the same execution layer will complete
|
||||
- The entire DAG execution will terminate after layer execution has finished and return **only the first panic**
|
||||
|
||||
**Recommendation**:
|
||||
|
||||
- Use `std::panic::catch_unwind` in tasks that may panic
|
||||
- Return `Result<T, E>` from tasks and handle errors explicitly
|
||||
- Test panic scenarios thoroughly
|
||||
|
||||
@@ -250,10 +248,9 @@ impl MonitoredTask {
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **No panic recovery**: Task panics will crash the DAG execution
|
||||
2. **No resource limits**: Tasks can consume unlimited CPU, memory, or time
|
||||
3. **No isolation**: All tasks run in the same process with same privileges
|
||||
4. **Shared executor**: Tasks compete for executor resources
|
||||
1. **No resource limits**: Tasks can consume unlimited CPU, memory, or time
|
||||
2. **No isolation**: All tasks run in the same process with same privileges
|
||||
3. **Shared executor**: Tasks compete for executor resources
|
||||
|
||||
### Not Security Features
|
||||
|
||||
@@ -303,7 +300,6 @@ Security updates will be:
|
||||
|
||||
- [Rust Security Guidelines](https://anssi-fr.github.io/rust-guide/)
|
||||
- [OWASP Secure Coding Practices](https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/)
|
||||
- [async-std Security](https://async.rs/security)
|
||||
- [Tokio Security](https://tokio.rs/tokio/topics/security)
|
||||
|
||||
## License
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn bench_dag_creation(c: &mut Criterion) {
|
||||
|
||||
c.bench_function("add_100_tasks", |b| {
|
||||
b.iter(|| {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
for i in 0..100 {
|
||||
black_box(dag.add_task(Value(i)));
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@ pub fn bench_dag_scaling(c: &mut Criterion) {
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
for i in 0..size {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
}
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
//! Long sequential chain: 100 tasks in sequence
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::TaskHandle;
|
||||
|
||||
pub fn bench_deep_chain(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("deep_chain_100_tasks");
|
||||
let mut group = c.benchmark_group("comparisons/deep_chain_100_tasks");
|
||||
|
||||
// dagx implementation
|
||||
group.bench_function("dagx", |b| {
|
||||
@@ -16,23 +15,24 @@ pub fn bench_deep_chain(c: &mut Criterion) {
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let first: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)).into();
|
||||
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
|
||||
let mut prev = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(first);
|
||||
.depends_on(&first);
|
||||
|
||||
for i in 2..100 {
|
||||
prev = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
||||
.depends_on(prev);
|
||||
.depends_on(&prev);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
dag.get(prev).unwrap()
|
||||
output.get(prev)
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
//! Classic diamond: A → B,C → D
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::TaskHandle;
|
||||
|
||||
pub fn bench_diamond_pattern(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("diamond_pattern");
|
||||
let mut group = c.benchmark_group("comparisons/diamond_pattern");
|
||||
|
||||
// dagx implementation
|
||||
group.bench_function("dagx", |b| {
|
||||
@@ -16,23 +15,24 @@ pub fn bench_diamond_pattern(c: &mut Criterion) {
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(a);
|
||||
.depends_on(&a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(a);
|
||||
.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() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
dag.get(d).unwrap()
|
||||
output.get(d)
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ use criterion::Criterion;
|
||||
|
||||
pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("etl_pipeline");
|
||||
let mut group = c.benchmark_group("comparisons/etl_pipeline");
|
||||
|
||||
// dagx implementation
|
||||
group.bench_function("dagx", |b| {
|
||||
@@ -15,7 +15,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Extract from 3 sources
|
||||
let source1 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]));
|
||||
@@ -27,19 +27,19 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
|
||||
data.iter().map(|x| x * 2).collect::<Vec<_>>()
|
||||
}))
|
||||
.depends_on(source1);
|
||||
.depends_on(&source1);
|
||||
|
||||
let transform2 = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
|
||||
data.iter().map(|x| x * 2).collect::<Vec<_>>()
|
||||
}))
|
||||
.depends_on(source2);
|
||||
.depends_on(&source2);
|
||||
|
||||
let transform3 = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
|
||||
data.iter().map(|x| x * 2).collect::<Vec<_>>()
|
||||
}))
|
||||
.depends_on(source3);
|
||||
.depends_on(&source3);
|
||||
|
||||
// Merge all transformed data
|
||||
let merge = dag
|
||||
@@ -61,19 +61,20 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
.filter(|&x| x > 0)
|
||||
.collect::<Vec<i32>>()
|
||||
}))
|
||||
.depends_on(merge);
|
||||
.depends_on(&merge);
|
||||
|
||||
// Load (compute sum)
|
||||
let load = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
|
||||
data.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(validate);
|
||||
.depends_on(&validate);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
dag.get(load).unwrap()
|
||||
output.get(load)
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ use criterion::Criterion;
|
||||
|
||||
pub fn bench_large_scale(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("large_scale_10k_tasks");
|
||||
let mut group = c.benchmark_group("comparisons/large_scale_10k_tasks");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(std::time::Duration::from_secs(20));
|
||||
|
||||
@@ -17,12 +17,13 @@ pub fn bench_large_scale(c: &mut Criterion) {
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2));
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ use criterion::Criterion;
|
||||
|
||||
pub fn bench_linear_pipeline(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("linear_pipeline_5_tasks");
|
||||
let mut group = c.benchmark_group("comparisons/linear_pipeline_5_tasks");
|
||||
|
||||
// dagx implementation
|
||||
group.bench_function("dagx", |b| {
|
||||
@@ -15,26 +15,27 @@ pub fn bench_linear_pipeline(c: &mut Criterion) {
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(a);
|
||||
.depends_on(&a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(b);
|
||||
.depends_on(&b);
|
||||
let d = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 1))
|
||||
.depends_on(c);
|
||||
.depends_on(&c);
|
||||
let e = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(d);
|
||||
.depends_on(&d);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
dag.get(e).unwrap()
|
||||
output.get(e)
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
//! One source task feeding 100 dependent tasks
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::TaskHandle;
|
||||
|
||||
pub fn bench_wide_fanout(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("wide_fanout_100_tasks");
|
||||
let mut group = c.benchmark_group("comparisons/wide_fanout_100_tasks");
|
||||
|
||||
// dagx implementation
|
||||
group.bench_function("dagx", |b| {
|
||||
@@ -16,16 +15,17 @@ pub fn bench_wide_fanout(c: &mut Criterion) {
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
let mut dag = DagRunner::new();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
// Create 100 tasks that all depend on source
|
||||
for i in 0..100 {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
|
||||
// a) Adding 10k tasks (construction only)
|
||||
group.bench_function("construction", |b| {
|
||||
b.iter(|| {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
}
|
||||
@@ -23,11 +23,12 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
|
||||
group.bench_function("full_execution", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
}
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
@@ -38,7 +39,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
|
||||
group.bench_function("execution_only", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
}
|
||||
@@ -46,7 +47,8 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
|
||||
},
|
||||
|dag| {
|
||||
rt.block_on(async {
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Diamond pattern benchmarks (fan-out then fan-in)
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
pub fn bench_diamond(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
@@ -9,24 +9,24 @@ pub fn bench_diamond(c: &mut Criterion) {
|
||||
c.bench_function("diamond_pattern_50", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
for i in 0..50 {
|
||||
let source: TaskHandle<_> =
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
|
||||
let left = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
let right = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
|
||||
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() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
||||
+14
-14
@@ -1,7 +1,7 @@
|
||||
//! Fan-out pattern benchmarks (1 → N dependencies)
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
pub fn bench_fanout(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
@@ -10,15 +10,16 @@ pub fn bench_fanout(c: &mut Criterion) {
|
||||
c.bench_function("fanout_1_to_100_i32", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
let mut dag = DagRunner::new();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
for i in 0..100 {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
@@ -29,23 +30,22 @@ pub fn bench_fanout(c: &mut Criterion) {
|
||||
c.bench_function("fanout_1_to_100_vec", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
// Create 1000 strings - framework wraps in Arc automatically
|
||||
(0..1000).map(|i| format!("Item {}", i)).collect::<Vec<_>>()
|
||||
}))
|
||||
.into();
|
||||
let mut dag = DagRunner::new();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
// Create 1000 strings - framework wraps in Arc automatically
|
||||
(0..1000).map(|i| format!("Item {}", i)).collect::<Vec<_>>()
|
||||
}));
|
||||
|
||||
for i in 0..100 {
|
||||
dag.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
|
||||
// Each consumer gets data extracted from Arc
|
||||
data.iter().filter(|s| s.contains(&i.to_string())).count()
|
||||
}))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
||||
+19
-20
@@ -1,7 +1,7 @@
|
||||
//! ETL (Extract, Transform, Load) pipeline benchmark
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
@@ -17,23 +17,21 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
|b, &count| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Extract: Load JSON-like data (framework wraps in Arc automatically)
|
||||
let extract: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
format!(
|
||||
"{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}",
|
||||
i,
|
||||
i,
|
||||
i * 10
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}))
|
||||
.into();
|
||||
let extract = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
format!(
|
||||
"{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}",
|
||||
i,
|
||||
i,
|
||||
i * 10
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}));
|
||||
|
||||
// Transform: Three parallel transformations
|
||||
|
||||
@@ -48,7 +46,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
})
|
||||
.count()
|
||||
}))
|
||||
.depends_on(extract);
|
||||
.depends_on(&extract);
|
||||
|
||||
// T2: Extract scores
|
||||
let scores = dag
|
||||
@@ -61,7 +59,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}))
|
||||
.depends_on(extract);
|
||||
.depends_on(&extract);
|
||||
|
||||
// T3: Extract names
|
||||
let names = dag
|
||||
@@ -75,7 +73,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}))
|
||||
.depends_on(extract);
|
||||
.depends_on(&extract);
|
||||
|
||||
// Load: Aggregate all transformations
|
||||
dag.add_task(task_fn::<(usize, Vec<_>, Vec<_>), _, _>(
|
||||
@@ -92,7 +90,8 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
))
|
||||
.depends_on((&validate, &scores, &names));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Memory efficiency benchmark with large data structures
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
use std::hint::black_box;
|
||||
|
||||
@@ -10,21 +10,19 @@ pub fn bench_memory_efficiency(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("memory_efficiency");
|
||||
|
||||
// Test with different data sizes (in MB)
|
||||
for mb_size in [1, 5, 10].iter() {
|
||||
for mb_size in [1, 10].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("large_data_20_consumers", format!("{}MB", mb_size)),
|
||||
mb_size,
|
||||
|b, &mb| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
// Create MB of data - framework wraps in Arc automatically
|
||||
vec![0u8; mb * 1024 * 1024]
|
||||
}))
|
||||
.into();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
// Create MB of data - framework wraps in Arc automatically
|
||||
vec![0u8; mb * 1024 * 1024]
|
||||
}));
|
||||
|
||||
// 20 consumers share the data via automatic Arc wrapping
|
||||
for i in 0..20 {
|
||||
@@ -38,10 +36,11 @@ pub fn bench_memory_efficiency(c: &mut Criterion) {
|
||||
.sum();
|
||||
black_box(sample_sum)
|
||||
}))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
||||
+13
-14
@@ -1,7 +1,7 @@
|
||||
//! Mixed realistic patterns - fan-out, fan-in, and processing
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use dagx::DagRunner;
|
||||
use dagx_test::task_fn;
|
||||
pub fn bench_mixed_patterns(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
@@ -9,33 +9,31 @@ pub fn bench_mixed_patterns(c: &mut Criterion) {
|
||||
c.bench_function("mixed_patterns_realistic", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Stage 1: Load data (framework wraps in Arc automatically)
|
||||
let data: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
(0..1000)
|
||||
.map(|i| format!("Record-{:04}", i))
|
||||
.collect::<Vec<_>>()
|
||||
}))
|
||||
.into();
|
||||
let data = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
(0..1000)
|
||||
.map(|i| format!("Record-{:04}", i))
|
||||
.collect::<Vec<_>>()
|
||||
}));
|
||||
|
||||
// Stage 2: Fan-out for parallel analysis
|
||||
let analysis1 = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|d: &Vec<String>| {
|
||||
d.iter().filter(|s| s.contains("00")).count()
|
||||
}))
|
||||
.depends_on(data);
|
||||
.depends_on(&data);
|
||||
|
||||
let analysis2 = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|d: &Vec<String>| {
|
||||
d.iter().map(|s| s.len()).sum::<usize>()
|
||||
}))
|
||||
.depends_on(data);
|
||||
.depends_on(&data);
|
||||
|
||||
let analysis3 = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|d: &Vec<String>| d.len()))
|
||||
.depends_on(data);
|
||||
.depends_on(&data);
|
||||
|
||||
// Stage 3: Fan-in to aggregate
|
||||
let summary = dag
|
||||
@@ -51,10 +49,11 @@ pub fn bench_mixed_patterns(c: &mut Criterion) {
|
||||
dag.add_task(task_fn::<String, _, _>(move |report: &String| {
|
||||
format!("Destination {}: {}", i, report)
|
||||
}))
|
||||
.depends_on(summary);
|
||||
.depends_on(&summary);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
||||
+9
-52
@@ -152,13 +152,13 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret
|
||||
/// }
|
||||
///
|
||||
/// // Generated:
|
||||
/// impl Task for Add {
|
||||
/// type Input = (i32, i32);
|
||||
/// impl Task<(i32, i32)> for Add {
|
||||
/// type Output = i32;
|
||||
///
|
||||
/// async fn run(&mut self, input: Self::Input) -> Self::Output {
|
||||
/// let (a, b) = input;
|
||||
/// Self::run_impl(&a, &b).await
|
||||
/// async fn run(&mut self, _input: Self::Input) -> Self::Output {
|
||||
/// let (a, _input) = _input.next();
|
||||
/// let (b, _input) = _input.next();
|
||||
/// Self::run_impl(a, b).await
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@@ -169,37 +169,6 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// For stateful tasks (with &mut self):
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Your code:
|
||||
/// #[task]
|
||||
/// impl Counter {
|
||||
/// async fn run(&mut self, inc: &i32) -> i32 {
|
||||
/// self.count += inc;
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Generated:
|
||||
/// impl Task for Counter {
|
||||
/// type Input = i32;
|
||||
/// type Output = i32;
|
||||
///
|
||||
/// async fn run(&mut self, input: Self::Input) -> Self::Output {
|
||||
/// let inc = input;
|
||||
/// self.run_impl(&inc).await
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl Counter {
|
||||
/// async fn run_impl(&mut self, inc: &i32) -> i32 {
|
||||
/// self.count += inc;
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let impl_block = parse_macro_input!(item as ItemImpl);
|
||||
@@ -275,18 +244,7 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Extract return type
|
||||
let output_type = match &run_method.sig.output {
|
||||
ReturnType::Default => syn::parse_quote!(()),
|
||||
ReturnType::Type(_, ty) => {
|
||||
if let Type::Tuple(_tuple) = &**ty {
|
||||
return syn::Error::new_spanned(
|
||||
ty,
|
||||
"Returning bare tuples from task functions is currently not supported.\n\n\
|
||||
Please wrap this in a newtype or struct.",
|
||||
)
|
||||
.into_compile_error()
|
||||
.into();
|
||||
}
|
||||
ty.clone()
|
||||
}
|
||||
ReturnType::Type(_, ty) => ty.clone(),
|
||||
};
|
||||
|
||||
// Build Input type based on parameter count
|
||||
@@ -301,7 +259,7 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let ident = Ident::new(&format!("__dagx_extract_{}", ident), Span::mixed_site());
|
||||
(
|
||||
quote! {
|
||||
let (#ident, input) = input.next();
|
||||
let (#ident, _input) = _input.next();
|
||||
},
|
||||
ident,
|
||||
)
|
||||
@@ -332,11 +290,10 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
|
||||
// Generate the Task trait implementation based on whether we have self and async/sync
|
||||
quote! {
|
||||
impl ::dagx::Task for #struct_name {
|
||||
type Input = #input_type;
|
||||
impl ::dagx::Task<#input_type> for #struct_name {
|
||||
type Output = #output_type;
|
||||
|
||||
async fn run(mut self, mut input: ::dagx::TaskInput<'_, Self::Input>) -> Self::Output {
|
||||
async fn run(mut self, mut _input: ::dagx::TaskInput<'_, #input_type>) -> Self::Output {
|
||||
#param_destructure
|
||||
#run_call
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Stephen Waits <steve@waits.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#![cfg(not(tarpaulin_include))]
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::extract::ExtractInput;
|
||||
|
||||
use dagx::{Task, TaskInput};
|
||||
@@ -18,16 +20,16 @@ where
|
||||
O: Send,
|
||||
{
|
||||
f: F,
|
||||
_phantom: std::marker::PhantomData<fn(I) -> O>,
|
||||
_phantom: PhantomData<fn(I) -> O>,
|
||||
}
|
||||
|
||||
impl<I, O, F> Task for TaskFn<I, O, F>
|
||||
impl<I, O, F> Task<I::Input> for TaskFn<I, O, F>
|
||||
where
|
||||
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
|
||||
I: ExtractInput + Send + Sync + 'static,
|
||||
O: Send + Sync,
|
||||
I::Input: Send + Sync + 'static,
|
||||
O: Send + Sync + 'static,
|
||||
{
|
||||
type Input = I::Input;
|
||||
type Output = O;
|
||||
|
||||
async fn run(mut self, input: TaskInput<'_, I::Input>) -> O {
|
||||
@@ -45,6 +47,6 @@ where
|
||||
{
|
||||
TaskFn {
|
||||
f,
|
||||
_phantom: std::marker::PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
+20
-65
@@ -15,7 +15,7 @@ impl Value { async fn run(&self) -> i32 { self.0 } }
|
||||
#[task]
|
||||
impl Add { async fn run(a: &i32, b: &i32) -> i32 { a + b } }
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let x = dag.add_task(Value(2));
|
||||
let y = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
@@ -66,36 +66,18 @@ graph.update_dependency(y, sum, 1)?;
|
||||
|
||||
## Quick Comparison
|
||||
|
||||
| Project | License | Runtime | Type Safety | API Complexity | Performance vs dagx | Key Features |
|
||||
| ------------------------------------------------------------- | -------------- | ------------------- | --------------------- | -------------- | ---------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| **dagx** | MIT | Any async runtime | Compile-time | Simple | Baseline (see benchmarks) | Primitives-as-scheduler, inline fast-path, automatic Arc wrapping, up to 8 deps/task |
|
||||
| [**dagrs**](https://github.com/dagrs-dev/dagrs) (v0.5) | MIT/Apache-2.0 | Creates own runtime | Runtime (async_trait) | Complex | 1.04-129x slower (see below) | Flow-based Programming, cyclic graphs, loops, conditional nodes, YAML config |
|
||||
| [**async_dag**](https://github.com/chubei-oppen/async_dag) | MIT | Any async runtime | Compile-time | Medium | No benchmarks | Slot-based dependencies, Graph/TryGraph modes, maximum parallelism |
|
||||
| [**dag-flow**](https://github.com/makisevon/dag-flow) | MIT/Apache-2.0 | Any async runtime | Runtime (HashMap) | Complex | No benchmarks | Experimental, all tasks run simultaneously, weak dependencies |
|
||||
| [**RenovZ/dag-runner**](https://github.com/RenovZ/dag-runner) | MIT | Tokio only | Unclear | Simple | No benchmarks | Edge-based API, cycle detection, stops on first error |
|
||||
| [**tasksitter**](https://github.com/lionkor/tasksitter) | Unspecified | Unclear | Unclear | Medium | No benchmarks | Cyclic graphs, dynamic runtime modification, pause/resume |
|
||||
| Project | License | Runtime | Type Safety | API Complexity | Performance vs dagx | Key Features |
|
||||
| ------------------------------------------------------------- | -------------- | ------------------- | --------------------- | -------------- | ------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| **dagx** | MIT | Any async runtime | Compile-time | Simple | Baseline (see benchmarks) | Primitives-as-scheduler, inline fast-path, automatic Arc wrapping, up to 8 deps/task |
|
||||
| [**dagrs**](https://github.com/dagrs-dev/dagrs) (v0.5) | MIT/Apache-2.0 | Creates own runtime | Runtime (async_trait) | Complex | 1-100x slower (see below) | Flow-based Programming, cyclic graphs, loops, conditional nodes, YAML config |
|
||||
| [**async_dag**](https://github.com/chubei-oppen/async_dag) | MIT | Any async runtime | Compile-time | Medium | No benchmarks | Slot-based dependencies, Graph/TryGraph modes, maximum parallelism |
|
||||
| [**dag-flow**](https://github.com/makisevon/dag-flow) | MIT/Apache-2.0 | Any async runtime | Runtime (HashMap) | Complex | No benchmarks | Experimental, all tasks run simultaneously, weak dependencies |
|
||||
| [**RenovZ/dag-runner**](https://github.com/RenovZ/dag-runner) | MIT | Tokio only | Unclear | Simple | No benchmarks | Edge-based API, cycle detection, stops on first error |
|
||||
| [**tasksitter**](https://github.com/lionkor/tasksitter) | Unspecified | Unclear | Unclear | Medium | No benchmarks | Cyclic graphs, dynamic runtime modification, pause/resume |
|
||||
|
||||
_GitHub stars (as of 2025): dagrs (449), async_dag (25), dag-flow (2), others (0-2)_
|
||||
**Performance**: As noted in the README, dagx is **1-100x faster** than dagrs across all benchmark patterns. The inline fast-path optimization eliminates spawning overhead for sequential workloads while maintaining excellent parallel performance. Most real-world DAGs mix sequential and parallel patterns. dagx automatically optimizes for both, performing up to 2 orders of magnitude faster than dagrs regardless of workload shape.
|
||||
|
||||
## Performance Benchmarks vs dagrs
|
||||
|
||||
Direct comparison benchmarks (lower is better):
|
||||
|
||||
| Workload | Tasks | dagx | dagrs | Speedup |
|
||||
| ----------------- | ------ | ------- | ------- | ------------------ |
|
||||
| Sequential chain | 5 | 3.0 µs | 385 µs | **129x faster** 🚀 |
|
||||
| Sequential chain | 100 | 79 µs | 703 µs | **8.9x faster** |
|
||||
| Diamond pattern | 4 | 11 µs | 387 µs | **34x faster** |
|
||||
| Fan-out (1→100) | 101 | 155 µs | 595 µs | **3.85x faster** |
|
||||
| Independent tasks | 10,000 | 12.7 ms | 13.3 ms | **1.04x faster** |
|
||||
|
||||
**Summary**: dagx is **1.04-129x faster** than dagrs across all benchmark patterns. The inline fast-path optimization eliminates spawning overhead for sequential workloads while maintaining excellent parallel performance.
|
||||
|
||||
**How does dagx achieve this?** The inline fast-path detects single-task layers (common in sequential chains) and executes them directly without spawning overhead. For parallel workloads, tasks still spawn to maximize concurrency. This adaptive execution strategy combines the best of both worlds: sub-microsecond overhead for sequential work, true parallelism for concurrent work.
|
||||
|
||||
**Key insight**: Most real-world DAGs mix sequential and parallel patterns. dagx automatically optimizes for both, delivering 1.04-129x better performance than dagrs regardless of workload shape.
|
||||
|
||||
_Benchmarks run on AMD Ryzen 7 7840U (Zen 4) @ 3.3GHz. Run `cargo bench` to test on your hardware._
|
||||
_Benchmarks run on Intel i9-13950HX @ 5.5GHz. Run `cargo bench` to test on your hardware._
|
||||
|
||||
## Detailed Comparison
|
||||
|
||||
@@ -107,7 +89,6 @@ _Benchmarks run on AMD Ryzen 7 7840U (Zen 4) @ 3.3GHz. Run `cargo bench` to test
|
||||
|
||||
- Most mature (449 GitHub stars, active community)
|
||||
- Rich feature set: Flow-based Programming, cyclic graphs, loops, conditional nodes
|
||||
- YAML configuration support for declarative workflows
|
||||
- Designed for complex orchestration patterns
|
||||
|
||||
**Trade-offs**:
|
||||
@@ -115,29 +96,7 @@ _Benchmarks run on AMD Ryzen 7 7840U (Zen 4) @ 3.3GHz. Run `cargo bench` to test
|
||||
- Creates own Tokio runtime internally (not runtime-agnostic, cannot be nested)
|
||||
- More complex API: `Action` trait, `InChannels`/`OutChannels`, `NodeTable`, `Content` wrappers, manual node ID tracking
|
||||
- Uses `async_trait` for type erasure (runtime overhead)
|
||||
- **1.04-129x slower than dagx** across all benchmark patterns (see comparison benchmarks)
|
||||
|
||||
**API Style** (v0.5):
|
||||
|
||||
```rust
|
||||
// Define action with channels for communication
|
||||
#[async_trait]
|
||||
impl Action for MyTask {
|
||||
async fn run(&self, input: &mut InChannels,
|
||||
output: &mut OutChannels, env: Arc<EnvVar>) -> Output {
|
||||
let inputs: Vec<T> = input.map(|c| ...).await; // Manually extract
|
||||
output.broadcast(Content::new(result)).await; // Manually wrap
|
||||
Output::Out(Some(Content::new(result)))
|
||||
}
|
||||
}
|
||||
|
||||
let mut table = NodeTable::new();
|
||||
let node = DefaultNode::with_action("name".into(), MyTask, &mut table);
|
||||
let node_id = node.id(); // Must capture ID before moving
|
||||
graph.add_node(node);
|
||||
graph.add_edge(source_id, vec![target_id]);
|
||||
graph.set_env(EnvVar::new(table)); graph.start().unwrap();
|
||||
```
|
||||
- **Slower than dagx** across all benchmark patterns (see comparison benchmarks)
|
||||
|
||||
### async_dag (Clean Type Safety)
|
||||
|
||||
@@ -154,8 +113,7 @@ graph.set_env(EnvVar::new(table)); graph.start().unwrap();
|
||||
|
||||
- Medium API complexity: slot-based dependency management
|
||||
- Must manually specify slot indices (0, 1, etc.) when connecting tasks
|
||||
- Less mature (25 stars)
|
||||
- No performance benchmarks
|
||||
- Less mature
|
||||
|
||||
**API Style**:
|
||||
|
||||
@@ -188,7 +146,7 @@ graph.run().await;
|
||||
- Runtime type safety via `HashMap<String, Input>`
|
||||
- Complex API: implement `Task` trait with `id()`, `dependencies()`, `run()`
|
||||
- Named dependencies (string-based lookup)
|
||||
- Very early stage (2 stars)
|
||||
- Very early stage
|
||||
|
||||
**API Style**:
|
||||
|
||||
@@ -219,7 +177,7 @@ impl Task<String, Bytes> for MyTask {
|
||||
- Requires Tokio runtime
|
||||
- Manual channel setup for task communication
|
||||
- Type safety model unclear
|
||||
- Very early stage (0 stars)
|
||||
- Very early stage
|
||||
|
||||
**API Style**:
|
||||
|
||||
@@ -245,29 +203,26 @@ dag.run().await?;
|
||||
|
||||
- Limited documentation
|
||||
- Runtime and type safety model unclear
|
||||
- Very early stage (0 stars)
|
||||
- Very early stage
|
||||
|
||||
## When to Choose dagx
|
||||
|
||||
Choose dagx when you value:
|
||||
|
||||
- **Performance**: 1.04-129x faster than dagrs across all workload patterns (see benchmarks)
|
||||
- **Runtime flexibility**: Works with Tokio, async-std, smol, or any async runtime
|
||||
- **Compile-time safety**: Full type safety with no runtime type errors in the public API
|
||||
- **Minimal overhead**: ~790 ns per task (sequential), ~1.3 µs per task (parallel)
|
||||
- **Performance**: Faster than dagrs across all workload patterns (see benchmarks)
|
||||
- **Runtime flexibility**: Works with Tokio, smol, or any async runtime
|
||||
- **Compile-time safety**: Full type safety with no runtime type errors possible
|
||||
- **Simple, ergonomic API**: `#[task]` macro, `add_task()`, `depends_on()` - that's it
|
||||
- **Automatic optimizations**: Arc wrapping, inline execution, adaptive spawning - all transparent
|
||||
- **Predictable performance**: Linear scaling, no hidden complexity, consistent sub-µs overhead
|
||||
|
||||
dagx is **not** the right choice if you need:
|
||||
|
||||
- Cyclic graphs or dynamic flow control (loops, conditions) → Consider **dagrs** or **tasksitter**
|
||||
- More than 8 dependencies per task → Consider **dagrs** or **async_dag**
|
||||
- YAML-based configuration → Consider **dagrs**
|
||||
|
||||
## When to Consider Alternatives
|
||||
|
||||
- **Choose dagrs** if you need advanced flow control (loops, conditionals, cyclic graphs), YAML configuration, or are already committed to Tokio and want a mature, feature-rich solution
|
||||
- **Choose dagrs** if you need advanced flow control (loops, conditionals, cyclic graphs), or are already committed to Tokio and want a mature, feature-rich solution
|
||||
- **Choose async_dag** if you want compile-time type safety with runtime flexibility and the slot-based API appeals to you
|
||||
- **Choose dag-flow** if you're building experimental projects and the all-tasks-run-simultaneously model fits your use case
|
||||
- **Choose RenovZ/dag-runner** if you need the simplest possible edge-based API and are already using Tokio
|
||||
|
||||
@@ -8,11 +8,11 @@ Most DAG libraries check for cycles when you execute the graph:
|
||||
|
||||
```rust
|
||||
// Other libraries: cycle detected at RUNTIME
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let a = dag.add_task(TaskA);
|
||||
let b = dag.add_task(TaskB);
|
||||
a.depends_on(b);
|
||||
b.depends_on(a); // Compiles fine...
|
||||
a.depends_on(&b);
|
||||
b.depends_on(&a); // Compiles fine...
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
|
||||
```rust
|
||||
// dagx: cycle prevented at COMPILE TIME
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let a_builder = dag.add_task(TaskA);
|
||||
let b_builder = dag.add_task(TaskB);
|
||||
|
||||
@@ -61,15 +61,13 @@ let b_handle = b_builder.into(); // Convert B to TaskHandle (consumes builder)
|
||||
let a_handle = a_builder.depends_on(&b_handle); // A now depends on B
|
||||
|
||||
// Now if we try to complete the cycle:
|
||||
// a_handle.depends_on(&...); // ❌ ERROR: no method named `depends_on` found
|
||||
// a_handle.depends_on(...); // ❌ ERROR: no method named `depends_on` found
|
||||
```
|
||||
|
||||
**The type system enforces strict topological ordering!** You can't create a `TaskHandle` until all its dependencies exist as handles, which prevents cycles.
|
||||
|
||||
## Proof & Examples
|
||||
|
||||
See [`src/cycle_prevention.rs`](../src/cycle_prevention.rs) for comprehensive documentation with `compile_fail` tests proving various cycle attempts don't compile.
|
||||
|
||||
See [`tests/cycle_prevention.rs`](../tests/cycle_prevention.rs) for runtime tests demonstrating the type-state pattern in action.
|
||||
|
||||
## Benefits
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
# Design Philosophy: Primitives as Scheduler
|
||||
|
||||
dagx takes an unconventional approach to task orchestration: **there is no traditional scheduler**. Instead of complex scheduling logic managing when tasks run, the entire system is built on communication and synchronization primitives that do the scheduling themselves.
|
||||
|
||||
## How It Works
|
||||
|
||||
Traditional DAG executors contain substantial scheduling code—algorithms that track task states, manage dependencies, coordinate execution order, and handle synchronization. This code is complex, error-prone, and difficult to verify.
|
||||
|
||||
dagx eliminates this entirely:
|
||||
|
||||
1. **Wire up tasks with primitives**: During DAG construction, tasks are organized into topological layers based on their dependencies
|
||||
2. **Start everything simultaneously**: When you call `run()`, all tasks spawn at once—there's no scheduler deciding when each task should start
|
||||
3. **Let the primitives handle coordination**: Layers naturally enforce execution order—tasks wait on their dependencies until . No custom orchestration needed.
|
||||
2. **Start everything simultaneously**: When you call `run()`, all tasks in the first layer spawn at once—there's no scheduler deciding when each task should start
|
||||
3. **Let the primitives handle coordination**: Layers naturally enforce execution order. No custom orchestration needed.
|
||||
4. **Runtime joins on completion**: The runtime simply spawns all tasks and waits for them to finish. That's it.
|
||||
|
||||
## The Implementation
|
||||
@@ -18,54 +12,14 @@ dagx eliminates this entirely:
|
||||
Under the hood, dagx uses:
|
||||
|
||||
- **Ownership model**: Tasks take ownership (`self`) and are consumed during execution - no Mutex needed for task state
|
||||
- **Direct data flow**: Outputs flow from producer(s) to consumer(s) through a shared hash map
|
||||
- **Direct data flow**: Arc-wrapped outputs flow from producer(s) to consumer(s) through a store in the runner
|
||||
- **Fast-path optimization**: Single-task layers execute inline without spawning overhead
|
||||
|
||||
The core execution logic in `run()` is remarkably simple:
|
||||
|
||||
```rust
|
||||
let outputs = HashMap::new();
|
||||
|
||||
// Execute layer by layer
|
||||
for layer in layers {
|
||||
if layer.len() == 1 {
|
||||
// Fast path: Execute inline, no spawning overhead
|
||||
// IMPORTANT: Panic handling is required here!
|
||||
let result = task.execute().catch_unwind().await;
|
||||
// Convert panic to error to match spawned task behavior
|
||||
} else {
|
||||
// Parallel path: Spawn all tasks in layer
|
||||
let tasks = layers
|
||||
.map(async {
|
||||
let input = get_inputs(map); // Deps are guaranteed to be in the map
|
||||
let output = task.run(input); // Consumes task
|
||||
output
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>(); // Run all tasks in parallel
|
||||
|
||||
while let Some(out) = tasks.next().await {
|
||||
outputs.insert(out.task_id, out.output); // Make result available for dependents
|
||||
}
|
||||
}
|
||||
// Wait for layer completion
|
||||
}
|
||||
|
||||
// FuturesUnordered coordinates task execution - no custom scheduler needed
|
||||
```
|
||||
|
||||
## Inline Execution Fast-Path
|
||||
|
||||
**Performance optimization for sequential workloads**: When a layer contains only a single task (common in deep chains and linear pipelines), dagx executes it inline rather than spawning it. This eliminates spawning overhead and context switching, resulting in a 10-100x performance improvements for sequential patterns.
|
||||
|
||||
**Panic handling guarantee**: To maintain behavioral consistency between inline and spawned execution, panics in inline tasks are caught using `FutureExt::catch_unwind()` and converted to errors. This matches the behavior of all major async runtimes (Tokio, async-std, smol, embassy-rs), which catch panics in spawned tasks and convert them to `JoinError` or equivalent.
|
||||
|
||||
**Why this matters**:
|
||||
|
||||
- **Spawned tasks** (layer.len() > 1): Runtime catches panics automatically → becomes error
|
||||
- **Inline tasks** (layer.len() == 1): We catch panics manually → becomes error
|
||||
- **Result**: Identical behavior regardless of execution path
|
||||
|
||||
This ensures your code behaves the same whether a task runs inline or spawned, making dagx's optimizations transparent and predictable.
|
||||
**Panic handling guarantee**: To maintain behavioral consistency between runtimes, as well as inline vs. spawned tasks, panics in tasks are caught using `FutureExt::catch_unwind()` and converted to errors. This ensures your code behaves the same whether a task runs inline or spawned, making dagx's optimizations transparent and predictable.
|
||||
|
||||
## Benefits
|
||||
|
||||
@@ -75,7 +29,7 @@ This ensures your code behaves the same whether a task runs inline or spawned, m
|
||||
|
||||
**Bug resistance**: Fewer moving parts means fewer places for bugs to hide. The type system enforces correct wiring at compile time. The ownership model prevents data races. What's left to break?
|
||||
|
||||
**Performance**: Near zero-overhead. No Mutex locks during execution. Arc reference counting for efficient fanout (atomic operations, not locks). Tasks start as soon as their dependencies complete - maximum parallelism.
|
||||
**Performance**: Near zero-overhead. No locks during execution. Arc reference counting for efficient fanout (atomic operations, not locks). Tasks start as soon as their dependencies complete - maximum parallelism.
|
||||
|
||||
**Auditability**: Want to verify correctness? Check the dependency wiring, verify tasks await their inputs, done. No need to trace through complex state machine transitions or wake-up cascades.
|
||||
|
||||
@@ -87,24 +41,14 @@ This is dagx's core philosophy: leverage the type system for correctness, use pr
|
||||
|
||||
## Measured Overhead
|
||||
|
||||
How much overhead does this approach actually add? Benchmarks on an AMD 7840U (Zen 4 laptop CPU) show:
|
||||
How much overhead does this approach actually add? Benchmarks on an Intel i9-13950HX show:
|
||||
|
||||
**DAG Construction**:
|
||||
|
||||
- Empty DAG creation: **~20 nanoseconds**
|
||||
- Adding tasks: **~100 nanoseconds per task**
|
||||
- Building a 10,000-task DAG: **~1.0 milliseconds** (100 ns/task)
|
||||
|
||||
**Execution Overhead** (framework coordination, excluding actual task work):
|
||||
|
||||
- Sequential workloads: **~790 nanoseconds per task** (inline execution fast-path)
|
||||
- Parallel workloads: **~1.3 microseconds per task**
|
||||
- 100-task sequential chain: **~79 microseconds total**
|
||||
- 100 independent tasks: **~122 microseconds total**
|
||||
- 10,000 independent tasks: **~12.7 milliseconds total**
|
||||
- Empty DAG creation: **~7 nanoseconds**
|
||||
- Adding tasks: **~5 nanoseconds per task**
|
||||
- Building a 10,000-task DAG: **~865 microseconds** (100 ns/task)
|
||||
|
||||
**Scaling**: Sub-microsecond per-task overhead across all workload patterns. Linear scaling verified to 10k+ tasks.
|
||||
|
||||
**Comparison**: dagx is **1.04-129x faster** than dagrs (v0.5) across all benchmark patterns (see [comparisons](COMPARISONS.md)).
|
||||
|
||||
The primitives-as-scheduler approach with inline fast-path optimization delivers exceptional performance: coordination overhead is sub-microsecond per task, and for real-world workloads where tasks do meaningful work (I/O, computation, etc.), framework overhead is negligible—typically well under 1% of total execution time.
|
||||
@@ -1,53 +0,0 @@
|
||||
# Tracing Support
|
||||
|
||||
dagx provides optional observability through the `tracing` crate with **zero runtime overhead when disabled**. The tracing instrumentation is conditionally compiled using feature flags, meaning when the feature is off, the logging code doesn't exist in the compiled binary at all.
|
||||
|
||||
## Enabling Tracing
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
dagx = { version = "0.3", features = ["tracing"] }
|
||||
tracing-subscriber = "0.3"
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```rust
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
// Initialize tracing subscriber
|
||||
fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("dagx=info"))
|
||||
)
|
||||
.init();
|
||||
|
||||
// Your DAG code here - tracing will log execution details
|
||||
```
|
||||
|
||||
## Log Levels
|
||||
|
||||
- **INFO**: DAG execution start/completion
|
||||
- **DEBUG**: Task additions, dependency wiring, layer computation
|
||||
- **TRACE**: Individual task execution (inline vs spawned), detailed execution flow
|
||||
- **ERROR**: Task panics, concurrent execution attempts
|
||||
|
||||
Run with different levels:
|
||||
|
||||
```bash
|
||||
RUST_LOG=dagx=info cargo run # High-level execution info
|
||||
RUST_LOG=dagx=debug cargo run # Task and layer details
|
||||
RUST_LOG=dagx=trace cargo run # All execution details
|
||||
```
|
||||
|
||||
## Zero-Cost Guarantee
|
||||
|
||||
When the `tracing` feature is disabled (the default), there is **literally 0ns overhead**:
|
||||
|
||||
- Logging code is removed at compile time via `#[cfg(feature = "tracing")]`
|
||||
- No branches, no function calls, nothing
|
||||
- The `tracing` crate isn't even linked
|
||||
- Benchmarks verify identical performance with/without the feature
|
||||
|
||||
This follows the same pattern used by Tokio, Hyper, and other performance-critical Rust async libraries.
|
||||
@@ -77,13 +77,13 @@ impl Add {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Step 3: Create a new DAG
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Step 4: Add source tasks (tasks with no dependencies)
|
||||
//
|
||||
// add_task() returns a TaskHandle<T> where T is the task's output type
|
||||
let x = dag.add_task(Value::new(2)).into(); // TaskHandle<i32>
|
||||
let y = dag.add_task(Value::new(3)).into(); // TaskHandle<i32>
|
||||
let x = dag.add_task(Value::new(2)); // TaskHandle<i32>
|
||||
let y = dag.add_task(Value::new(3)); // TaskHandle<i32>
|
||||
|
||||
// Step 5: Add a task with dependencies
|
||||
//
|
||||
@@ -105,7 +105,8 @@ async fn main() {
|
||||
// 1. Tasks with no dependencies run first (x and y in parallel)
|
||||
// 2. Once x and y complete, sum runs (it depends on both)
|
||||
println!("Running DAG...\n");
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -113,9 +114,9 @@ async fn main() {
|
||||
//
|
||||
// Use .get() with a TaskHandle to retrieve the output.
|
||||
// Results are not cloned, so you can only retrieve them once.
|
||||
println!("\nResult: {}", dag.get(sum).unwrap());
|
||||
println!("\nResult: {}", output.get(sum));
|
||||
|
||||
// Note: You can also retrieve intermediate results
|
||||
assert_eq!(dag.get(x).unwrap(), 2);
|
||||
assert_eq!(dag.get(y).unwrap(), 3);
|
||||
assert_eq!(output.get(x), 2);
|
||||
assert_eq!(output.get(y), 3);
|
||||
}
|
||||
+12
-11
@@ -55,7 +55,7 @@
|
||||
//! Squared: 100
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
// Source task with a value field
|
||||
struct Source {
|
||||
@@ -127,27 +127,28 @@ impl Power {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Single source task - constructed with ::new()
|
||||
let source: TaskHandle<_> = dag.add_task(Source::new(10)).into();
|
||||
let source = dag.add_task(Source::new(10));
|
||||
|
||||
// Multiple downstream tasks consuming the same value
|
||||
// Each constructed differently to show flexibility
|
||||
let plus_one = dag.add_task(AddOffset::new(1)).depends_on(source);
|
||||
let times_two = dag.add_task(Multiply::new(2)).depends_on(source);
|
||||
let squared = dag.add_task(Power { exponent: 2 }).depends_on(source);
|
||||
let plus_one = dag.add_task(AddOffset::new(1)).depends_on(&source);
|
||||
let times_two = dag.add_task(Multiply::new(2)).depends_on(&source);
|
||||
let squared = dag.add_task(Power { exponent: 2 }).depends_on(&source);
|
||||
|
||||
// Run the DAG
|
||||
println!("Running fan-out DAG...\n");
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Print all results
|
||||
println!("\nResults:");
|
||||
println!(" Source: {}", dag.get(source).unwrap());
|
||||
println!(" Plus One: {}", dag.get(plus_one).unwrap());
|
||||
println!(" Times Two: {}", dag.get(times_two).unwrap());
|
||||
println!(" Squared: {}", dag.get(squared).unwrap());
|
||||
println!(" Source: {}", output.get(source));
|
||||
println!(" Plus One: {}", output.get(plus_one));
|
||||
println!(" Times Two: {}", output.get(times_two));
|
||||
println!(" Squared: {}", output.get(squared));
|
||||
}
|
||||
@@ -118,7 +118,7 @@ impl BuildProfile {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Multiple independent source tasks
|
||||
let name = dag.add_task(FetchName);
|
||||
@@ -129,14 +129,15 @@ async fn main() {
|
||||
// Single downstream task consuming all values
|
||||
let profile = dag
|
||||
.add_task(BuildProfile)
|
||||
.depends_on((name, age, city, active));
|
||||
.depends_on((&name, &age, &city, &active));
|
||||
|
||||
// Run the DAG
|
||||
println!("Running fan-in DAG...\n");
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Print result
|
||||
println!("\n{}", dag.get(profile).unwrap());
|
||||
println!("\n{}", output.get(profile));
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
//! This proves dagx runs tasks on multiple CPU cores simultaneously.
|
||||
//! ```
|
||||
|
||||
use dagx::{task, DagResult, DagRunner, Task, TaskInput};
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
@@ -115,7 +115,7 @@ async fn main() -> DagResult<()> {
|
||||
println!("Running PARALLEL computation (4 workers on separate threads)...");
|
||||
let (parallel_time, parallel_result) = {
|
||||
let start = Instant::now();
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Create 4 workers - dagx will run them in parallel
|
||||
let sum1 = dag.add_task(ComputeSum::new(1, 251, WORK_DELAY));
|
||||
@@ -123,12 +123,15 @@ async fn main() -> DagResult<()> {
|
||||
let sum3 = dag.add_task(ComputeSum::new(501, 751, WORK_DELAY));
|
||||
let sum4 = dag.add_task(ComputeSum::new(751, 1001, WORK_DELAY));
|
||||
|
||||
let total = dag.add_task(Aggregate).depends_on((sum1, sum2, sum3, sum4));
|
||||
let total = dag
|
||||
.add_task(Aggregate)
|
||||
.depends_on((&sum1, &sum2, &sum3, &sum4));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(total)?;
|
||||
let result = output.get(total);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
println!(
|
||||
@@ -148,15 +151,15 @@ async fn main() -> DagResult<()> {
|
||||
let start = Instant::now();
|
||||
|
||||
// Run tasks sequentially (not using DAG, just raw execution)
|
||||
let task1 = ComputeSum::new(1, 251, WORK_DELAY);
|
||||
let task2 = ComputeSum::new(251, 501, WORK_DELAY);
|
||||
let task3 = ComputeSum::new(501, 751, WORK_DELAY);
|
||||
let task4 = ComputeSum::new(751, 1001, WORK_DELAY);
|
||||
let mut task1 = ComputeSum::new(1, 251, WORK_DELAY);
|
||||
let mut task2 = ComputeSum::new(251, 501, WORK_DELAY);
|
||||
let mut task3 = ComputeSum::new(501, 751, WORK_DELAY);
|
||||
let mut task4 = ComputeSum::new(751, 1001, WORK_DELAY);
|
||||
|
||||
let r1 = task1.run(TaskInput::empty()).await;
|
||||
let r2 = task2.run(TaskInput::empty()).await;
|
||||
let r3 = task3.run(TaskInput::empty()).await;
|
||||
let r4 = task4.run(TaskInput::empty()).await;
|
||||
let r1 = task1.run_impl().await;
|
||||
let r2 = task2.run_impl().await;
|
||||
let r3 = task3.run_impl().await;
|
||||
let r4 = task4.run_impl().await;
|
||||
|
||||
let result = r1 + r2 + r3 + r4;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
+29
-22
@@ -381,7 +381,7 @@ async fn main() {
|
||||
// Create shared circuit breaker: 3 failures trigger open, 3 second recovery timeout
|
||||
let circuit = Arc::new(CircuitBreaker::new(3, Duration::from_secs(3)));
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Request 1: Success (circuit closed)
|
||||
let call1 = dag.add_task(ProtectedServiceCall::new(
|
||||
@@ -390,56 +390,60 @@ async fn main() {
|
||||
false, // success
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 1: {:?}\n", dag.get(call1).unwrap());
|
||||
println!("Result 1: {:?}\n", output.get(call1));
|
||||
|
||||
// Request 2: Failure (circuit closed, count = 1)
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let call2 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-2",
|
||||
circuit.clone(),
|
||||
true, // fail
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 2: {:?}\n", dag.get(call2).unwrap());
|
||||
println!("Result 2: {:?}\n", output.get(call2));
|
||||
|
||||
// Request 3: Failure (circuit closed, count = 2)
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let call3 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-3",
|
||||
circuit.clone(),
|
||||
true, // fail
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 3: {:?}\n", dag.get(call3).unwrap());
|
||||
println!("Result 3: {:?}\n", output.get(call3));
|
||||
|
||||
// Request 4: Failure (circuit closed, count = 3, opens!)
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let call4 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-4",
|
||||
circuit.clone(),
|
||||
true, // fail
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 4: {:?}\n", dag.get(call4).unwrap());
|
||||
println!("Result 4: {:?}\n", output.get(call4));
|
||||
|
||||
// Request 5: Circuit is now open, fails fast with fallback
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let call5 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-5",
|
||||
circuit.clone(),
|
||||
@@ -448,13 +452,14 @@ async fn main() {
|
||||
|
||||
let fallback5 = dag
|
||||
.add_task(FallbackService::new("ServiceCall-5"))
|
||||
.depends_on(call5);
|
||||
.depends_on(&call5);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 5: {:?}\n", dag.get(fallback5).unwrap());
|
||||
println!("Result 5: {:?}\n", output.get(fallback5));
|
||||
|
||||
// Example 2: Circuit recovery after timeout
|
||||
println!("Example 2: Circuit recovery after timeout\n");
|
||||
@@ -462,32 +467,34 @@ async fn main() {
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
|
||||
// Request 6: Circuit should be half-open, test succeeds, closes circuit
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let call6 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-6",
|
||||
circuit.clone(),
|
||||
false, // success
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 6: {:?}\n", dag.get(call6).unwrap());
|
||||
println!("Result 6: {:?}\n", output.get(call6));
|
||||
|
||||
// Request 7: Circuit is now closed again, normal operation
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let call7 = dag.add_task(ProtectedServiceCall::new(
|
||||
"ServiceCall-7",
|
||||
circuit.clone(),
|
||||
false, // success
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Result 7: {:?}\n", dag.get(call7).unwrap());
|
||||
println!("Result 7: {:?}\n", output.get(call7));
|
||||
}
|
||||
|
||||
println!("=== Circuit Breaker Example Complete ===");
|
||||
|
||||
@@ -178,7 +178,7 @@ impl GenerateReport {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
println!("Building data pipeline DAG...\n");
|
||||
|
||||
@@ -190,15 +190,15 @@ async fn main() {
|
||||
// Stage 2: Data transformation (with stateful tasks)
|
||||
let transformed_1 = dag
|
||||
.add_task(DataTransformer::new(1.5))
|
||||
.depends_on(raw_value_1);
|
||||
.depends_on(&raw_value_1);
|
||||
|
||||
let transformed_2 = dag
|
||||
.add_task(DataTransformer::new(2.0))
|
||||
.depends_on(raw_value_2);
|
||||
.depends_on(&raw_value_2);
|
||||
|
||||
let transformed_3 = dag
|
||||
.add_task(DataTransformer::new(0.5))
|
||||
.depends_on(raw_value_3);
|
||||
.depends_on(&raw_value_3);
|
||||
|
||||
// Stage 3: Statistical analysis (parallel operations on transformed data)
|
||||
let sum = dag
|
||||
@@ -220,10 +220,11 @@ async fn main() {
|
||||
|
||||
// Execute the pipeline
|
||||
println!("Executing pipeline...\n");
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Print final report
|
||||
println!("{}", dag.get(report).unwrap());
|
||||
println!("{}", output.get(report));
|
||||
}
|
||||
+30
-26
@@ -209,24 +209,25 @@ async fn main() {
|
||||
// Example 1: Successful path
|
||||
println!("Example 1: Valid input");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(StringSource("42"));
|
||||
|
||||
let parsed = dag.add_task(ParseInt).depends_on(input);
|
||||
let parsed = dag.add_task(ParseInt).depends_on(&input);
|
||||
|
||||
let validated = dag
|
||||
.add_task(ValidateRange { min: 0, max: 100 })
|
||||
.depends_on(parsed);
|
||||
.depends_on(&parsed);
|
||||
|
||||
let processed = dag.add_task(ProcessData).depends_on(validated);
|
||||
let logged = dag.add_task(LogError).depends_on(processed);
|
||||
let processed = dag.add_task(ProcessData).depends_on(&validated);
|
||||
let logged = dag.add_task(LogError).depends_on(&processed);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match dag.get(logged).unwrap().as_ref() {
|
||||
match output.get(logged).as_ref() {
|
||||
Ok(result) => println!("Final result: {}\n", result),
|
||||
Err(e) => eprintln!("Final error: {}\n", e),
|
||||
}
|
||||
@@ -235,24 +236,25 @@ async fn main() {
|
||||
// Example 2: Parse error
|
||||
println!("Example 2: Invalid input (parse error)");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(StringSource("not a number"));
|
||||
|
||||
let parsed = dag.add_task(ParseInt).depends_on(input);
|
||||
let parsed = dag.add_task(ParseInt).depends_on(&input);
|
||||
|
||||
let validated = dag
|
||||
.add_task(ValidateRange { min: 0, max: 100 })
|
||||
.depends_on(parsed);
|
||||
.depends_on(&parsed);
|
||||
|
||||
let processed = dag.add_task(ProcessData).depends_on(validated);
|
||||
let logged = dag.add_task(LogError).depends_on(processed);
|
||||
let processed = dag.add_task(ProcessData).depends_on(&validated);
|
||||
let logged = dag.add_task(LogError).depends_on(&processed);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match dag.get(logged).unwrap().as_ref() {
|
||||
match output.get(logged).as_ref() {
|
||||
Ok(result) => println!("Final result: {}\n", result),
|
||||
Err(e) => eprintln!("Final error: {}\n", e),
|
||||
}
|
||||
@@ -261,24 +263,25 @@ async fn main() {
|
||||
// Example 3: Validation error
|
||||
println!("Example 3: Out of range");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(StringSource("150"));
|
||||
|
||||
let parsed = dag.add_task(ParseInt).depends_on(input);
|
||||
let parsed = dag.add_task(ParseInt).depends_on(&input);
|
||||
|
||||
let validated = dag
|
||||
.add_task(ValidateRange { min: 0, max: 100 })
|
||||
.depends_on(parsed);
|
||||
.depends_on(&parsed);
|
||||
|
||||
let processed = dag.add_task(ProcessData).depends_on(validated);
|
||||
let logged = dag.add_task(LogError).depends_on(processed);
|
||||
let processed = dag.add_task(ProcessData).depends_on(&validated);
|
||||
let logged = dag.add_task(LogError).depends_on(&processed);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match dag.get(logged).unwrap().as_ref() {
|
||||
match output.get(logged).as_ref() {
|
||||
Ok(result) => println!("Final result: {}\n", result),
|
||||
Err(e) => eprintln!("Final error: {}\n", e),
|
||||
}
|
||||
@@ -287,21 +290,22 @@ async fn main() {
|
||||
// Example 4: Error recovery with fallback
|
||||
println!("Example 4: Error recovery with fallback");
|
||||
{
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(StringSource("invalid"));
|
||||
|
||||
let parsed = dag.add_task(ParseInt).depends_on(input);
|
||||
let parsed = dag.add_task(ParseInt).depends_on(&input);
|
||||
|
||||
let with_fallback = dag
|
||||
.add_task(WithFallback { fallback: 0 })
|
||||
.depends_on(parsed);
|
||||
.depends_on(&parsed);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = dag.get(with_fallback).unwrap();
|
||||
let result = output.get(with_fallback);
|
||||
println!("Recovered with fallback: {}\n", result);
|
||||
}
|
||||
|
||||
|
||||
+13
-40
@@ -88,7 +88,7 @@ check_pass "Zero clippy warnings"
|
||||
|
||||
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)
|
||||
TODO_COUNT=$(grep -r "TODO\|FIXME\|XXX\|HACK" src/ dagx-macros/src/ dagx-test/src/ 2>/dev/null | wc -l)
|
||||
if [ "$TODO_COUNT" -gt 0 ]; then
|
||||
check_warn "Found $TODO_COUNT TODO/FIXME/HACK comments in source"
|
||||
else
|
||||
@@ -157,14 +157,14 @@ section "TESTING"
|
||||
|
||||
echo "→ Running library unit tests..."
|
||||
cargo test --lib || check_fail "Library unit tests failed"
|
||||
check_pass "Library unit tests pass (70 tests)"
|
||||
check_pass "Library unit tests pass"
|
||||
|
||||
echo ""
|
||||
echo "→ Running integration tests with all feature combinations..."
|
||||
cargo test --test '*' --no-default-features || check_fail "Integration tests failed (no features)"
|
||||
cargo test --test '*' --all-features || check_fail "Integration tests failed (all features)"
|
||||
cargo test --test '*' || check_fail "Integration tests failed (default features)"
|
||||
check_pass "Integration tests pass across all feature combinations (280+ tests total)"
|
||||
check_pass "Integration tests pass across all feature combinations"
|
||||
|
||||
echo ""
|
||||
echo "→ Running doc tests with all features..."
|
||||
@@ -209,6 +209,7 @@ echo ""
|
||||
echo "→ Verifying LICENSE..."
|
||||
[ -f LICENSE ] || check_fail "LICENSE missing"
|
||||
[ -f dagx-macros/LICENSE ] || check_fail "dagx-macros/LICENSE missing"
|
||||
[ -f dagx-test/LICENSE ] || check_fail "dagx-test/LICENSE missing"
|
||||
check_pass "LICENSE files present in both crates"
|
||||
|
||||
# =============================================================================
|
||||
@@ -259,45 +260,13 @@ echo "→ Verifying benchmarks compile..."
|
||||
cargo bench --no-run || check_fail "Benchmarks failed to compile"
|
||||
check_pass "Benchmarks compile successfully"
|
||||
|
||||
# =============================================================================
|
||||
# CARGO.TOML METADATA VERIFICATION
|
||||
# =============================================================================
|
||||
|
||||
section "CARGO.TOML METADATA"
|
||||
|
||||
echo "→ Checking dagx Cargo.toml..."
|
||||
grep -q 'name = "dagx"' Cargo.toml || check_fail "Package name incorrect"
|
||||
grep -q "version = \"$VERSION\"" Cargo.toml || check_fail "Version not $VERSION"
|
||||
grep -q 'edition = "2021"' Cargo.toml || check_fail "Edition not 2021"
|
||||
grep -q 'license = "MIT"' Cargo.toml || check_fail "License not MIT"
|
||||
grep -q 'description = "A minimal, type-safe' Cargo.toml || check_fail "Description missing"
|
||||
grep -q 'repository = "https://github.com/swaits/dagx"' Cargo.toml || check_fail "Repository URL incorrect"
|
||||
grep -q 'homepage = "https://github.com/swaits/dagx"' Cargo.toml || check_fail "Homepage URL incorrect"
|
||||
grep -q 'documentation = "https://docs.rs/dagx"' Cargo.toml || check_fail "Documentation URL incorrect"
|
||||
grep -q 'readme = "README.md"' Cargo.toml || check_fail "README not specified"
|
||||
grep -q 'keywords = \["dag"' Cargo.toml || check_fail "Keywords missing"
|
||||
grep -q 'categories = \["asynchronous"' Cargo.toml || check_fail "Categories missing"
|
||||
check_pass "dagx Cargo.toml metadata complete and correct"
|
||||
|
||||
echo ""
|
||||
echo "→ Checking dagx-macros Cargo.toml..."
|
||||
grep -q 'name = "dagx-macros"' dagx-macros/Cargo.toml || check_fail "dagx-macros name incorrect"
|
||||
grep -q "version = \"$VERSION\"" dagx-macros/Cargo.toml || check_fail "dagx-macros version not $VERSION"
|
||||
grep -q 'edition = "2021"' dagx-macros/Cargo.toml || check_fail "dagx-macros edition not 2021"
|
||||
grep -q 'license = "MIT"' dagx-macros/Cargo.toml || check_fail "dagx-macros license not MIT"
|
||||
grep -q 'description = "Procedural macros for dagx"' dagx-macros/Cargo.toml || check_fail "dagx-macros description missing"
|
||||
grep -q 'repository = "https://github.com/swaits/dagx"' dagx-macros/Cargo.toml || check_fail "dagx-macros repository URL incorrect"
|
||||
grep -q 'homepage = "https://github.com/swaits/dagx"' dagx-macros/Cargo.toml || check_fail "dagx-macros homepage URL incorrect"
|
||||
grep -q 'documentation = "https://docs.rs/dagx-macros"' dagx-macros/Cargo.toml || check_fail "dagx-macros documentation URL incorrect"
|
||||
grep -q 'readme = "README.md"' dagx-macros/Cargo.toml || check_fail "dagx-macros README not specified"
|
||||
check_pass "dagx-macros Cargo.toml metadata complete and correct"
|
||||
|
||||
echo ""
|
||||
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"
|
||||
TEST_VERSION=$(grep '^version = ' dagx-test/Cargo.toml | head -1 | cut -d'"' -f2)
|
||||
if [ "$DAGX_VERSION" != "$MACROS_VERSION" ] || [ "$DAGX_VERSION" != "$TEST_VERSION" ]; then
|
||||
check_fail "Version mismatch: dagx=$DAGX_VERSION, dagx-macros=$MACROS_VERSION, dagx-test=$TEST_VERSION"
|
||||
else
|
||||
check_pass "Versions match: both are $DAGX_VERSION"
|
||||
fi
|
||||
@@ -344,7 +313,7 @@ fi
|
||||
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
|
||||
if grep -r "api[_-]key\|secret\|password\|token" src/ dagx-macros/src/ dagx-test/src 2>/dev/null | grep -v "^Binary" | grep -v "token for a node" | grep -q .; then
|
||||
check_fail "Potential sensitive data found in source"
|
||||
else
|
||||
check_pass "No sensitive data found"
|
||||
@@ -387,7 +356,11 @@ echo "4. Then publish dagx:"
|
||||
echo " cd .."
|
||||
echo " cargo publish"
|
||||
echo ""
|
||||
echo "5. Push changes and bookmark:"
|
||||
echo "5. Then publish dagx-test:"
|
||||
echo " cd dagx-test"
|
||||
echo " cargo publish"
|
||||
echo ""
|
||||
echo "6. Push changes and bookmark:"
|
||||
echo " jj git push"
|
||||
echo ""
|
||||
echo -e "${BLUE}Note: dagx-macros MUST be published before dagx${NC}"
|
||||
|
||||
+132
-30
@@ -1,22 +1,20 @@
|
||||
//! Task builder with type-state pattern for dependency tracking.
|
||||
//!
|
||||
//! TaskBuilder uses compile-time type checking to ensure dependencies are wired correctly.
|
||||
//! The type parameter tracks whether dependencies have been specified yet.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::deps::DepsTuple;
|
||||
use crate::runner::DagRunner;
|
||||
use crate::task::Task;
|
||||
use crate::types::{NodeId, TaskHandle};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use tracing::debug;
|
||||
|
||||
/// Node builder that tracks dependency completion via type state.
|
||||
///
|
||||
/// A `TaskBuilder<Tk, Deps>` is returned from [`DagRunner::add_task`] and uses the type-state
|
||||
/// pattern to ensure dependencies are wired correctly at compile time:
|
||||
/// - `TaskBuilder<Tk, Pending>`: No dependencies specified yet
|
||||
/// - `TaskBuilder<Tk, Tk::Input>`: Dependencies complete; acts as a [`TaskHandle<Tk::Output>`]
|
||||
/// A `TaskBuilder<Tk, Input>` is returned from [`DagRunner::add_task`] for tasks with non-unit inputs.
|
||||
/// After calling [`TaskBuilder::depends_on`], it becomes a [`TaskHandle`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -46,37 +44,48 @@ use tracing::debug;
|
||||
/// # async {
|
||||
/// let mut dag = DagRunner::new();
|
||||
///
|
||||
/// // Construct with initial value (tuple struct)
|
||||
/// let a = dag.add_task(Constant(10));
|
||||
/// // a is TaskBuilder<_, Pending> since no dependencies needed (Input = ())
|
||||
/// // a is TaskHandle since no dependencies needed (Input = ())
|
||||
///
|
||||
/// // Construct with ::new() pattern
|
||||
/// let b = dag.add_task(Multiplier::new(2));
|
||||
/// // b is TaskBuilder<_, Pending> until we call depends_on()
|
||||
/// // b is TaskBuilder until we call depends_on()
|
||||
///
|
||||
/// let b = b.depends_on(a);
|
||||
/// let b = b.depends_on(&a);
|
||||
/// // Now b is a TaskHandle<i32>
|
||||
///
|
||||
/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
/// assert_eq!(dag.get(b).unwrap(), 20);
|
||||
///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
/// assert_eq!(output.get(b), 20);
|
||||
/// # };
|
||||
/// ```
|
||||
pub struct TaskBuilder<'a, Tk: Task, Deps> {
|
||||
#[must_use]
|
||||
pub struct TaskBuilder<'a, Input, Tk>
|
||||
where
|
||||
Tk: Task<Input>,
|
||||
Input: Send + Sync + 'static,
|
||||
{
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) dag: &'a DagRunner,
|
||||
pub(crate) _phantom: std::marker::PhantomData<(Tk, Deps)>,
|
||||
pub(crate) dag: &'a mut DagRunner,
|
||||
pub(crate) _phantom: PhantomData<(Tk, Input)>,
|
||||
}
|
||||
|
||||
impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
|
||||
impl<'a, Input, Tk> TaskBuilder<'a, Input, Tk>
|
||||
where
|
||||
Tk: Task<Input>,
|
||||
Input: Send + Sync + 'static,
|
||||
{
|
||||
/// Provide all dependencies exactly once as a tuple.
|
||||
///
|
||||
/// The dependencies must match the task's `Input` type exactly:
|
||||
/// - `Input = ()`: Do **not** call `depends_on`
|
||||
/// - `Input = T`: Pass `&TaskHandle` or `(&TaskHandle,)`
|
||||
/// - `Input = A`: Pass `&TaskHandle<A>`
|
||||
/// - `Input = (A, B, ...)`: Pass `(&TaskHandle<A>, &TaskHandle<B>, ...)`
|
||||
///
|
||||
/// The order of dependencies in the tuple must match the order in `Input`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will cause a panic in [`DagRunner::run`] if called with a [`TaskHandle`] from a `DagRunner` instance
|
||||
/// other than the one which created this TaskBuilder.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
@@ -107,24 +116,24 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
|
||||
/// }
|
||||
///
|
||||
/// # async {
|
||||
/// let dag = DagRunner::new();
|
||||
/// let mut dag = DagRunner::new();
|
||||
///
|
||||
/// let x = dag.add_task(Value(2)).into();
|
||||
/// let x = dag.add_task(Value(2));
|
||||
/// let y = dag.add_task(Value(3));
|
||||
///
|
||||
/// // Single dependency
|
||||
/// let double = dag.add_task(Scale(2)).depends_on(&x);
|
||||
///
|
||||
/// // Multiple dependencies: tuple form
|
||||
/// let sum = dag.add_task(Add).depends_on((&x, y));
|
||||
/// let sum = dag.add_task(Add).depends_on((&x, &y));
|
||||
///
|
||||
/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
#[allow(private_bounds)]
|
||||
pub fn depends_on<D>(self, deps: D) -> TaskHandle<Tk::Output>
|
||||
where
|
||||
D: DepsTuple<Tk::Input>,
|
||||
D: DepsTuple<Input>,
|
||||
{
|
||||
// Register dependencies in the DAG
|
||||
let dep_ids = deps.to_node_ids();
|
||||
@@ -137,27 +146,120 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
|
||||
"wiring task dependencies"
|
||||
);
|
||||
|
||||
let mut edges = self.dag.edges.lock();
|
||||
let mut dependents = self.dag.dependents.lock();
|
||||
|
||||
for &dep_id in &dep_ids {
|
||||
// Add edge from this node to dependency
|
||||
if let Some(node_edges) = edges.get_mut(&self.id) {
|
||||
if let Some(node_edges) = self.dag.edges.get_mut(&self.id) {
|
||||
node_edges.push(dep_id);
|
||||
}
|
||||
|
||||
// Add this node as dependent of the dependency
|
||||
if let Some(node_dependents) = dependents.get_mut(&dep_id) {
|
||||
if let Some(node_dependents) = self.dag.dependents.get_mut(&dep_id) {
|
||||
node_dependents.push(self.id);
|
||||
}
|
||||
}
|
||||
|
||||
TaskHandle {
|
||||
id: self.id,
|
||||
_phantom: std::marker::PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque node identifier
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct NodeId(pub u32);
|
||||
|
||||
/// Opaque, typed token for a node's output.
|
||||
///
|
||||
/// A `TaskHandle<T>` provides compile-time type-safe access to a task's output.
|
||||
/// You can:
|
||||
/// 1. Pass it to [`crate::TaskBuilder::depends_on`] to wire up dependencies
|
||||
/// 2. Use it with [`crate::DagOutput::get`] to retrieve the output after [`crate::DagRunner::run`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use dagx::{task, DagRunner, Task};
|
||||
/// #
|
||||
/// # struct LoadValue { value: i32 }
|
||||
/// # impl LoadValue { pub fn new(v: i32) -> Self { Self { value: v } } }
|
||||
/// # #[task]
|
||||
/// # impl LoadValue {
|
||||
/// # async fn run(&mut self) -> i32 { self.value }
|
||||
/// # }
|
||||
/// # async {
|
||||
/// let mut dag = DagRunner::new();
|
||||
/// let node = dag.add_task(LoadValue::new(42));
|
||||
///
|
||||
///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
///
|
||||
/// assert_eq!(output.get(node), 42);
|
||||
/// # };
|
||||
/// ```
|
||||
pub struct TaskHandle<T> {
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) _phantom: PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
/// Takes a task and converts it to either a TaskBuilder or a TaskHandle,
|
||||
/// depending on whether it has inputs or not.
|
||||
///
|
||||
/// This is useful to enforce at compile time that a TaskBuilder is never created for task with unit input,
|
||||
/// and that it can be used directly as a dependency without converting it manually to a TaskHandle.
|
||||
pub trait TaskWire<Input>: Task<Input> + Sync + 'static
|
||||
where
|
||||
Input: Send + Sync + 'static,
|
||||
{
|
||||
type Retval<'dag>;
|
||||
|
||||
fn new_from_dag<'dag>(id: NodeId, dag: &'dag mut DagRunner) -> Self::Retval<'dag>;
|
||||
}
|
||||
|
||||
impl<Tk> TaskWire<()> for Tk
|
||||
where
|
||||
Tk: Task<()> + Sync + 'static,
|
||||
{
|
||||
type Retval<'dag> = TaskHandle<Tk::Output>;
|
||||
|
||||
fn new_from_dag(id: NodeId, _dag: &mut DagRunner) -> Self::Retval<'static> {
|
||||
Self::Retval {
|
||||
id,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro to implement TaskWire for different tuple sizes.
|
||||
macro_rules! impl_wire_tuple {
|
||||
($($T:ident),+) => {
|
||||
impl<Tk, $($T: Send + Sync + 'static),+> TaskWire<($($T,)+)> for Tk
|
||||
where
|
||||
Tk: Task<($($T,)+)> + Sync + 'static
|
||||
{
|
||||
type Retval<'dag> = TaskBuilder<'dag, ($($T,)+), Tk>;
|
||||
|
||||
fn new_from_dag<'dag>(id: NodeId, dag: &'dag mut DagRunner) -> Self::Retval<'dag> {
|
||||
Self::Retval {
|
||||
id,
|
||||
dag,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_wire_tuple!(T1);
|
||||
impl_wire_tuple!(T1, T2);
|
||||
impl_wire_tuple!(T1, T2, T3);
|
||||
impl_wire_tuple!(T1, T2, T3, T4);
|
||||
impl_wire_tuple!(T1, T2, T3, T4, T5);
|
||||
impl_wire_tuple!(T1, T2, T3, T4, T5, T6);
|
||||
impl_wire_tuple!(T1, T2, T3, T4, T5, T6, T7);
|
||||
impl_wire_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod coverage_tests;
|
||||
@@ -25,19 +25,20 @@ async fn test_unit_struct_with_state() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Create task with initial state
|
||||
let builder = dag.add_task(StatefulTask::new(21));
|
||||
|
||||
// Explicit conversion - triggers line 71
|
||||
let handle: TaskHandle<_> = builder.into();
|
||||
let handle = builder;
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(handle).unwrap(), 42);
|
||||
assert_eq!(output.get(handle), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -51,12 +52,12 @@ fn test_taskbuilder_conversion_without_running() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let builder = dag.add_task(TestTask);
|
||||
let builder_id = builder.id;
|
||||
|
||||
// Convert without running the DAG
|
||||
let handle: TaskHandle<String> = builder.into();
|
||||
let handle: TaskHandle<String> = builder;
|
||||
|
||||
// Verify the ID was transferred correctly
|
||||
assert_eq!(handle.id, builder_id);
|
||||
+13
-14
@@ -1,7 +1,6 @@
|
||||
//! Unit tests for builder module
|
||||
|
||||
use crate::runner::DagRunner;
|
||||
use crate::types::TaskHandle;
|
||||
|
||||
// Initialize tracing subscriber for tests (idempotent)
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -47,11 +46,11 @@ impl TestTaskWithInput {
|
||||
#[test]
|
||||
fn test_task_builder_from_conversion() {
|
||||
// Test line 165 in builder.rs - the From implementation
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let builder = dag.add_task(TestTask { value: 42 });
|
||||
|
||||
// Convert builder to TaskHandle
|
||||
let handle: TaskHandle<_> = builder.into();
|
||||
let handle = builder;
|
||||
|
||||
// Check that the ID is preserved
|
||||
assert_eq!(handle.id.0, 0); // First task should have ID 0
|
||||
@@ -59,17 +58,17 @@ fn test_task_builder_from_conversion() {
|
||||
|
||||
#[test]
|
||||
fn test_task_builder_chain() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Test chaining multiple tasks
|
||||
let t1 = dag.add_task(TestTask { value: 1 }).into();
|
||||
let t2 = dag.add_task(TestTaskWithInput).depends_on(t1);
|
||||
let t3 = dag.add_task(TestTaskWithInput).depends_on(t2);
|
||||
let t1 = dag.add_task(TestTask { value: 1 });
|
||||
let t2 = dag.add_task(TestTaskWithInput).depends_on(&t1);
|
||||
let t3 = dag.add_task(TestTaskWithInput).depends_on(&t2);
|
||||
|
||||
// All should return TaskHandles
|
||||
let _: TaskHandle<_> = t1;
|
||||
let _: TaskHandle<_> = t2;
|
||||
let _: TaskHandle<_> = t3;
|
||||
let _ = t1;
|
||||
let _ = t2;
|
||||
let _ = t3;
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -84,21 +83,21 @@ fn test_multiple_dependencies() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(TestTask { value: 10 });
|
||||
let b = dag.add_task(TestTask { value: 20 });
|
||||
|
||||
// Test multiple dependencies
|
||||
let sum = dag.add_task(AddTask).depends_on((a, b));
|
||||
let sum = dag.add_task(AddTask).depends_on((&a, &b));
|
||||
|
||||
// Should return a TaskHandle
|
||||
let _: TaskHandle<_> = sum;
|
||||
let _ = sum;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_builder_stores_correct_id() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let builder1 = dag.add_task(TestTask { value: 1 });
|
||||
let builder2 = dag.add_task(TestTask { value: 2 });
|
||||
|
||||
+19
-66
@@ -1,42 +1,9 @@
|
||||
//! Dependency tuple trait and implementations.
|
||||
//!
|
||||
//! This module defines the DepsTuple trait that allows specifying dependencies in
|
||||
//! various forms (handles, builders, tuples). It uses macro-generated implementations
|
||||
//! for different tuple sizes.
|
||||
//!
|
||||
//! This trait is internal and not meant for external implementation. Users should only
|
||||
//! use the provided implementations.
|
||||
//! various combinations of handles for tuples of different sizes.
|
||||
|
||||
use crate::builder::TaskBuilder;
|
||||
use crate::task::Task;
|
||||
use crate::types::{NodeId, TaskHandle};
|
||||
|
||||
trait IntoNodeRef {
|
||||
type Output;
|
||||
fn into_node_ref(self) -> NodeId;
|
||||
}
|
||||
|
||||
impl<T> IntoNodeRef for &TaskHandle<T> {
|
||||
type Output = T;
|
||||
fn into_node_ref(self) -> NodeId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoNodeRef for TaskHandle<T> {
|
||||
type Output = T;
|
||||
fn into_node_ref(self) -> NodeId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'runner, Tk: Task, Deps> IntoNodeRef for TaskBuilder<'runner, Tk, Deps> {
|
||||
type Output = Tk::Output;
|
||||
|
||||
fn into_node_ref(self) -> NodeId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
use crate::builder::{NodeId, TaskHandle};
|
||||
|
||||
pub(crate) trait DepsTuple<Input> {
|
||||
fn to_node_ids(self) -> Vec<NodeId>;
|
||||
@@ -50,59 +17,45 @@ impl DepsTuple<()> for () {
|
||||
}
|
||||
|
||||
// Implementation for single dependency (all forms)
|
||||
impl<T> DepsTuple<(T::Output,)> for T
|
||||
where
|
||||
T: IntoNodeRef,
|
||||
{
|
||||
impl<O> DepsTuple<(O,)> for &TaskHandle<O> {
|
||||
fn to_node_ids(self) -> Vec<NodeId> {
|
||||
vec![self.into_node_ref()]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DepsTuple<T::Output> for (T,)
|
||||
where
|
||||
T: IntoNodeRef,
|
||||
{
|
||||
fn to_node_ids(self) -> Vec<NodeId> {
|
||||
vec![self.0.into_node_ref()]
|
||||
vec![self.id]
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro to implement DepsTuple for different tuple sizes.
|
||||
///
|
||||
/// This macro exists because Rust lacks variadic generics - we need separate implementations
|
||||
/// for each tuple size.
|
||||
///
|
||||
/// This allows flexible dependency specification:
|
||||
/// ```ignore
|
||||
/// // Using handles
|
||||
/// task.depends_on((&handle_a, &handle_b))
|
||||
///
|
||||
/// // Using builders directly
|
||||
/// task.depends_on((&dag.add_task(TaskA), &dag.add_task(TaskB)))
|
||||
/// task.depends_on((dag.add_task(TaskA), dag.add_task(TaskB)))
|
||||
/// ```
|
||||
macro_rules! impl_deps_tuple {
|
||||
($($T:ident),+) => {
|
||||
// For tuples of any combination of &TaskHandle, TaskHandle, and TaskBuilder
|
||||
impl<$($T: IntoNodeRef),+> DepsTuple<($($T::Output,)+)> for ($($T,)+) {
|
||||
($($O:ident),+) => {
|
||||
impl<$($O),+> DepsTuple<($($O,)+)> for ($(&TaskHandle<$O>,)+)
|
||||
{
|
||||
#[allow(non_snake_case)]
|
||||
fn to_node_ids(self) -> Vec<NodeId> {
|
||||
let ($($T,)+) = self;
|
||||
vec![$($T.into_node_ref(),)+]
|
||||
let ($($O,)+) = self;
|
||||
vec![$($O.id,)+]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Generate DepsTuple implementations for tuples of size 2-8.
|
||||
// Generate DepsTuple implementations for tuples of size 1-8.
|
||||
// Supporting up to 8 elements covers the vast majority of use cases.
|
||||
impl_deps_tuple!(T1, T2);
|
||||
impl_deps_tuple!(T1, T2, T3);
|
||||
impl_deps_tuple!(T1, T2, T3, T4);
|
||||
impl_deps_tuple!(T1, T2, T3, T4, T5);
|
||||
impl_deps_tuple!(T1, T2, T3, T4, T5, T6);
|
||||
impl_deps_tuple!(T1, T2, T3, T4, T5, T6, T7);
|
||||
impl_deps_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
|
||||
impl_deps_tuple!(O1);
|
||||
impl_deps_tuple!(O1, O2);
|
||||
impl_deps_tuple!(O1, O2, O3);
|
||||
impl_deps_tuple!(O1, O2, O3, O4);
|
||||
impl_deps_tuple!(O1, O2, O3, O4, O5);
|
||||
impl_deps_tuple!(O1, O2, O3, O4, O5, O6);
|
||||
impl_deps_tuple!(O1, O2, O3, O4, O5, O6, O7);
|
||||
impl_deps_tuple!(O1, O2, O3, O4, O5, O6, O7, O8);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+4
-65
@@ -1,8 +1,8 @@
|
||||
//! Unit tests for deps module
|
||||
|
||||
use crate::builder::{NodeId, TaskHandle};
|
||||
use crate::deps::DepsTuple;
|
||||
use crate::runner::DagRunner;
|
||||
use crate::types::{NodeId, TaskHandle};
|
||||
|
||||
// Test task for dependency testing
|
||||
struct TestTask {
|
||||
@@ -37,19 +37,6 @@ fn test_deps_tuple_single_handle_ref() {
|
||||
assert_eq!(node_ids[0], NodeId(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deps_tuple_single_handle_owned() {
|
||||
// Test lines 32-36 in deps.rs - TaskHandle<T>
|
||||
let handle: TaskHandle<String> = TaskHandle {
|
||||
id: NodeId(99),
|
||||
_phantom: std::marker::PhantomData,
|
||||
};
|
||||
let handle_copy = handle; // Copy for use
|
||||
let node_ids = handle_copy.to_node_ids();
|
||||
assert_eq!(node_ids.len(), 1);
|
||||
assert_eq!(node_ids[0], NodeId(99));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deps_tuple_single_handle_tuple_ref() {
|
||||
// Test lines 38-42 in deps.rs - (&TaskHandle<T>,)
|
||||
@@ -62,23 +49,10 @@ fn test_deps_tuple_single_handle_tuple_ref() {
|
||||
assert_eq!(node_ids[0], NodeId(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deps_tuple_single_handle_tuple_owned() {
|
||||
// Test lines 44-48 in deps.rs - (TaskHandle<T>,)
|
||||
let handle: TaskHandle<Vec<u8>> = TaskHandle {
|
||||
id: NodeId(13),
|
||||
_phantom: std::marker::PhantomData,
|
||||
};
|
||||
let handle_copy = handle; // Copy for use
|
||||
let node_ids = (handle_copy,).to_node_ids();
|
||||
assert_eq!(node_ids.len(), 1);
|
||||
assert_eq!(node_ids[0], NodeId(13));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deps_tuple_task_builder_ref() {
|
||||
// Test lines 51-55 in deps.rs - &TaskBuilder
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let builder = dag.add_task(TestTask { value: 42 });
|
||||
let builder_id = builder.id;
|
||||
|
||||
@@ -92,11 +66,11 @@ fn test_deps_tuple_task_builder_ref() {
|
||||
#[test]
|
||||
fn test_deps_tuple_task_builder_tuple() {
|
||||
// Test lines 57-61 in deps.rs - (&TaskBuilder,)
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let builder = dag.add_task(TestTask { value: 100 });
|
||||
let builder_id = builder.id;
|
||||
|
||||
let node_ids = (builder,).to_node_ids();
|
||||
let node_ids = (&builder,).to_node_ids();
|
||||
assert_eq!(node_ids.len(), 1);
|
||||
assert_eq!(node_ids[0], builder_id);
|
||||
}
|
||||
@@ -131,25 +105,6 @@ fn test_deps_tuple_multiple_handles() {
|
||||
assert_eq!(node_ids[2], NodeId(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deps_tuple_multiple_builders() {
|
||||
// Test macro-generated implementations for multiple builders
|
||||
let dag = DagRunner::new();
|
||||
let builder1 = dag.add_task(TestTask { value: 10 });
|
||||
let builder1_id = builder1.id;
|
||||
let builder2 = dag.add_task(TestTask { value: 20 });
|
||||
let builder2_id = builder2.id;
|
||||
let builder3 = dag.add_task(TestTask { value: 30 });
|
||||
let builder3_id = builder3.id;
|
||||
|
||||
// Test 3-tuple of builders
|
||||
let node_ids = (builder1, builder2, builder3).to_node_ids();
|
||||
assert_eq!(node_ids.len(), 3);
|
||||
assert_eq!(node_ids[0], builder1_id);
|
||||
assert_eq!(node_ids[1], builder2_id);
|
||||
assert_eq!(node_ids[2], builder3_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deps_tuple_large_tuples() {
|
||||
// Test that larger tuples work (up to 16 elements)
|
||||
@@ -185,19 +140,3 @@ fn test_deps_tuple_large_tuples() {
|
||||
assert_eq!(*node_id, NodeId(i as u32));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deps_handle_ref_explicit() {
|
||||
// Test explicit &TaskHandle DepsTuple impl (line 27-28 in deps.rs)
|
||||
let handle: TaskHandle<i32> = TaskHandle {
|
||||
id: NodeId(77),
|
||||
_phantom: std::marker::PhantomData,
|
||||
};
|
||||
|
||||
// Create a reference and explicitly call to_node_ids
|
||||
let handle_ref = &handle;
|
||||
let node_ids = DepsTuple::<(i32,)>::to_node_ids(handle_ref);
|
||||
|
||||
assert_eq!(node_ids.len(), 1);
|
||||
assert_eq!(node_ids[0], NodeId(77));
|
||||
}
|
||||
@@ -6,44 +6,13 @@
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum DagError {
|
||||
/// Invalid dependency: task does not exist
|
||||
InvalidDependency { task_id: u32 },
|
||||
/// Type mismatch in task dependencies
|
||||
TypeMismatch {
|
||||
expected: &'static str,
|
||||
found: &'static str,
|
||||
},
|
||||
/// Task panicked during execution
|
||||
TaskPanicked { task_id: u32, panic_message: String },
|
||||
/// Result not found for task
|
||||
ResultNotFound { task_id: u32 },
|
||||
/// Attempted to run the DAG while it's already running
|
||||
ConcurrentExecution,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DagError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DagError::InvalidDependency { task_id } => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid dependency: task #{} does not exist.\n\
|
||||
\n\
|
||||
Ensure all task handles reference tasks that have been added to the DAG.",
|
||||
task_id
|
||||
)
|
||||
}
|
||||
DagError::TypeMismatch { expected, found } => {
|
||||
write!(
|
||||
f,
|
||||
"Type mismatch in task dependencies.\n\
|
||||
Expected: {}\n\
|
||||
Found: {}\n\
|
||||
\n\
|
||||
Verify that dependency types match the task's Input type exactly.",
|
||||
expected, found
|
||||
)
|
||||
}
|
||||
DagError::TaskPanicked {
|
||||
task_id,
|
||||
panic_message,
|
||||
@@ -56,23 +25,6 @@ impl std::fmt::Display for DagError {
|
||||
task_id, panic_message
|
||||
)
|
||||
}
|
||||
DagError::ResultNotFound { task_id } => {
|
||||
write!(
|
||||
f,
|
||||
"Result not found for task #{}.\n\
|
||||
\n\
|
||||
Call dag.run(|fut| async move {{ tokio::spawn(fut).await.unwrap() }}).",
|
||||
task_id
|
||||
)
|
||||
}
|
||||
DagError::ConcurrentExecution => {
|
||||
write!(
|
||||
f,
|
||||
"DAG is already running - concurrent execution not supported.\n\
|
||||
\n\
|
||||
Wait for the current execution to complete before starting another."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-48
@@ -2,33 +2,6 @@
|
||||
|
||||
use crate::error::DagError;
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_display_invalid_dependency() {
|
||||
// Test lines 47-54 in error.rs
|
||||
let err = DagError::InvalidDependency { task_id: 42 };
|
||||
let display = format!("{}", err);
|
||||
|
||||
assert!(display.contains("Invalid dependency"));
|
||||
assert!(display.contains("task #42"));
|
||||
assert!(display.contains("does not exist"));
|
||||
assert!(display.contains("Ensure all task handles"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_display_type_mismatch() {
|
||||
// Test lines 56-65 in error.rs
|
||||
let err = DagError::TypeMismatch {
|
||||
expected: "i32",
|
||||
found: "String",
|
||||
};
|
||||
let display = format!("{}", err);
|
||||
|
||||
assert!(display.contains("Type mismatch"));
|
||||
assert!(display.contains("Expected: i32"));
|
||||
assert!(display.contains("Found: String"));
|
||||
assert!(display.contains("Verify that dependency types"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_display_task_panicked() {
|
||||
// Test lines 67-77 in error.rs
|
||||
@@ -44,30 +17,13 @@ fn test_dag_error_display_task_panicked() {
|
||||
assert!(display.contains("entire DAG execution is aborted"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_display_result_not_found() {
|
||||
let err = DagError::ResultNotFound { task_id: 7 };
|
||||
let display = format!("{}", err);
|
||||
|
||||
assert!(display.contains("Result not found"));
|
||||
assert!(display.contains("task #7"));
|
||||
assert!(display.contains("Call dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_display_concurrent_execution() {
|
||||
let err = DagError::ConcurrentExecution;
|
||||
let display = format!("{}", err);
|
||||
|
||||
assert!(display.contains("already running"));
|
||||
assert!(display.contains("concurrent execution not supported"));
|
||||
assert!(display.contains("Wait for the current execution"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dag_error_std_error_impl() {
|
||||
// Test that DagError implements std::error::Error
|
||||
let err = DagError::InvalidDependency { task_id: 1 };
|
||||
let err = DagError::TaskPanicked {
|
||||
task_id: 1,
|
||||
panic_message: "test panic".to_string(),
|
||||
};
|
||||
let err_ref: &dyn std::error::Error = &err;
|
||||
|
||||
// Should be able to call Error trait methods
|
||||
|
||||
+50
-345
@@ -10,15 +10,8 @@
|
||||
//! - **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.
|
||||
//! - **Works with ANY type**: Custom types work automatically with the `#[task]` macro—just
|
||||
//! implement `Send + Sync`, no trait implementations needed! The macro generates
|
||||
//! type-specific extraction logic for seamless integration.
|
||||
//! - **Runtime-agnostic**: Works with any async runtime (Tokio, async-std, smol, Embassy, etc.)
|
||||
//! - **Type-state pattern**: The API guides you with compile-time errors if you wire dependencies
|
||||
//! incorrectly. This is what prevents cycles at compile time.
|
||||
//! - **Runtime-agnostic**: Works with any async runtime (Tokio, smol, Embassy, etc.)
|
||||
//! - **Optimal execution**: Topological scheduling with maximum safe parallelism
|
||||
//! - **Zero-cost abstractions**: Leverages generics and monomorphization for minimal overhead
|
||||
//! - **Error handling**: Result-based error handling with [`DagResult<T>`] for validation
|
||||
//!
|
||||
//! # Quick Start
|
||||
//!
|
||||
@@ -42,18 +35,18 @@
|
||||
//! }
|
||||
//!
|
||||
//! # async {
|
||||
//! let dag = DagRunner::new();
|
||||
//! let mut dag = DagRunner::new();
|
||||
//!
|
||||
//! // Add source tasks
|
||||
//! let x = dag.add_task(Value(2));
|
||||
//! let y: TaskHandle<_> = dag.add_task(Value(3)).into(); // Allows y to be reused as a dependency
|
||||
//! let y = dag.add_task(Value(3));
|
||||
//!
|
||||
//! // Add task with dependencies
|
||||
//! let sum = dag.add_task(Add).depends_on((x, &y));
|
||||
//! let sum = dag.add_task(Add).depends_on((&x, &y));
|
||||
//!
|
||||
//! // Execute and retrieve results
|
||||
//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//! assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//! assert_eq!(output.get(sum), 5);
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
@@ -66,10 +59,9 @@
|
||||
//! that task executes inline rather than being spawned. This eliminates spawning overhead,
|
||||
//! and context switching.
|
||||
//!
|
||||
//! **Panic handling**: To maintain consistent behavior between inline and spawned execution,
|
||||
//! panics in inline tasks are caught using `FutureExt::catch_unwind()` and converted to
|
||||
//! `DagError::TaskPanicked`. This matches the behavior of async runtimes like Tokio, async-std,
|
||||
//! and smol, which catch panics in spawned tasks and convert them to errors.
|
||||
//! **Panic handling**: To maintain consistent behavior between runtimes,
|
||||
//! panics in tasks are caught using `FutureExt::catch_unwind()` and converted to
|
||||
//! `DagError::TaskPanicked`. This matches the behavior of async runtimes like Tokio.
|
||||
//!
|
||||
//! **What this means for you**:
|
||||
//! - Tasks behave identically whether executed inline or spawned
|
||||
@@ -78,8 +70,6 @@
|
||||
//! - The optimization is completely transparent - no code changes needed
|
||||
//!
|
||||
//! **When inline execution happens**:
|
||||
//! - Single-task layers (e.g., long sequential chains)
|
||||
//! - Linear pipelines (A→B→C→D)
|
||||
//! - Any layer where `layer.len() == 1` after topological ordering
|
||||
//!
|
||||
//! ## Dependency Limits
|
||||
@@ -89,71 +79,11 @@
|
||||
//! 2. Use intermediate aggregation tasks
|
||||
//! 3. Consider if 8+ dependencies indicates a design issue
|
||||
//!
|
||||
//! ## Task Output Limitations
|
||||
//!
|
||||
//! **IMPORTANT**: Tasks **cannot return bare tuples** as output types. This is a technical
|
||||
//! limitation of the current implementation. If you need to return multiple values, use one
|
||||
//! of these workarounds:
|
||||
//!
|
||||
//! ### Workaround 1: Wrap in Result (Recommended for Fallible Operations)
|
||||
//!
|
||||
//! ```
|
||||
//! use dagx::{task, Task};
|
||||
//!
|
||||
//! struct ProcessData;
|
||||
//! #[task]
|
||||
//! impl ProcessData {
|
||||
//! async fn run(input: &String) -> Result<(String, i32, bool), String> {
|
||||
//! // Wrap the tuple in Result - this works!
|
||||
//! Ok(("Alice".to_string(), 30, true))
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Workaround 2: Use a Struct (Recommended for Most Cases)
|
||||
//!
|
||||
//! ```
|
||||
//! use dagx::{task, Task};
|
||||
//!
|
||||
//! // Define a struct to hold your multiple values
|
||||
//! struct UserData {
|
||||
//! name: String,
|
||||
//! age: i32,
|
||||
//! active: bool,
|
||||
//! }
|
||||
//!
|
||||
//! struct ProcessDataStruct;
|
||||
//! #[task]
|
||||
//! impl ProcessDataStruct {
|
||||
//! async fn run(input: &String) -> UserData {
|
||||
//! // Return the struct - clean and self-documenting
|
||||
//! UserData {
|
||||
//! name: "Alice".to_string(),
|
||||
//! age: 30,
|
||||
//! active: true,
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Why Use Structs Over `Result<(...)>`?
|
||||
//!
|
||||
//! While both workarounds solve the technical limitation, **structs are the better choice**
|
||||
//! for most cases:
|
||||
//!
|
||||
//! - **Self-documenting**: `user.name` is clearer than `user.0`
|
||||
//! - **Refactorable**: Easy to add/remove fields without breaking all dependencies
|
||||
//! - **Type-safe**: Named fields prevent field order mistakes
|
||||
//! - **Semantic clarity**: `Result` should indicate fallibility, not just wrap data
|
||||
//!
|
||||
//! Use `Result<(...), E>` only when your operation can genuinely fail and you need to
|
||||
//! return multiple values on success
|
||||
//!
|
||||
//! # Core Concepts
|
||||
//!
|
||||
//! ## Task
|
||||
//!
|
||||
//! A [`Task`] is a unit of async work with typed inputs and outputs. Use the `#[task]` macro.
|
||||
//! A [`Task`] is a unit of async work with typed inputs and outputs. Use the `#[task]` macro to generate an implementation.
|
||||
//!
|
||||
//! ### Task Patterns
|
||||
//!
|
||||
@@ -212,7 +142,7 @@
|
||||
//!
|
||||
//! A [`TaskHandle<T>`] is a typed, opaque reference to a task's output. Use it to:
|
||||
//! - Wire dependencies between tasks
|
||||
//! - Retrieve results after execution with [`DagRunner::get`]
|
||||
//! - Retrieve results after execution with [`DagOutput::get`]
|
||||
//!
|
||||
//! ## Dependency Patterns
|
||||
//!
|
||||
@@ -220,7 +150,7 @@
|
||||
//!
|
||||
//! ### No Dependencies (Source Tasks)
|
||||
//!
|
||||
//! Tasks with no dependencies don't call `depends_on()`:
|
||||
//! Tasks with no dependencies return a [`TaskHandle`] directly and don't call `depends_on()`:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use dagx::{task, DagRunner, Task};
|
||||
@@ -230,7 +160,7 @@
|
||||
//! # async fn run(&self) -> i32 { self.0 }
|
||||
//! # }
|
||||
//! # async {
|
||||
//! # let dag = DagRunner::new();
|
||||
//! # let mut dag = DagRunner::new();
|
||||
//! let source = dag.add_task(Value(42));
|
||||
//! # };
|
||||
//! ```
|
||||
@@ -252,12 +182,14 @@
|
||||
//! async fn run(input: &i32) -> i32 { input * 2 }
|
||||
//! }
|
||||
//! # async {
|
||||
//! # let dag = DagRunner::new();
|
||||
//! # let mut dag = DagRunner::new();
|
||||
//! # let source = dag.add_task(Value(42));
|
||||
//! let doubled = dag.add_task(Double).depends_on(source);
|
||||
//! let doubled = dag.add_task(Double).depends_on(&source);
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! To manually implement [`Task`] for a single-dependency type, you must implement [`Task<(T,)>`], not [`Task<T>`].
|
||||
//!
|
||||
//! ### Multiple Dependencies
|
||||
//!
|
||||
//! Tasks with multiple dependencies receive separate reference parameters (order matters!):
|
||||
@@ -275,10 +207,10 @@
|
||||
//! async fn run(a: &i32, b: &i32) -> i32 { a + b }
|
||||
//! }
|
||||
//! # async {
|
||||
//! # let dag = DagRunner::new();
|
||||
//! # let mut dag = DagRunner::new();
|
||||
//! # let x = dag.add_task(Value(2));
|
||||
//! # let y = dag.add_task(Value(3));
|
||||
//! let sum = dag.add_task(Add).depends_on((x, y));
|
||||
//! let sum = dag.add_task(Add).depends_on((&x, &y));
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
@@ -317,16 +249,16 @@
|
||||
//! }
|
||||
//!
|
||||
//! # async {
|
||||
//! let dag = DagRunner::new();
|
||||
//! let mut dag = DagRunner::new();
|
||||
//!
|
||||
//! let base = dag.add_task(Value(10)).into();
|
||||
//! let base = dag.add_task(Value(10));
|
||||
//! let plus1 = dag.add_task(Add(1)).depends_on(&base);
|
||||
//! let times2 = dag.add_task(Scale(2)).depends_on(&base);
|
||||
//!
|
||||
//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//!
|
||||
//! assert_eq!(dag.get(plus1).unwrap(), 11);
|
||||
//! assert_eq!(dag.get(times2).unwrap(), 20);
|
||||
//! assert_eq!(output.get(plus1), 11);
|
||||
//! assert_eq!(output.get(times2), 20);
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
@@ -373,122 +305,23 @@
|
||||
//! }
|
||||
//!
|
||||
//! # async {
|
||||
//! let dag = DagRunner::new();
|
||||
//! let mut dag = DagRunner::new();
|
||||
//!
|
||||
//! let name = dag.add_task(Name("Alice".to_string()));
|
||||
//! let age = dag.add_task(Age(30));
|
||||
//! let active = dag.add_task(Active(true));
|
||||
//! let result = dag.add_task(FormatUser).depends_on((name, age, active));
|
||||
//! let result = dag.add_task(FormatUser).depends_on((&name, &age, &active));
|
||||
//!
|
||||
//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//!
|
||||
//! assert_eq!(dag.get(result).unwrap(), "User: Alice, Age: 30, Active: true");
|
||||
//! assert_eq!(output.get(result), "User: Alice, Age: 30, Active: true");
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! ## Many-to-Many Pattern (m ↔ n)
|
||||
//!
|
||||
//! Complex DAGs with multiple layers and dependencies:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use dagx::{task, DagRunner, Task};
|
||||
//!
|
||||
//!
|
||||
//! // Source task (tuple struct)
|
||||
//! struct Value(i32);
|
||||
//!
|
||||
//! #[task]
|
||||
//! impl Value {
|
||||
//! async fn run(&self) -> i32 { self.0 }
|
||||
//! }
|
||||
//!
|
||||
//! // Stateless addition (unit struct)
|
||||
//! struct Add;
|
||||
//!
|
||||
//! #[task]
|
||||
//! impl Add {
|
||||
//! async fn run(a: &i32, b: &i32) -> i32 { a + b }
|
||||
//! }
|
||||
//!
|
||||
//! // Stateless multiplication (unit struct)
|
||||
//! struct Multiply;
|
||||
//!
|
||||
//! #[task]
|
||||
//! impl Multiply {
|
||||
//! async fn run(a: &i32, b: &i32) -> i32 { a * b }
|
||||
//! }
|
||||
//!
|
||||
//! # async {
|
||||
//! let dag = DagRunner::new();
|
||||
//!
|
||||
//! // Layer 1: Sources
|
||||
//! let x = dag.add_task(Value(2));
|
||||
//! let y = dag.add_task(Value(3)).into();
|
||||
//! let z = dag.add_task(Value(5));
|
||||
//!
|
||||
//! // Layer 2: Intermediate computations
|
||||
//! let sum_xy = dag.add_task(Add).depends_on((x, &y)); // 2 + 3 = 5
|
||||
//! let prod_yz = dag.add_task(Multiply).depends_on((&y, z)); // 3 * 5 = 15
|
||||
//!
|
||||
//! // Layer 3: Final result
|
||||
//! let total = dag.add_task(Add).depends_on((&sum_xy, &prod_yz)); // 5 + 15 = 20
|
||||
//!
|
||||
//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//!
|
||||
//! assert_eq!(dag.get(total).unwrap(), 20);
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! ## Custom Types
|
||||
//!
|
||||
//! **dagx works with ANY type automatically!** As long as your type is `Send + Sync + 'static`,
|
||||
//! the `#[task]` macro generates the necessary extraction logic:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use dagx::{task, DagRunner, Task};
|
||||
//! #
|
||||
//!
|
||||
//! struct User {
|
||||
//! name: String,
|
||||
//! age: u32,
|
||||
//! }
|
||||
//!
|
||||
//! struct CreateUser;
|
||||
//! #[task]
|
||||
//! impl CreateUser {
|
||||
//! async fn run() -> User {
|
||||
//! User { name: "Alice".to_string(), age: 30 }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! struct FormatUser;
|
||||
//! #[task]
|
||||
//! impl FormatUser {
|
||||
//! async fn run(user: &User) -> String {
|
||||
//! format!("{} is {} years old", user.name, user.age)
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! # async {
|
||||
//! let dag = DagRunner::new();
|
||||
//!
|
||||
//! let user = dag.add_task(CreateUser);
|
||||
//! let formatted = dag.add_task(FormatUser).depends_on(user);
|
||||
//!
|
||||
//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//!
|
||||
//! assert_eq!(dag.get(formatted).unwrap(), "Alice is 30 years old");
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! **No trait implementations needed!** Works with nested structs, collections, enums, and any
|
||||
//! other custom type.
|
||||
//!
|
||||
//! # Runtime Agnostic
|
||||
//!
|
||||
//! dagx works with any async runtime. The library has been tested with:
|
||||
//! - **Tokio** - Most popular async runtime
|
||||
//! - **async-std** - Alternative async runtime
|
||||
//! - **smol** - Lightweight async runtime
|
||||
//!
|
||||
//! Examples with different runtimes:
|
||||
@@ -497,22 +330,17 @@
|
||||
//! // With Tokio
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! let dag = DagRunner::new();
|
||||
//! // ... build and run DAG
|
||||
//! }
|
||||
//!
|
||||
//! // With async-std
|
||||
//! #[async_std::main]
|
||||
//! async fn main() {
|
||||
//! let dag = DagRunner::new();
|
||||
//! // ... build and run DAG
|
||||
//! let mut dag = DagRunner::new();
|
||||
//! // ... build DAG
|
||||
//! let result = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await;
|
||||
//! }
|
||||
//!
|
||||
//! // With smol
|
||||
//! fn main() {
|
||||
//! smol::block_on(async {
|
||||
//! let dag = DagRunner::new();
|
||||
//! let mut dag = DagRunner::new();
|
||||
//! // ... build and run DAG
|
||||
//! let result = dag.run(|fut| smol::spawn(fut)).await;
|
||||
//! });
|
||||
//! }
|
||||
//! ```
|
||||
@@ -522,13 +350,11 @@
|
||||
//! dagx uses [`DagResult<T>`] (an alias for `Result<T, DagError>`) for operations that
|
||||
//! can fail:
|
||||
//!
|
||||
//! - [`DagRunner::run`] returns `DagResult<()>` and can fail if tasks panic or if multiple
|
||||
//! - [`DagRunner::run`] returns `DagResult<DagOutput>` and can fail if tasks panic or if multiple
|
||||
//! concurrent runs are attempted
|
||||
//! - [`DagRunner::get`] returns `DagResult<T>` and can fail if the task hasn't executed or
|
||||
//! - [`DagOutput::get`] returns `DagResult<T>` and can fail if the task hasn't executed or
|
||||
//! the handle is invalid
|
||||
//!
|
||||
//! You can handle errors explicitly or use `.unwrap()` for simple cases:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use dagx::{task, DagRunner, Task};
|
||||
//! #
|
||||
@@ -538,11 +364,12 @@
|
||||
//! # async fn run(&self) -> i32 { self.0 }
|
||||
//! # }
|
||||
//! # async {
|
||||
//! # let dag = DagRunner::new();
|
||||
//! # let mut dag = DagRunner::new();
|
||||
//! # let node = dag.add_task(Value(42));
|
||||
//! // Simple approach with .unwrap()
|
||||
//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//! let result = dag.get(node).unwrap();
|
||||
//! let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//! let result = output.get(node);
|
||||
//! # let mut dag = DagRunner::new();
|
||||
//!
|
||||
//! // Or handle errors explicitly
|
||||
//! match dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await {
|
||||
@@ -551,25 +378,6 @@
|
||||
//! }
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! # Documentation Accuracy
|
||||
//!
|
||||
//! This library maintains documentation accuracy through:
|
||||
//!
|
||||
//! 1. **Doctests**: All code examples are tested via `cargo test --doc`
|
||||
//! 2. **Examples**: All files in `examples/` are tested via `cargo build --examples`
|
||||
//! 3. **Type signatures**: Documentation reflects actual API return types
|
||||
//! 4. **Error handling**: All examples show proper `DagResult<T>` handling
|
||||
//! 5. **Safety claims**: All safety guarantees are verified by tests
|
||||
//!
|
||||
//! Last audited: 2025-10-06 for v0.1.0
|
||||
//!
|
||||
//! Changes that require documentation updates:
|
||||
//! - API signature changes (`run()`, `get()`, etc.)
|
||||
//! - New error types or error handling changes
|
||||
//! - Safety guarantee changes
|
||||
//! - Example code updates
|
||||
//!
|
||||
//! # API Design Principles
|
||||
//!
|
||||
//! dagx follows these API design principles:
|
||||
@@ -577,48 +385,13 @@
|
||||
//! 1. **Type Safety**: Dependencies validated at compile time via type-state pattern
|
||||
//! 2. **Builder Pattern**: Fluent interface with `add_task().depends_on()`
|
||||
//! 3. **Error Handling**: All fallible operations return `DagResult<T>`
|
||||
//! 4. **Minimal Surface**: Small, focused API with 4 core types and 1 trait
|
||||
//! 5. **Zero Cost**: Leverages generics for monomorphization
|
||||
//! 6. **Consistency**:
|
||||
//! - All types are PascalCase
|
||||
//! - All methods are snake_case
|
||||
//! - Mutable operations take `&mut self`
|
||||
//! - Builder methods consume `self`
|
||||
//! - Results are always `DagResult<T>`
|
||||
//! 4. **Minimal Surface**: Small, focused API
|
||||
//!
|
||||
//! # Performance Characteristics
|
||||
//!
|
||||
//! dagx is designed for minimal overhead and optimal parallel execution.
|
||||
//!
|
||||
//! ## Benchmarks
|
||||
//!
|
||||
//! Run benchmarks with:
|
||||
//! ```bash
|
||||
//! cargo bench
|
||||
//! ```
|
||||
//!
|
||||
//! View the detailed HTML reports:
|
||||
//! ```bash
|
||||
//! # macOS
|
||||
//! open target/criterion/report/index.html
|
||||
//!
|
||||
//! # Linux
|
||||
//! xdg-open target/criterion/report/index.html
|
||||
//!
|
||||
//! # Windows
|
||||
//! start target/criterion/report/index.html
|
||||
//!
|
||||
//! # Or manually open target/criterion/report/index.html in your browser
|
||||
//! ```
|
||||
//!
|
||||
//! Typical performance on modern hardware (AMD 7840U - Zen 4 laptop CPU):
|
||||
//! - **DAG creation**: ~20ns empty DAG, ~96ns per task added
|
||||
//! - **Execution overhead**: ~119ns per task (framework coordination only)
|
||||
//! - **Total overhead** (construction + execution): ~223ns per task
|
||||
//! - **Scaling**: Linear overhead with task count - 100 tasks in ~123µs, 1000 tasks in ~1.15ms
|
||||
//!
|
||||
//! For real-world workloads where tasks perform meaningful work (I/O, computation), the framework
|
||||
//! overhead is typically well under 1% of total execution time.
|
||||
//! See the [repository](https://github.com/swaits/dagx) for up-to-date benchmarks.
|
||||
//!
|
||||
//! ## Performance Tips
|
||||
//!
|
||||
@@ -651,92 +424,35 @@
|
||||
//! }
|
||||
//!
|
||||
//! # async {
|
||||
//! let dag = DagRunner::new();
|
||||
//! let data = dag.add_task(FetchData).into();
|
||||
//! let mut dag = DagRunner::new();
|
||||
//! let data = dag.add_task(FetchData);
|
||||
//!
|
||||
//! // All three tasks get efficient Arc-wrapped sharing automatically
|
||||
//! let task1 = dag.add_task(ProcessData).depends_on(&data);
|
||||
//! let task2 = dag.add_task(ProcessData).depends_on(&data);
|
||||
//! let task3 = dag.add_task(ProcessData).depends_on(&data);
|
||||
//!
|
||||
//! dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//!let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! **How it works:**
|
||||
//! - Your task outputs `T` (e.g., `Vec<String>`)
|
||||
//! - Framework wraps it in `Arc<T>` internally
|
||||
//! - For fan-out (1→N), Arc is cloned N times (just atomic pointer increments - O(1))
|
||||
//! - For fan-out (1→N), Arc is cloned N times
|
||||
//! - Each downstream task receives `&T` after extracting from Arc
|
||||
//! - When you call `dag.get()`, the Arc is cloned and inner value extracted
|
||||
//!
|
||||
//! **Performance characteristics:**
|
||||
//! - **Heap types** (Vec, String, HashMap): Arc overhead is negligible (~few ns)
|
||||
//! - **Copy types** (i32, usize): Small Arc overhead but usually still negligible
|
||||
//! - See `cargo bench` for actual measurements on your hardware
|
||||
//!
|
||||
//! **Advanced - Zero-copy optimization:**
|
||||
//! If you want true zero-copy sharing (no extraction), output `Arc<T>` explicitly:
|
||||
//! ```
|
||||
//! # use dagx::{task, Task};
|
||||
//! # use std::sync::Arc;
|
||||
//! struct ZeroCopyData;
|
||||
//! #[task]
|
||||
//! impl ZeroCopyData {
|
||||
//! // Output Arc<T> directly
|
||||
//! async fn run() -> Arc<Vec<String>> {
|
||||
//! Arc::new(vec!["data".to_string(); 10_000])
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! struct ZeroCopyProcess;
|
||||
//! #[task]
|
||||
//! impl ZeroCopyProcess {
|
||||
//! // Receive &Arc<T> - just clones the Arc pointer, no data extraction
|
||||
//! async fn run(data: &Arc<Vec<String>>) -> usize {
|
||||
//! data.len()
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//! This becomes `Arc<Arc<T>>` internally, but ExtractInput unwraps one layer automatically.
|
||||
//! Use this when you have many dependents AND want to avoid the clone() of inner data
|
||||
//!
|
||||
//! ### Other Tips
|
||||
//!
|
||||
//! 1. **Minimize task count**: Combine small operations into larger tasks
|
||||
//! 2. **Use appropriate granularity**: Don't create tasks for trivial work (< 1µs)
|
||||
//! 3. **Parallel execution**: dagx automatically maximizes parallelism
|
||||
//!
|
||||
//! Run `cargo bench` to see comprehensive benchmarks including basic operations, scaling
|
||||
//! characteristics, common patterns (fan-out, diamond), and realistic workloads.
|
||||
//!
|
||||
//! # Optional Tracing Support
|
||||
//!
|
||||
//! dagx provides optional observability through the `tracing` crate with **zero runtime overhead
|
||||
//! when disabled**. The tracing instrumentation is conditionally compiled using feature flags.
|
||||
//!
|
||||
//! ## Enabling Tracing
|
||||
//!
|
||||
//! Add the `tracing` feature to your `Cargo.toml`:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! dagx = { version = "0.2", features = ["tracing"] }
|
||||
//! tracing-subscriber = "0.3"
|
||||
//! ```
|
||||
//!
|
||||
//! Then initialize a tracing subscriber in your application:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use tracing_subscriber::{fmt, EnvFilter};
|
||||
//!
|
||||
//! fmt()
|
||||
//! .with_env_filter(
|
||||
//! EnvFilter::try_from_default_env()
|
||||
//! .unwrap_or_else(|_| EnvFilter::new("dagx=info"))
|
||||
//! )
|
||||
//! .init();
|
||||
//! ```
|
||||
//! dagx provides optional observability through the `tracing` crate. It can be enabled with the `tracing` feature flag.
|
||||
//!
|
||||
//! ## Log Levels
|
||||
//!
|
||||
@@ -752,35 +468,24 @@
|
||||
//! RUST_LOG=dagx=debug cargo run # Task and layer details
|
||||
//! RUST_LOG=dagx=trace cargo run # All execution details
|
||||
//! ```
|
||||
//!
|
||||
//! ## Zero-Cost Guarantee
|
||||
//!
|
||||
//! When the `tracing` feature is disabled (the default), there is **literally 0ns overhead**:
|
||||
//! - Logging code is removed at compile time via `#[cfg(feature = "tracing")]`
|
||||
//! - No branches, no function calls, nothing in the compiled binary
|
||||
//! - The `tracing` crate isn't even linked
|
||||
//! - Benchmarks verify identical performance with/without the feature
|
||||
//!
|
||||
//! This follows the same zero-cost pattern used by Tokio, Hyper, and other performance-critical
|
||||
//! Rust async libraries.
|
||||
|
||||
extern crate self as dagx;
|
||||
|
||||
// Module declarations
|
||||
mod builder;
|
||||
mod deps;
|
||||
mod error;
|
||||
mod node;
|
||||
mod output;
|
||||
mod runner;
|
||||
mod task;
|
||||
mod types;
|
||||
|
||||
// Public re-exports
|
||||
pub use builder::TaskBuilder;
|
||||
pub use builder::{TaskBuilder, TaskHandle};
|
||||
pub use error::{DagError, DagResult};
|
||||
pub use output::DagOutput;
|
||||
pub use runner::DagRunner;
|
||||
pub use task::{Task, TaskInput};
|
||||
pub use types::{Pending, TaskHandle};
|
||||
|
||||
// Re-export the procedural macro
|
||||
#[cfg(feature = "derive")]
|
||||
pub use dagx_macros::task;
|
||||
+22
-14
@@ -8,6 +8,7 @@
|
||||
|
||||
use std::any::Any;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -26,8 +27,6 @@ type ExecuteFuture =
|
||||
///
|
||||
/// This is the ONLY place where type erasure occurs - tasks are created with full type
|
||||
/// information and only erased at this trait boundary.
|
||||
///
|
||||
/// This is an implementation detail and not part of the public API.
|
||||
pub(crate) trait ExecutableNode: Send {
|
||||
/// Execute the task with type-erased inputs and output
|
||||
///
|
||||
@@ -42,28 +41,40 @@ pub(crate) trait ExecutableNode: Send {
|
||||
/// Fully-typed node storage for a single task.
|
||||
///
|
||||
/// Stores a task of type T. Implements ExecutableNode for type erasure,
|
||||
/// allowing heterogeneous tasks to be stored in a single Vec.
|
||||
/// allowing heterogeneous tasks to be stored together.
|
||||
///
|
||||
/// # Ownership Model
|
||||
///
|
||||
/// The task is owned directly (no Mutex needed) and consumed during execution.
|
||||
/// Each task executes exactly once, taking ownership and producing an output.
|
||||
///
|
||||
/// The output is also returned from execute_with_deps for central storage.
|
||||
pub(crate) struct TypedNode<T: Task> {
|
||||
/// The output is also returned from execute_with_deps to be passed to dependents and returned as an output
|
||||
pub(crate) struct TypedNode<Input, T>
|
||||
where
|
||||
Input: Send + Sync + 'static,
|
||||
T: Task<Input> + 'static,
|
||||
{
|
||||
pub(crate) task: T,
|
||||
phantom: PhantomData<Input>,
|
||||
}
|
||||
|
||||
impl<T: Task + 'static> TypedNode<T> {
|
||||
impl<Input, T> TypedNode<Input, T>
|
||||
where
|
||||
T: Task<Input>,
|
||||
Input: Send + Sync + 'static,
|
||||
{
|
||||
pub(crate) fn new(task: T) -> Self {
|
||||
Self { task }
|
||||
Self {
|
||||
task,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Task + 'static> ExecutableNode for TypedNode<T>
|
||||
impl<Input, T> ExecutableNode for TypedNode<Input, T>
|
||||
where
|
||||
T::Input: 'static,
|
||||
T::Output: 'static,
|
||||
T: Task<Input>,
|
||||
Input: Send + Sync + 'static,
|
||||
{
|
||||
fn execute_with_deps(
|
||||
self: Box<Self>,
|
||||
@@ -71,14 +82,11 @@ where
|
||||
) -> ExecuteFuture {
|
||||
Box::pin(async move {
|
||||
// Use the task's run method which has inline extraction logic
|
||||
// generated by the #[task] macro. This allows ANY type to work without
|
||||
// requiring ExtractInput trait implementations.
|
||||
// generated by the #[task] macro.
|
||||
let output = self.task.run(TaskInput::new(dependencies.iter())).await;
|
||||
|
||||
// Arc Wrapping Strategy:
|
||||
// Wrap output in Arc ONCE for efficient fan-out sharing.
|
||||
// For 1:N dependencies, this enables O(1) sharing (Arc clone = atomic increment)
|
||||
// instead of O(N) cloning (N deep copies of output data).
|
||||
// Downstream tasks receive &T after extraction from Arc.
|
||||
let arc_output = Arc::new(output);
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use crate::{builder::NodeId, runner::PassThroughHashMap, TaskHandle};
|
||||
|
||||
pub struct DagOutput {
|
||||
outputs: PassThroughHashMap<NodeId, Arc<dyn Any + Send + Sync + 'static>>,
|
||||
}
|
||||
|
||||
impl DagOutput {
|
||||
/// Retrieve a task's output after [`crate::DagRunner::run`].
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// All task outputs are stored after execution and can be retrieved via get().
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if called with a [`TaskHandle`] from a [`crate::DagRunner`] instance other than the
|
||||
/// one which produced this DagOutput.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use dagx::{task, DagRunner, Task};
|
||||
/// #
|
||||
/// struct Configuration {
|
||||
/// setting: i32,
|
||||
/// }
|
||||
///
|
||||
/// impl Configuration {
|
||||
/// fn new(setting: i32) -> Self {
|
||||
/// Self { setting }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Configuration {
|
||||
/// async fn run(&mut self) -> i32 { self.setting }
|
||||
/// }
|
||||
///
|
||||
/// # async {
|
||||
/// let mut dag = DagRunner::new();
|
||||
///
|
||||
/// // Construct task with specific setting value
|
||||
/// let task = dag.add_task(Configuration::new(42));
|
||||
///
|
||||
/// let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
///
|
||||
/// assert_eq!(output.get(task), 42);
|
||||
/// # };
|
||||
/// ```
|
||||
pub fn get<T: 'static + Send + Sync>(&mut self, handle: TaskHandle<T>) -> T {
|
||||
// Because this consumes the task handle, it can only be retrieved once, so is certain to be there
|
||||
let arc_output = self.outputs.remove(&handle.id).unwrap();
|
||||
|
||||
// Downcast Arc<dyn Any> to Arc<T>
|
||||
let output = arc_output.downcast::<T>().unwrap();
|
||||
|
||||
// Because we never give the user access to anything but a reference to the Arc,
|
||||
// we can guarantee that the strong reference count is exactly one here
|
||||
Arc::into_inner(output).unwrap()
|
||||
}
|
||||
|
||||
pub(super) fn new(
|
||||
outputs: PassThroughHashMap<NodeId, Arc<dyn Any + Send + Sync + 'static>>,
|
||||
) -> Self {
|
||||
Self { outputs }
|
||||
}
|
||||
}
|
||||
+61
-238
@@ -2,43 +2,26 @@
|
||||
//!
|
||||
//! Provides DagRunner for building and executing directed acyclic graphs of async tasks
|
||||
//! with compile-time type-safe dependencies.
|
||||
//!
|
||||
//! Uses Mutex for interior mutability to enable builder pattern (`&self` instead of `&mut self`).
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::future::Future;
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::{stream::FuturesUnordered, FutureExt, StreamExt, TryFutureExt};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
use crate::builder::TaskBuilder;
|
||||
use crate::builder::{NodeId, TaskWire};
|
||||
use crate::error::{DagError, DagResult};
|
||||
use crate::node::{ExecutableNode, TypedNode};
|
||||
use crate::task::Task;
|
||||
use crate::types::{NodeId, Pending, TaskHandle};
|
||||
|
||||
// Guard to ensure run_lock is released even on early return or panic
|
||||
struct RunGuard<'a> {
|
||||
lock: &'a AtomicBool,
|
||||
}
|
||||
|
||||
impl<'a> Drop for RunGuard<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.lock.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
use crate::DagOutput;
|
||||
|
||||
/// Fast hasher using values as hashes
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct PassThroughHasher {
|
||||
hash: u64,
|
||||
@@ -76,10 +59,10 @@ pub(crate) type PassThroughHashMap<K, V> = HashMap<K, V, PassThroughHasher>;
|
||||
/// # Workflow
|
||||
///
|
||||
/// 1. Create a new DAG with [`DagRunner::new`]
|
||||
/// 2. Add tasks with [`DagRunner::add_task`] to get [`TaskBuilder`] builders
|
||||
/// 3. Wire dependencies with [`TaskBuilder::depends_on`]
|
||||
/// 2. Add tasks with [`DagRunner::add_task`] to get builders
|
||||
/// 3. Wire dependencies with [`crate::TaskBuilder::depends_on`]
|
||||
/// 4. Execute all tasks with [`DagRunner::run`]
|
||||
/// 5. Optionally retrieve outputs with [`DagRunner::get`]
|
||||
/// 5. Optionally retrieve outputs with [`DagOutput::get`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -98,7 +81,6 @@ pub(crate) type PassThroughHashMap<K, V> = HashMap<K, V, PassThroughHasher>;
|
||||
/// async fn run(&mut self) -> i32 { self.value }
|
||||
/// }
|
||||
///
|
||||
/// // Unit struct - no fields needed
|
||||
/// struct Add;
|
||||
///
|
||||
/// #[task]
|
||||
@@ -107,33 +89,24 @@ pub(crate) type PassThroughHashMap<K, V> = HashMap<K, V, PassThroughHasher>;
|
||||
/// }
|
||||
///
|
||||
/// # async {
|
||||
/// let dag = DagRunner::new();
|
||||
/// let mut dag = DagRunner::new();
|
||||
///
|
||||
/// // Construct instances using ::new() pattern
|
||||
/// let x = dag.add_task(LoadValue::new(2));
|
||||
/// let y = dag.add_task(LoadValue::new(3));
|
||||
/// let sum = dag.add_task(Add).depends_on((x, y));
|
||||
/// let sum = dag.add_task(Add).depends_on((&x, &y));
|
||||
///
|
||||
/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
///
|
||||
/// assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
/// assert_eq!(output.get(sum), 5);
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// Uses Mutex for interior mutability to enable the builder pattern (`&self` not `&mut self`).
|
||||
/// This allows fluent chaining of `add_task()` calls.
|
||||
///
|
||||
/// Nodes use Option to allow taking ownership during execution.
|
||||
/// Outputs are Arc-wrapped and stored separately for retrieval via get().
|
||||
/// Arc enables efficient sharing during fanout without cloning data.
|
||||
pub struct DagRunner {
|
||||
pub(crate) nodes: Mutex<Vec<Option<Box<dyn ExecutableNode + Sync>>>>,
|
||||
pub(crate) outputs:
|
||||
Mutex<PassThroughHashMap<NodeId, std::sync::Arc<dyn std::any::Any + Send + Sync>>>,
|
||||
pub(crate) edges: Mutex<PassThroughHashMap<NodeId, Vec<NodeId>>>, // node -> dependencies
|
||||
pub(crate) dependents: Mutex<PassThroughHashMap<NodeId, Vec<NodeId>>>, // node -> tasks that depend on it
|
||||
pub(crate) next_id: Mutex<u32>,
|
||||
pub(crate) run_lock: AtomicBool, // Ensures only one run() at a time
|
||||
pub(crate) nodes: Vec<Option<Box<dyn ExecutableNode + Sync>>>,
|
||||
/// node -> dependencies
|
||||
pub(crate) edges: PassThroughHashMap<NodeId, Vec<NodeId>>,
|
||||
/// node -> tasks that depend on it
|
||||
pub(crate) dependents: PassThroughHashMap<NodeId, Vec<NodeId>>,
|
||||
}
|
||||
|
||||
impl Default for DagRunner {
|
||||
@@ -150,31 +123,21 @@ impl DagRunner {
|
||||
/// ```
|
||||
/// use dagx::DagRunner;
|
||||
///
|
||||
/// let dag = DagRunner::new();
|
||||
/// let mut dag = DagRunner::new();
|
||||
/// ```
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: Mutex::new(Vec::new()),
|
||||
outputs: Mutex::new(HashMap::default()),
|
||||
edges: Mutex::new(HashMap::default()),
|
||||
dependents: Mutex::new(HashMap::default()),
|
||||
next_id: Mutex::new(0),
|
||||
run_lock: AtomicBool::new(false),
|
||||
nodes: Vec::new(),
|
||||
edges: HashMap::default(),
|
||||
dependents: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn alloc_id(&self) -> NodeId {
|
||||
let mut next_id = self.next_id.lock();
|
||||
let id = NodeId(*next_id);
|
||||
*next_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
/// Add a task instance to the DAG, returning a node builder for wiring dependencies.
|
||||
///
|
||||
/// The returned [`TaskBuilder<Tk, Pending>`](TaskBuilder) can be used to:
|
||||
/// - Specify dependencies via [`TaskBuilder::depends_on`]
|
||||
/// - Used directly as a [`TaskHandle`] to the task's output
|
||||
/// If the task has no dependencies, a [`crate::TaskHandle`] will be returned.
|
||||
/// If not, dependencies should be specified for the returned [`crate::TaskBuilder`]
|
||||
/// using [`crate::TaskBuilder::depends_on`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -214,25 +177,24 @@ impl DagRunner {
|
||||
/// }
|
||||
///
|
||||
/// # async {
|
||||
/// let dag = DagRunner::new();
|
||||
/// let mut dag = DagRunner::new();
|
||||
///
|
||||
/// // Construct task with initial value of 10
|
||||
/// let base = dag.add_task(LoadValue::new(10));
|
||||
///
|
||||
/// // Construct task with offset of 1
|
||||
/// let inc = dag.add_task(AddOffset::new(1)).depends_on(base);
|
||||
/// let inc = dag.add_task(AddOffset::new(1)).depends_on(&base);
|
||||
///
|
||||
/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
/// assert_eq!(dag.get(inc).unwrap(), 11);
|
||||
///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
/// assert_eq!(output.get(inc), 11);
|
||||
/// # };
|
||||
/// ```
|
||||
pub fn add_task<Tk>(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending>
|
||||
pub fn add_task<'dag, Input, Tk>(&'dag mut self, task: Tk) -> Tk::Retval<'dag>
|
||||
where
|
||||
Tk: Task + Sync + 'static,
|
||||
Tk::Input: 'static,
|
||||
Tk::Output: 'static,
|
||||
Tk: TaskWire<Input>,
|
||||
Input: Send + Sync + 'static,
|
||||
{
|
||||
let id = self.alloc_id();
|
||||
let id = NodeId(self.nodes.len() as u32);
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(
|
||||
@@ -242,24 +204,18 @@ impl DagRunner {
|
||||
);
|
||||
|
||||
let node = TypedNode::new(task);
|
||||
self.nodes.lock().push(Some(Box::new(node)));
|
||||
self.edges.lock().insert(id, Vec::new());
|
||||
self.dependents.lock().insert(id, Vec::new());
|
||||
self.nodes.push(Some(Box::new(node)));
|
||||
self.edges.insert(id, Vec::new());
|
||||
self.dependents.insert(id, Vec::new());
|
||||
|
||||
TaskBuilder {
|
||||
id,
|
||||
dag: self,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
Tk::new_from_dag(id, self)
|
||||
}
|
||||
|
||||
/// Run the entire DAG to completion using the provided spawner.
|
||||
///
|
||||
/// This method:
|
||||
/// - Executes tasks in topological order (respecting dependencies)
|
||||
/// - Runs ready tasks with maximum parallelism (executor-limited)
|
||||
/// - Executes each task at most once
|
||||
/// - Waits for **all sinks** (tasks with no dependents) to complete
|
||||
/// - Is runtime-agnostic via the spawner function
|
||||
///
|
||||
/// # Parameters
|
||||
@@ -268,13 +224,11 @@ impl DagRunner {
|
||||
/// and returns a handle to the task. This is the only way to run tasks on separate threads. Examples:
|
||||
/// - Tokio: `|fut| { tokio::spawn(fut).await.unwrap() }`
|
||||
/// - Smol: `|fut| { smol::spawn(fut) }`
|
||||
/// - Async-std: `|fut| { async_std::task::spawn(fut) }`
|
||||
/// - Single-threaded: `|fut| fut`
|
||||
/// - For computationally light tasks, concurrency without parallelism can be significantly faster.
|
||||
/// - Single-threaded on invoking runtime: `|fut| fut`
|
||||
/// - Can be faster in situations where waiting time dominates
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - Returns `DagError::ConcurrentExecution` if the DAG is already executing
|
||||
/// - Returns `DagError::TaskPanicked` if any task panics during execution
|
||||
///
|
||||
/// # Examples
|
||||
@@ -299,41 +253,24 @@ impl DagRunner {
|
||||
/// }
|
||||
///
|
||||
/// # async {
|
||||
/// let dag = DagRunner::new();
|
||||
/// let mut dag = DagRunner::new();
|
||||
///
|
||||
/// let a = dag.add_task(Value(1));
|
||||
/// let b = dag.add_task(Value(2));
|
||||
/// let sum = dag.add_task(Add).depends_on((a, b));
|
||||
/// let sum = dag.add_task(Add).depends_on((&a, &b));
|
||||
///
|
||||
/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); // Executes all tasks
|
||||
///let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); // Executes all tasks
|
||||
/// # };
|
||||
/// ```
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))]
|
||||
pub async fn run<S, F>(&self, spawner: S) -> DagResult<()>
|
||||
pub async fn run<S, F>(mut self, spawner: S) -> DagResult<DagOutput>
|
||||
where
|
||||
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");
|
||||
// Acquire run lock to prevent concurrent executions
|
||||
// Use atomic compare_exchange to check if already running
|
||||
if self
|
||||
.run_lock
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_err()
|
||||
{
|
||||
#[cfg(feature = "tracing")]
|
||||
error!("DAG is already running - concurrent execution not supported");
|
||||
|
||||
return Err(DagError::ConcurrentExecution);
|
||||
}
|
||||
|
||||
// Guard ensures lock is released on drop (even on early return or panic)
|
||||
let _run_guard = RunGuard {
|
||||
lock: &self.run_lock,
|
||||
};
|
||||
|
||||
// Build topological layers
|
||||
let layers = self.compute_layers()?;
|
||||
@@ -346,42 +283,28 @@ impl DagRunner {
|
||||
total_tasks, "computed topological layers"
|
||||
);
|
||||
|
||||
let edges = self.edges.lock().clone();
|
||||
|
||||
let mut outputs: PassThroughHashMap<NodeId, Arc<dyn Any + Send + Sync>> =
|
||||
HashMap::with_capacity_and_hasher(total_tasks, PassThroughHasher::default());
|
||||
let mut first_error = None;
|
||||
|
||||
// Panic handling is required to maintain behavioral consistency, as
|
||||
// different async runtimes (Tokio, async-std, smol, embassy-rs) handle panics in
|
||||
// spawned tasks differently.
|
||||
//
|
||||
// This ensures tasks behave in the same way whether executed inline or on the spawner
|
||||
// across every runtime.
|
||||
for layer in layers {
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
debug!(task_count = layer.len(), "executing layer");
|
||||
}
|
||||
// ============================================================================
|
||||
// PERFORMANCE OPTIMIZATION: Inline execution for single-task layers
|
||||
// ============================================================================
|
||||
// Performance optimization: Inline execution for single-task layers
|
||||
//
|
||||
// When a layer contains exactly one task (common in deep chains, linear
|
||||
// pipelines), we execute it inline rather than spawning it. This provides
|
||||
// 10-100x performance improvements for sequential workloads by eliminating:
|
||||
// - Task spawning overhead
|
||||
// - Context switching to/from the runtime
|
||||
//
|
||||
// CRITICAL: Panic handling is required to maintain behavioral consistency.
|
||||
// All async runtimes (Tokio, async-std, smol, embassy-rs) catch panics in
|
||||
// spawned tasks and convert them to errors. We must do the same for inline
|
||||
// execution to ensure tasks behave identically regardless of execution path.
|
||||
//
|
||||
// Without panic catching:
|
||||
// - Spawned task panics → caught by runtime → becomes error (expected)
|
||||
// - Inline task panics → bubbles up → crashes program (surprising!)
|
||||
//
|
||||
// With panic catching (current implementation):
|
||||
// - Spawned task panics → caught by runtime → becomes error
|
||||
// - Inline task panics → caught by us → becomes error (consistent!)
|
||||
//
|
||||
// This ensures the optimization is transparent to users.
|
||||
// ============================================================================
|
||||
if layer.len() == 1 {
|
||||
let node_id = layer[0];
|
||||
|
||||
@@ -392,24 +315,16 @@ impl DagRunner {
|
||||
);
|
||||
|
||||
// Take ownership of the node
|
||||
let node = {
|
||||
let mut nodes_lock = self.nodes.lock();
|
||||
nodes_lock[node_id.0 as usize].take()
|
||||
};
|
||||
let node = self.nodes[node_id.0 as usize].take();
|
||||
|
||||
if let Some(node) = node {
|
||||
let dependencies: Vec<_> = edges[&node_id]
|
||||
let dependencies: Vec<_> = self.edges[&node_id]
|
||||
.iter()
|
||||
.flat_map(|dep| outputs.get(dep))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Execute inline with panic handling.
|
||||
//
|
||||
// FutureExt::catch_unwind() ensures panics are caught and converted to
|
||||
// DagError::TaskPanicked, matching the behavior of async runtimes when
|
||||
// they spawn tasks. This guarantees consistent error handling whether
|
||||
// a task executes inline (single-task layer) or spawned (multi-task layer).
|
||||
let result = AssertUnwindSafe(node.execute_with_deps(dependencies))
|
||||
.catch_unwind()
|
||||
.await
|
||||
@@ -456,12 +371,9 @@ impl DagRunner {
|
||||
trace!(task_id = node_id.0, "spawning task");
|
||||
|
||||
// Take ownership of the node
|
||||
let node = {
|
||||
let mut nodes_lock = self.nodes.lock();
|
||||
nodes_lock[node_id.0 as usize].take()
|
||||
};
|
||||
let node = self.nodes[node_id.0 as usize].take();
|
||||
if let Some(node) = node {
|
||||
let dependencies: Vec<_> = edges[&node_id]
|
||||
let dependencies: Vec<_> = self.edges[&node_id]
|
||||
.iter()
|
||||
.flat_map(|dep| outputs.get(dep))
|
||||
.cloned()
|
||||
@@ -520,95 +432,21 @@ impl DagRunner {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert all successful outputs (avoiding holding lock across await)
|
||||
let mut outputs_lock = self.outputs.lock();
|
||||
for (node_id, output) in outputs {
|
||||
outputs_lock.insert(node_id, output);
|
||||
}
|
||||
drop(outputs_lock);
|
||||
|
||||
// Return first error if any
|
||||
if let Some(err) = first_error {
|
||||
#[cfg(feature = "tracing")]
|
||||
error!(?err, "DAG execution failed");
|
||||
return Err(err);
|
||||
// Return first error if any, aborting execution after this layer
|
||||
if let Some(err) = first_error {
|
||||
#[cfg(feature = "tracing")]
|
||||
error!(?err, "DAG execution failed");
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
info!("DAG execution completed successfully");
|
||||
|
||||
Ok(())
|
||||
Ok(DagOutput::new(outputs))
|
||||
}
|
||||
|
||||
/// Retrieve a task's output after [`DagRunner::run`].
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// All task outputs are stored after execution and can be retrieved via get().
|
||||
/// Each task's output can only be taken once, after which it will return `DagError::ResultNotFound`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `DagError::ResultNotFound` if:
|
||||
/// - The task hasn't been executed yet
|
||||
/// - The handle is invalid
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use dagx::{task, DagRunner, Task};
|
||||
/// #
|
||||
/// struct Configuration {
|
||||
/// setting: i32,
|
||||
/// }
|
||||
///
|
||||
/// impl Configuration {
|
||||
/// fn new(setting: i32) -> Self {
|
||||
/// Self { setting }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Configuration {
|
||||
/// async fn run(&mut self) -> i32 { self.setting }
|
||||
/// }
|
||||
///
|
||||
/// # async {
|
||||
/// let dag = DagRunner::new();
|
||||
///
|
||||
/// // Construct task with specific setting value
|
||||
/// let task = dag.add_task(Configuration::new(42));
|
||||
///
|
||||
/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
///
|
||||
/// assert_eq!(dag.get(task).unwrap(), 42);
|
||||
/// # };
|
||||
/// ```
|
||||
pub fn get<T: 'static + Send + Sync, H>(&self, handle: H) -> DagResult<T>
|
||||
where
|
||||
H: Into<TaskHandle<T>>,
|
||||
{
|
||||
let handle: TaskHandle<T> = handle.into();
|
||||
let mut outputs = self.outputs.lock();
|
||||
|
||||
let arc_output = outputs.remove(&handle.id).ok_or(DagError::ResultNotFound {
|
||||
task_id: handle.id.0,
|
||||
})?;
|
||||
|
||||
// Downcast Arc<dyn Any> to Arc<T>
|
||||
let output = arc_output
|
||||
.downcast::<T>()
|
||||
.map_err(|_| DagError::TypeMismatch {
|
||||
expected: std::any::type_name::<T>(),
|
||||
found: "unknown",
|
||||
})?;
|
||||
|
||||
Ok(Arc::into_inner(output).unwrap())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn compute_layers(&self) -> DagResult<Vec<Vec<NodeId>>> {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!("computing topological layers");
|
||||
@@ -617,18 +455,11 @@ impl DagRunner {
|
||||
let mut layers = Vec::new();
|
||||
|
||||
// Calculate in-degrees: for each node, count how many dependencies it has
|
||||
let edges = self.edges.lock();
|
||||
|
||||
// Compiler optimization hint: provides bounds information for HashSet sizing.
|
||||
// This variable guides the optimizer's understanding of graph size, improving
|
||||
// hash table and iterator performance in the topological sort below.
|
||||
let _total_nodes = edges.len();
|
||||
|
||||
for (&node, deps) in edges.iter() {
|
||||
for (&node, deps) in self.edges.iter() {
|
||||
let degree = deps.len();
|
||||
in_degree.insert(node, degree);
|
||||
}
|
||||
drop(edges); // Release lock early
|
||||
|
||||
// Find all nodes with in-degree 0 (sources - nodes with no dependencies)
|
||||
let mut queue: VecDeque<NodeId> = in_degree
|
||||
@@ -653,8 +484,7 @@ impl DagRunner {
|
||||
visited.insert(node);
|
||||
|
||||
// For each node that depends on the current node
|
||||
let dependents = self.dependents.lock();
|
||||
if let Some(deps) = dependents.get(&node) {
|
||||
if let Some(deps) = self.dependents.get(&node) {
|
||||
for &dependent in deps {
|
||||
if let Some(degree) = in_degree.get_mut(&dependent) {
|
||||
*degree -= 1;
|
||||
@@ -682,10 +512,6 @@ impl DagRunner {
|
||||
//
|
||||
// This eliminates the need for runtime cycle detection, providing both
|
||||
// safety guarantees and performance benefits (no validation overhead).
|
||||
|
||||
// Compiler optimization barrier: ensures visited set is fully populated.
|
||||
// This assertion compiles to nothing in release builds but guides the optimizer's
|
||||
// understanding of the HashSet state, improving subsequent code generation.
|
||||
debug_assert!(!visited.is_empty() || layers.is_empty());
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -695,8 +521,5 @@ impl DagRunner {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We cannot implement Default for DagRunner anymore since new() returns Arc<Self>.
|
||||
// Users should call DagRunner::new() directly.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+24
-118
@@ -3,9 +3,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hasher;
|
||||
|
||||
use crate::error::DagError;
|
||||
use crate::builder::TaskHandle;
|
||||
use crate::runner::{DagRunner, PassThroughHasher};
|
||||
use crate::types::TaskHandle;
|
||||
|
||||
// Initialize tracing subscriber for tests (idempotent)
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -40,16 +39,16 @@ impl TestTask {
|
||||
#[test]
|
||||
fn test_dag_runner_new() {
|
||||
// Test that DagRunner::new() creates a new instance
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Initial state should be empty
|
||||
assert_eq!(dag.nodes.lock().len(), 0);
|
||||
assert_eq!(dag.edges.lock().len(), 0);
|
||||
assert_eq!(dag.dependents.lock().len(), 0);
|
||||
assert_eq!(dag.nodes.len(), 0);
|
||||
assert_eq!(dag.edges.len(), 0);
|
||||
assert_eq!(dag.dependents.len(), 0);
|
||||
|
||||
// Adding a task works
|
||||
dag.add_task(TestTask { value: 42 });
|
||||
assert_eq!(dag.nodes.lock().len(), 1);
|
||||
assert_eq!(dag.nodes.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -58,19 +57,21 @@ fn test_dag_runner_default() {
|
||||
let dag = DagRunner::default();
|
||||
|
||||
// Should behave the same as new()
|
||||
assert_eq!(dag.nodes.lock().len(), 0);
|
||||
assert_eq!(dag.edges.lock().len(), 0);
|
||||
assert_eq!(dag.dependents.lock().len(), 0);
|
||||
assert_eq!(dag.nodes.len(), 0);
|
||||
assert_eq!(dag.edges.len(), 0);
|
||||
assert_eq!(dag.dependents.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic]
|
||||
async fn test_get_wrong_type() {
|
||||
init_tracing();
|
||||
// Test getting with wrong type - this should return TypeMismatch
|
||||
let dag = DagRunner::new();
|
||||
// Test getting with wrong type - this should panic
|
||||
let mut dag = DagRunner::new();
|
||||
let handle = dag.add_task(TestTask { value: 42 });
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -80,98 +81,11 @@ async fn test_get_wrong_type() {
|
||||
_phantom: std::marker::PhantomData,
|
||||
};
|
||||
|
||||
let result = dag.get(fake_handle);
|
||||
assert!(result.is_err());
|
||||
|
||||
// When downcast fails, we get TypeMismatch
|
||||
match result.unwrap_err() {
|
||||
DagError::TypeMismatch { expected, .. } => {
|
||||
assert_eq!(expected, std::any::type_name::<String>());
|
||||
}
|
||||
_ => panic!("Expected TypeMismatch error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_result_not_found() {
|
||||
init_tracing();
|
||||
// Test getting a result before running the DAG
|
||||
let dag = DagRunner::new();
|
||||
let handle = dag.add_task(TestTask { value: 42 });
|
||||
let handle_id = handle.id.0; // Save ID before moving handle
|
||||
|
||||
let result = dag.get(handle);
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
DagError::ResultNotFound { task_id } => {
|
||||
assert_eq!(task_id, handle_id);
|
||||
}
|
||||
_ => panic!("Expected ResultNotFound error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_run_protection() {
|
||||
init_tracing();
|
||||
// Test that run_lock prevents concurrent runs
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Add a task that takes some time
|
||||
struct SlowTask;
|
||||
#[crate::task]
|
||||
impl SlowTask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
42
|
||||
}
|
||||
}
|
||||
|
||||
dag.add_task(SlowTask);
|
||||
|
||||
// Wrap in Arc for sharing between tasks
|
||||
let dag = Arc::new(dag);
|
||||
let dag1 = Arc::clone(&dag);
|
||||
let dag2 = Arc::clone(&dag);
|
||||
|
||||
// Use a barrier to ensure both runs start at exactly the same time
|
||||
let barrier = Arc::new(tokio::sync::Barrier::new(2));
|
||||
let barrier1 = barrier.clone();
|
||||
let barrier2 = barrier.clone();
|
||||
|
||||
// Start two runs concurrently
|
||||
let handle1 = tokio::spawn(async move {
|
||||
barrier1.wait().await;
|
||||
dag1.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
});
|
||||
|
||||
let handle2 = tokio::spawn(async move {
|
||||
barrier2.wait().await;
|
||||
dag2.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
});
|
||||
|
||||
// One should succeed, one should fail (concurrent execution not supported)
|
||||
let result1 = handle1.await.unwrap();
|
||||
let result2 = handle2.await.unwrap();
|
||||
|
||||
// Exactly one should be ok, one should be err
|
||||
assert!(result1.is_ok() != result2.is_ok());
|
||||
|
||||
// The error should be about concurrent execution
|
||||
if let Err(e) = result1 {
|
||||
assert!(matches!(e, crate::error::DagError::ConcurrentExecution));
|
||||
}
|
||||
if let Err(e) = result2 {
|
||||
assert!(matches!(e, crate::error::DagError::ConcurrentExecution));
|
||||
}
|
||||
let _result = output.get(fake_handle);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic]
|
||||
async fn test_invalid_node_id_in_get() {
|
||||
init_tracing();
|
||||
// Test getting with an invalid node ID
|
||||
@@ -179,24 +93,16 @@ async fn test_invalid_node_id_in_get() {
|
||||
|
||||
// Create a handle with an ID that doesn't exist
|
||||
let invalid_handle: TaskHandle<i32> = TaskHandle {
|
||||
id: crate::types::NodeId(999),
|
||||
id: crate::builder::NodeId(999),
|
||||
_phantom: std::marker::PhantomData,
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = dag.get(invalid_handle);
|
||||
assert!(result.is_err());
|
||||
|
||||
// With centralized output storage, we get ResultNotFound for invalid node IDs
|
||||
match result.unwrap_err() {
|
||||
DagError::ResultNotFound { task_id } => {
|
||||
assert_eq!(task_id, 999);
|
||||
}
|
||||
_ => panic!("Expected ResultNotFound error"),
|
||||
}
|
||||
let _result = output.get(invalid_handle);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -228,12 +134,12 @@ async fn test_task_panic_in_multi_task_layer() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let source: TaskHandle<_> = dag.add_task(Source).into();
|
||||
let mut dag = DagRunner::new();
|
||||
let source = dag.add_task(Source);
|
||||
|
||||
// Create two tasks in the same layer - one panics, one doesn't
|
||||
let _panic_task = dag.add_task(PanicTask).depends_on(source);
|
||||
let _good_task = dag.add_task(GoodTask).depends_on(source);
|
||||
let _panic_task = dag.add_task(PanicTask).depends_on(&source);
|
||||
let _good_task = dag.add_task(GoodTask).depends_on(&source);
|
||||
|
||||
// Run the DAG - the panic should be caught
|
||||
let result = dag
|
||||
|
||||
+19
-79
@@ -7,72 +7,14 @@ use std::{any::Any, future::Future, marker::PhantomData, slice::Iter, sync::Arc}
|
||||
|
||||
/// A unit of async work with typed inputs and outputs.
|
||||
///
|
||||
/// Use the [`task`](crate::task) macro to implement this trait. The macro automatically
|
||||
/// Use the [`crate::task`] macro to implement this trait. The macro automatically
|
||||
/// derives `Input` and `Output` types from your `run()` method signature and generates
|
||||
/// type-specific extraction logic in `run()`.
|
||||
/// extraction logic in `run()`.
|
||||
///
|
||||
/// **Custom types work automatically!** As long as your output type implements
|
||||
/// `Send + Sync + 'static`, the macro handles everything. No trait implementations needed!
|
||||
/// `Send + Sync + 'static`, the macro handles everything.
|
||||
///
|
||||
/// # Task Patterns
|
||||
///
|
||||
/// dagx supports three main task patterns:
|
||||
///
|
||||
/// ## 1. Stateless Tasks (Pure Functions)
|
||||
///
|
||||
/// Tasks with no internal state - just transform inputs to outputs:
|
||||
///
|
||||
/// ```
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Add;
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Add {
|
||||
/// async fn run(a: &i32, b: &i32) -> i32 {
|
||||
/// a + b
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## 2. Stateful Tasks with Shared State (`&self`)
|
||||
///
|
||||
/// Tasks that read from internal state but don't modify it:
|
||||
///
|
||||
/// ```
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Multiplier {
|
||||
/// factor: i32,
|
||||
/// }
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Multiplier {
|
||||
/// async fn run(&self, input: &i32) -> i32 {
|
||||
/// input * self.factor
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## 3. Stateful Tasks with Mutable State (`&mut self`)
|
||||
///
|
||||
/// Tasks that modify internal state during execution:
|
||||
///
|
||||
/// ```
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Counter {
|
||||
/// count: i32,
|
||||
/// }
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Counter {
|
||||
/// async fn run(&mut self, increment: &i32) -> i32 {
|
||||
/// self.count += increment;
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// **Note:** When implementing this trait manually for a task with a single dependency, the Input type must be (T,) instead of T.
|
||||
///
|
||||
/// # Input Patterns
|
||||
///
|
||||
@@ -80,10 +22,6 @@ use std::{any::Any, future::Future, marker::PhantomData, slice::Iter, sync::Arc}
|
||||
/// - `&T`: Single dependency
|
||||
/// - `&A, &B, &C, ...`: Multiple dependencies (up to 8, order matters)
|
||||
///
|
||||
/// # The Underlying Trait
|
||||
///
|
||||
/// The `#[task]` macro generates an implementation of this trait:
|
||||
///
|
||||
/// # Output Wrapping
|
||||
///
|
||||
/// **Task outputs are automatically wrapped in `Arc<T>` internally** for efficient fan-out.
|
||||
@@ -92,13 +30,23 @@ use std::{any::Any, future::Future, marker::PhantomData, slice::Iter, sync::Arc}
|
||||
///
|
||||
/// Downstream tasks receive `&T` after ExtractInput extracts from the Arc. This is transparent
|
||||
/// to your task code - you work with `T`, the framework handles Arc wrapping/unwrapping.
|
||||
pub trait Task: Send {
|
||||
type Input: Send;
|
||||
type Output: Send + Sync; // Sync required for Arc-wrapping and cross-thread sharing
|
||||
pub trait Task<Input>: Send
|
||||
where
|
||||
Input: Send + Sync + 'static,
|
||||
{
|
||||
type Output: Send + Sync + 'static; // Sync required for Arc-wrapping and cross-thread sharing
|
||||
|
||||
fn run(self, input: TaskInput<Self::Input>) -> impl Future<Output = Self::Output> + Send;
|
||||
fn run(self, input: TaskInput<Input>) -> impl Future<Output = Self::Output> + Send;
|
||||
}
|
||||
|
||||
/// Linear type providing a [`Task`] access to its dependencies.
|
||||
///
|
||||
/// The framework ensures that all dependencies will be present when this task runs, and
|
||||
/// can be retrieved with [`TaskInput::next`]. The nth call to `next` consumes this TaskInput instances and returns a
|
||||
/// (&T, TaskInput) tuple with the nth dependency specified in the call to [`crate::TaskBuilder::depends_on`] and a
|
||||
/// TaskInput with the remaining dependencies.
|
||||
///
|
||||
/// No dependencies can be retrieved from an empty TaskInput (Input = ()).
|
||||
pub struct TaskInput<'inputs, Input: Send> {
|
||||
inputs: Iter<'inputs, Arc<dyn Any + Send + Sync + 'static>>,
|
||||
phantom: PhantomData<Input>,
|
||||
@@ -116,6 +64,7 @@ impl<'inputs, Input: Send> TaskInput<'inputs, Input> {
|
||||
macro_rules! impl_task_input {
|
||||
($First:ident $(, $T:ident)*) => {
|
||||
impl<'inputs, $First: 'static + Send, $($T: 'static + Send),*> TaskInput<'inputs, ($First, $($T,)*)> {
|
||||
/// Get a reference to the next dependency from this TaskInput, and a [`TaskInput`] instance with the remaining dependencies.
|
||||
#[must_use]
|
||||
pub fn next(mut self) -> (&'inputs $First, TaskInput<'inputs, ($($T,)*)>) {
|
||||
let value = self.inputs.next().and_then(|value| value.downcast_ref()).unwrap();
|
||||
@@ -138,14 +87,5 @@ impl_task_input!(A, B, C, D, E, F);
|
||||
impl_task_input!(A, B, C, D, E, F, G);
|
||||
impl_task_input!(A, B, C, D, E, F, G, H);
|
||||
|
||||
impl TaskInput<'static, ()> {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
inputs: [].iter(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
+2
-9
@@ -9,11 +9,10 @@ struct CustomTask {
|
||||
state: i32,
|
||||
}
|
||||
|
||||
impl Task for CustomTask {
|
||||
type Input = (i32,);
|
||||
impl Task<(i32,)> for CustomTask {
|
||||
type Output = i32;
|
||||
|
||||
async fn run(mut self, input: TaskInput<'_, Self::Input>) -> i32 {
|
||||
async fn run(mut self, input: TaskInput<'_, (i32,)>) -> i32 {
|
||||
self.state += input.next().0;
|
||||
self.state
|
||||
}
|
||||
@@ -33,9 +32,3 @@ async fn test_custom_task_implementation() {
|
||||
// Can't call run again since task was consumed
|
||||
// This demonstrates the ownership model
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_task_input() {
|
||||
let empty = TaskInput::empty();
|
||||
assert_eq!(empty.inputs.len(), 0);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
//! Core type definitions for DAG nodes and handles.
|
||||
//!
|
||||
//! This module defines the fundamental types used throughout the DAG system,
|
||||
//! including node identifiers, task handles, and type-state markers.
|
||||
|
||||
use crate::builder::TaskBuilder;
|
||||
use crate::task::Task;
|
||||
|
||||
/// Opaque node identifier
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct NodeId(pub u32);
|
||||
|
||||
/// Opaque, typed token for a node's output.
|
||||
///
|
||||
/// A `TaskHandle<T>` provides compile-time type-safe access to a task's output.
|
||||
/// You can:
|
||||
/// 1. Pass it to [`crate::TaskBuilder::depends_on`] to wire up dependencies
|
||||
/// 2. Use it with [`crate::DagRunner::get`] to retrieve the output after [`crate::DagRunner::run`]
|
||||
///
|
||||
/// Handles are cheap to clone and copy.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use dagx::{task, DagRunner, Task};
|
||||
/// #
|
||||
/// # struct LoadValue { value: i32 }
|
||||
/// # impl LoadValue { pub fn new(v: i32) -> Self { Self { value: v } } }
|
||||
/// # #[task]
|
||||
/// # impl LoadValue {
|
||||
/// # async fn run(&mut self) -> i32 { self.value }
|
||||
/// # }
|
||||
/// # async {
|
||||
/// let dag = DagRunner::new();
|
||||
/// let node = dag.add_task(LoadValue::new(42));
|
||||
///
|
||||
/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
||||
///
|
||||
/// assert_eq!(dag.get(node).unwrap(), 42);
|
||||
/// # };
|
||||
/// ```
|
||||
pub struct TaskHandle<T> {
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) _phantom: std::marker::PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for TaskHandle<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for TaskHandle<T> {}
|
||||
|
||||
/// Type-level marker for empty dependency list.
|
||||
///
|
||||
/// This type appears in the type signature of [`crate::TaskBuilder<Tk, Pending>`](crate::TaskBuilder) before
|
||||
/// dependencies are specified with [`crate::TaskBuilder::depends_on`].
|
||||
///
|
||||
/// You don't interact with this type directly; it's part of the type-state
|
||||
/// pattern that ensures dependencies are wired correctly at compile time.
|
||||
pub struct Pending;
|
||||
|
||||
// TaskBuilder can be converted to a TaskHandle ONLY if Input is () (unit type)
|
||||
// This enforces at compile time that tasks with non-unit inputs MUST call .depends_on()
|
||||
impl<'a, Tk: Task<Input = ()>, Deps> From<TaskBuilder<'a, Tk, Deps>> for TaskHandle<Tk::Output> {
|
||||
fn from(node: TaskBuilder<'a, Tk, Deps>) -> Self {
|
||||
TaskHandle {
|
||||
id: node.id,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TaskHandle can be converted from &TaskHandle (for .get() calls)
|
||||
impl<T> From<&TaskHandle<T>> for TaskHandle<T> {
|
||||
fn from(handle: &TaskHandle<T>) -> Self {
|
||||
*handle
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod coverage_tests;
|
||||
@@ -1,50 +0,0 @@
|
||||
//! Unit tests for types module
|
||||
|
||||
use crate::types::{NodeId, TaskHandle};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::clone_on_copy)]
|
||||
fn test_task_handle_clone() {
|
||||
let handle: TaskHandle<(i32,)> = TaskHandle {
|
||||
id: NodeId(42),
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
// Test Clone implementation
|
||||
let cloned = handle.clone();
|
||||
assert_eq!(handle.id.0, cloned.id.0);
|
||||
|
||||
let handle2 = handle.clone();
|
||||
let handle3 = handle2.clone();
|
||||
assert_eq!(handle3.id.0, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_handle_copy() {
|
||||
let handle: TaskHandle<String> = TaskHandle {
|
||||
id: NodeId(99),
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
// Test that TaskHandle implements Copy
|
||||
let copied = handle;
|
||||
let copied2 = handle; // This wouldn't compile if not Copy
|
||||
assert_eq!(copied.id.0, 99);
|
||||
assert_eq!(copied2.id.0, 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_handle_ref_from_conversion() {
|
||||
// Test From<&TaskHandle<T>> for TaskHandle<T> (line 90-93)
|
||||
let handle: TaskHandle<String> = TaskHandle {
|
||||
id: NodeId(123),
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
let handle_ref = &handle;
|
||||
let converted: TaskHandle<String> = handle_ref.into();
|
||||
|
||||
assert_eq!(converted.id.0, 123);
|
||||
assert_eq!(handle.id, converted.id);
|
||||
}
|
||||
+6
-24
@@ -4,22 +4,6 @@
|
||||
|
||||
Integration and performance tests for dagx. Unit tests live in `src/{module}/tests.rs`.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── common/ # Shared test utilities
|
||||
├── dependencies/ # Dependency resolution tests
|
||||
├── execution/ # Task execution and DAG running
|
||||
├── parallelism/ # Parallel execution verification
|
||||
├── panic/ # Panic handling and isolation
|
||||
├── performance/ # Stress tests and benchmarks
|
||||
├── runtimes/ # Runtime compatibility tests
|
||||
├── task_patterns/ # Common task pattern tests
|
||||
├── type_safety/ # Type system validation
|
||||
└── lib_tests.rs # Main test entry point
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
@@ -56,19 +40,17 @@ cargo test --release -- --ignored
|
||||
|
||||
### Integration Tests (`tests/`)
|
||||
|
||||
- **execution**: End-to-end DAG execution
|
||||
- **boundaries**: Edge cases (very large/very small graphs)
|
||||
- **dependencies**: Complex dependency graphs
|
||||
- **errors**: Error handling
|
||||
- **execution**: End-to-end DAG execution
|
||||
- **interleaving**: Layer interleaving patterns
|
||||
- **parallelism**: Concurrent task execution
|
||||
- **panic**: Error propagation and isolation
|
||||
- **performance**: Large DAGs and stress tests
|
||||
- **runtimes**: Tokio, async-std, smol compatibility
|
||||
- **task_patterns**: Common usage patterns
|
||||
- **type_safety**: Type system guarantees
|
||||
- **runtimes**: Tokio, smol compatibility
|
||||
- **tracing**: Execution observability
|
||||
|
||||
## Writing Tests
|
||||
|
||||
Integration tests use common utilities from `tests/common/mod.rs`.
|
||||
|
||||
Unit tests should:
|
||||
|
||||
- Test single units of functionality
|
||||
|
||||
@@ -9,18 +9,18 @@ use std::sync::Arc;
|
||||
#[tokio::test]
|
||||
async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
||||
// Test 10 parallel chains of depth 500 each
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let mut chain_ends = Vec::new();
|
||||
|
||||
for chain_id in 0..8 {
|
||||
let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| chain_id * 1000));
|
||||
let mut current: dagx::TaskHandle<i32> = first.into();
|
||||
let mut current: dagx::TaskHandle<i32> = first;
|
||||
|
||||
for _ in 0..500 {
|
||||
current = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&prev: &i32| prev + 1))
|
||||
.depends_on(current);
|
||||
.depends_on(¤t);
|
||||
}
|
||||
|
||||
chain_ends.push(current);
|
||||
@@ -51,7 +51,8 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
||||
&chain_ends[7],
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Each chain: start + 500
|
||||
@@ -59,7 +60,7 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
||||
// Chain 1: 1000 + 500 = 1500
|
||||
// ...
|
||||
// Sum = 500*8 + (0+1000+2000+...+7000) = 4000 + 28000 = 32000
|
||||
assert_eq!(dag.get(final_task)?, 32000);
|
||||
assert_eq!(output.get(final_task), 32000);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -67,7 +68,7 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_execution_order() -> DagResult<()> {
|
||||
// Verify tasks in a deep chain execute in correct order
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let order = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let first = dag.add_task(task_fn::<(), _, _>({
|
||||
@@ -78,7 +79,7 @@ async fn test_deep_chain_execution_order() -> DagResult<()> {
|
||||
0
|
||||
}
|
||||
}));
|
||||
let mut current: dagx::TaskHandle<i32> = first.into();
|
||||
let mut current: dagx::TaskHandle<i32> = first;
|
||||
|
||||
for expected in 1..100 {
|
||||
current = dag
|
||||
@@ -90,14 +91,15 @@ async fn test_deep_chain_execution_order() -> DagResult<()> {
|
||||
prev + 1
|
||||
}
|
||||
}))
|
||||
.depends_on(current);
|
||||
.depends_on(¤t);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(order.load(Ordering::SeqCst), 100);
|
||||
assert_eq!(dag.get(current)?, 99);
|
||||
assert_eq!(output.get(current), 99);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -105,12 +107,12 @@ async fn test_deep_chain_execution_order() -> DagResult<()> {
|
||||
#[tokio::test]
|
||||
async fn test_deep_binary_tree() -> DagResult<()> {
|
||||
// Test a deep binary tree structure (depth 10 = 1023 nodes)
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> {
|
||||
fn build_tree(dag: &mut DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> {
|
||||
if depth == 0 {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value));
|
||||
task.into() // Convert TaskBuilder to TaskHandle
|
||||
task // Convert TaskBuilder to TaskHandle
|
||||
} else {
|
||||
let left = build_tree(dag, depth - 1, value * 2);
|
||||
let right = build_tree(dag, depth - 1, value * 2 + 1);
|
||||
@@ -122,14 +124,15 @@ async fn test_deep_binary_tree() -> DagResult<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let root = build_tree(&dag, 10, 1);
|
||||
let root = build_tree(&mut dag, 10, 1);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Sum of all values in a complete binary tree with values 1024..2047
|
||||
// This is sum from 2^10 to 2^11-1 = 1024 * 1023 / 2 + 1024 * 512 = 1047552
|
||||
let result = dag.get(root)?;
|
||||
let result = output.get(root);
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
@@ -138,13 +141,13 @@ async fn test_deep_binary_tree() -> DagResult<()> {
|
||||
#[tokio::test]
|
||||
async fn test_fibonacci_chain() -> DagResult<()> {
|
||||
// Test a Fibonacci-like dependency pattern
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let f0_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64));
|
||||
let f1_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 1i64));
|
||||
|
||||
let mut prev2: dagx::TaskHandle<i64> = f0_builder.into();
|
||||
let mut prev1: dagx::TaskHandle<i64> = f1_builder.into();
|
||||
let mut prev2: dagx::TaskHandle<i64> = f0_builder;
|
||||
let mut prev1: dagx::TaskHandle<i64> = f1_builder;
|
||||
|
||||
for _ in 2..50 {
|
||||
let next = dag
|
||||
@@ -155,11 +158,12 @@ async fn test_fibonacci_chain() -> DagResult<()> {
|
||||
prev1 = next;
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// 50th Fibonacci number (0-indexed) = 7778742049
|
||||
assert_eq!(dag.get(prev1)?, 7778742049);
|
||||
assert_eq!(output.get(prev1), 7778742049);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -167,25 +171,25 @@ async fn test_fibonacci_chain() -> DagResult<()> {
|
||||
#[tokio::test]
|
||||
async fn test_zigzag_dependencies() -> DagResult<()> {
|
||||
// Test a zigzag pattern: two chains that cross-reference each other
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let a_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
let b_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
|
||||
|
||||
let mut chain_a: dagx::TaskHandle<i32> = a_first.into();
|
||||
let mut chain_b: dagx::TaskHandle<i32> = b_first.into();
|
||||
let mut chain_a: dagx::TaskHandle<i32> = a_first;
|
||||
let mut chain_b: dagx::TaskHandle<i32> = b_first;
|
||||
|
||||
for i in 0..100 {
|
||||
if i % 2 == 0 {
|
||||
// A depends on B
|
||||
chain_a = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |b_val: &i32| b_val + 1))
|
||||
.depends_on(chain_b);
|
||||
.depends_on(&chain_b);
|
||||
} else {
|
||||
// B depends on A
|
||||
chain_b = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |a_val: &i32| a_val + 2))
|
||||
.depends_on(chain_a);
|
||||
.depends_on(&chain_a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,10 +198,11 @@ async fn test_zigzag_dependencies() -> DagResult<()> {
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((&chain_a, &chain_b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(final_task)?;
|
||||
let result = output.get(final_task);
|
||||
assert!(result > 100);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Tests for type system edge cases and limits
|
||||
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
@@ -16,13 +16,13 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> {
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Task producing ZST
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Empty));
|
||||
|
||||
// Task that also produces ZST (independent since we can't extract custom types)
|
||||
let t2: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
// Task with PhantomData
|
||||
let _t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| PhantomWrapper::<i32> {
|
||||
@@ -35,14 +35,15 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> {
|
||||
assert_eq!(val, 42);
|
||||
"success"
|
||||
}))
|
||||
.depends_on(t2);
|
||||
.depends_on(&t2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t1)?, Empty);
|
||||
assert_eq!(dag.get(t2)?, 42);
|
||||
assert_eq!(dag.get(t4)?, "success");
|
||||
assert_eq!(output.get(t1), Empty);
|
||||
assert_eq!(output.get(t2), 42);
|
||||
assert_eq!(output.get(t4), "success");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -56,7 +57,7 @@ async fn test_large_value_types() -> DagResult<()> {
|
||||
more_data: Vec<u64>,
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
let large = LargeStruct {
|
||||
@@ -71,39 +72,13 @@ async fn test_large_value_types() -> DagResult<()> {
|
||||
.add_task(task_fn::<usize, _, _>(|&val: &usize| {
|
||||
val // Just pass through to verify
|
||||
}))
|
||||
.depends_on(t1);
|
||||
.depends_on(&t1);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t2)?, 1042);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unit_type_chains() -> DagResult<()> {
|
||||
// Test chains of unit type operations
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| {})).into();
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {}))
|
||||
.depends_on((t1,));
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {}))
|
||||
.depends_on((t2,));
|
||||
let t4 = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| "done"))
|
||||
.depends_on((t3,));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
dag.get(t1)?; // Verify no error
|
||||
dag.get(t2)?;
|
||||
dag.get(t3)?;
|
||||
assert_eq!(dag.get(t4)?, "done");
|
||||
assert_eq!(output.get(t2), 1042);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -113,7 +88,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
|
||||
// Test types that wrap references
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
let arc = Arc::new(Mutex::new(100));
|
||||
@@ -124,12 +99,13 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
|
||||
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&val: &i32| val * 2))
|
||||
.depends_on(t1);
|
||||
.depends_on(&t1);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t2)?, 200);
|
||||
assert_eq!(output.get(t2), 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -8,17 +8,18 @@ use tokio::task::yield_now;
|
||||
#[tokio::test]
|
||||
async fn test_dag_with_only_source_tasks() -> DagResult<()> {
|
||||
// Test a DAG with multiple source tasks but no dependencies
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..100)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
assert_eq!(output.get(task), i);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -38,20 +39,21 @@ async fn test_single_self_contained_stateful_task() -> DagResult<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let task = dag.add_task(Counter { value: 21 });
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, 42);
|
||||
assert_eq!(output.get(task), 42);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> {
|
||||
// Test a task that does the absolute minimum async work
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
struct YieldTask;
|
||||
|
||||
@@ -65,9 +67,10 @@ async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> {
|
||||
|
||||
let task = dag.add_task(YieldTask);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, 1337);
|
||||
assert_eq!(output.get(task), 1337);
|
||||
Ok(())
|
||||
}
|
||||
+24
-21
@@ -1,5 +1,5 @@
|
||||
// Test that custom types work with the #[task] macro
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Person {
|
||||
@@ -51,33 +51,35 @@ impl PersonToCompany {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_type_single_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let fetch = dag.add_task(FetchPerson);
|
||||
let process = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||
let process = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = dag.get(process).unwrap();
|
||||
let result = output.get(process);
|
||||
assert_eq!(result, "Alice is 30 years old");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_type_passthrough() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let fetch: TaskHandle<_> = dag.add_task(FetchPerson).into();
|
||||
let transform = dag.add_task(PersonToCompany).depends_on(fetch);
|
||||
let process = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||
let fetch = dag.add_task(FetchPerson);
|
||||
let transform = dag.add_task(PersonToCompany).depends_on(&fetch);
|
||||
let process = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let person_result = dag.get(process).unwrap();
|
||||
let company_result = dag.get(transform).unwrap();
|
||||
let person_result = output.get(process);
|
||||
let company_result = output.get(transform);
|
||||
|
||||
assert_eq!(person_result.as_str(), "Alice is 30 years old");
|
||||
assert_eq!(company_result.name, "Alice's Company");
|
||||
@@ -86,20 +88,21 @@ async fn test_custom_type_passthrough() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_type_fan_out() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let fetch: TaskHandle<_> = dag.add_task(FetchPerson).into();
|
||||
let process1 = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||
let process2 = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||
let process3 = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||
let fetch = dag.add_task(FetchPerson);
|
||||
let process1 = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||
let process2 = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||
let process3 = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result1 = dag.get(process1).unwrap();
|
||||
let result2 = dag.get(process2).unwrap();
|
||||
let result3 = dag.get(process3).unwrap();
|
||||
let result1 = output.get(process1);
|
||||
let result2 = output.get(process2);
|
||||
let result3 = output.get(process3);
|
||||
|
||||
assert_eq!(result1, "Alice is 30 years old");
|
||||
assert_eq!(result2, "Alice is 30 years old");
|
||||
|
||||
+24
-52
@@ -3,7 +3,7 @@
|
||||
//! These tests show that even if you try to create cycles, the Rust compiler
|
||||
//! prevents it through the type-state pattern.
|
||||
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
/// Demonstrates that TaskBuilder is consumed by depends_on,
|
||||
/// preventing you from wiring the same task multiple times
|
||||
@@ -25,45 +25,16 @@ fn test_builder_consumed_by_depends_on() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let source = dag.add_task(Source);
|
||||
let consumer_builder = dag.add_task(Consumer);
|
||||
|
||||
// This consumes consumer_builder and returns a TaskHandle
|
||||
let _consumer_handle = consumer_builder.depends_on(source);
|
||||
let _consumer_handle = consumer_builder.depends_on(&source);
|
||||
|
||||
// We CANNOT use consumer_builder again because it was moved
|
||||
// This would fail to compile:
|
||||
// let _consumer2 = consumer_builder.depends_on(source); // ERROR: use of moved value
|
||||
}
|
||||
|
||||
/// Demonstrates that you can only get a TaskHandle by finalizing a builder,
|
||||
/// and handles are immutable references with no methods to add dependencies
|
||||
#[test]
|
||||
fn test_handle_is_immutable_reference() {
|
||||
struct NoInput;
|
||||
#[task]
|
||||
impl NoInput {
|
||||
async fn run(&self) -> i32 {
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let builder = dag.add_task(NoInput);
|
||||
|
||||
// Convert to handle (only works because Input = ())
|
||||
let handle: TaskHandle<_> = builder.into();
|
||||
|
||||
// TaskHandle is Copy/Clone - just an ID wrapper
|
||||
let _copy1 = handle;
|
||||
let _copy2 = handle;
|
||||
let _copy3 = handle;
|
||||
|
||||
// But TaskHandle has NO methods to modify the DAG
|
||||
// There's no way to add dependencies to it
|
||||
// This would fail to compile:
|
||||
// handle.depends_on(something); // ERROR: no method named `depends_on`
|
||||
// let _consumer2 = consumer_builder.depends_on(&source); // ERROR: use of moved value
|
||||
}
|
||||
|
||||
/// Documents the ordering constraint that prevents cycles
|
||||
@@ -85,7 +56,7 @@ fn test_ordering_prevents_cycles() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let a_builder = dag.add_task(TaskA);
|
||||
let b_builder = dag.add_task(TaskB);
|
||||
|
||||
@@ -107,9 +78,9 @@ fn test_ordering_prevents_cycles() {
|
||||
// So we can only create DAGs, never cycles!
|
||||
|
||||
// Let's create a valid DAG instead: A→B (B depends on A)
|
||||
let a_handle: TaskHandle<_> = a_builder.into(); // A has no deps (unit input)
|
||||
let _b_handle = b_builder.depends_on(a_handle);
|
||||
// Now we could try: a_handle.depends_on(b_handle)?
|
||||
let a_handle = a_builder; // A has no deps (unit input)
|
||||
let _b_handle = b_builder.depends_on(&a_handle);
|
||||
// Now we could try: a_handle.depends_on(&b_handle)?
|
||||
// But TaskHandle has no such method!
|
||||
}
|
||||
|
||||
@@ -132,13 +103,13 @@ fn test_unit_tasks_cannot_create_cycles() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let task1 = dag.add_task(Task1);
|
||||
let task2 = dag.add_task(Task2);
|
||||
|
||||
// Both have unit input, so we can convert to handles
|
||||
let _handle1: TaskHandle<_> = task1.into();
|
||||
let _handle2: TaskHandle<_> = task2.into();
|
||||
let _handle1 = task1;
|
||||
let _handle2 = task2;
|
||||
|
||||
// Now we have two handles, but...
|
||||
// TaskHandle has no methods to add dependencies!
|
||||
@@ -176,15 +147,15 @@ fn test_three_way_cycle_impossible() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let _a_builder = dag.add_task(A);
|
||||
let _b_builder = dag.add_task(B);
|
||||
let _c_builder = dag.add_task(C);
|
||||
|
||||
// To create A→C→B→A:
|
||||
// 1. a_builder.depends_on(c_handle) // Need c_handle
|
||||
// 2. c_builder.depends_on(b_handle) // Need b_handle
|
||||
// 3. b_builder.depends_on(a_handle) // Need a_handle (from step 1!)
|
||||
// 1. a_builder.depends_on(&c_handle) // Need c_handle
|
||||
// 2. c_builder.depends_on(&b_handle) // Need b_handle
|
||||
// 3. b_builder.depends_on(&a_handle) // Need a_handle (from step 1!)
|
||||
//
|
||||
// But step 3 needs a_handle which doesn't exist until step 1 completes.
|
||||
// And step 1 needs c_handle which doesn't exist until step 2 completes.
|
||||
@@ -205,11 +176,11 @@ fn test_self_loop_prevented() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let _builder = dag.add_task(SelfReferential);
|
||||
|
||||
// To create A→A (self-loop):
|
||||
// builder.depends_on(builder)
|
||||
// builder.depends_on(&builder)
|
||||
//
|
||||
// But this would fail because:
|
||||
// 1. depends_on() takes `self` by value (moves builder)
|
||||
@@ -217,7 +188,7 @@ fn test_self_loop_prevented() {
|
||||
// 3. We can't also use &builder as an argument because it's being moved
|
||||
//
|
||||
// This would fail to compile:
|
||||
// let _handle = builder.depends_on(builder); // ERROR: use of moved value
|
||||
// let _handle = builder.depends_on(&builder); // ERROR: use of moved value
|
||||
}
|
||||
|
||||
/// Comprehensive test showing all the guardrails
|
||||
@@ -256,25 +227,26 @@ async fn test_type_safety_prevents_invalid_graphs() {
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Build a valid DAG: two sources, two transforms, one aggregator
|
||||
let s1 = dag.add_task(Source);
|
||||
let s2 = dag.add_task(Source);
|
||||
|
||||
// Transform depends on sources
|
||||
let t1 = dag.add_task(Transform).depends_on(s1);
|
||||
let t2 = dag.add_task(Transform).depends_on(s2);
|
||||
let t1 = dag.add_task(Transform).depends_on(&s1);
|
||||
let t2 = dag.add_task(Transform).depends_on(&s2);
|
||||
|
||||
// Aggregate depends on transforms
|
||||
let result = dag.add_task(Aggregate).depends_on((&t1, &t2));
|
||||
|
||||
// Execute and verify
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(result).unwrap(), 40); // (10*2) + (10*2)
|
||||
assert_eq!(output.get(result), 40); // (10*2) + (10*2)
|
||||
|
||||
// Now if we wanted to add a cycle, we'd need to:
|
||||
// - Make one of the Source tasks depend on the Aggregate result
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex};
|
||||
#[tokio::test]
|
||||
async fn test_dependency_resolution_order() -> DagResult<()> {
|
||||
// Test that dependencies are resolved in the correct order
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let execution_log = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
@@ -40,7 +40,7 @@ async fn test_dependency_resolution_order() -> DagResult<()> {
|
||||
log.lock().unwrap().push(("B_end", x + 1));
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(a)
|
||||
.depends_on(&a)
|
||||
};
|
||||
|
||||
let c = {
|
||||
@@ -52,7 +52,7 @@ async fn test_dependency_resolution_order() -> DagResult<()> {
|
||||
log.lock().unwrap().push(("C_end", x + 1));
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(b)
|
||||
.depends_on(&b)
|
||||
};
|
||||
|
||||
let d = {
|
||||
@@ -64,7 +64,7 @@ async fn test_dependency_resolution_order() -> DagResult<()> {
|
||||
log.lock().unwrap().push(("D_end", x + 1));
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(c)
|
||||
.depends_on(&c)
|
||||
};
|
||||
|
||||
let e = {
|
||||
@@ -76,13 +76,14 @@ async fn test_dependency_resolution_order() -> DagResult<()> {
|
||||
log.lock().unwrap().push(("E_end", x + 1));
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(d)
|
||||
.depends_on(&d)
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(e)?, 5);
|
||||
assert_eq!(output.get(e), 5);
|
||||
|
||||
// Verify execution order
|
||||
let log = execution_log.lock().unwrap();
|
||||
|
||||
@@ -5,24 +5,25 @@ use dagx_test::task_fn;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_two_dependencies() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
|
||||
let sum = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
|
||||
.depends_on((a, b));
|
||||
.depends_on((&a, &b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sum)?, 30);
|
||||
assert_eq!(output.get(sum), 30);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_three_dependencies() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
|
||||
@@ -31,21 +32,22 @@ async fn test_three_dependencies() -> DagResult<()> {
|
||||
.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
|(x, y, z): (&i32, &i32, &i32)| x + y + z,
|
||||
))
|
||||
.depends_on((a, b, c));
|
||||
.depends_on((&a, &b, &c));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sum)?, 6);
|
||||
assert_eq!(output.get(sum), 6);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_four_dependencies() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let deps: Vec<_> = (1..=4)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||
.collect();
|
||||
|
||||
let sum = dag
|
||||
@@ -54,19 +56,20 @@ async fn test_four_dependencies() -> DagResult<()> {
|
||||
))
|
||||
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sum)?, 10); // 1+2+3+4
|
||||
assert_eq!(output.get(sum), 10); // 1+2+3+4
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_five_dependencies() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let deps: Vec<_> = (1..=5)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||
.collect();
|
||||
|
||||
let sum = dag
|
||||
@@ -75,19 +78,20 @@ async fn test_five_dependencies() -> DagResult<()> {
|
||||
))
|
||||
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sum)?, 15); // 1+2+3+4+5
|
||||
assert_eq!(output.get(sum), 15); // 1+2+3+4+5
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_six_dependencies() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let deps: Vec<_> = (1..=6)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||
.collect();
|
||||
|
||||
let sum = dag
|
||||
@@ -96,19 +100,20 @@ async fn test_six_dependencies() -> DagResult<()> {
|
||||
))
|
||||
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sum)?, 21); // 1+2+3+4+5+6
|
||||
assert_eq!(output.get(sum), 21); // 1+2+3+4+5+6
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_seven_dependencies() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let deps: Vec<_> = (1..=7)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||
.collect();
|
||||
|
||||
let sum = dag
|
||||
@@ -121,19 +126,20 @@ async fn test_seven_dependencies() -> DagResult<()> {
|
||||
&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6],
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sum)?, 28); // 1+2+3+4+5+6+7
|
||||
assert_eq!(output.get(sum), 28); // 1+2+3+4+5+6+7
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_eight_dependencies() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let deps: Vec<_> = (1..=8)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||
.collect();
|
||||
|
||||
let sum = dag
|
||||
@@ -146,9 +152,10 @@ async fn test_eight_dependencies() -> DagResult<()> {
|
||||
&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6], &deps[7],
|
||||
));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sum)?, 36); // Sum of 1..=8
|
||||
assert_eq!(output.get(sum), 36); // Sum of 1..=8
|
||||
Ok(())
|
||||
}
|
||||
+57
-81
@@ -1,47 +1,38 @@
|
||||
//! Tests for error propagation through the DAG
|
||||
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
use dagx::{task, DagError, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_linear_error_propagation() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| -> i32 {
|
||||
panic!("Error at t2 with input {}", x);
|
||||
}))
|
||||
.depends_on(t1);
|
||||
.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 * 2))
|
||||
.depends_on(t3);
|
||||
.depends_on(&t2);
|
||||
dag.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(&t3);
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// t1 succeeds
|
||||
assert_eq!(dag.get(t1).unwrap(), 1);
|
||||
|
||||
// t2 fails
|
||||
assert!(dag.get(t2).is_err());
|
||||
|
||||
// t3 and t4 fail due to dependency
|
||||
assert!(dag.get(t3).is_err());
|
||||
assert!(dag.get(t4).is_err());
|
||||
assert!(matches!(
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await,
|
||||
Err(DagError::TaskPanicked { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_error_propagation() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// source
|
||||
// / \
|
||||
@@ -49,73 +40,57 @@ async fn test_diamond_error_propagation() {
|
||||
// \ /
|
||||
// sink
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
|
||||
let left = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x / 2))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
|
||||
let right = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| {
|
||||
panic!("Right path fails with {}", x);
|
||||
}))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
|
||||
let sink = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
|
||||
.depends_on((&left, &right));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
assert_eq!(dag.get(source).unwrap(), 100);
|
||||
assert_eq!(dag.get(left).unwrap(), 50);
|
||||
assert!(dag.get(right).is_err());
|
||||
assert!(dag.get(sink).is_err()); // Fails due to right dependency
|
||||
assert!(matches!(
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await,
|
||||
Err(DagError::TaskPanicked { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_in_wide_fanout() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| -> i32 {
|
||||
panic!("Source fails");
|
||||
}))
|
||||
.into();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| -> i32 {
|
||||
panic!("Source fails");
|
||||
}));
|
||||
|
||||
// Create wide fanout from failing source
|
||||
let dependents: Vec<_> = (0..20)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// Source fails
|
||||
assert!(dag.get(source).is_err());
|
||||
|
||||
// All dependents fail
|
||||
for dependent in &dependents {
|
||||
assert!(dag.get(dependent).is_err());
|
||||
for i in 0..20 {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
||||
.depends_on(&source);
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await,
|
||||
Err(DagError::TaskPanicked { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_selective_error_propagation() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Source that produces a Result
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Result::<i32, String>::Err("Source error".to_string())
|
||||
}))
|
||||
.into();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Result::<i32, String>::Err("Source error".to_string())
|
||||
}));
|
||||
|
||||
// Handler that processes the Result
|
||||
let handler = dag
|
||||
@@ -127,7 +102,7 @@ async fn test_selective_error_propagation() {
|
||||
}
|
||||
},
|
||||
))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
|
||||
// Further processing
|
||||
let final_task = dag
|
||||
@@ -138,36 +113,37 @@ async fn test_selective_error_propagation() {
|
||||
format!("Success: {}", val)
|
||||
}
|
||||
}))
|
||||
.depends_on(handler);
|
||||
.depends_on(&handler);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Source returns an Err variant (not a panic)
|
||||
assert_eq!(dag.get(source).unwrap(), Err("Source error".to_string()));
|
||||
assert_eq!(output.get(source), Err("Source error".to_string()));
|
||||
|
||||
// Handler processes it
|
||||
assert_eq!(dag.get(handler).unwrap(), -1);
|
||||
assert_eq!(output.get(handler), -1);
|
||||
|
||||
// Final task handles the error case
|
||||
assert_eq!(dag.get(final_task).unwrap(), "Handled error case");
|
||||
assert_eq!(output.get(final_task), "Handled error case");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_propagation_timing() {
|
||||
let dag = DagRunner::new();
|
||||
let propagation_order = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
let mut dag = DagRunner::new();
|
||||
let propagation_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct T1Task {
|
||||
order: Arc<parking_lot::Mutex<Vec<&'static str>>>,
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl T1Task {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
self.order.lock().push("t1_complete");
|
||||
self.order.lock().unwrap().push("t1_complete");
|
||||
1
|
||||
}
|
||||
}
|
||||
@@ -177,13 +153,13 @@ async fn test_error_propagation_timing() {
|
||||
});
|
||||
|
||||
struct T2Task {
|
||||
order: Arc<parking_lot::Mutex<Vec<&'static str>>>,
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl T2Task {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.order.lock().push("t2_start");
|
||||
self.order.lock().unwrap().push("t2_start");
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
panic!("t2 fails with {x}");
|
||||
}
|
||||
@@ -193,24 +169,24 @@ async fn test_error_propagation_timing() {
|
||||
.add_task(T2Task {
|
||||
order: propagation_order.clone(),
|
||||
})
|
||||
.depends_on(t1);
|
||||
.depends_on(&t1);
|
||||
|
||||
let _t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>({
|
||||
let order = propagation_order.clone();
|
||||
move |x: &i32| {
|
||||
let order = order.clone();
|
||||
order.lock().push("t3_should_not_run");
|
||||
order.lock().unwrap().push("t3_should_not_run");
|
||||
x + 1
|
||||
}
|
||||
}))
|
||||
.depends_on(t2);
|
||||
.depends_on(&t2);
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
let order = propagation_order.lock().clone();
|
||||
let order = propagation_order.lock().unwrap().clone();
|
||||
|
||||
// t1 completes, t2 starts and fails, t3 never runs
|
||||
assert!(order.contains(&"t1_complete"));
|
||||
|
||||
+29
-22
@@ -4,11 +4,11 @@ use dagx::{DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_retry_pattern_simulation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let attempt_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Task that would fail on first attempts (simulated)
|
||||
@@ -29,18 +29,19 @@ async fn test_retry_pattern_simulation() -> DagResult<()> {
|
||||
}
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Task completes with error on first "attempt"
|
||||
assert_eq!(dag.get(task)?, Err("Simulated failure"));
|
||||
assert_eq!(output.get(task), Err("Simulated failure"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_circuit_breaker_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let failure_count = Arc::new(AtomicUsize::new(0));
|
||||
let circuit_open = Arc::new(AtomicBool::new(false));
|
||||
|
||||
@@ -71,12 +72,13 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// First few tasks fail and open circuit
|
||||
for task in tasks.into_iter().take(4) {
|
||||
assert!(dag.get(task)?.is_err());
|
||||
assert!(output.get(task).is_err());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -84,8 +86,8 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let errors = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
let mut dag = DagRunner::new();
|
||||
let errors = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// Tasks that may produce errors
|
||||
let mut tasks: Vec<_> = (0..10)
|
||||
@@ -94,7 +96,10 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let errors = errors.clone();
|
||||
if i % 2 == 0 {
|
||||
errors.lock().push(format!("Error from task {}", i));
|
||||
errors
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("Error from task {}", i));
|
||||
return Err(format!("Task {} error", i));
|
||||
}
|
||||
Ok(i)
|
||||
@@ -102,11 +107,12 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Check accumulated errors
|
||||
let error_list = errors.lock().clone();
|
||||
let error_list = errors.lock().unwrap().clone();
|
||||
assert_eq!(error_list.len(), 5); // Even numbered tasks
|
||||
|
||||
tasks.truncate(2);
|
||||
@@ -114,15 +120,15 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||
let [first, second] = <[_; 2]>::try_from(tasks).map_err(|_e| ()).unwrap();
|
||||
|
||||
// Verify specific results
|
||||
assert!(dag.get(first)?.is_err());
|
||||
assert_eq!(dag.get(second)?, Ok(1));
|
||||
assert!(output.get(first).is_err());
|
||||
assert_eq!(output.get(second), Ok(1));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_boundary_isolation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Group A: contains an error - use Result
|
||||
let a1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
@@ -130,28 +136,29 @@ async fn test_error_boundary_isolation() -> DagResult<()> {
|
||||
.add_task(task_fn::<i32, _, _>(|_x: &i32| {
|
||||
Err::<i32, &str>("Group A error")
|
||||
}))
|
||||
.depends_on(a1);
|
||||
.depends_on(&a1);
|
||||
|
||||
// Group B: should be isolated from Group A's error
|
||||
let b1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
|
||||
let b2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(b1);
|
||||
.depends_on(&b1);
|
||||
|
||||
// Group C: depends on B but not A
|
||||
let c1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(b2);
|
||||
.depends_on(&b2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Group A fails (returns Err)
|
||||
assert_eq!(dag.get(a2)?, Err("Group A error"));
|
||||
assert_eq!(output.get(a2), Err("Group A error"));
|
||||
|
||||
// Groups B and C succeed
|
||||
assert_eq!(dag.get(b2)?, 40);
|
||||
assert_eq!(dag.get(c1)?, 45);
|
||||
assert_eq!(output.get(b2), 40);
|
||||
assert_eq!(output.get(c1), 45);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+50
-42
@@ -6,60 +6,64 @@ use dagx::*;
|
||||
#[tokio::test]
|
||||
async fn test_empty_dag() {
|
||||
let dag = DagRunner::new();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should complete without error
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(task).unwrap(), 42);
|
||||
assert_eq!(output.get(task), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wide_parallel() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
assert_eq!(output.get(task), i * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(a);
|
||||
.depends_on(&a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(a);
|
||||
.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() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Note: a, b, and c are not sinks (they have dependents)
|
||||
// Only d is a sink and can be retrieved
|
||||
assert_eq!(dag.get(d).unwrap(), 35); // 15 + 20
|
||||
assert_eq!(output.get(d), 35); // 15 + 20
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -70,7 +74,7 @@ struct User {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_different_output_types() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
|
||||
let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3]));
|
||||
@@ -79,91 +83,95 @@ async fn test_different_output_types() {
|
||||
age: 30,
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&dag.get(string_task).unwrap(), "hello");
|
||||
assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]);
|
||||
let user = dag.get(struct_task).unwrap();
|
||||
assert_eq!(&output.get(string_task), "hello");
|
||||
assert_eq!(output.get(vec_task), vec![1, 2, 3]);
|
||||
let user = output.get(struct_task);
|
||||
assert_eq!(user.name, "Alice");
|
||||
assert_eq!(user.age, 30);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_sinks() {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
|
||||
// Branch 1
|
||||
let branch1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
let sink1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(branch1);
|
||||
.depends_on(&branch1);
|
||||
|
||||
// Branch 2
|
||||
let branch2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(source);
|
||||
.depends_on(&source);
|
||||
let sink2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(branch2);
|
||||
.depends_on(&branch2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should wait for both sinks
|
||||
|
||||
assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1
|
||||
assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3
|
||||
assert_eq!(output.get(sink1), 21); // (10 * 2) + 1
|
||||
assert_eq!(output.get(sink2), 45); // (10 + 5) * 3
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task_inline_path() {
|
||||
// Test single-task layer inline execution path (line 367-410 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Single task in its own layer
|
||||
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(t1);
|
||||
.depends_on(&t1);
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(t2);
|
||||
.depends_on(&t2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5
|
||||
assert_eq!(output.get(t3), 25); // (10 * 2) + 5
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multi_consumer_fanout() {
|
||||
// Test fanout with multiple consumers from one producer
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let producer: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
let producer = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
// Multiple consumers
|
||||
let c1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(producer);
|
||||
.depends_on(&producer);
|
||||
let c2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 10))
|
||||
.depends_on(producer);
|
||||
.depends_on(&producer);
|
||||
let c3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
|
||||
.depends_on(producer);
|
||||
.depends_on(&producer);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(c1).unwrap(), 84);
|
||||
assert_eq!(dag.get(c2).unwrap(), 52);
|
||||
assert_eq!(dag.get(c3).unwrap(), 37);
|
||||
assert_eq!(output.get(c1), 84);
|
||||
assert_eq!(output.get(c2), 52);
|
||||
assert_eq!(output.get(c3), 37);
|
||||
}
|
||||
@@ -4,19 +4,19 @@ use dagx::{task, DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::Barrier;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
||||
// Force specific interleaving using barriers
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let barrier = Arc::new(Barrier::new(3));
|
||||
let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
let order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct InterleaveTask {
|
||||
barrier: Arc<Barrier>,
|
||||
order: Arc<parking_lot::Mutex<Vec<String>>>,
|
||||
order: Arc<Mutex<Vec<String>>>,
|
||||
prefix: &'static str,
|
||||
value: i32,
|
||||
}
|
||||
@@ -24,9 +24,15 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
||||
#[task]
|
||||
impl InterleaveTask {
|
||||
async fn run(&self) -> i32 {
|
||||
self.order.lock().push(format!("{}_start", self.prefix));
|
||||
self.order
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("{}_start", self.prefix));
|
||||
self.barrier.wait().await;
|
||||
self.order.lock().push(format!("{}_end", self.prefix));
|
||||
self.order
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("{}_end", self.prefix));
|
||||
self.value
|
||||
}
|
||||
}
|
||||
@@ -56,15 +62,16 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
||||
.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
|(a, b, c): (&i32, &i32, &i32)| a + b + c,
|
||||
))
|
||||
.depends_on((t1, t2, t3));
|
||||
.depends_on((&t1, &t2, &t3));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(collector)?, 6);
|
||||
assert_eq!(output.get(collector), 6);
|
||||
|
||||
// Check interleaving pattern
|
||||
let events = order.lock().clone();
|
||||
let events = order.lock().unwrap().clone();
|
||||
assert_eq!(events.len(), 6);
|
||||
|
||||
// All starts should come before all ends due to barrier
|
||||
@@ -80,14 +87,13 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
||||
#[tokio::test]
|
||||
async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
// Create an alternating execution pattern
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let turn = Arc::new(AtomicUsize::new(0));
|
||||
let events: Arc<parking_lot::Mutex<Vec<String>>> =
|
||||
Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
let events: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct FirstA {
|
||||
turn: Arc<AtomicUsize>,
|
||||
events: Arc<parking_lot::Mutex<Vec<String>>>,
|
||||
events: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
@@ -96,22 +102,20 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
while self.turn.load(Ordering::SeqCst) != 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push("A0".to_string());
|
||||
self.events.lock().unwrap().push("A0".to_string());
|
||||
self.turn.store(1, Ordering::SeqCst);
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
let first_a = dag.add_task(FirstA {
|
||||
let mut chain_a = dag.add_task(FirstA {
|
||||
turn: turn.clone(),
|
||||
events: events.clone(),
|
||||
});
|
||||
|
||||
let mut chain_a: dagx::TaskHandle<i32> = first_a.into();
|
||||
|
||||
struct FirstB {
|
||||
turn: Arc<AtomicUsize>,
|
||||
events: Arc<parking_lot::Mutex<Vec<String>>>,
|
||||
events: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
@@ -120,22 +124,20 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
while self.turn.load(Ordering::SeqCst) != 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push("B0".to_string());
|
||||
self.events.lock().unwrap().push("B0".to_string());
|
||||
self.turn.store(2, Ordering::SeqCst);
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
let first_b = dag.add_task(FirstB {
|
||||
let mut chain_b = dag.add_task(FirstB {
|
||||
turn: turn.clone(),
|
||||
events: events.clone(),
|
||||
});
|
||||
|
||||
let mut chain_b: dagx::TaskHandle<i32> = first_b.into();
|
||||
|
||||
struct ChainA {
|
||||
turn: Arc<AtomicUsize>,
|
||||
events: Arc<parking_lot::Mutex<Vec<String>>>,
|
||||
events: Arc<Mutex<Vec<String>>>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
@@ -145,7 +147,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
while self.turn.load(Ordering::SeqCst) != self.idx * 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push(format!("A{}", self.idx));
|
||||
self.events.lock().unwrap().push(format!("A{}", self.idx));
|
||||
self.turn.store(self.idx * 2 + 1, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
@@ -153,7 +155,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
|
||||
struct ChainB {
|
||||
turn: Arc<AtomicUsize>,
|
||||
events: Arc<parking_lot::Mutex<Vec<String>>>,
|
||||
events: Arc<Mutex<Vec<String>>>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
@@ -163,7 +165,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
while self.turn.load(Ordering::SeqCst) != self.idx * 2 + 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push(format!("B{}", self.idx));
|
||||
self.events.lock().unwrap().push(format!("B{}", self.idx));
|
||||
self.turn.store(self.idx * 2 + 2, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
@@ -176,7 +178,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
turn: turn.clone(),
|
||||
idx: i,
|
||||
})
|
||||
.depends_on(chain_a);
|
||||
.depends_on(&chain_a);
|
||||
|
||||
chain_b = dag
|
||||
.add_task(ChainB {
|
||||
@@ -184,14 +186,15 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
events: events.clone(),
|
||||
idx: i,
|
||||
})
|
||||
.depends_on(chain_b);
|
||||
.depends_on(&chain_b);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let _output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Check alternating pattern
|
||||
let event_list = events.lock().clone();
|
||||
let event_list = events.lock().unwrap().clone();
|
||||
assert_eq!(
|
||||
event_list,
|
||||
vec!["A0", "B0", "A1", "B1", "A2", "B2", "A3", "B3", "A4", "B4"]
|
||||
@@ -203,7 +206,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
#[tokio::test]
|
||||
async fn test_layered_interleaving() -> DagResult<()> {
|
||||
// Test interleaving across DAG layers
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let layer_active = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
struct Layer1Task {
|
||||
@@ -228,7 +231,6 @@ async fn test_layered_interleaving() -> DagResult<()> {
|
||||
active: layer_active.clone(),
|
||||
idx: i,
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -264,11 +266,12 @@ async fn test_layered_interleaving() -> DagResult<()> {
|
||||
})
|
||||
.depends_on((&layer2[0], &layer2[1]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Sum of 0+1+2+3 = 6
|
||||
assert_eq!(dag.get(layer3)?, 6);
|
||||
assert_eq!(output.get(layer3), 6);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@ mod boundaries;
|
||||
mod dependencies;
|
||||
mod errors;
|
||||
mod execution;
|
||||
mod interleaving;
|
||||
mod parallelism;
|
||||
mod runtimes;
|
||||
mod timing;
|
||||
mod tracing;
|
||||
@@ -1,425 +0,0 @@
|
||||
//! Concurrent access tests
|
||||
|
||||
use dagx::{task, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
struct SleepTask(usize);
|
||||
|
||||
#[task]
|
||||
impl SleepTask {
|
||||
async fn run(&self) -> usize {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_run_calls() {
|
||||
// Test that multiple run() calls are handled correctly
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
|
||||
// Add some tasks
|
||||
let tasks: Vec<_> = (0..10).map(|i| dag.add_task(SleepTask(i))).collect();
|
||||
|
||||
// Try to run the DAG multiple times concurrently
|
||||
let mut handles = JoinSet::new();
|
||||
|
||||
for _ in 0..5 {
|
||||
let dag = Arc::clone(&dag);
|
||||
handles.spawn(async move {
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
// Only one run should succeed, others should fail with concurrent execution error
|
||||
let mut success_count = 0;
|
||||
let mut error_count = 0;
|
||||
|
||||
while let Some(result) = handles.join_next().await {
|
||||
match result.unwrap() {
|
||||
Ok(_) => success_count += 1,
|
||||
Err(_) => error_count += 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Exactly one should succeed, the rest should fail
|
||||
assert_eq!(success_count, 1);
|
||||
assert_eq!(error_count, 4);
|
||||
|
||||
// Verify results are consistent from the successful run
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_execution_and_building() {
|
||||
// Test that we can build and execute concurrently
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
|
||||
// Start with some initial tasks
|
||||
let initial: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||
.collect();
|
||||
|
||||
// Execute the initial DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now try to add more tasks while also reading results
|
||||
let mut handles = JoinSet::new();
|
||||
|
||||
// Reader tasks
|
||||
for task in initial {
|
||||
let dag = Arc::clone(&dag);
|
||||
let task_handle: dagx::TaskHandle<i32> = task.into();
|
||||
handles.spawn(async move { dag.get(task_handle).unwrap() });
|
||||
}
|
||||
|
||||
// Builder tasks (adding new nodes)
|
||||
for i in 10..20 {
|
||||
let dag = Arc::clone(&dag);
|
||||
handles.spawn(async move {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
i
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for all operations
|
||||
while let Some(result) = handles.join_next().await {
|
||||
result.unwrap();
|
||||
}
|
||||
|
||||
// Execute again with the new tasks
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dag_builder_thread_safety() {
|
||||
// Test that the DAG builder is thread-safe
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut handles = JoinSet::new();
|
||||
|
||||
// Spawn tasks that build complex sub-graphs concurrently
|
||||
for thread_id in 0..10 {
|
||||
let dag = Arc::clone(&dag);
|
||||
let counter = Arc::clone(&counter);
|
||||
|
||||
handles.spawn(async move {
|
||||
// Each thread creates a small pipeline
|
||||
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| thread_id));
|
||||
|
||||
let transform1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * 10))
|
||||
.depends_on(source);
|
||||
|
||||
let transform2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + thread_id))
|
||||
.depends_on(transform1);
|
||||
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
transform2
|
||||
});
|
||||
}
|
||||
|
||||
let mut final_tasks = vec![];
|
||||
while let Some(result) = handles.join_next().await {
|
||||
final_tasks.push(result.unwrap());
|
||||
}
|
||||
|
||||
// Verify all threads completed
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
||||
|
||||
// Execute the combined DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify each pipeline computed correctly
|
||||
for task in final_tasks.iter() {
|
||||
let result = dag.get(task).unwrap();
|
||||
// Each pipeline: thread_id * 10 + thread_id
|
||||
assert!((0..100).contains(&result));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_get_operations() {
|
||||
// Test concurrent get() operations
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
|
||||
// Create and execute tasks
|
||||
let tasks: Vec<_> = (0..100)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now access results concurrently
|
||||
let mut handles = JoinSet::new();
|
||||
|
||||
for task in tasks.into_iter() {
|
||||
let dag = Arc::clone(&dag);
|
||||
let task_handle: dagx::TaskHandle<i32> = task.into();
|
||||
handles.spawn(async move { dag.get(task_handle).unwrap() });
|
||||
}
|
||||
|
||||
// Collect and verify results
|
||||
let mut results = vec![];
|
||||
while let Some(result) = handles.join_next().await {
|
||||
results.push(result.unwrap());
|
||||
}
|
||||
|
||||
// Results should contain all values 0..100
|
||||
results.sort();
|
||||
for (i, val) in results.iter().enumerate() {
|
||||
assert_eq!(*val, i as i32);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_race_in_dependency_tracking() {
|
||||
// Test that concurrent dependency modifications don't cause races
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
let mut handles = JoinSet::new();
|
||||
|
||||
// Multiple threads create tasks depending on the same source
|
||||
let source_handle: dagx::TaskHandle<i32> = source.into();
|
||||
for i in 0..50 {
|
||||
let dag = Arc::clone(&dag);
|
||||
let source_clone = source_handle;
|
||||
|
||||
handles.spawn(async move {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
||||
.depends_on(source_clone)
|
||||
});
|
||||
}
|
||||
|
||||
let mut dependents = vec![];
|
||||
while let Some(result) = handles.join_next().await {
|
||||
dependents.push(result.unwrap());
|
||||
}
|
||||
|
||||
// Execute
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// All dependents should have correct values
|
||||
for dep in dependents.iter() {
|
||||
let result = dag.get(dep).unwrap();
|
||||
assert!((42..92).contains(&result)); // 42 + (0..50)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_high_concurrency_stress() {
|
||||
// Stress test with high concurrency
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
let completed = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut handles = JoinSet::new();
|
||||
|
||||
// 1000 concurrent operations
|
||||
for i in 0..1000 {
|
||||
let dag = Arc::clone(&dag);
|
||||
let completed = Arc::clone(&completed);
|
||||
|
||||
handles.spawn(async move {
|
||||
// Mix of operations
|
||||
match i % 3 {
|
||||
0 => {
|
||||
// Add a source task
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
}
|
||||
1 => {
|
||||
// Add a task with a simple dependency
|
||||
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
}
|
||||
2 => {
|
||||
// Add a more complex subgraph
|
||||
let a = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
let b = dag.add_task(task_fn::<(), _, _>(move |_: ()| i + 1));
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| {
|
||||
x + y
|
||||
}))
|
||||
.depends_on((a, b));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
completed.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for all operations to complete
|
||||
while let Some(result) = handles.join_next().await {
|
||||
result.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 1000);
|
||||
|
||||
// The DAG should still be executable
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lock_contention_measurement() {
|
||||
// Test to measure lock contention under concurrent load
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
let start = tokio::time::Instant::now();
|
||||
let operations = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut handles = JoinSet::new();
|
||||
|
||||
// Create many concurrent operations that will contend for locks
|
||||
for i in 0..100 {
|
||||
let dag = Arc::clone(&dag);
|
||||
let ops = Arc::clone(&operations);
|
||||
|
||||
handles.spawn(async move {
|
||||
for j in 0..10 {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10 + j));
|
||||
ops.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
// Small yield to increase contention likelihood
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
while let Some(result) = handles.join_next().await {
|
||||
result.unwrap();
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
let total_ops = operations.load(Ordering::SeqCst);
|
||||
|
||||
println!(
|
||||
"Completed {} operations in {:?} ({:.0} ops/sec)",
|
||||
total_ops,
|
||||
duration,
|
||||
total_ops as f64 / duration.as_secs_f64()
|
||||
);
|
||||
|
||||
// Should complete reasonably quickly even with contention
|
||||
assert!(
|
||||
duration < Duration::from_secs(5),
|
||||
"Too much lock contention"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_atomic_dag_operations() {
|
||||
// Test that DAG operations are atomic (all-or-nothing)
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
|
||||
// Create a base task
|
||||
let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
|
||||
// Try to create dependent tasks concurrently
|
||||
let mut handles = JoinSet::new();
|
||||
let success_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let base_handle: dagx::TaskHandle<i32> = base.into();
|
||||
for i in 0..50 {
|
||||
let dag = Arc::clone(&dag);
|
||||
let base_clone = base_handle;
|
||||
let success = Arc::clone(&success_count);
|
||||
|
||||
handles.spawn(async move {
|
||||
let dep = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
|
||||
.depends_on(base_clone);
|
||||
|
||||
success.fetch_add(1, Ordering::SeqCst);
|
||||
dep
|
||||
});
|
||||
}
|
||||
|
||||
let mut deps = vec![];
|
||||
while let Some(result) = handles.join_next().await {
|
||||
deps.push(result.unwrap());
|
||||
}
|
||||
|
||||
// All operations should have succeeded
|
||||
assert_eq!(success_count.load(Ordering::SeqCst), 50);
|
||||
assert_eq!(deps.len(), 50);
|
||||
|
||||
// Execute and verify
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for dep in deps.iter() {
|
||||
let result = dag.get(dep).unwrap();
|
||||
assert!((100..150).contains(&result));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_deadlock_on_circular_waiting() {
|
||||
// Test that we don't deadlock even with complex concurrent operations
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
|
||||
// Create multiple threads that might cause circular waiting patterns
|
||||
let mut handles = JoinSet::new();
|
||||
|
||||
for i in 0..10 {
|
||||
let dag = Arc::clone(&dag);
|
||||
|
||||
handles.spawn(async move {
|
||||
// Each thread creates a chain of tasks
|
||||
let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
let mut chain = vec![first.into()];
|
||||
|
||||
for j in 0..5 {
|
||||
let next = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + j))
|
||||
.depends_on(chain.last().unwrap());
|
||||
chain.push(next);
|
||||
|
||||
// Yield to potentially cause interleaving
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
*chain.last().unwrap()
|
||||
});
|
||||
}
|
||||
|
||||
// Set a timeout to detect deadlocks
|
||||
let result = tokio::time::timeout(Duration::from_secs(5), async {
|
||||
let mut finals = vec![];
|
||||
while let Some(result) = handles.join_next().await {
|
||||
finals.push(result.unwrap());
|
||||
}
|
||||
finals
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok(), "Deadlock detected - operation timed out");
|
||||
|
||||
// Should be able to execute the DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Parallelism tests module
|
||||
|
||||
pub mod concurrent;
|
||||
pub mod spawning;
|
||||
pub mod threads;
|
||||
pub mod timing;
|
||||
@@ -9,7 +9,7 @@ use std::sync::Arc;
|
||||
#[tokio::test]
|
||||
async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
|
||||
// Verify that the spawner function is actually called for each task
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let spawn_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create 10 independent tasks
|
||||
@@ -27,12 +27,13 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
|
||||
.collect();
|
||||
|
||||
// Custom spawner that counts invocations
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all tasks were spawned
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, i as i32);
|
||||
assert_eq!(output.get(task), i as i32);
|
||||
}
|
||||
|
||||
// The spawner should have been called once for each task
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Tests for multi-threading and concurrent execution
|
||||
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
@@ -11,7 +11,7 @@ use tokio::time::sleep;
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||
// Prove concurrent execution using atomic counters to track simultaneous tasks
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
let concurrent_count = Arc::new(AtomicUsize::new(0));
|
||||
let max_concurrent = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
@@ -62,12 +62,13 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all tasks completed
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
assert_eq!(output.get(task), i);
|
||||
}
|
||||
|
||||
// Check that we achieved real parallelism
|
||||
@@ -90,13 +91,13 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||
#[tokio::test]
|
||||
async fn test_massive_parallel_fanout() -> DagResult<()> {
|
||||
// Test extreme parallelism with 1000 parallel tasks
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let completed = Arc::new(AtomicUsize::new(0));
|
||||
let max_concurrent = Arc::new(AtomicUsize::new(0));
|
||||
let current = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
|
||||
struct FanoutTask {
|
||||
comp: Arc<AtomicUsize>,
|
||||
@@ -134,7 +135,7 @@ async fn test_massive_parallel_fanout() -> DagResult<()> {
|
||||
}
|
||||
|
||||
// Create 1000 tasks all depending on the same source
|
||||
let tasks: Vec<_> = (0..1000)
|
||||
let mut tasks: Vec<_> = (0..1000)
|
||||
.map(|i| {
|
||||
dag.add_task(FanoutTask {
|
||||
comp: completed.clone(),
|
||||
@@ -142,12 +143,13 @@ async fn test_massive_parallel_fanout() -> DagResult<()> {
|
||||
curr: current.clone(),
|
||||
idx: i,
|
||||
})
|
||||
.depends_on(source)
|
||||
.depends_on(&source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
@@ -155,9 +157,9 @@ async fn test_massive_parallel_fanout() -> DagResult<()> {
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 1000);
|
||||
|
||||
// Check some results
|
||||
assert_eq!(dag.get(tasks[0])?, 1);
|
||||
assert_eq!(dag.get(tasks[500])?, 501);
|
||||
assert_eq!(dag.get(tasks[999])?, 1000);
|
||||
assert_eq!(output.get(tasks.remove(999)), 1000);
|
||||
assert_eq!(output.get(tasks.remove(500)), 501);
|
||||
assert_eq!(output.get(tasks.remove(0)), 1);
|
||||
|
||||
let max_seen = max_concurrent.load(Ordering::SeqCst);
|
||||
println!(
|
||||
|
||||
@@ -22,7 +22,7 @@ impl SleepTask {
|
||||
#[tokio::test]
|
||||
async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
// Prove parallel execution by comparing timing
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let work_duration = Duration::from_millis(50);
|
||||
let num_tasks = 10;
|
||||
@@ -33,13 +33,14 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
.collect();
|
||||
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Verify all tasks completed
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
assert_eq!(output.get(task), i);
|
||||
}
|
||||
|
||||
// If tasks ran sequentially: 10 * 50ms = 500ms
|
||||
@@ -53,7 +54,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
|
||||
// Parallel execution should be significantly faster
|
||||
assert!(
|
||||
elapsed < sequential_time / 4,
|
||||
elapsed < sequential_time / 2,
|
||||
"Parallel execution too slow: {:?} vs sequential {:?}",
|
||||
elapsed,
|
||||
sequential_time
|
||||
@@ -65,7 +66,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
#[tokio::test]
|
||||
async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||
// Test that tasks in the same topological layer execute in parallel
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let layer_timing = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
@@ -124,7 +125,7 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||
idx: i,
|
||||
timing: layer_timing.clone(),
|
||||
})
|
||||
.depends_on(source)
|
||||
.depends_on(&source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -142,10 +143,11 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||
.depends_on((&layer1[0], &layer1[1], &layer1[2]))
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1)
|
||||
assert_eq!(output.get(final_task), 33); // (0+1) + (10+1) + (20+1)
|
||||
|
||||
// Analyze timing
|
||||
let timing = layer_timing.lock().unwrap();
|
||||
|
||||
+12
-12
@@ -95,15 +95,15 @@ impl Add {
|
||||
#[test_case(AsyncExecutorRuntimeTest)]
|
||||
fn test_basic_dag(runner: impl RuntimeTest) {
|
||||
runner.run_dagx_test(|spawner| async {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(Value(2));
|
||||
let y = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
let sum = dag.add_task(Add).depends_on((&x, &y));
|
||||
|
||||
dag.run(spawner).await.unwrap();
|
||||
let mut output = dag.run(spawner).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
assert_eq!(output.get(sum), 5);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,16 +113,16 @@ fn test_basic_dag(runner: impl RuntimeTest) {
|
||||
#[test_case(AsyncExecutorRuntimeTest)]
|
||||
fn test_parallel_execution(runner: impl RuntimeTest) {
|
||||
runner.run_dagx_test(|spawner| async {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(spawner).await.unwrap();
|
||||
let mut output = dag.run(spawner).await.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
assert_eq!(output.get(task), i * 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -133,17 +133,17 @@ fn test_parallel_execution(runner: impl RuntimeTest) {
|
||||
#[test_case(AsyncExecutorRuntimeTest)]
|
||||
fn test_complex_dependencies(runner: impl RuntimeTest) {
|
||||
runner.run_dagx_test(|spawner| async {
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(10));
|
||||
let b = dag.add_task(Value(20));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
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);
|
||||
.depends_on(&sum);
|
||||
|
||||
dag.run(spawner).await.unwrap();
|
||||
let mut output = dag.run(spawner).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(double).unwrap().clone(), 60);
|
||||
assert_eq!(output.get(double).clone(), 60);
|
||||
});
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
//! Tests for concurrent access patterns and race conditions
|
||||
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
use dagx_test::task_fn;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_result_retrieval() -> DagResult<()> {
|
||||
// Test concurrent get() calls after execution
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
|
||||
struct DelayTask(usize, Duration);
|
||||
|
||||
#[task]
|
||||
impl DelayTask {
|
||||
async fn run(&self) -> usize {
|
||||
tokio::time::sleep(self.1).await;
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Add tasks with minimal delays
|
||||
let tasks: Vec<dagx::TaskHandle<usize>> = (0..10)
|
||||
.map(|i| {
|
||||
let task = dag.add_task(DelayTask(i, Duration::from_millis(1)));
|
||||
task.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Execute the DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Now try to get results concurrently from multiple threads
|
||||
let mut get_handles = Vec::new();
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
let dag_clone = dag.clone();
|
||||
let task = *task;
|
||||
get_handles.push(tokio::spawn(async move {
|
||||
// Results should be available immediately
|
||||
dag_clone.get(task).map(|v| (i, v))
|
||||
}));
|
||||
}
|
||||
|
||||
// Verify all gets succeeded
|
||||
for handle in get_handles {
|
||||
let (i, result) = handle.await.unwrap()?;
|
||||
assert_eq!(result, i);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> {
|
||||
// Test building DAG while another part is executing
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
let building_complete = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Start with a few initial tasks
|
||||
let initial: Vec<_> = (0..5)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
|
||||
.collect();
|
||||
|
||||
// Start building more tasks in background
|
||||
let dag_build = dag.clone();
|
||||
let building = building_complete.clone();
|
||||
let build_handle = tokio::spawn(async move {
|
||||
let mut tasks: Vec<dagx::TaskHandle<i32>> = Vec::new();
|
||||
for i in 5..50 {
|
||||
let task = dag_build.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
tasks.push(task.into());
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
|
||||
}
|
||||
building.store(true, Ordering::SeqCst);
|
||||
tasks
|
||||
});
|
||||
|
||||
// Wait a bit for some tasks to be added
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
|
||||
// Execute the DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Get the additional tasks
|
||||
let additional_tasks = build_handle.await.unwrap();
|
||||
|
||||
// Initial tasks should have results
|
||||
for (i, task) in initial.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
}
|
||||
|
||||
// Additional tasks added during execution won't have results yet
|
||||
// (they need another run cycle)
|
||||
if building_complete.load(Ordering::SeqCst) {
|
||||
// Run again for the new tasks
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Now they should have results
|
||||
for (i, task) in additional_tasks.iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, (i + 5) as i32);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_dependency_resolution() -> DagResult<()> {
|
||||
// Test multiple tasks resolving dependencies concurrently
|
||||
let dag = DagRunner::new();
|
||||
let resolution_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create a source task
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>({
|
||||
let counter = resolution_counter.clone();
|
||||
move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
42
|
||||
}
|
||||
}))
|
||||
.into();
|
||||
|
||||
// Create many tasks depending on the same source
|
||||
let dependents: Vec<_> = (0..100)
|
||||
.map(|i| {
|
||||
let counter = resolution_counter.clone();
|
||||
dag.add_task(task_fn::<i32, _, _>(move |val: &i32| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
val + i
|
||||
}))
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Check all completed
|
||||
assert_eq!(resolution_counter.load(Ordering::SeqCst), 101);
|
||||
|
||||
// Verify results
|
||||
for (i, task) in dependents.iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, 42 + i as i32);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_write_race_patterns() -> DagResult<()> {
|
||||
// Test read-write patterns with shared state
|
||||
let dag = DagRunner::new();
|
||||
let shared_state = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
struct WriterTask {
|
||||
state: Arc<RwLock<Vec<i32>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl WriterTask {
|
||||
async fn run(&self) -> i32 {
|
||||
let mut guard = self.state.write().await;
|
||||
guard.push(self.idx);
|
||||
tokio::task::yield_now().await; // Force potential race
|
||||
guard.push(self.idx * 10);
|
||||
self.idx
|
||||
}
|
||||
}
|
||||
|
||||
// Writers
|
||||
let writers: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
dag.add_task(WriterTask {
|
||||
state: shared_state.clone(),
|
||||
idx: i,
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
struct ReaderTask {
|
||||
state: Arc<RwLock<Vec<i32>>>,
|
||||
}
|
||||
|
||||
struct ReaderTaskOutput((i32, bool, usize));
|
||||
|
||||
#[task]
|
||||
impl ReaderTask {
|
||||
async fn run(&self, writer_id: &i32) -> ReaderTaskOutput {
|
||||
let guard = self.state.read().await;
|
||||
let contains_writer = guard.contains(&writer_id);
|
||||
let len = guard.len();
|
||||
ReaderTaskOutput((*writer_id, contains_writer, len))
|
||||
}
|
||||
}
|
||||
|
||||
// Readers (depend on writers to ensure ordering)
|
||||
let readers: Vec<_> = writers
|
||||
.iter()
|
||||
.map(|writer| {
|
||||
dag.add_task(ReaderTask {
|
||||
state: shared_state.clone(),
|
||||
})
|
||||
.depends_on(writer)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify readers saw their writer's data
|
||||
for reader in &readers {
|
||||
let ReaderTaskOutput((writer_id, saw_writer, _len)) = dag.get(reader)?;
|
||||
assert!(saw_writer, "Reader didn't see writer {}", writer_id);
|
||||
}
|
||||
|
||||
// Final state should have all writes
|
||||
let final_state = shared_state.read().await;
|
||||
assert_eq!(final_state.len(), 20); // 10 writers * 2 writes each
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_atomic_counter_races() -> DagResult<()> {
|
||||
// Test atomic operations under high concurrency
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let max_seen = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create tasks that increment and track max
|
||||
let _tasks: Vec<_> = (0..200)
|
||||
.map(|_| {
|
||||
let counter = counter.clone();
|
||||
let max_seen = max_seen.clone();
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
let max_seen = max_seen.clone();
|
||||
let mut local_max = 0;
|
||||
for _ in 0..100 {
|
||||
let val = counter.fetch_add(1, Ordering::SeqCst);
|
||||
local_max = local_max.max(val);
|
||||
|
||||
// Update global max
|
||||
let mut current_max = max_seen.load(Ordering::SeqCst);
|
||||
while val > current_max {
|
||||
match max_seen.compare_exchange_weak(
|
||||
current_max,
|
||||
val,
|
||||
Ordering::SeqCst,
|
||||
Ordering::SeqCst,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(x) => current_max = x,
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Final counter should be 200 * 100 = 20000
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 20000);
|
||||
|
||||
// Max seen should be at least 19999 (last increment - 1)
|
||||
assert!(max_seen.load(Ordering::SeqCst) >= 19999);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_dag_runners_interaction() -> DagResult<()> {
|
||||
// Test multiple DAG runners potentially interfering
|
||||
let shared_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for dag_id in 0..5 {
|
||||
let counter = shared_counter.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..20)
|
||||
.map(|task_id| {
|
||||
let counter = counter.clone();
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
(dag_id, task_id)
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Collect results
|
||||
let mut results = Vec::new();
|
||||
for task in tasks {
|
||||
results.push(dag.get(task)?);
|
||||
}
|
||||
|
||||
Ok::<_, dagx::DagError>(results)
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all DAGs to complete
|
||||
for handle in handles {
|
||||
handle.await.unwrap()?;
|
||||
}
|
||||
|
||||
// Should have executed 5 * 20 = 100 tasks total
|
||||
assert_eq!(shared_counter.load(Ordering::SeqCst), 100);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
//! Timing and race condition tests
|
||||
//!
|
||||
//! Tests for temporal edge cases, execution ordering, and race conditions.
|
||||
|
||||
mod concurrent_access;
|
||||
mod interleaving;
|
||||
@@ -39,17 +39,18 @@ async fn test_tracing_with_subscriber() {
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(2));
|
||||
let b = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
let sum = dag.add_task(Add).depends_on((&a, &b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
assert_eq!(output.get(sum), 5);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -60,19 +61,20 @@ async fn test_tracing_inline_execution() {
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let mut dag = DagRunner::new();
|
||||
|
||||
// Create a linear chain (should trigger inline execution)
|
||||
let a = dag.add_task(Value(1)).into();
|
||||
let a = dag.add_task(Value(1));
|
||||
let b = dag.add_task(Add).depends_on((&a, &a));
|
||||
let c = dag.add_task(Multiply).depends_on((&b, &b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
let mut output = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// a = 1
|
||||
// b = 1 + 1 = 2
|
||||
// c = 2 * 2 = 4
|
||||
assert_eq!(dag.get(c).unwrap(), 4);
|
||||
assert_eq!(output.get(c), 4);
|
||||
}
|
||||
Reference in new issue
Block a user