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
37pub 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 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 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 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
140fn 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 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); 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 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 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 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 _ => {
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 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); 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 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 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(¤t_node),
434 "Current node is not an end node"
435 );
436 return Ok(GraphNodeResponse::Complete { result: response });
438 }
439 1 => {
440 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 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}