1use std::{marker::PhantomData, task::Context};
2
3use apalis_core::{
4 backend::{Backend, BackendConfig, TaskSinkError, WireFormatBackend, codec::Codec},
5 error::BoxDynError,
6 task::task_fn::{TaskFn, task_fn},
7 task::{
8 Task,
9 builder::TaskBuilder,
10 metadata::{Metadata, MetadataError, MetadataStore},
11 task_id::GenerateId,
12 },
13};
14use futures_util::{FutureExt, Sink, SinkExt, future::BoxFuture};
15use serde::{Deserialize, Serialize};
16use serde_json::to_value;
17use tower::Service;
18
19use crate::{
20 SteppedService,
21 sequential::{
22 context::{StepContext, WorkflowContext},
23 router::{GoTo, StepResponse, WorkflowRouter},
24 step::{Layer, Stack, Step},
25 workflow::SteppedFlow,
26 },
27};
28
29#[derive(Clone, Debug)]
31pub struct Fold<F, Init> {
32 fold: F,
33 _marker: std::marker::PhantomData<Init>,
34}
35
36impl<F, Init, S> Layer<S> for Fold<F, Init>
37where
38 F: Clone,
39 Init: Clone,
40{
41 type Step = FoldStep<S, F, Init>;
42
43 fn layer(&self, step: S) -> Self::Step {
44 FoldStep {
45 inner: step,
46 fold: self.fold.clone(),
47 _marker: std::marker::PhantomData,
48 }
49 }
50}
51impl<Start, C, L, I: IntoIterator<Item = C>, B: Backend> SteppedFlow<Start, I, B, L> {
52 #[allow(clippy::type_complexity)]
54 pub fn fold<F, Output, FnArgs, Init>(
55 self,
56 fold: F,
57 ) -> SteppedFlow<Start, Output, B, Stack<Fold<TaskFn<F, (Init, C), FnArgs>, Init>, L>>
58 where
59 TaskFn<F, (Init, C), FnArgs>: Service<Task<(Init, C)>, Response = Output>,
60 {
61 self.add_step(Fold {
62 fold: task_fn(fold),
63 _marker: PhantomData,
64 })
65 }
66}
67
68#[derive(Clone, Debug)]
70pub struct FoldStep<S, F, Init> {
71 inner: S,
72 fold: F,
73 _marker: std::marker::PhantomData<Init>,
74}
75
76impl<S, F, Input, I: IntoIterator<Item = Input>, Init, B, Err, CodecError> Step<I, B>
77 for FoldStep<S, F, Init>
78where
79 F: Service<Task<(Init, Input)>, Response = Init> + Send + Sync + 'static + Clone,
80 S: Step<Init, B>,
81 B: Backend<Error = Err>
82 + WireFormatBackend
83 + BackendConfig
84 + Send
85 + Sync
86 + Clone
87 + Sink<Task<B::Compact>, Error = Err>
88 + Unpin
89 + 'static,
90 I: IntoIterator<Item = Input> + Send + Sync + 'static,
91 B::Codec: Codec<(Init, Vec<Input>), Error = CodecError, Compact = B::Compact>
92 + Codec<Init, Error = CodecError, Compact = B::Compact>
93 + Codec<I, Error = CodecError, Compact = B::Compact>
94 + Codec<(Init, Input), Error = CodecError, Compact = B::Compact>
95 + Send
96 + Sync
97 + Clone
98 + 'static,
99 B::Id: GenerateId + Sync + Send + 'static + Clone,
100 Init: Default + Serialize + Send + Sync + 'static,
101 Err: std::error::Error + Send + Sync + 'static,
102 CodecError: std::error::Error + Send + Sync + 'static,
103 F::Error: Into<BoxDynError> + Send + 'static,
104 F::Future: Send + 'static,
105 B::Compact: Send + 'static,
106 Input: Send + 'static,
107{
108 type Response = Init;
109 type Error = F::Error;
110 fn register(&mut self, ctx: &mut WorkflowRouter<B>) -> Result<(), BoxDynError> {
111 let svc = SteppedService::new(FoldService {
112 fold: self.fold.clone(),
113 _marker: PhantomData::<(Init, I, B)>,
114 });
115 let count = ctx.steps.len();
116 ctx.steps.insert(count, svc);
117 self.inner.register(ctx)
118 }
119}
120
121#[derive(Debug)]
123pub struct FoldService<F, Init, I, B> {
124 fold: F,
125 _marker: std::marker::PhantomData<(Init, I, B)>,
126}
127
128impl<F: Clone, Init, I, B> Clone for FoldService<F, Init, I, B> {
129 fn clone(&self) -> Self {
130 Self {
131 fold: self.fold.clone(),
132 _marker: std::marker::PhantomData,
133 }
134 }
135}
136
137impl<F, Init, I, B> FoldService<F, Init, I, B> {
138 pub fn new(fold: F) -> Self {
140 Self {
141 fold,
142 _marker: std::marker::PhantomData,
143 }
144 }
145}
146
147impl<F, Init, I, B, Input, CodecError, Err> Service<Task<B::Compact>> for FoldService<F, Init, I, B>
148where
149 F: Service<Task<(Init, Input)>, Response = Init> + Send + 'static + Clone,
150 B: Backend<Error = Err>
151 + WireFormatBackend
152 + BackendConfig
153 + Clone
154 + Sink<Task<B::Compact>, Error = Err>
155 + Send
156 + Sync
157 + Unpin
158 + 'static,
159 I: IntoIterator<Item = Input> + Send + 'static,
160 B::Codec: Codec<(Init, Vec<Input>), Error = CodecError, Compact = B::Compact>
161 + Codec<Init, Error = CodecError, Compact = B::Compact>
162 + Codec<I, Error = CodecError, Compact = B::Compact>
163 + Codec<(Init, Input), Error = CodecError, Compact = B::Compact>
164 + Send
165 + Sync
166 + Clone
167 + 'static,
168 B::Id: GenerateId + Sync + Send + 'static,
169 Init: Default + Serialize + Send + 'static,
170 Err: std::error::Error + Send + Sync + 'static,
171 CodecError: std::error::Error + Send + Sync + 'static,
172 F::Error: Into<BoxDynError> + Send + 'static,
173 F::Future: Send + 'static,
174 B::Compact: Send + 'static,
175 Input: Send + 'static,
176{
177 type Response = GoTo<StepResponse>;
178 type Error = BoxDynError;
179 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
180
181 fn poll_ready(&mut self, cx: &mut Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
182 self.fold.poll_ready(cx).map_err(|e| e.into())
183 }
184
185 fn call(&mut self, task: Task<B::Compact>) -> Self::Future {
186 let state = FoldState::extract(task.metadata()).unwrap_or(FoldState::Init);
187 let mut ctx = task.data().get::<StepContext<B>>().cloned().unwrap();
188 let codec = ctx.backend.codec().clone();
189 let mut fold = self.fold.clone();
190
191 match state {
192 FoldState::Init => async move {
193 let task_id = B::Id::generate();
194 let steps: Task<I> = task.try_map_args(|arg| B::Codec::decode(&codec, &arg))?;
195 let steps = steps.args.into_iter().collect::<Vec<_>>();
196 let task = TaskBuilder::new(B::Codec::encode(&codec, &(Init::default(), steps))?)
197 .metadata(&WorkflowContext {
198 step_index: ctx.current_step,
199 })
200 .task_id(task_id.clone())
201 .metadata(&FoldState::Collection)
202 .build();
203 ctx.backend
204 .send(task)
205 .await
206 .map_err(TaskSinkError::PushError)?;
207 Ok(GoTo::Next(StepResponse {
208 result: to_value(Init::default())?,
209 next_task_id: Some(task_id),
210 }))
211 }
212 .boxed(),
213 FoldState::Collection => async move {
214 let args: (Init, Vec<Input>) = B::Codec::decode(&codec, &task.args)?;
215 let (acc, items) = args;
216
217 let mut items = items.into_iter();
218 let next = items.next().unwrap();
219 let rest = items.collect::<Vec<_>>();
220 let fold_task = task.map_args(|_| (acc, next));
221 let response = fold.call(fold_task).await.map_err(|e| e.into())?;
222
223 match rest.len() {
224 0 if ctx.has_next => {
225 let task_id = B::Id::generate();
226 let result = B::Codec::encode(&codec, &response)?;
227 let next_step = TaskBuilder::new(result)
228 .task_id(task_id.clone())
229 .metadata(&WorkflowContext {
230 step_index: ctx.current_step + 1,
231 })
232 .build();
233 ctx.backend
234 .send(next_step)
235 .await
236 .map_err(TaskSinkError::PushError)?;
237 Ok(GoTo::Break(StepResponse {
238 result: to_value(&response)?,
239 next_task_id: Some(task_id),
240 }))
241 }
242 0 => Ok(GoTo::Break(StepResponse {
243 result: to_value(&response)?,
244 next_task_id: None,
245 })),
246 1.. => {
247 let task_id = B::Id::generate();
249 let result = to_value(&response)?;
250 let steps = TaskBuilder::new(B::Codec::encode(&codec, &(response, rest))?)
251 .task_id(task_id.clone())
252 .metadata(&WorkflowContext {
253 step_index: ctx.current_step,
254 })
255 .metadata(&FoldState::Collection)
256 .build();
257 ctx.backend
258 .send(steps)
259 .await
260 .map_err(TaskSinkError::PushError)?;
261 Ok(GoTo::Next(StepResponse {
262 result,
263 next_task_id: Some(task_id),
264 }))
265 }
266 }
267 }
268 .boxed(),
269 }
270 }
271}
272
273#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
275#[non_exhaustive]
276pub enum FoldState {
277 Init,
279 Collection,
281}
282
283const FOLD_STATE_KEY: &str = "apalis_workflow.fold.state";
284
285#[derive(Debug, thiserror::Error)]
287#[non_exhaustive]
288pub enum FoldStateError {
289 #[error("the data for key {FOLD_STATE_KEY} is missing")]
291 MissingKey,
292
293 #[error("Duplicate entry: {0}")]
295 DuplicateEntry(#[from] MetadataError),
296}
297
298impl Metadata for FoldState {
299 type Error = FoldStateError;
300
301 fn extract(map: &MetadataStore) -> Result<Self, Self::Error> {
302 let value = map.get(FOLD_STATE_KEY).ok_or(FoldStateError::MissingKey)?;
303
304 match value.as_str() {
305 "Collection" => Ok(Self::Collection),
306 _ => Ok(Self::Init),
307 }
308 }
309
310 fn inject(&self, map: &mut MetadataStore) -> Result<(), FoldStateError> {
311 let value = match self {
312 Self::Init => "Init",
313 Self::Collection => "Collection",
314 };
315 map.insert(FOLD_STATE_KEY, value)?;
316 Ok(())
317 }
318}