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#[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 #[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#[derive(Clone, Debug)]
65pub struct RepeatUntilStep<S, R, Input, Output> {
66 inner: S,
67 repeater: R,
68 _marker: PhantomData<(Input, Output)>,
69}
70
71#[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, 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#[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 #[must_use]
205 pub fn iterations(&self) -> usize {
206 self.iterations
207 }
208
209 #[must_use]
211 pub fn previous_task_id(&self) -> Option<&TaskId> {
212 self.prev_task_id.as_ref()
213 }
214}
215
216#[derive(Debug, thiserror::Error)]
218#[non_exhaustive]
219pub enum RepeaterStateError {
220 #[error("the data for key {REPEATER_ITERATIONS_KEY} is missing")]
222 MissingIterations,
223
224 #[error("could not parse key {REPEATER_ITERATIONS_KEY}")]
226 ParseIterations(#[from] ParseIntError),
227
228 #[error("could not parse key {REPEATER_PREV_TASK_ID_KEY}")]
230 ParseTaskId,
231
232 #[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, 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}