277 lines
7.3 KiB
Markdown
277 lines
7.3 KiB
Markdown
# dagx
|
|
|
|
[](https://crates.io/crates/dagx)
|
|
[](https://docs.rs/dagx)
|
|
[](https://github.com/swaits/dagx/actions)
|
|
[](LICENSE)
|
|
[](https://www.rust-lang.org)
|
|
|
|
A minimal, type-safe, runtime-agnostic async DAG (Directed Acyclic Graph) executor with compile-time cycle prevention and true parallel execution.
|
|
|
|
## Why dagx?
|
|
|
|
### Blazing Fast: 1-100x faster than dagrs
|
|
|
|
| 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** |
|
|
|
|
### Simple API
|
|
|
|
```rust
|
|
let sum = dag.add_task(Add).depends_on((x, y));
|
|
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await?;
|
|
```
|
|
|
|
That's it. No trait boilerplate, no manual channels, no node IDs.
|
|
|
|
## Quick Start
|
|
|
|
Add to your `Cargo.toml`:
|
|
|
|
```toml
|
|
[dependencies]
|
|
dagx = "0.3"
|
|
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
|
```
|
|
|
|
Basic example:
|
|
|
|
```rust
|
|
use dagx::{task, DagRunner, Task};
|
|
|
|
// Define tasks with the #[task] macro
|
|
|
|
struct Value(i32);
|
|
|
|
#[task]
|
|
impl Value {
|
|
async fn run(&self) -> i32 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
struct Add;
|
|
|
|
#[task]
|
|
impl Add {
|
|
async fn run(a: &i32, b: &i32) -> i32 {
|
|
a + b
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
let mut dag = DagRunner::new();
|
|
|
|
// Add source tasks with no dependencies
|
|
let x = dag.add_task(Value(2));
|
|
let y = dag.add_task(Value(3));
|
|
|
|
// Add task that depends on both x and y.
|
|
let sum = dag.add_task(Add).depends_on((x, y));
|
|
|
|
// Execute with true parallelism
|
|
let mut output = dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
|
|
|
|
// Retrieve results
|
|
assert_eq!(output.get(sum), 5);
|
|
}
|
|
```
|
|
|
|
## Features
|
|
|
|
### 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).
|
|
|
|
### 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
|
|
|
|
dagx supports three task patterns:
|
|
|
|
**1. Stateless** - Pure functions with no state:
|
|
|
|
```rust
|
|
struct Add;
|
|
|
|
#[task]
|
|
impl Add {
|
|
async fn run(a: &i32, b: &i32) -> i32 { a + b }
|
|
}
|
|
```
|
|
|
|
**2. Read-only state** - Configuration accessed via `&self`:
|
|
|
|
```rust
|
|
struct Multiplier(i32);
|
|
|
|
#[task]
|
|
impl Multiplier {
|
|
async fn run(&self, input: &i32) -> i32 { input * self.0 }
|
|
}
|
|
```
|
|
|
|
**3. Mutable state** - State modification via `&mut self`:
|
|
|
|
```rust
|
|
struct Counter(i32);
|
|
|
|
#[task]
|
|
impl Counter {
|
|
async fn run(&mut self, value: &i32) -> i32 {
|
|
self.0 += value;
|
|
self.0
|
|
}
|
|
}
|
|
```
|
|
|
|
### Tracing
|
|
|
|
dagx provides optional observability using the `tracing` crate, controlled by the `tracing` feature flag.
|
|
|
|
**Enabling Tracing**
|
|
|
|
```toml
|
|
[dependencies]
|
|
dagx = { version = "0.3", features = ["tracing"] }
|
|
tracing-subscriber = "0.3"
|
|
```
|
|
|
|
**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
|
|
|
|
### Other
|
|
|
|
- **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.
|
|
|
|
## Performance
|
|
|
|
dagx provides true parallel execution with sub-microsecond overhead per task.
|
|
|
|
**How is dagx so fast?**
|
|
|
|
- **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
|
|
|
|
See [design philosophy](docs/DESIGN_PHILOSOPHY.md) for details.
|
|
|
|
## Tutorials & Examples
|
|
|
|
### Tutorials (Start Here)
|
|
|
|
Step-by-step introduction to dagx:
|
|
|
|
- [`01_basic.rs`](examples/01_basic.rs) - Your first DAG
|
|
- [`02_fan_out.rs`](examples/02_fan_out.rs) - One task feeds many (1→N)
|
|
- [`03_fan_in.rs`](examples/03_fan_in.rs) - Many tasks feed one (N→1)
|
|
- [`04_parallel_computation.rs`](examples/04_parallel_computation.rs) - Map-reduce with true parallelism
|
|
|
|
Run tutorial examples:
|
|
|
|
```bash
|
|
cargo run --example 01_basic
|
|
cargo run --example 02_fan_out
|
|
cargo run --example 03_fan_in
|
|
cargo run --example 04_parallel_computation
|
|
```
|
|
|
|
### Advanced Examples
|
|
|
|
Real-world patterns:
|
|
|
|
- [`circuit_breaker.rs`](examples/circuit_breaker.rs) - Circuit breaker pattern for resilient systems
|
|
- [`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 circuit_breaker`
|
|
|
|
## Documentation
|
|
|
|
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
|
|
- [**Library Comparisons**](docs/COMPARISONS.md) - Detailed comparison with dagrs, async_dag, and others
|
|
|
|
## When to Use dagx
|
|
|
|
dagx is ideal for:
|
|
|
|
- **Data pipelines** with complex dependencies between stages
|
|
- **Build systems** where tasks depend on outputs of other tasks
|
|
- **Parallel computation** where work can be split and aggregated
|
|
- **Workflow engines** with typed data flow between stages
|
|
- **ETL processes** with validation and transformation steps
|
|
|
|
## Benchmarks
|
|
|
|
Run the full benchmark suite:
|
|
|
|
```bash
|
|
cargo bench
|
|
```
|
|
|
|
View 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
|
|
```
|
|
|
|
_Benchmarks run on Intel i9-13950HX @ 5.5GHz._
|
|
|
|
## Code of Conduct
|
|
|
|
This project follows the [Builder's Code of Conduct](https://builderscode.org).
|
|
|
|
## Contributing
|
|
|
|
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
|
|
For security issues, see [SECURITY.md](SECURITY.md).
|
|
|
|
## License
|
|
|
|
Licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
|
|
Copyright (c) 2025 Stephen Waits <steve@waits.net>
|