Skip to main content

apalis_workflow/dag/
service.rs

1use apalis_core::backend::BackendExt;
2use apalis_core::backend::codec::Codec;
3use apalis_core::task::builder::TaskBuilder;
4use apalis_core::task::metadata::Meta;
5use apalis_core::task::status::Status;
6use apalis_core::{
7    backend::WaitForCompletion,
8    error::BoxDynError,
9    task::{Task, metadata::MetadataExt, task_id::TaskId},
10};
11use futures::future::BoxFuture;
12use futures::{FutureExt, Sink, SinkExt, StreamExt};
13use petgraph::Direction;
14use petgraph::graph::NodeIndex;
15use std::collections::HashMap;
16use std::fmt::Debug;
17use tower::Service;
18
19use crate::DagExecutor;
20use crate::dag::context::DagFlowContext;
21use crate::dag::error::{DagFlowError, DagServiceError};
22use crate::dag::response::DagExecutionResponse;
23use crate::id_generator::GenerateId;
24
25/// Service that manages the execution of a DAG workflow
26pub struct RootDagService<B>
27where
28    B: BackendExt,
29{
30    executor: DagExecutor<B>,
31    backend: B,
32}
33
34impl<B> std::fmt::Debug for RootDagService<B>
35where
36    B: BackendExt,
37{
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("RootDagService")
40            .field("executor", &"<DagExecutor>")
41            .field("backend", &"<Backend>")
42            .finish()
43    }
44}
45
46impl<B> RootDagService<B>
47where
48    B: BackendExt,
49{
50    pub(crate) fn new(executor: DagExecutor<B>, backend: B) -> Self {
51        Self { executor, backend }
52    }
53}
54
55impl<B> Clone for RootDagService<B>
56where
57    B: BackendExt + Clone,
58{
59    fn clone(&self) -> Self {
60        Self {
61            executor: self.executor.clone(),
62            backend: self.backend.clone(),
63        }
64    }
65}
66
67/// Determine if the previous node is the designated predecessor in a fan-in scenario
68fn find_designated_fan_in_handler(
69    incoming_nodes: &[NodeIndex],
70) -> Result<&NodeIndex, DagFlowError> {
71    let designated_handler = incoming_nodes.iter().max_by_key(|n| n.index());
72    designated_handler.ok_or(DagFlowError::Service(DagServiceError::MissingFaninHandler))
73}
74
75impl<B, Err, CdcErr, MetaError, IdType> Service<Task<B::Compact, B::Context, B::IdType>>
76    for RootDagService<B>
77where
78    B: BackendExt<Error = Err, IdType = IdType>
79        + Send
80        + Sync
81        + 'static
82        + Clone
83        + WaitForCompletion<DagExecutionResponse<B::Compact, IdType>>,
84    IdType: GenerateId + Send + Sync + 'static + PartialEq + Debug + Clone,
85    B::Compact: Send + Sync + 'static + Clone,
86    B::Context:
87        Send + Sync + Default + MetadataExt<DagFlowContext<B::IdType>, Error = MetaError> + 'static,
88    Err: std::error::Error + Send + Sync + 'static,
89    B: Sink<Task<B::Compact, B::Context, B::IdType>, Error = Err> + Unpin,
90    B::Codec: Codec<Vec<B::Compact>, Compact = B::Compact, Error = CdcErr>
91        + 'static
92        + Codec<DagExecutionResponse<B::Compact, B::IdType>, Compact = B::Compact, Error = CdcErr>,
93    CdcErr: Into<BoxDynError>,
94    MetaError: Into<BoxDynError> + Send + Sync + 'static,
95{
96    type Response = DagExecutionResponse<B::Compact, B::IdType>;
97    type Error = DagFlowError;
98    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
99
100    fn poll_ready(
101        &mut self,
102        cx: &mut std::task::Context<'_>,
103    ) -> std::task::Poll<Result<(), Self::Error>> {
104        self.executor.poll_ready(cx)
105    }
106
107    fn call(&mut self, mut req: Task<B::Compact, B::Context, B::IdType>) -> Self::Future {
108        let mut executor = self.executor.clone();
109        let mut backend = self.backend.clone();
110        let start_nodes = executor.start_nodes.clone();
111        let end_nodes = executor.end_nodes.clone();
112        async move {
113            let ctx = req.extract::<Meta<DagFlowContext<B::IdType>>>().await;
114            let (response, context) = if let Ok(Meta(context)) = ctx {
115                #[cfg(feature = "tracing")]
116                tracing::debug!(
117                    node = ?context.current_node,
118                    "Extracted DagFlowContext for task"
119                );
120                let incoming_nodes = executor
121                    .graph
122                    .neighbors_directed(context.current_node, Direction::Incoming)
123                    .collect::<Vec<_>>();
124                match incoming_nodes.len() {
125                    // Entry node
126                    0 if start_nodes.len() == 1 => {
127                        let response = executor.call(req).await?;
128                        (response, context)
129                    }
130                    // Entry node with multiple start nodes
131                    0 if start_nodes.len() > 1 => {
132                        let response = executor.call(req).await?;
133                        (response, context)
134                    }
135                    // Single incoming node, proceed normally
136                    1 => {
137                        let response = executor.call(req).await?;
138                        (response, context)
139                    }
140                    // Multiple incoming nodes, fan-in scenario
141                    _ => {
142                        let dependency_task_ids = context.get_dependency_task_ids(&incoming_nodes);
143                        #[cfg(feature = "tracing")]
144                        tracing::debug!(
145                            prev_node = ?context.prev_node,
146                            node = ?context.current_node,
147                            deps = ?dependency_task_ids,
148                            "Fanning in from multiple dependencies",
149                        );
150
151                        let prev_node = context
152                            .prev_node
153                            .ok_or(DagFlowError::Service(DagServiceError::MissingPreviousNode))?;
154
155                        if *find_designated_fan_in_handler(&incoming_nodes)? != prev_node {
156                            return Ok(DagExecutionResponse::WaitingForDependencies {
157                                pending_dependencies: dependency_task_ids,
158                            });
159                        }
160
161                        let results = backend
162                            .wait_for(dependency_task_ids.values().cloned().collect::<Vec<_>>())
163                            .collect::<Vec<_>>()
164                            .await
165                            .into_iter()
166                            .collect::<Result<Vec<_>, _>>()
167                            .map_err(|e| DagFlowError::Backend(e.into()))?;
168                        if results.iter().all(|s| matches!(s.status, Status::Done)) {
169                            let sorted_results = {
170                                // Match the order of incoming_nodes by matching NodeIndex
171                                let res = incoming_nodes
172                                    .iter()
173                                    .rev()
174                                    .map(|node_index| {
175                                        let task_id = context
176                                            .node_task_ids
177                                            .iter()
178                                            .find(|(n, _)| *n == node_index)
179                                            .map(|(_, task_id)| task_id)
180                                            .ok_or(DagFlowError::Service(
181                                                DagServiceError::MissingIncomingTaskId,
182                                            ))?;
183                                        let task_result = results
184                                            .iter()
185                                            .find(|r| &r.task_id == task_id)
186                                            .ok_or(DagFlowError::Service(
187                                                DagServiceError::MissingTaskIdResult(format!(
188                                                    "{:?}",
189                                                    task_id.inner()
190                                                )),
191                                            ))?;
192                                        Ok(task_result)
193                                    })
194                                    .collect::<Result<Vec<_>, DagFlowError>>();
195                                match res {
196                                    Ok(v) => v,
197                                    Err(_) => {
198                                        return Ok(DagExecutionResponse::WaitingForDependencies {
199                                            pending_dependencies: dependency_task_ids,
200                                        });
201                                    }
202                                }
203                            };
204                            let res = sorted_results
205                                .iter()
206                                .map(|s| match &s.result {
207                                    Ok(val) => match val {
208                                        DagExecutionResponse::FanOut { response, .. } => {
209                                            Ok(response.clone())
210                                        }
211                                        DagExecutionResponse::EnqueuedNext { result }
212                                        | DagExecutionResponse::Complete { result } => {
213                                            Ok(result.clone())
214                                        }
215                                        _ => Err(DagFlowError::Service(
216                                            DagServiceError::InvalidFanInDependencyResult,
217                                        )),
218                                    },
219                                    Err(e) => Err(DagFlowError::Service(
220                                        DagServiceError::DependencyTaskFailed(e.as_str().into()),
221                                    )),
222                                })
223                                .collect::<Result<Vec<_>, _>>()?;
224                            let encoded_input = B::Codec::encode(&res)
225                                .map_err(|e| DagFlowError::Codec(e.into()))?;
226
227                            let req = req.map(|_| encoded_input); // Replace args with fan-in input
228                            let response = executor.call(req).await?;
229                            (response, context)
230                        } else {
231                            return Err(DagFlowError::Service(
232                                DagServiceError::DependencyTaskFailed(
233                                    "An adjacent node failed. Terminating".into(),
234                                ),
235                            ));
236                        }
237                    }
238                }
239            } else {
240                #[cfg(feature = "tracing")]
241                tracing::debug!("Extracting DagFlowContext for task without meta");
242                // if no metadata, we assume its an entry task
243                if start_nodes.len() == 1 {
244                    #[cfg(feature = "tracing")]
245                    tracing::debug!("Single start node detected, proceeding with execution");
246                    let context = DagFlowContext::new(req.parts.task_id.clone());
247                    req.parts
248                        .ctx
249                        .inject(context.clone())
250                        .map_err(|e| DagFlowError::Metadata(e.into()))?;
251                    let response = executor.call(req).await?;
252                    #[cfg(feature = "tracing")]
253                    tracing::debug!(node = ?context.current_node, "Execution complete at node");
254                    (response, context)
255                } else {
256                    let new_node_task_ids = fan_out_entry_nodes(
257                        &executor,
258                        &backend,
259                        &DagFlowContext::new(req.parts.task_id.clone()),
260                        &req.args,
261                    )
262                    .await?;
263                    return Ok(DagExecutionResponse::EntryFanOut {
264                        node_task_ids: new_node_task_ids,
265                    });
266                }
267            };
268            // At this point we know a node was executed and we have its context
269            // We need to figure out the outgoing nodes and enqueue tasks for them
270            let current_node = context.current_node;
271            let outgoing_nodes = executor
272                .graph
273                .neighbors_directed(current_node, Direction::Outgoing)
274                .collect::<Vec<_>>();
275
276            match outgoing_nodes.len() {
277                0 => {
278                    assert!(
279                        end_nodes.contains(&current_node),
280                        "Current node is not an end node"
281                    );
282                    // This was an end node
283                    return Ok(DagExecutionResponse::Complete { result: response });
284                }
285                1 => {
286                    // Single outgoing node, enqueue task for it
287                    let next_node = outgoing_nodes[0];
288                    let mut new_context = context.clone();
289                    new_context.prev_node = Some(current_node);
290                    new_context.current_node = next_node;
291                    new_context.current_position += 1;
292                    new_context.is_initial = false;
293
294                    let task = TaskBuilder::new(response.clone())
295                        .with_task_id(TaskId::new(B::IdType::generate()))
296                        .meta(new_context)
297                        .build();
298                    backend
299                        .send(task)
300                        .await
301                        .map_err(|e| DagFlowError::Backend(e.into()))?;
302                }
303                _ => {
304                    // Multiple outgoing nodes, fan out
305                    let mut new_context = context.clone();
306                    new_context.prev_node = Some(current_node);
307                    new_context.current_position += 1;
308                    new_context.is_initial = false;
309
310                    let next_task_ids = fan_out_next_nodes(
311                        &executor,
312                        outgoing_nodes,
313                        &backend,
314                        &new_context,
315                        &response,
316                    )
317                    .await?;
318                    return Ok(DagExecutionResponse::FanOut {
319                        response,
320                        node_task_ids: next_task_ids,
321                    });
322                }
323            }
324            Ok(DagExecutionResponse::EnqueuedNext { result: response })
325        }
326        .boxed()
327    }
328}
329
330async fn fan_out_next_nodes<B, Err, CdcErr>(
331    _executor: &DagExecutor<B>,
332    outgoing_nodes: Vec<NodeIndex>,
333    backend: &B,
334    context: &DagFlowContext<B::IdType>,
335    input: &B::Compact,
336) -> Result<HashMap<NodeIndex, TaskId<B::IdType>>, DagFlowError>
337where
338    B::IdType: GenerateId + Send + Sync + 'static + PartialEq,
339    B::Compact: Send + Sync + 'static + Clone,
340    B::Context: Send + Sync + Default + MetadataExt<DagFlowContext<B::IdType>> + 'static,
341    B: Sink<Task<B::Compact, B::Context, B::IdType>, Error = Err> + Unpin,
342    Err: std::error::Error + Send + Sync + 'static,
343    B: BackendExt<Error = Err> + Send + Sync + 'static + Clone,
344    B::Codec: Codec<Vec<B::Compact>, Compact = B::Compact, Error = CdcErr>,
345    CdcErr: Into<BoxDynError>,
346{
347    let mut enqueue_futures = vec![];
348    let next_nodes = outgoing_nodes
349        .iter()
350        .map(|node| (*node, TaskId::new(B::IdType::generate())))
351        .collect::<HashMap<NodeIndex, TaskId<B::IdType>>>();
352    let mut node_task_ids = next_nodes.clone();
353    node_task_ids.extend(context.node_task_ids.clone());
354    for outgoing_node in outgoing_nodes.into_iter() {
355        let task_id = next_nodes
356            .get(&outgoing_node)
357            .ok_or(DagFlowError::Service(DagServiceError::MissingNextNode))?
358            .clone();
359        let task = TaskBuilder::new(input.clone())
360            .with_task_id(task_id)
361            .meta(DagFlowContext {
362                prev_node: context.prev_node,
363                current_node: outgoing_node,
364                completed_nodes: context.completed_nodes.clone(),
365                node_task_ids: node_task_ids.clone(),
366                current_position: context.current_position + 1,
367                is_initial: context.is_initial,
368                root_task_id: context.root_task_id.clone(),
369            })
370            .build();
371        let mut b = backend.clone();
372        enqueue_futures.push(
373            async move {
374                b.send(task)
375                    .await
376                    .map_err(|e| DagFlowError::Backend(e.into()))?;
377                Ok::<(), DagFlowError>(())
378            }
379            .boxed(),
380        );
381    }
382    futures::future::try_join_all(enqueue_futures).await?;
383    Ok(next_nodes)
384}
385
386async fn fan_out_entry_nodes<B, Err, CdcErr>(
387    executor: &DagExecutor<B>,
388    backend: &B,
389    context: &DagFlowContext<B::IdType>,
390    input: &B::Compact,
391) -> Result<HashMap<NodeIndex, TaskId<B::IdType>>, DagFlowError>
392where
393    B::IdType: GenerateId + Send + Sync + 'static + PartialEq + Debug,
394    B::Compact: Send + Sync + 'static + Clone,
395    B::Context: Send + Sync + Default + MetadataExt<DagFlowContext<B::IdType>> + 'static,
396    B: Sink<Task<B::Compact, B::Context, B::IdType>, Error = Err> + Unpin,
397    Err: std::error::Error + Send + Sync + 'static,
398    B: BackendExt<Error = Err> + Send + Sync + 'static + Clone,
399    B::Codec: Codec<Vec<B::Compact>, Compact = B::Compact, Error = CdcErr>,
400    CdcErr: Into<BoxDynError>,
401{
402    let values: Vec<B::Compact> =
403        B::Codec::decode(input).map_err(|e: CdcErr| DagFlowError::Codec(e.into()))?;
404    let start_nodes = executor.start_nodes.clone();
405    if values.len() != start_nodes.len() {
406        return Err(DagFlowError::InputCountMismatch {
407            expected: start_nodes.len(),
408            actual: values.len(),
409        });
410    }
411    let mut enqueue_futures = vec![];
412    let next_nodes = start_nodes
413        .iter()
414        .map(|node| (*node, TaskId::new(B::IdType::generate())))
415        .collect::<HashMap<NodeIndex, TaskId<B::IdType>>>();
416    let mut node_task_ids = next_nodes.clone();
417    node_task_ids.extend(context.node_task_ids.clone());
418    for (outgoing_node, input) in start_nodes.into_iter().zip(values) {
419        let task_id = next_nodes
420            .get(&outgoing_node)
421            .ok_or(DagFlowError::Service(DagServiceError::MissingNextNode))?;
422        let task = TaskBuilder::new(input)
423            .with_task_id(task_id.clone())
424            .meta(DagFlowContext {
425                prev_node: None,
426                current_node: outgoing_node,
427                completed_nodes: Default::default(),
428                node_task_ids: node_task_ids.clone(),
429                current_position: context.current_position,
430                is_initial: true,
431                root_task_id: context.root_task_id.clone(),
432            })
433            .build();
434        let mut b = backend.clone();
435        enqueue_futures.push(
436            async move {
437                b.send(task)
438                    .await
439                    .map_err(|e| DagFlowError::Backend(BoxDynError::from(e)))?;
440                Ok::<(), DagFlowError>(())
441            }
442            .boxed(),
443        );
444    }
445    futures::future::try_join_all(enqueue_futures).await?;
446    Ok(next_nodes)
447}