Files
dagx/src/node.rs
T

106 lines
3.6 KiB
Rust

//! Internal node types for DAG execution.
//!
//! Provides type erasure to store tasks with different types in a single collection.
//! The public API remains fully type-safe; type erasure is purely an internal implementation detail.
//!
//! - **TypedNode\<T\>**: Stores a task with full type information
//! - **ExecutableNode**: Trait for executing type-erased tasks
use std::any::Any;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
use crate::error::DagError;
use crate::task::{Task, TaskInput};
/// Type-erased async execution result (Arc-wrapped for efficient fanout)
type ExecuteFuture =
Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, DagError>> + Send>>;
/// Internal trait for executing heterogeneous tasks.
///
/// This trait provides type erasure necessary for storing different task types in a single
/// collection. While the public API (Task, Handle, Node) is fully type-safe, internally we
/// need dynamic dispatch to execute tasks with different input/output types.
///
/// 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
///
/// The TypedNode implementation knows the concrete types and can safely downcast.
/// Consumes the node and returns the output value.
fn execute_with_deps(
self: Box<Self>,
dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> ExecuteFuture;
}
/// 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.
///
/// # 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<Input, T>
where
Input: Send + Sync + 'static,
T: Task<Input> + 'static,
{
pub(crate) task: T,
phantom: PhantomData<Input>,
}
impl<Input, T> TypedNode<Input, T>
where
T: Task<Input>,
Input: Send + Sync + 'static,
{
pub(crate) fn new(task: T) -> Self {
Self {
task,
phantom: PhantomData,
}
}
}
impl<Input, T> ExecutableNode for TypedNode<Input, T>
where
T: Task<Input>,
Input: Send + Sync + 'static,
{
fn execute_with_deps(
self: Box<Self>,
dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> 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.
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);
// Return Arc-wrapped output (for storage by runner)
Ok(arc_output as Arc<dyn Any + Send + Sync>)
})
}
}
#[cfg(test)]
mod tests;