rskit_pipeline/
executor.rs1use std::future::Future;
4use std::pin::Pin;
5use std::time::{Duration, Instant};
6
7use rskit_errors::{AppError, AppResult, ErrorCode};
8use tokio_util::sync::CancellationToken;
9
10pub type StepFn<T> =
12 Box<dyn Fn(T) -> Pin<Box<dyn Future<Output = AppResult<T>> + Send>> + Send + Sync>;
13
14pub struct Step<T> {
16 id: String,
17 name: String,
18 execute: StepFn<T>,
19}
20
21impl<T> Step<T> {
22 #[must_use]
24 pub fn new(id: impl Into<String>, name: impl Into<String>, execute: StepFn<T>) -> Self {
25 Self {
26 id: id.into(),
27 name: name.into(),
28 execute,
29 }
30 }
31
32 #[must_use]
34 pub fn id(&self) -> &str {
35 &self.id
36 }
37
38 #[must_use]
40 pub fn name(&self) -> &str {
41 &self.name
42 }
43}
44
45#[derive(Debug, Clone)]
47#[non_exhaustive]
48pub enum StepStatus {
49 Started {
51 step_id: String,
53 },
54 Completed {
56 step_id: String,
58 elapsed: Duration,
60 },
61 Failed {
63 step_id: String,
65 error: String,
67 elapsed: Duration,
69 },
70}
71
72pub struct StepExecutor<T> {
75 steps: Vec<Step<T>>,
76}
77
78impl<T: Send + 'static> StepExecutor<T> {
79 pub fn new(steps: Vec<Step<T>>) -> Self {
81 Self { steps }
82 }
83
84 pub async fn execute(
92 &self,
93 input: T,
94 on_progress: impl Fn(StepStatus) + Send,
95 cancel: CancellationToken,
96 ) -> AppResult<T> {
97 let mut current = input;
98
99 for step in &self.steps {
100 if cancel.is_cancelled() {
102 return Err(AppError::new(ErrorCode::Cancelled, "pipeline cancelled"));
103 }
104
105 on_progress(StepStatus::Started {
106 step_id: step.id.clone(),
107 });
108
109 let start = Instant::now();
110 let fut = (step.execute)(current);
111
112 let result = tokio::select! {
114 biased;
115 _ = cancel.cancelled() => {
116 let elapsed = start.elapsed();
117 on_progress(StepStatus::Failed {
118 step_id: step.id.clone(),
119 error: "cancelled".into(),
120 elapsed,
121 });
122 return Err(AppError::new(ErrorCode::Cancelled, "pipeline cancelled"));
123 }
124 r = fut => r,
125 };
126
127 let elapsed = start.elapsed();
128
129 match result {
130 Ok(value) => {
131 on_progress(StepStatus::Completed {
132 step_id: step.id.clone(),
133 elapsed,
134 });
135 current = value;
136 }
137 Err(e) => {
138 on_progress(StepStatus::Failed {
139 step_id: step.id.clone(),
140 error: e.to_string(),
141 elapsed,
142 });
143 return Err(e);
144 }
145 }
146 }
147
148 Ok(current)
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn make_step(id: &str, transform: fn(i32) -> AppResult<i32>) -> Step<i32> {
157 let id_owned = id.to_string();
158 Step::new(
159 id,
160 id,
161 Box::new(move |val| {
162 let _id = id_owned.clone();
163 Box::pin(async move { transform(val) })
164 }),
165 )
166 }
167
168 #[tokio::test]
169 async fn executes_steps_sequentially() {
170 let steps = vec![
171 make_step("add-one", |v| Ok(v + 1)),
172 make_step("double", |v| Ok(v * 2)),
173 ];
174 let executor = StepExecutor::new(steps);
175 let statuses = std::sync::Arc::new(parking_lot::Mutex::new(Vec::new()));
176 let s = statuses.clone();
177
178 let result = executor
179 .execute(
180 5,
181 move |status| s.lock().push(status),
182 CancellationToken::new(),
183 )
184 .await
185 .unwrap();
186
187 assert_eq!(result, 12);
189
190 let statuses = statuses.lock();
191 assert_eq!(statuses.len(), 4); assert!(matches!(&statuses[0], StepStatus::Started { step_id } if step_id == "add-one"));
193 assert!(
194 matches!(&statuses[1], StepStatus::Completed { step_id, .. } if step_id == "add-one")
195 );
196 assert!(matches!(&statuses[2], StepStatus::Started { step_id } if step_id == "double"));
197 assert!(
198 matches!(&statuses[3], StepStatus::Completed { step_id, .. } if step_id == "double")
199 );
200 }
201
202 #[tokio::test]
203 async fn propagates_step_failure() {
204 let steps = vec![
205 make_step("ok", |v| Ok(v + 1)),
206 make_step("fail", |_| {
207 Err(AppError::new(ErrorCode::Internal, "step broke"))
208 }),
209 make_step("unreachable", Ok),
210 ];
211 let executor = StepExecutor::new(steps);
212
213 let err = executor
214 .execute(0, |_| {}, CancellationToken::new())
215 .await
216 .unwrap_err();
217 assert_eq!(err.code(), ErrorCode::Internal);
218 }
219
220 #[tokio::test]
221 async fn cancellation_stops_execution() {
222 let token = CancellationToken::new();
223 let token_clone = token.clone();
224
225 let steps = vec![
226 make_step("first", |v| Ok(v + 1)),
227 make_step("second", |v| Ok(v + 1)),
228 ];
229 let executor = StepExecutor::new(steps);
230
231 token_clone.cancel();
233
234 let err = executor.execute(0, |_| {}, token).await.unwrap_err();
235 assert_eq!(err.code(), ErrorCode::Cancelled);
236 }
237
238 #[tokio::test]
239 async fn empty_steps_returns_input() {
240 let executor = StepExecutor::<i32>::new(vec![]);
241 let result = executor
242 .execute(42, |_| {}, CancellationToken::new())
243 .await
244 .unwrap();
245 assert_eq!(result, 42);
246 }
247}