Skip to main content

apalis_workflow/sequential/repeat_until/
mod.rs

1use std::fmt::Display;
2use std::marker::PhantomData;
3use std::num::ParseIntError;
4use std::str::FromStr;
5use std::task::Context;
6
7use apalis_core::backend::codec::Codec;
8use apalis_core::backend::{BackendConfig, TaskSinkError, WireFormatBackend};
9use apalis_core::error::BoxDynError;
10use apalis_core::task::builder::TaskBuilder;
11use apalis_core::task::metadata::{Metadata, MetadataError, MetadataStore};
12use apalis_core::task::task_fn::{TaskFn, task_fn};
13use apalis_core::task::task_id::{GenerateId, TaskId};
14use apalis_core::{backend::Backend, task::Task};
15use futures_util::future::BoxFuture;
16use futures_util::{FutureExt, Sink, SinkExt};
17use serde::{Deserialize, Serialize};
18use serde_json::to_value;
19use tower::Service;
20
21use crate::sequential::router::WorkflowRouter;
22use crate::sequential::{GoTo, Layer, Stack, Step, StepContext, StepResponse, WorkflowContext};
23use crate::{SteppedFlow, SteppedService};
24
25/// A layer that represents a `repeat_until` step in the workflow.
26#[derive(Clone, Debug)]
27pub struct RepeatUntil<F, Input, Output> {
28    repeater: F,
29    _marker: PhantomData<(Input, Output)>,
30}
31
32impl<F, Input, Output, S> Layer<S> for RepeatUntil<F, Input, Output>
33where
34    F: Clone,
35{
36    type Step = RepeatUntilStep<S, F, Input, Output>;
37
38    fn layer(&self, step: S) -> Self::Step {
39        RepeatUntilStep {
40            inner: step,
41            repeater: self.repeater.clone(),
42            _marker: std::marker::PhantomData,
43        }
44    }
45}
46impl<Start, L, Input, B: Backend> SteppedFlow<Start, Input, B, L> {
47    /// Folds over a collection of items in the workflow.
48    #[allow(clippy::type_complexity)]
49    pub fn repeat_until<F, Output, FnArgs>(
50        self,
51        repeater: F,
52    ) -> SteppedFlow<Start, Output, B, Stack<RepeatUntil<TaskFn<F, Input, FnArgs>, Input, Output>, L>>
53    where
54        TaskFn<F, Input, FnArgs>: Service<Task<Input>, Response = Option<Output>>,
55    {
56        self.add_step(RepeatUntil {
57            repeater: task_fn(repeater),
58            _marker: PhantomData::<(Input, Output)>,
59        })
60    }
61}
62
63/// The step implementation for the `repeat_until` layer.
64#[derive(Clone, Debug)]
65pub struct RepeatUntilStep<S, R, Input, Output> {
66    inner: S,
67    repeater: R,
68    _marker: PhantomData<(Input, Output)>,
69}
70
71/// The service that handles the `repeat_until` logic
72#[derive(Debug)]
73pub struct RepeatUntilService<F, B, Input, Output> {
74    repeater: F,
75    _marker: std::marker::PhantomData<(B, Input, Output)>,
76}
77
78impl<F, B, Input, Output> Clone for RepeatUntilService<F, B, Input, Output>
79where
80    F: Clone,
81{
82    fn clone(&self) -> Self {
83        Self {
84            repeater: self.repeater.clone(),
85            _marker: std::marker::PhantomData,
86        }
87    }
88}
89
90impl<F, Res, B, Input, CodecError, Err> Service<Task<B::Compact>>
91    for RepeatUntilService<F, B, Input, Res>
92where
93    F: Service<Task<Input>, Response = Option<Res>> + Send + 'static + Clone,
94    B: Backend<Error = Err>
95        + WireFormatBackend
96        + BackendConfig
97        + Send
98        + Sync
99        + Clone
100        + Sink<Task<B::Compact>, Error = Err>
101        + Unpin
102        + 'static,
103    B::Codec: Codec<Input, Error = CodecError, Compact = B::Compact>
104        + Codec<Res, Error = CodecError, Compact = B::Compact>
105        + Codec<Option<Res>, Error = CodecError, Compact = B::Compact>
106        + Send
107        + Sync
108        + Clone
109        + 'static,
110    B::Id: GenerateId + Send + Sync + Display + FromStr + 'static,
111    Err: std::error::Error + Send + Sync + 'static,
112    CodecError: std::error::Error + Send + Sync + 'static,
113    F::Error: Into<BoxDynError> + Send + 'static,
114    F::Future: Send + 'static,
115    B::Compact: Send + 'static,
116    Input: Send + 'static, // We don't need Clone because decoding just needs a reference
117    Res: Send + Serialize + 'static,
118{
119    type Response = GoTo<StepResponse>;
120    type Error = BoxDynError;
121    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
122
123    fn poll_ready(&mut self, cx: &mut Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
124        self.repeater.poll_ready(cx).map_err(|e| e.into())
125    }
126
127    fn call(&mut self, task: Task<B::Compact>) -> Self::Future {
128        let state: RepeaterState = Metadata::extract(task.metadata()).unwrap_or_default();
129        let mut ctx =
130            task.data().get::<StepContext<B>>().cloned().expect(
131                "StepContext missing, Did you call the repeater outside of a workflow step?",
132            );
133        let mut repeater = self.repeater.clone();
134        let codec = ctx.backend.codec().clone();
135
136        (async move {
137            let mut compact = None;
138            let decoded: Input = B::Codec::decode(&codec, &task.args)?;
139            let prev_task_id = task.task_id().cloned();
140            let repeat_task = task.map_args(|c| {
141                compact = Some(c);
142                decoded
143            });
144            let response = repeater.call(repeat_task).await.map_err(|e| e.into())?;
145            Ok(match response {
146                Some(res) if ctx.has_next => {
147                    let task_id = B::Id::generate();
148                    let next_step = TaskBuilder::new(B::Codec::encode(&codec, &res)?)
149                        .task_id(task_id.clone())
150                        .metadata(&WorkflowContext {
151                            step_index: ctx.current_step + 1,
152                        })
153                        .build();
154                    ctx.backend
155                        .send(next_step)
156                        .await
157                        .map_err(TaskSinkError::PushError)?;
158                    GoTo::Next(StepResponse {
159                        result: to_value(&res)?,
160                        next_task_id: Some(task_id),
161                    })
162                }
163                Some(res) => GoTo::Break(StepResponse {
164                    result: to_value(&res)?,
165                    next_task_id: None,
166                }),
167                None => {
168                    let task_id = B::Id::generate();
169                    let next_step =
170                        TaskBuilder::new(compact.take().expect("Compact args should be set"))
171                            .task_id(task_id.clone())
172                            .metadata(&WorkflowContext {
173                                step_index: ctx.current_step,
174                            })
175                            .metadata(&RepeaterState {
176                                iterations: state.iterations + 1,
177                                prev_task_id,
178                            })
179                            .build();
180                    ctx.backend
181                        .send(next_step)
182                        .await
183                        .map_err(TaskSinkError::PushError)?;
184                    GoTo::Break(StepResponse {
185                        result: to_value(&None::<Res>)?,
186                        next_task_id: Some(task_id),
187                    })
188                }
189            })
190        }
191        .boxed()) as _
192    }
193}
194
195/// The state of the repeat operation
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
197pub struct RepeaterState {
198    iterations: usize,
199    prev_task_id: Option<TaskId>,
200}
201
202impl RepeaterState {
203    /// Get the number of iterations completed so far.
204    #[must_use]
205    pub fn iterations(&self) -> usize {
206        self.iterations
207    }
208
209    /// Get the previous task id.
210    #[must_use]
211    pub fn previous_task_id(&self) -> Option<&TaskId> {
212        self.prev_task_id.as_ref()
213    }
214}
215
216/// An error representing an invalid [`RepeaterState`]
217#[derive(Debug, thiserror::Error)]
218#[non_exhaustive]
219pub enum RepeaterStateError {
220    /// Missing iterations key
221    #[error("the data for key {REPEATER_ITERATIONS_KEY} is missing")]
222    MissingIterations,
223
224    /// Could not parse iterations
225    #[error("could not parse key {REPEATER_ITERATIONS_KEY}")]
226    ParseIterations(#[from] ParseIntError),
227
228    /// Could not parse a task id
229    #[error("could not parse key {REPEATER_PREV_TASK_ID_KEY}")]
230    ParseTaskId,
231
232    /// Duplicate entry
233    #[error("Duplicate entry: {0}")]
234    DuplicateEntry(#[from] MetadataError),
235}
236
237const REPEATER_ITERATIONS_KEY: &str = "apalis_workflow.repeater.iterations";
238const REPEATER_PREV_TASK_ID_KEY: &str = "apalis_workflow.repeater.prev_task_id";
239
240impl Metadata for RepeaterState {
241    type Error = RepeaterStateError;
242
243    fn extract(map: &MetadataStore) -> Result<Self, Self::Error> {
244        let iterations = map
245            .get(REPEATER_ITERATIONS_KEY)
246            .map(|s| s.parse::<usize>())
247            .transpose()?
248            .unwrap_or(0);
249
250        let prev_task_id = map
251            .get(REPEATER_PREV_TASK_ID_KEY)
252            .map(|value| TaskId::from_str(value).map_err(|_| RepeaterStateError::ParseTaskId))
253            .transpose()?;
254
255        Ok(Self {
256            iterations,
257            prev_task_id,
258        })
259    }
260
261    fn inject(&self, map: &mut MetadataStore) -> Result<(), RepeaterStateError> {
262        map.insert(REPEATER_ITERATIONS_KEY, self.iterations.to_string())?;
263
264        if let Some(task_id) = &self.prev_task_id {
265            map.insert(REPEATER_PREV_TASK_ID_KEY, task_id.to_string())?;
266        }
267        Ok(())
268    }
269}
270
271impl<B, F, Input, Res, S, Err, CodecError> Step<Input, B> for RepeatUntilStep<S, F, Input, Res>
272where
273    F: Service<Task<Input>, Response = Option<Res>> + Send + Sync + 'static + Clone,
274    B: Backend<Error = Err>
275        + Send
276        + Sync
277        + Clone
278        + Sink<Task<B::Compact>, Error = Err>
279        + Unpin
280        + WireFormatBackend
281        + BackendConfig
282        + 'static,
283    B::Codec: Codec<Input, Error = CodecError, Compact = B::Compact>
284        + Codec<Res, Error = CodecError, Compact = B::Compact>
285        + Codec<Option<Res>, Error = CodecError, Compact = B::Compact>
286        + Send
287        + Sync
288        + Clone
289        + 'static,
290    B::Id: GenerateId + Send + 'static,
291    Err: std::error::Error + Send + Sync + 'static,
292    CodecError: std::error::Error + Send + Sync + 'static,
293    F::Error: Into<BoxDynError> + Send + 'static,
294    F::Future: Send + 'static,
295    B::Compact: Send + 'static,
296    Input: Send + Sync + 'static, // We don't need Clone because decoding just needs a reference
297    Res: Serialize + Send + Sync + 'static,
298    S: Step<Input, B> + Send + 'static,
299    B::Id: FromStr + Display + Sync,
300{
301    type Response = Res;
302    type Error = F::Error;
303    fn register(&mut self, ctx: &mut WorkflowRouter<B>) -> Result<(), BoxDynError> {
304        let svc = SteppedService::new(RepeatUntilService {
305            repeater: self.repeater.clone(),
306            _marker: PhantomData::<(B, Input, Res)>,
307        });
308        let count = ctx.steps.len();
309        ctx.steps.insert(count, svc);
310        self.inner.register(ctx)
311    }
312}