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
25pub 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
67fn 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 0 if start_nodes.len() == 1 => {
127 let response = executor.call(req).await?;
128 (response, context)
129 }
130 0 if start_nodes.len() > 1 => {
132 let response = executor.call(req).await?;
133 (response, context)
134 }
135 1 => {
137 let response = executor.call(req).await?;
138 (response, context)
139 }
140 _ => {
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 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); 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 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 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(¤t_node),
280 "Current node is not an end node"
281 );
282 return Ok(DagExecutionResponse::Complete { result: response });
284 }
285 1 => {
286 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 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}