feat: dagrunner, initial version

This commit is contained in:
swaits committed 2025-10-07 16:31:03 -07:00
commit 7412287ee4
122 files changed
+23101

No files matched your search

+324
View File
@@ -0,0 +1,324 @@
//! Tests for concurrent access patterns and race conditions
use crate::common::task_fn;
use dagx::{DagResult, DagRunner};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
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());
// Add tasks with minimal delays
let tasks: Vec<dagx::TaskHandle<usize>> = (0..10)
.map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move {
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
i as usize
}));
(&task).into()
})
.collect();
// Execute the DAG
dag.run(|fut| {
tokio::spawn(fut);
})
.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 |_: ()| async 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 |_: ()| async 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| {
tokio::spawn(fut);
})
.await?;
// Get the additional tasks
let additional_tasks = build_handle.await.unwrap();
// Initial tasks should have results
for (i, task) in initial.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| {
tokio::spawn(fut);
})
.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 = dag.add_task(task_fn({
let counter = resolution_counter.clone();
move |_: ()| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
42
}
}
}));
// 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(move |val: i32| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
val + i
}
}))
.depends_on(&source)
})
.collect();
dag.run(|fut| {
tokio::spawn(fut);
})
.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()));
// Writers
let writers: Vec<_> = (0..10)
.map(|i| {
let state = shared_state.clone();
dag.add_task(task_fn(move |_: ()| {
let state = state.clone();
async move {
let mut guard = state.write().await;
guard.push(i);
tokio::task::yield_now().await; // Force potential race
guard.push(i * 10);
i
}
}))
})
.collect();
// Readers (depend on writers to ensure ordering)
let readers: Vec<_> = writers
.iter()
.map(|writer| {
let state = shared_state.clone();
dag.add_task(task_fn(move |writer_id: i32| {
let state = state.clone();
async move {
let guard = state.read().await;
let contains_writer = guard.contains(&writer_id);
let len = guard.len();
(writer_id, contains_writer, len)
}
}))
.depends_on(writer)
})
.collect();
dag.run(|fut| {
tokio::spawn(fut);
})
.await?;
// Verify readers saw their writer's data
for reader in &readers {
let (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();
async move {
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,
}
}
}
local_max
}
}))
})
.collect();
dag.run(|fut| {
tokio::spawn(fut);
})
.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();
async move {
counter.fetch_add(1, Ordering::SeqCst);
(dag_id, task_id)
}
}))
})
.collect();
dag.run(|fut| {
tokio::spawn(fut);
})
.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(())
}
+350
View File
@@ -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(())
}
+6
View File
@@ -0,0 +1,6 @@
//! Timing and race condition tests
//!
//! Tests for temporal edge cases, execution ordering, and race conditions.
mod concurrent_access;
mod interleaving;