Skip to main content

apalis_workflow/graph/
service.rs

1use apalis_core::backend::Backend;
2use apalis_core::backend::BackendConfig;
3use apalis_core::backend::WireFormatBackend;
4use apalis_core::backend::codec::Codec;
5use apalis_core::task::builder::TaskBuilder;
6use apalis_core::task::metadata::Meta;
7use apalis_core::task::status::Status;
8use apalis_core::task::task_id::GenerateId;
9use apalis_core::worker::service::IntoWorkerService;
10use apalis_core::worker::service::WorkerService;
11use apalis_core::{
12    backend::WaitForCompletion,
13    error::BoxDynError,
14    task::{Task, task_id::TaskId},
15};
16use futures_util::future::BoxFuture;
17use futures_util::future::try_join_all;
18use futures_util::{FutureExt, Sink, SinkExt, StreamExt};
19use petgraph::Direction;
20use petgraph::graph::DiGraph;
21use petgraph::graph::NodeIndex;
22use serde_json::Value;
23use std::collections::HashMap;
24use std::collections::VecDeque;
25use std::fmt::{Debug, Display};
26use std::str::FromStr;
27use std::task::Poll;
28use tower::Service;
29
30use crate::GraphFlow;
31use crate::NodeService;
32use crate::graph::NodeInput;
33use crate::graph::context::GraphFlowContext;
34use crate::graph::error::{GraphFlowError, GraphServiceError};
35use crate::graph::response::GraphNodeResponse;
36
37/// Service that manages the execution of a Graph workflow
38pub struct RootGraphService<B>
39where
40    B: Backend + WireFormatBackend,
41{
42    pub(super) graph: DiGraph<NodeService<B::Compact>, ()>,
43    pub(super) node_mapping: HashMap<String, NodeIndex>,
44    pub(super) topological_order: Vec<NodeIndex>,
45    pub(super) start_nodes: Vec<NodeIndex>,
46    pub(super) end_nodes: Vec<NodeIndex>,
47    pub(super) not_ready: VecDeque<NodeIndex>,
48    pub(super) backend: B,
49}
50
51impl<B> std::fmt::Debug for RootGraphService<B>
52where
53    B: Backend + WireFormatBackend,
54{
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct("RootGraphService")
57            .field("executor", &"<GraphExecutor>")
58            .field("backend", &"<Backend>")
59            .finish()
60    }
61}
62
63impl<B> RootGraphService<B>
64where
65    B: Backend + WireFormatBackend,
66{
67    /// Get a node by name
68    pub fn get_node_by_name_mut(&mut self, name: &str) -> Option<&mut NodeService<B::Compact>> {
69        self.node_mapping
70            .get(name)
71            .and_then(|&idx| self.graph.node_weight_mut(idx))
72    }
73
74    async fn handle_task(
75        graph: &mut DiGraph<NodeService<B::Compact>, ()>,
76        task: Task<B::Compact>,
77    ) -> Result<(B::Compact, Value), GraphFlowError>
78    where
79        B::Compact: Send + Sync,
80    {
81        let context = task
82            .extract::<Meta<GraphFlowContext>>()
83            .await
84            .map_err(|e| GraphFlowError::Metadata(e.into()))?
85            .0;
86        // Get the service for this node
87        let service = graph
88            .node_weight_mut(context.current_node)
89            .ok_or_else(|| GraphFlowError::MissingService(context.current_node))?;
90
91        let result = service
92            .call(task.map_args(NodeInput::Single))
93            .await
94            .map_err(GraphFlowError::NodeExecutionError)?;
95        Ok(result)
96    }
97
98    async fn handle_fan_in(
99        graph: &mut DiGraph<NodeService<B::Compact>, ()>,
100        task: Task<Vec<Value>>,
101    ) -> Result<(B::Compact, Value), GraphFlowError>
102    where
103        B::Compact: Send + Sync,
104    {
105        let context = task
106            .extract::<Meta<GraphFlowContext>>()
107            .await
108            .map_err(|e| GraphFlowError::Metadata(e.into()))?
109            .0;
110        // Get the service for this node
111        let service = graph
112            .node_weight_mut(context.current_node)
113            .ok_or_else(|| GraphFlowError::MissingService(context.current_node))?;
114
115        let result = service
116            .call(task.map_args(NodeInput::FanIn))
117            .await
118            .map_err(GraphFlowError::NodeExecutionError)?;
119        Ok(result)
120    }
121}
122
123impl<B> Clone for RootGraphService<B>
124where
125    B: Backend + WireFormatBackend + Clone,
126{
127    fn clone(&self) -> Self {
128        Self {
129            graph: self.graph.clone(),
130            node_mapping: self.node_mapping.clone(),
131            topological_order: self.topological_order.clone(),
132            start_nodes: self.start_nodes.clone(),
133            end_nodes: self.end_nodes.clone(),
134            not_ready: self.not_ready.clone(),
135            backend: self.backend.clone(),
136        }
137    }
138}
139
140/// Determine if the previous node is the designated predecessor in a fan-in scenario
141fn find_designated_fan_in_handler(
142    incoming_nodes: &[NodeIndex],
143) -> Result<&NodeIndex, GraphFlowError> {
144    let designated_handler = incoming_nodes.iter().max_by_key(|n| n.index());
145    designated_handler.ok_or(GraphFlowError::Service(
146        GraphServiceError::MissingFaninHandler,
147    ))
148}
149
150impl<B, Err, CdcErr, Id, Compact> Service<Task<Compact>> for RootGraphService<B>
151where
152    B: Backend<Error = Err>
153        + BackendConfig<Id = Id>
154        + WireFormatBackend<Compact = Compact>
155        + Send
156        + Sync
157        + 'static
158        + Clone
159        + WaitForCompletion<GraphNodeResponse>,
160    Id: GenerateId + Send + Sync + 'static + PartialEq + Debug + FromStr + Display,
161    Compact: Send + Sync + 'static + Clone,
162    Err: std::error::Error + Send + Sync + 'static,
163    B: Sink<Task<B::Compact>, Error = Err> + Unpin,
164    B::Codec:
165        Codec<Vec<Compact>, Compact = Compact, Error = CdcErr> + Send + Sync + Clone + 'static,
166    CdcErr: Into<BoxDynError>,
167    <Id as FromStr>::Err: std::error::Error + Send + Sync + 'static,
168{
169    type Response = GraphNodeResponse;
170    type Error = GraphFlowError;
171    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
172
173    fn poll_ready(
174        &mut self,
175        cx: &mut std::task::Context<'_>,
176    ) -> std::task::Poll<Result<(), Self::Error>> {
177        loop {
178            // must wait for *all* services to be ready.
179            // this will cause head-of-line blocking unless the underlying services are always ready.
180            if self.not_ready.is_empty() {
181                return Poll::Ready(Ok(()));
182            } else {
183                if self
184                    .graph
185                    .node_weight_mut(self.not_ready[0])
186                    .ok_or(GraphFlowError::MissingService(self.not_ready[0]))?
187                    .poll_ready(cx)
188                    .map_err(GraphServiceError::PollError)
189                    .map_err(GraphFlowError::Service)?
190                    .is_pending()
191                {
192                    return Poll::Pending;
193                }
194
195                self.not_ready.pop_front();
196            }
197        }
198    }
199
200    fn call(&mut self, mut req: Task<B::Compact>) -> Self::Future {
201        let backend = self.backend.clone();
202        let codec = backend.codec().clone();
203        let start_nodes = self.start_nodes.clone();
204        let end_nodes = self.end_nodes.clone();
205        let mut graph = self.graph.clone();
206        let mut backend = self.backend.clone();
207
208        req.inject_data(codec); // Inject codec
209
210        async move {
211            let ctx = req.extract::<Meta<GraphFlowContext>>().await;
212            let ((compact, response), context) = if let Ok(Meta(context)) = ctx {
213                #[cfg(feature = "tracing")]
214                tracing::debug!(
215                    node = ?context.current_node,
216                    "Extracted GraphFlowContext for task"
217                );
218                let incoming_nodes = graph
219                    .neighbors_directed(context.current_node, Direction::Incoming)
220                    .collect::<Vec<_>>();
221                match incoming_nodes.len() {
222                    // Single entry node
223                    0 if start_nodes.len() == 1 => {
224                        #[cfg(feature = "tracing")]
225                        tracing::trace!(
226                            node = ?context.current_node,
227                            "Found a single entry node"
228                        );
229                        let response = Self::handle_task(&mut graph, req).await?;
230                        #[cfg(feature = "tracing")]
231                        tracing::trace!(
232                            node = ?context.current_node,
233                            "Found a single entry node: done"
234                        );
235                        (response, context)
236                    }
237                    // Entry node with multiple start nodes
238                    0 if start_nodes.len() > 1 => {
239                        #[cfg(feature = "tracing")]
240                        tracing::trace!(
241                            node = ?context.current_node,
242                            "Entry node with multiple start node"
243                        );
244                        let response = Self::handle_task(&mut graph, req).await?;
245                        #[cfg(feature = "tracing")]
246                        tracing::trace!(
247                            node = ?context.current_node,
248                            "Entry node with multiple start node: done"
249                        );
250                        (response, context)
251                    }
252                    // Single incoming node, proceed normally
253                    1 => {
254                        #[cfg(feature = "tracing")]
255                        tracing::trace!(
256                            node = ?context.current_node,
257                            "Single incoming node"
258                        );
259                        let response = Self::handle_task(&mut graph, req).await?;
260                        #[cfg(feature = "tracing")]
261                        tracing::trace!(
262                            node = ?context.current_node,
263                            "Single incoming node complete"
264                        );
265                        (response, context)
266                    }
267                    // Multiple incoming nodes, fan-in scenario
268                    _ => {
269                        #[cfg(feature = "tracing")]
270                        tracing::debug!(
271                            node = ?context.current_node,
272                            "Multiple incoming nodes, fan-in scenario"
273                        );
274                        let dependency_task_ids = context.get_dependency_task_ids(&incoming_nodes);
275
276                        let prev_node = context.prev_node.ok_or(GraphFlowError::Service(
277                            GraphServiceError::MissingPreviousNode,
278                        ))?;
279
280                        let fan_in_node = *find_designated_fan_in_handler(&incoming_nodes)?;
281                        #[cfg(feature = "tracing")]
282                        tracing::debug!(
283                            prev_node = ?prev_node,
284                            node = ?context.current_node,
285                            deps = ?dependency_task_ids.len(),
286                            fan_in = ?fan_in_node,
287                            "Fanning in from multiple dependencies",
288                        );
289                        if fan_in_node != prev_node {
290                            return Ok(GraphNodeResponse::WaitingForDependencies {
291                                pending_dependencies: dependency_task_ids,
292                            });
293                        }
294
295                        #[cfg(feature = "tracing")]
296                        tracing::debug!(
297                            prev_node = ?prev_node,
298                            node = ?context.current_node,
299                            deps = ?dependency_task_ids.len(),
300                            fan_in = ?fan_in_node,
301                            "designated_fan_in_handler: Waiting for other dependencies",
302                        );
303
304                        let results = backend
305                            .wait_for(dependency_task_ids.values().cloned().collect::<Vec<_>>())
306                            .collect::<Vec<_>>()
307                            .await
308                            .into_iter()
309                            .collect::<Result<Vec<_>, _>>()
310                            .map_err(|e| GraphFlowError::Backend(e.into()))?;
311
312                        #[cfg(feature = "tracing")]
313                        tracing::debug!(
314                            prev_node = ?prev_node,
315                            node = ?context.current_node,
316                            deps = ?dependency_task_ids.len(),
317                            results = ?results.len(),
318                            fan_in = ?fan_in_node,
319                            "Found results",
320                        );
321                        if results.iter().all(|s| matches!(s.status, Status::Done)) {
322                            let sorted_results = {
323                                // Match the order of incoming_nodes by matching NodeIndex
324                                let res = incoming_nodes
325                                    .iter()
326                                    .rev()
327                                    .map(|node_index| {
328                                        let task_id = context
329                                            .node_task_ids
330                                            .iter()
331                                            .find(|(n, _)| *n == node_index)
332                                            .map(|(_, task_id)| task_id)
333                                            .ok_or(GraphFlowError::Service(
334                                                GraphServiceError::MissingIncomingTaskId,
335                                            ))?;
336                                        let task_result = results
337                                            .iter()
338                                            .find(|r| &r.task_id == task_id)
339                                            .ok_or(GraphFlowError::Service(
340                                                GraphServiceError::MissingTaskIdResult(format!(
341                                                    "{task_id:?}"
342                                                )),
343                                            ))?;
344                                        Ok(task_result)
345                                    })
346                                    .collect::<Result<Vec<_>, GraphFlowError>>();
347                                match res {
348                                    Ok(v) => v,
349                                    Err(e) => {
350                                        #[cfg(feature = "tracing")]
351                                        tracing::error!(
352                                            node = ?context.current_node,
353                                            error = ?e,
354                                            "Encountered an error resolving result",
355                                        );
356                                        return Ok(GraphNodeResponse::WaitingForDependencies {
357                                            pending_dependencies: dependency_task_ids,
358                                        });
359                                    }
360                                }
361                            };
362                            let res = sorted_results
363                                .iter()
364                                .map(|s| match &s.result {
365                                    Ok(val) => match val {
366                                        GraphNodeResponse::FanOut { response, .. } => {
367                                            Ok(response.clone())
368                                        }
369                                        GraphNodeResponse::EnqueuedNext { result }
370                                        | GraphNodeResponse::Complete { result } => {
371                                            Ok(result.clone())
372                                        }
373                                        _ => Err(GraphFlowError::Service(
374                                            GraphServiceError::InvalidFanInDependencyResult,
375                                        )),
376                                    },
377                                    Err(e) => Err(GraphFlowError::Service(
378                                        GraphServiceError::DependencyTaskFailed(e.as_str().into()),
379                                    )),
380                                })
381                                .collect::<Result<Vec<_>, _>>()?;
382
383                            let req = req.map_args(|_| res); // Replace args with fan-in input
384                            let response = Self::handle_fan_in(&mut graph, req).await?;
385                            (response, context)
386                        } else {
387                            return Err(GraphFlowError::Service(
388                                GraphServiceError::DependencyTaskFailed(
389                                    "An adjacent node failed. Terminating".into(),
390                                ),
391                            ));
392                        }
393                    }
394                }
395            } else {
396                #[cfg(feature = "tracing")]
397                tracing::debug!("Extracting GraphFlowContext for task without meta");
398                // if no metadata, we assume its an entry task
399                if start_nodes.len() == 1 {
400                    #[cfg(feature = "tracing")]
401                    tracing::debug!("Single start node detected, proceeding with execution");
402                    let context = GraphFlowContext::new(req.task_id().cloned());
403                    req.inject_metadata(&context)?;
404                    let response = Self::handle_task(&mut graph, req).await?;
405                    #[cfg(feature = "tracing")]
406                    tracing::debug!(node = ?context.current_node, "Execution complete at node");
407                    (response, context)
408                } else {
409                    #[cfg(feature = "tracing")]
410                    tracing::debug!("Multiple nodes detected, proceeding with fan_out_entry_nodes");
411                    let new_node_task_ids = fan_out_entry_nodes(
412                        &backend,
413                        &start_nodes,
414                        &GraphFlowContext::new(req.task_id().cloned()),
415                        &req.args,
416                    )
417                    .await?;
418                    return Ok(GraphNodeResponse::EntryFanOut {
419                        node_task_ids: new_node_task_ids,
420                    });
421                }
422            };
423            // At this point we know a node was executed and we have its context
424            // We need to figure out the outgoing nodes and enqueue tasks for them
425            let current_node = context.current_node;
426            let outgoing_nodes = graph
427                .neighbors_directed(current_node, Direction::Outgoing)
428                .collect::<Vec<_>>();
429
430            match outgoing_nodes.len() {
431                0 => {
432                    assert!(
433                        end_nodes.contains(&current_node),
434                        "Current node is not an end node"
435                    );
436                    // This was an end node
437                    return Ok(GraphNodeResponse::Complete { result: response });
438                }
439                1 => {
440                    // Single outgoing node, enqueue task for it
441                    let next_node = outgoing_nodes[0];
442                    let mut new_context = context.clone();
443                    new_context.prev_node = Some(current_node);
444                    new_context.current_node = next_node;
445                    new_context.current_position += 1;
446                    new_context.is_initial = false;
447
448                    let task = TaskBuilder::new(compact)
449                        .task_id(B::Id::generate())
450                        .metadata(&new_context)
451                        .build();
452
453                    backend
454                        .send(task)
455                        .await
456                        .map_err(|e| GraphFlowError::Backend(e.into()))?;
457                }
458                _ => {
459                    // Multiple outgoing nodes, fan out
460                    let mut new_context = context.clone();
461                    new_context.prev_node = Some(current_node);
462                    new_context.current_position += 1;
463                    new_context.is_initial = false;
464
465                    let next_task_ids =
466                        fan_out_next_nodes(&backend, outgoing_nodes, &new_context, &compact)
467                            .await?;
468                    return Ok(GraphNodeResponse::FanOut {
469                        response,
470                        node_task_ids: next_task_ids,
471                    });
472                }
473            }
474            Ok(GraphNodeResponse::EnqueuedNext { result: response })
475        }
476        .boxed()
477    }
478}
479
480async fn fan_out_next_nodes<B, Err, CdcErr>(
481    backend: &B,
482    outgoing_nodes: Vec<NodeIndex>,
483    context: &GraphFlowContext,
484    input: &B::Compact,
485) -> Result<HashMap<NodeIndex, TaskId>, GraphFlowError>
486where
487    B::Id: GenerateId + Send + Sync + 'static + PartialEq,
488    B::Compact: Send + Sync + 'static + Clone,
489    B: Sink<Task<B::Compact>, Error = Err> + BackendConfig + Unpin,
490    Err: std::error::Error + Send + Sync + 'static,
491    B: Backend<Error = Err> + WireFormatBackend + Send + Sync + 'static + Clone,
492    B::Codec: Codec<Vec<B::Compact>, Compact = B::Compact, Error = CdcErr>,
493    CdcErr: Into<BoxDynError>,
494    B::Id: FromStr + Display,
495    <B::Id as FromStr>::Err: std::error::Error + Send + Sync + 'static,
496{
497    let mut enqueue_futures = vec![];
498    let next_nodes = outgoing_nodes
499        .iter()
500        .map(|node| (*node, B::Id::generate()))
501        .collect::<HashMap<NodeIndex, TaskId>>();
502    let mut node_task_ids = next_nodes.clone();
503    node_task_ids.extend(context.node_task_ids.clone());
504    for outgoing_node in outgoing_nodes.into_iter() {
505        let task_id = next_nodes
506            .get(&outgoing_node)
507            .ok_or(GraphFlowError::Service(GraphServiceError::MissingNextNode))?
508            .clone();
509        let task = TaskBuilder::new(input.clone())
510            .task_id(task_id)
511            .metadata(&GraphFlowContext {
512                prev_node: context.prev_node,
513                current_node: outgoing_node,
514                completed_nodes: context.completed_nodes.clone(),
515                node_task_ids: node_task_ids.clone(),
516                current_position: context.current_position + 1,
517                is_initial: context.is_initial,
518                root_task_id: context.root_task_id.clone(),
519            })
520            .build();
521        let mut b = backend.clone();
522        enqueue_futures.push(
523            async move {
524                b.send(task)
525                    .await
526                    .map_err(|e| GraphFlowError::Backend(e.into()))?;
527                Ok::<(), GraphFlowError>(())
528            }
529            .boxed(),
530        );
531    }
532    try_join_all(enqueue_futures).await?;
533    Ok(next_nodes)
534}
535
536async fn fan_out_entry_nodes<B, Err, CdcErr>(
537    backend: &B,
538    start_nodes: &[NodeIndex],
539    context: &GraphFlowContext,
540    input: &B::Compact,
541) -> Result<HashMap<NodeIndex, TaskId>, GraphFlowError>
542where
543    B::Id: GenerateId + Send + Sync + 'static + PartialEq + Debug,
544    B::Compact: Send + Sync + 'static + Clone,
545    B: Sink<Task<B::Compact>, Error = Err> + Unpin,
546    Err: std::error::Error + Send + Sync + 'static,
547    B: Backend<Error = Err> + WireFormatBackend + BackendConfig + Send + Sync + 'static + Clone,
548    B::Codec: Codec<Vec<B::Compact>, Compact = B::Compact, Error = CdcErr> + Clone,
549    CdcErr: Into<BoxDynError>,
550    B::Id: FromStr + Display,
551    <B::Id as FromStr>::Err: std::error::Error + Send + Sync + 'static,
552{
553    let codec = backend.codec().clone();
554    let values: Vec<B::Compact> =
555        B::Codec::decode(&codec, input).map_err(|e: CdcErr| GraphFlowError::Codec(e.into()))?;
556    if values.len() != start_nodes.len() {
557        return Err(GraphFlowError::InputCountMismatch {
558            expected: start_nodes.len(),
559            actual: values.len(),
560        });
561    }
562    let mut enqueue_futures = vec![];
563    let next_nodes = start_nodes
564        .iter()
565        .map(|node| (*node, B::Id::generate()))
566        .collect::<HashMap<NodeIndex, TaskId>>();
567    let mut node_task_ids = next_nodes.clone();
568    node_task_ids.extend(context.node_task_ids.clone());
569    for (outgoing_node, input) in start_nodes.iter().zip(values) {
570        let task_id = next_nodes
571            .get(outgoing_node)
572            .ok_or(GraphFlowError::Service(GraphServiceError::MissingNextNode))?;
573        let task = TaskBuilder::new(input)
574            .task_id(task_id.clone())
575            .metadata(&GraphFlowContext {
576                prev_node: None,
577                current_node: *outgoing_node,
578                completed_nodes: Default::default(),
579                node_task_ids: node_task_ids.clone(),
580                current_position: context.current_position,
581                is_initial: true,
582                root_task_id: context.root_task_id.clone(),
583            })
584            .build();
585        let mut b = backend.clone();
586        enqueue_futures.push(
587            async move {
588                b.send(task)
589                    .await
590                    .map_err(|e| GraphFlowError::Backend(BoxDynError::from(e)))?;
591                Ok::<(), GraphFlowError>(())
592            }
593            .boxed(),
594        );
595    }
596    try_join_all(enqueue_futures).await?;
597    Ok(next_nodes)
598}
599
600impl<B, Compact, Err> IntoWorkerService<B, RootGraphService<B>> for GraphFlow<B>
601where
602    B: Backend<Error = Err, Task = Task<Compact>> + WireFormatBackend<Compact = Compact> + Clone,
603    Err: std::error::Error + Send + Sync + 'static,
604    B::Compact: Send + Sync + 'static + Clone,
605    RootGraphService<B>: Service<Task<Compact>>,
606{
607    type Task = Task<Compact>;
608    type Backend = B;
609    fn into_service(self, b: B) -> WorkerService<B, RootGraphService<B>> {
610        let service = self.build(b.clone()).expect("Execution should be valid");
611        WorkerService {
612            backend: b,
613            service,
614        }
615    }
616}