WIP: switch to announcement, remove extract input mess
This commit is contained in:
48 files changed
+685
-1187
No files matched your search
+83
-75
@@ -11,138 +11,146 @@ use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
|
||||
use crate::error::DagError;
|
||||
use crate::extract::ExtractInput;
|
||||
use crate::task::Task;
|
||||
use crate::types::NodeId;
|
||||
|
||||
/// Type-erased channel endpoints (senders or receivers)
|
||||
type ErasedChannels = Vec<Box<dyn Any + Send>>;
|
||||
|
||||
/// Type-erased channel pair (senders, receivers)
|
||||
type ChannelPair = (ErasedChannels, ErasedChannels);
|
||||
/// Type-erased listeners (for receiving inputs from dependencies)
|
||||
type ErasedListeners = Vec<Box<dyn Any + Send>>;
|
||||
|
||||
/// 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>>;
|
||||
|
||||
/// Type-erased async execution result for TaskExecute trait (returns String errors)
|
||||
type TaskExecuteFuture =
|
||||
Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, String>> + 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 - channels are created with full type
|
||||
/// This is the ONLY place where type erasure occurs - announcements 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 {
|
||||
/// Create type-erased output channels for this node's dependents.
|
||||
/// Create a type-erased announcement for this node's output.
|
||||
///
|
||||
/// Returns (senders, receivers) where:
|
||||
/// - senders: Type-erased senders that this node will use to send outputs
|
||||
/// - receivers: Type-erased receivers that dependents will use to receive inputs
|
||||
fn create_output_channels(&self, num_dependents: usize) -> ChannelPair;
|
||||
/// Returns a tuple of (Announcer, Announcement) both type-erased.
|
||||
/// The Announcer will be used by this node to broadcast its output.
|
||||
/// The Announcement can be used to create listeners for dependents.
|
||||
fn create_announcement(&self) -> (Box<dyn Any + Send>, Box<dyn Any + Send>);
|
||||
|
||||
/// Execute the task with type-erased input receivers and output senders.
|
||||
/// Create a type-erased listener from an announcement.
|
||||
///
|
||||
/// This allows dependents to listen to this node's output.
|
||||
fn create_listener(&self, announcement: &dyn Any) -> Box<dyn Any + Send>;
|
||||
|
||||
/// Execute the task with type-erased input listeners and an output announcer.
|
||||
///
|
||||
/// The TypedNode implementation knows the concrete types and can safely downcast.
|
||||
/// Consumes the node and returns the output value.
|
||||
fn execute_with_channels(
|
||||
fn execute_with_listeners(
|
||||
self: Box<Self>,
|
||||
receivers: ErasedChannels,
|
||||
senders: ErasedChannels,
|
||||
listeners: ErasedListeners,
|
||||
announcer: Box<dyn Any + Send>,
|
||||
) -> 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.
|
||||
/// Stores a task that has an `execute()` method (generated by the #[task] macro).
|
||||
/// 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.
|
||||
///
|
||||
/// # Oneshot Channels
|
||||
/// # Announcement Broadcasting
|
||||
///
|
||||
/// Outputs are communicated via oneshot channels for inter-task communication.
|
||||
/// The output is also returned from execute_with_channels for central storage.
|
||||
pub(crate) struct TypedNode<T: Task> {
|
||||
/// Outputs are communicated via announcement channels for inter-task communication.
|
||||
/// Each task has ONE announcer that broadcasts to multiple listeners.
|
||||
/// The output is also returned from execute_with_listeners for central storage.
|
||||
pub(crate) struct TypedNode<T> {
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) task: T,
|
||||
pub(crate) _output_type: std::marker::PhantomData<fn() -> ()>,
|
||||
}
|
||||
|
||||
impl<T: Task + 'static> TypedNode<T> {
|
||||
impl<T> TypedNode<T> {
|
||||
pub(crate) fn new(id: NodeId, task: T) -> Self {
|
||||
Self { id, task }
|
||||
Self {
|
||||
id,
|
||||
task,
|
||||
_output_type: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Task + 'static> ExecutableNode for TypedNode<T>
|
||||
// Implementation for tasks created with the #[task] macro
|
||||
// These tasks have an execute() method generated by the macro
|
||||
impl<T: 'static + Send> ExecutableNode for TypedNode<T>
|
||||
where
|
||||
T::Input: 'static + Clone + ExtractInput,
|
||||
T::Output: 'static + Clone,
|
||||
T: TaskExecute,
|
||||
{
|
||||
fn create_output_channels(&self, num_dependents: usize) -> ChannelPair {
|
||||
let mut senders = Vec::new();
|
||||
let mut receivers = Vec::new();
|
||||
|
||||
for _ in 0..num_dependents {
|
||||
// Create channels that pass Arc-wrapped outputs (efficient for fanout)
|
||||
let (tx, rx) = oneshot::channel::<Arc<T::Output>>();
|
||||
senders.push(Box::new(tx) as Box<dyn Any + Send>);
|
||||
receivers.push(Box::new(rx) as Box<dyn Any + Send>);
|
||||
}
|
||||
|
||||
(senders, receivers)
|
||||
fn create_announcement(&self) -> (Box<dyn Any + Send>, Box<dyn Any + Send>) {
|
||||
T::create_announcement_erased()
|
||||
}
|
||||
|
||||
fn execute_with_channels(
|
||||
fn create_listener(&self, announcement: &dyn Any) -> Box<dyn Any + Send> {
|
||||
T::create_listener_erased(announcement)
|
||||
}
|
||||
|
||||
fn execute_with_listeners(
|
||||
self: Box<Self>,
|
||||
receivers: ErasedChannels,
|
||||
senders: ErasedChannels,
|
||||
listeners: ErasedListeners,
|
||||
announcer: Box<dyn Any + Send>,
|
||||
) -> ExecuteFuture {
|
||||
Box::pin(async move {
|
||||
// Extract typed input from type-erased receivers
|
||||
// TypedNode<T> knows T::Input, so it can safely downcast
|
||||
let input = match T::Input::extract_from_channels(receivers).await {
|
||||
Ok(input) => input,
|
||||
Err(_msg) => {
|
||||
// Failed to extract input - likely channel closed or type mismatch
|
||||
return Err(DagError::InvalidDependency { task_id: self.id.0 });
|
||||
}
|
||||
};
|
||||
// Call the task's execute() method (generated by the #[task] macro)
|
||||
// This handles listener extraction, calling run(), and announcing
|
||||
let result = self.task.execute(listeners, announcer).await;
|
||||
|
||||
// Execute task (consumes self.task)
|
||||
let output = self.task.run(input).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 ExtractInput extracts from Arc.
|
||||
let arc_output = Arc::new(output);
|
||||
|
||||
// Send Arc to all dependents via their channels
|
||||
// TypedNode<T> knows T::Output, so downcast is safe
|
||||
for sender_any in senders {
|
||||
if let Ok(sender) = sender_any.downcast::<oneshot::Sender<Arc<T::Output>>>() {
|
||||
// Clone Arc (just atomic refcount increment, NOT data clone)
|
||||
// This is the key performance optimization for fan-out patterns
|
||||
// Ignore send errors - dependent might have been cancelled
|
||||
let _ = sender.send(Arc::clone(&arc_output));
|
||||
match result {
|
||||
Ok(output_arc) => Ok(output_arc),
|
||||
Err(err_msg) => {
|
||||
// Execution failed - convert error message to DagError
|
||||
eprintln!("Task execution failed: {}", err_msg);
|
||||
Err(DagError::InvalidDependency { task_id: self.id.0 })
|
||||
}
|
||||
}
|
||||
|
||||
// Return Arc-wrapped output (for storage by runner)
|
||||
Ok(arc_output as Arc<dyn Any + Send + Sync>)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for tasks that have an execute() method (generated by #[task] macro).
|
||||
///
|
||||
/// This trait is automatically implemented by the macro and should not be
|
||||
/// implemented manually.
|
||||
pub trait TaskExecute: Send {
|
||||
/// Input type as a tuple of parameter types (for dependency checking)
|
||||
/// - `()` for no parameters
|
||||
/// - `T` for single parameter
|
||||
/// - `(A, B, C, ...)` for multiple parameters
|
||||
type Input: Send;
|
||||
|
||||
/// Output type of the task
|
||||
type Output: Send + Sync + Clone;
|
||||
|
||||
fn execute(
|
||||
self,
|
||||
listeners: Vec<Box<dyn Any + Send>>,
|
||||
announcer: Box<dyn Any + Send>,
|
||||
) -> TaskExecuteFuture;
|
||||
|
||||
fn create_announcement_erased() -> (Box<dyn Any + Send>, Box<dyn Any + Send>);
|
||||
|
||||
fn create_listener_erased(announcement: &dyn Any) -> Box<dyn Any + Send>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user