feat: dagrunner, initial version
This commit is contained in:
122 files changed
+23101
No files matched your search
@@ -0,0 +1,350 @@
|
||||
//! Tests for forced execution interleaving patterns
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Barrier;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
||||
// Force specific interleaving using barriers
|
||||
let dag = DagRunner::new();
|
||||
let barrier = Arc::new(Barrier::new(3));
|
||||
let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
// Three parallel tasks that synchronize at barrier
|
||||
let t1 = dag.add_task(task_fn({
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
move |_: ()| {
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
async move {
|
||||
order.lock().push("t1_start");
|
||||
barrier.wait().await;
|
||||
order.lock().push("t1_end");
|
||||
1
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let t2 = dag.add_task(task_fn({
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
move |_: ()| {
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
async move {
|
||||
order.lock().push("t2_start");
|
||||
barrier.wait().await;
|
||||
order.lock().push("t2_end");
|
||||
2
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let t3 = dag.add_task(task_fn({
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
move |_: ()| {
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
async move {
|
||||
order.lock().push("t3_start");
|
||||
barrier.wait().await;
|
||||
order.lock().push("t3_end");
|
||||
3
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Collector
|
||||
let collector = dag
|
||||
.add_task(task_fn(
|
||||
|(a, b, c): (i32, i32, i32)| async move { a + b + c },
|
||||
))
|
||||
.depends_on((&t1, &t2, &t3));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(collector)?, 6);
|
||||
|
||||
// Check interleaving pattern
|
||||
let events = order.lock().clone();
|
||||
assert_eq!(events.len(), 6);
|
||||
|
||||
// All starts should come before all ends due to barrier
|
||||
let start_count = events[0..3]
|
||||
.iter()
|
||||
.filter(|e| e.ends_with("_start"))
|
||||
.count();
|
||||
assert_eq!(start_count, 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
// Create an alternating execution pattern
|
||||
let 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()));
|
||||
|
||||
// Two chains that alternate execution
|
||||
let first_a = dag.add_task(task_fn({
|
||||
let turn = turn.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
let turn = turn.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
while turn.load(Ordering::SeqCst) != 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("A0".to_string());
|
||||
turn.store(1, Ordering::SeqCst);
|
||||
0
|
||||
}
|
||||
}
|
||||
}));
|
||||
let mut chain_a: dagx::TaskHandle<i32> = (&first_a).into();
|
||||
|
||||
let first_b = dag.add_task(task_fn({
|
||||
let turn = turn.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
let turn = turn.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
while turn.load(Ordering::SeqCst) != 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("B0".to_string());
|
||||
turn.store(2, Ordering::SeqCst);
|
||||
0
|
||||
}
|
||||
}
|
||||
}));
|
||||
let mut chain_b: dagx::TaskHandle<i32> = (&first_b).into();
|
||||
|
||||
for i in 1..5 {
|
||||
let turn_clone = turn.clone();
|
||||
let events_clone = events.clone();
|
||||
chain_a = dag
|
||||
.add_task(task_fn(move |x: i32| {
|
||||
let turn_clone = turn_clone.clone();
|
||||
let events_clone = events_clone.clone();
|
||||
async move {
|
||||
while turn_clone.load(Ordering::SeqCst) != i * 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events_clone.lock().push(format!("A{}", i));
|
||||
turn_clone.store(i * 2 + 1, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
}))
|
||||
.depends_on(chain_a);
|
||||
|
||||
let turn_clone = turn.clone();
|
||||
let events_clone = events.clone();
|
||||
chain_b = dag
|
||||
.add_task(task_fn(move |x: i32| {
|
||||
let turn_clone = turn_clone.clone();
|
||||
let events_clone = events_clone.clone();
|
||||
async move {
|
||||
while turn_clone.load(Ordering::SeqCst) != i * 2 + 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events_clone.lock().push(format!("B{}", i));
|
||||
turn_clone.store(i * 2 + 2, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
}))
|
||||
.depends_on(chain_b);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Check alternating pattern
|
||||
let event_list = events.lock().clone();
|
||||
assert_eq!(
|
||||
event_list,
|
||||
vec!["A0", "B0", "A1", "B1", "A2", "B2", "A3", "B3", "A4", "B4"]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layered_interleaving() -> DagResult<()> {
|
||||
// Test interleaving across DAG layers
|
||||
let dag = DagRunner::new();
|
||||
let layer_active = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Layer 1: 4 tasks
|
||||
let layer1: Vec<_> = (0..4)
|
||||
.map(|i| {
|
||||
let active = layer_active.clone();
|
||||
dag.add_task(task_fn(move |_: ()| {
|
||||
let active = active.clone();
|
||||
async move {
|
||||
assert_eq!(active.fetch_add(1, Ordering::SeqCst), i);
|
||||
tokio::task::yield_now().await;
|
||||
active.fetch_sub(1, Ordering::SeqCst);
|
||||
i as i32
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: 2 tasks, each depends on 2 from layer1
|
||||
let layer2: Vec<_> = (0..2)
|
||||
.map(|i| {
|
||||
let active = layer_active.clone();
|
||||
let idx = i * 2;
|
||||
dag.add_task(task_fn(move |(a, b): (i32, i32)| {
|
||||
let active = active.clone();
|
||||
async move {
|
||||
active.fetch_add(1, Ordering::SeqCst);
|
||||
tokio::task::yield_now().await;
|
||||
active.fetch_sub(1, Ordering::SeqCst);
|
||||
a + b
|
||||
}
|
||||
}))
|
||||
.depends_on((&layer1[idx], &layer1[idx + 1]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 3: 1 task depends on both layer2
|
||||
let layer3 = dag
|
||||
.add_task(task_fn({
|
||||
let active = layer_active.clone();
|
||||
move |(a, b): (i32, i32)| {
|
||||
let active = active.clone();
|
||||
async move {
|
||||
active.fetch_add(1, Ordering::SeqCst);
|
||||
tokio::task::yield_now().await;
|
||||
active.fetch_sub(1, Ordering::SeqCst);
|
||||
a + b
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on((&layer2[0], &layer2[1]));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Sum of 0+1+2+3 = 6
|
||||
assert_eq!(dag.get(layer3)?, 6);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cross_branch_synchronization() -> DagResult<()> {
|
||||
// Test synchronization between parallel branches
|
||||
let dag = DagRunner::new();
|
||||
let sync_point = Arc::new(AtomicUsize::new(0));
|
||||
let events = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
// Branch A
|
||||
let a1 = dag.add_task(task_fn({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
let sync = sync.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
events.lock().push("A1");
|
||||
sync.fetch_add(1, Ordering::SeqCst);
|
||||
1
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let a2 = dag
|
||||
.add_task(task_fn({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |x: i32| {
|
||||
let sync = sync.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
// Wait for B1
|
||||
while sync.load(Ordering::SeqCst) < 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("A2");
|
||||
sync.fetch_add(1, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(&a1);
|
||||
|
||||
// Branch B
|
||||
let b1 = dag.add_task(task_fn({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
let sync = sync.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
// Wait for A1
|
||||
while sync.load(Ordering::SeqCst) < 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("B1");
|
||||
sync.fetch_add(1, Ordering::SeqCst);
|
||||
10
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let b2 = dag
|
||||
.add_task(task_fn({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |x: i32| {
|
||||
let sync = sync.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
// Wait for A2
|
||||
while sync.load(Ordering::SeqCst) < 3 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("B2");
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(&b1);
|
||||
|
||||
// Merge
|
||||
let merge = dag
|
||||
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
|
||||
.depends_on((&a2, &b2));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(merge)?, 13); // 2 + 11
|
||||
|
||||
// Check forced interleaving
|
||||
let event_list = events.lock().clone();
|
||||
assert_eq!(event_list, vec!["A1", "B1", "A2", "B2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user