orka 0.3.1

An asynchronous, pluggable, and type-safe workflow engine for Rust, designed for orchestrating complex multi-step business processes.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
//! Contains methods for registering `before`, `on`, and `after` handlers
//! for pipeline steps.
//!
//! Handlers take a `ContextData<TData>` (or `ContextData<SData>` for sub-context
//! handlers) and return a future resolving to `Result<PipelineControl, Err>`, where
//! `Err` is the pipeline's own error type. Because `Err` is fixed, a plain `Ok(...)`
//! infers correctly and `?` converts other error types through `From` as usual:
//!
//! ```ignore
//! pipeline
//!   .on_root("load", |ctx| async move {
//!     let cfg = std::fs::read_to_string("cfg.toml")?; // converts via From<io::Error>
//!     ctx.write().config = cfg;
//!     Ok(PipelineControl::Continue)
//!   })
//!   .on_root("notify", |ctx| async move {
//!     Ok(PipelineControl::Continue)
//!   });
//! ```

use tracing::{event, instrument, Level};

use crate::core::context::{
  downcast_context_data,
  AnyContextDataExtractor,
  ContextDataExtractorImpl,
  FinishHandler,
  Handler,
};
use crate::core::context_data::ContextData;
use crate::core::control::PipelineControl;
use crate::core::trace::RunOutcome;
use crate::error::OrkaError;
use crate::pipeline::definition::Pipeline;
use std::future::Future;
use std::sync::Arc;

impl<TData, Err> Pipeline<TData, Err>
where
  TData: 'static + Send + Sync,
  Err: std::error::Error + From<OrkaError> + Send + Sync + 'static,
{
  /// Registers a `before` hook for a step. Returns `&mut Self` so registrations chain.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn before_root<F>(
    &mut self,
    step_name: impl AsRef<str>,
    handler_fn: impl Fn(ContextData<TData>) -> F + Send + Sync + 'static,
  ) -> &mut Self
  where
    F: Future<Output = Result<PipelineControl, Err>> + Send + 'static,
  {
    let step_name = step_name.as_ref();
    self.ensure_step_exists(step_name);
    let final_handler: Handler<TData, Err> = Box::new(move |ctx_data| Box::pin(handler_fn(ctx_data)));
    self
      .before
      .entry(step_name.to_string())
      .or_default()
      .push(final_handler);
    self
  }

  /// Registers an `on` hook for a step. Returns `&mut Self` so registrations chain.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn on_root<F>(
    &mut self,
    step_name: impl AsRef<str>,
    handler_fn: impl Fn(ContextData<TData>) -> F + Send + Sync + 'static,
  ) -> &mut Self
  where
    F: Future<Output = Result<PipelineControl, Err>> + Send + 'static,
  {
    let step_name = step_name.as_ref();
    self.ensure_step_exists(step_name);
    let final_handler: Handler<TData, Err> = Box::new(move |ctx_data| Box::pin(handler_fn(ctx_data)));
    self.on.entry(step_name.to_string()).or_default().push(final_handler);
    self
  }

  /// Registers an `after` hook for a step. Returns `&mut Self` so registrations chain.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn after_root<F>(
    &mut self,
    step_name: impl AsRef<str>,
    handler_fn: impl Fn(ContextData<TData>) -> F + Send + Sync + 'static,
  ) -> &mut Self
  where
    F: Future<Output = Result<PipelineControl, Err>> + Send + 'static,
  {
    let step_name = step_name.as_ref();
    self.ensure_step_exists(step_name);
    let final_handler: Handler<TData, Err> = Box::new(move |ctx_data| Box::pin(handler_fn(ctx_data)));
    self.after.entry(step_name.to_string()).or_default().push(final_handler);
    self
  }


  /// Registers a run-level finish handler: an async "finally" awaited on **every exit of a
  /// full [`run`](Self::run)**, whether the pipeline completed, was stopped by a handler,
  /// or failed (including the missing-handler configuration error). It receives the final
  /// shared context and the run's [`RunOutcome`].
  ///
  /// This is the home for cleanup that must not be lost on the error path: releasing a
  /// lock, restoring a traffic drain, deleting a temp dir, compensating a half-applied
  /// change.
  ///
  /// Multiple finish handlers run in registration order, and all of them run even if one
  /// fails. Error policy: on a run that returned `Ok` (Completed or Stopped), the first
  /// finish-handler error becomes the run's error, since a cleanup failure on a success
  /// path must surface. On an already-failed run, finish-handler errors are logged via
  /// `tracing` and the original error is returned, since cleanup must not mask the real
  /// failure.
  ///
  /// The partial runners ([`run_step`](Self::run_step), [`run_from`](Self::run_from),
  /// [`run_until`](Self::run_until)) and [`resolve_plan`](Self::resolve_plan) never fire
  /// finish handlers; use `run()` when you want finish semantics.
  ///
  /// Finish handlers run *before* the context's
  /// [`resources`](ContextData::resources) bag is released, so a finalizer can still use
  /// a temp dir or a lock guard that the run is holding. Use `on_finish` for cleanup that
  /// must be awaited, and the resource bag for values that clean themselves up in `Drop`.
  pub fn on_finish<F>(
    &mut self,
    handler_fn: impl Fn(ContextData<TData>, RunOutcome) -> F + Send + Sync + 'static,
  ) -> &mut Self
  where
    F: Future<Output = Result<(), Err>> + Send + 'static,
  {
    let final_handler: FinishHandler<TData, Err> =
      Box::new(move |ctx_data, outcome| Box::pin(handler_fn(ctx_data, outcome)));
    self.finish_handlers.push(final_handler);
    self
  }

  /// Removes every finish handler registered via [`on_finish`](Self::on_finish).
  ///
  /// [`stub_step`](Self::stub_step) deliberately leaves finish handlers alone; a test that
  /// wants to run without the cleanup ring drops it explicitly with this.
  pub fn clear_finish_handlers(&mut self) -> &mut Self {
    self.finish_handlers.clear();
    self
  }

  //
  // Granularity is replace-all per (step, phase): handlers are boxed closures with no
  // identity, so per-handler targeting is not expressible. The `clear_*`/`replace_*`
  // methods are surgical (they touch only the named phase); `stub_step` is the blessed
  // "make this whole step a no-op" path.

  /// Removes every `before` handler for the step. The step definition itself is untouched.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn clear_before(&mut self, step_name: impl AsRef<str>) -> &mut Self {
    let step_name = step_name.as_ref();
    self.ensure_step_exists(step_name);
    self.before.remove(step_name);
    self
  }

  /// Removes every `on` handler for the step, including any `on::<SData>` wrappers and any
  /// conditional master handler finalized onto it.
  ///
  /// Surgical: the step's extractor registration is left in place. If that orphans an
  /// extractor (nothing consumes it anymore), [`validate`](Self::validate) fails loudly,
  /// which is the correct error; re-register a consumer or call
  /// [`remove_extractor`](Self::remove_extractor). For whole-step neutralization use
  /// [`stub_step`](Self::stub_step) instead.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn clear_on(&mut self, step_name: impl AsRef<str>) -> &mut Self {
    let step_name = step_name.as_ref();
    self.ensure_step_exists(step_name);
    self.on.remove(step_name);
    self.sub_handler_steps.remove(step_name);
    self
  }

  /// Removes every `after` handler for the step. The step definition itself is untouched.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn clear_after(&mut self, step_name: impl AsRef<str>) -> &mut Self {
    let step_name = step_name.as_ref();
    self.ensure_step_exists(step_name);
    self.after.remove(step_name);
    self
  }

  /// Removes the step's extractor registration and sub-handler bookkeeping, if any.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn remove_extractor(&mut self, step_name: impl AsRef<str>) -> &mut Self {
    let step_name = step_name.as_ref();
    self.ensure_step_exists(step_name);
    self.extractors.remove(step_name);
    self.sub_handler_steps.remove(step_name);
    self
  }

  /// [`clear_before`](Self::clear_before) followed by [`before_root`](Self::before_root):
  /// the step ends up with exactly this `before` handler.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn replace_before_root<F>(
    &mut self,
    step_name: impl AsRef<str>,
    handler_fn: impl Fn(ContextData<TData>) -> F + Send + Sync + 'static,
  ) -> &mut Self
  where
    F: Future<Output = Result<PipelineControl, Err>> + Send + 'static,
  {
    let step_name = step_name.as_ref();
    self.clear_before(step_name);
    self.before_root(step_name, handler_fn)
  }

  /// [`clear_on`](Self::clear_on) followed by [`on_root`](Self::on_root): the step ends up
  /// with exactly this `on` handler. This is the step-stubbing primitive for tests: the
  /// real handler set (including any conditional master handler) is dropped and the stub
  /// is all that remains in the `on` phase.
  ///
  /// Like `clear_on`, this leaves the step's extractor in place; see
  /// [`clear_on`](Self::clear_on) for the validate interaction.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn replace_on_root<F>(
    &mut self,
    step_name: impl AsRef<str>,
    handler_fn: impl Fn(ContextData<TData>) -> F + Send + Sync + 'static,
  ) -> &mut Self
  where
    F: Future<Output = Result<PipelineControl, Err>> + Send + 'static,
  {
    let step_name = step_name.as_ref();
    self.clear_on(step_name);
    self.on_root(step_name, handler_fn)
  }

  /// [`clear_after`](Self::clear_after) followed by [`after_root`](Self::after_root): the
  /// step ends up with exactly this `after` handler.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn replace_after_root<F>(
    &mut self,
    step_name: impl AsRef<str>,
    handler_fn: impl Fn(ContextData<TData>) -> F + Send + Sync + 'static,
  ) -> &mut Self
  where
    F: Future<Output = Result<PipelineControl, Err>> + Send + 'static,
  {
    let step_name = step_name.as_ref();
    self.clear_after(step_name);
    self.after_root(step_name, handler_fn)
  }

  /// Neutralizes a whole step: clears all three phases (dropping any conditional master
  /// handler along with everything else), removes the step's extractor and sub-handler
  /// bookkeeping, and installs a single `Continue` `on` handler so
  /// [`validate`](Self::validate) still passes and a trace still shows the step as
  /// completed.
  ///
  /// Finish handlers registered via [`on_finish`](Self::on_finish) are untouched; drop
  /// those explicitly with [`clear_finish_handlers`](Self::clear_finish_handlers).
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn stub_step(&mut self, step_name: impl AsRef<str>) -> &mut Self {
    let step_name = step_name.as_ref();
    self.clear_before(step_name);
    self.clear_on(step_name);
    self.clear_after(step_name);
    self.remove_extractor(step_name);
    self.on_root(step_name, |_ctx| async { Ok(PipelineControl::Continue) })
  }


  /// Registers an extractor producing a `ContextData<SData>` sub-context for a step.
  ///
  /// The sub-context is **detached**: it is a separate `ContextData`, so writes made by
  /// the `on::<SData>` handler are *not* reflected in the root context. Use
  /// [`set_extractor_with_merge`](Self::set_extractor_with_merge) when you need them back.
  ///
  /// [`ContextData::project`] is the usual way to write one of these.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn set_extractor<SData>(
    &mut self,
    step_name: impl AsRef<str>,
    extractor_fn: impl Fn(ContextData<TData>) -> Result<ContextData<SData>, OrkaError> + Send + Sync + 'static,
  ) -> &mut Self
  where
    SData: 'static + Send + Sync,
  {
    let step_name = step_name.as_ref();
    let extractor_impl = ContextDataExtractorImpl::<TData, SData>::new(extractor_fn);
    event!(Level::DEBUG, %step_name, sub_context_data_type = %std::any::type_name::<SData>(), "Extractor set.");
    self.set_extractor_impl(step_name, Arc::new(extractor_impl))
  }

  /// Registers a caller-supplied, type-erased extractor implementation. This is the
  /// injection seam behind [`set_extractor`](Self::set_extractor) and
  /// [`set_extractor_with_merge`](Self::set_extractor_with_merge): hand in your own
  /// [`AnyContextDataExtractor`] (a recording fake, an instrumented wrapper) and every
  /// `on::<SData>` handler registered for the step afterwards uses it.
  ///
  /// Replaces any previously registered extractor for the step. Note that `on::<SData>`
  /// captures the extractor at registration time, so replace the extractor **before**
  /// registering the handlers that should use it.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn set_extractor_impl(
    &mut self,
    step_name: impl AsRef<str>,
    extractor: Arc<dyn AnyContextDataExtractor<TData>>,
  ) -> &mut Self {
    let step_name = step_name.as_ref();
    self.ensure_step_exists(step_name);
    self.extractors.insert(step_name.to_string(), extractor);
    self
  }

  /// Registers an extractor that also folds the sub-context back into the root context.
  ///
  /// After the step's `on::<SData>` handler succeeds, `merge_fn` runs with a write lock on
  /// the root context and a read lock on the sub-context, letting the sub-pipeline's work
  /// land in the parent:
  ///
  /// ```ignore
  /// pipeline
  ///   .set_extractor_with_merge(
  ///     "validate",
  ///     |main| Ok(main.project(|d| d.customer.clone())),
  ///     |root, sub| root.customer = sub.clone(),
  ///   )
  ///   .on("validate", |sub: ContextData<Customer>| async move {
  ///     sub.write().is_validated = true;
  ///     Ok(PipelineControl::Continue)
  ///   });
  /// ```
  ///
  /// The merge runs **only when the handler returns `Ok`**; a failed sub-handler leaves
  /// the root context untouched.
  ///
  /// # Panics
  /// Panics if the step does not exist.
  pub fn set_extractor_with_merge<SData>(
    &mut self,
    step_name: impl AsRef<str>,
    extractor_fn: impl Fn(ContextData<TData>) -> Result<ContextData<SData>, OrkaError> + Send + Sync + 'static,
    merge_fn: impl Fn(&mut TData, &SData) + Send + Sync + 'static,
  ) -> &mut Self
  where
    SData: 'static + Send + Sync,
  {
    let step_name = step_name.as_ref();
    let extractor_impl = ContextDataExtractorImpl::<TData, SData>::with_merge(extractor_fn, merge_fn);
    event!(Level::DEBUG, %step_name, sub_context_data_type = %std::any::type_name::<SData>(), "Extractor with merge set.");
    self.set_extractor_impl(step_name, Arc::new(extractor_impl))
  }

  /// Registers an `on` hook that operates on the step's extracted `ContextData<SData>`.
  ///
  /// Annotate the closure parameter (`|sub: ContextData<MyType>|`): that is what tells
  /// Orka which `SData` you mean.
  ///
  /// # Panics
  /// Panics if the step does not exist, or if no extractor has been registered for it via
  /// [`set_extractor`](Self::set_extractor) / [`set_extractor_with_merge`](Self::set_extractor_with_merge).
  #[instrument(
        name = "Pipeline::on<SData>",
        skip_all,
        fields(step_name, sub_context_data_type = %std::any::type_name::<SData>())
    )]
  pub fn on<SData, F>(
    &mut self,
    step_name: impl AsRef<str>,
    handler_fn: impl Fn(ContextData<SData>) -> F + Send + Sync + 'static,
  ) -> &mut Self
  where
    SData: 'static + Send + Sync, // SData is the underlying data type for the sub-context
    F: Future<Output = Result<PipelineControl, Err>> + Send + 'static,
  {
    let step_name = step_name.as_ref();
    self.ensure_step_exists(step_name);

    let extractor_arc = self.extractors.get(step_name).cloned().unwrap_or_else(|| {
      panic!(
        "Orka setup error: No extractor found for step '{}' when registering on<{}> handler. Call set_extractor first.",
        step_name,
        std::any::type_name::<SData>()
      )
    });

    let step_name_for_handler = step_name.to_string();
    let user_sdata_handler_arc = Arc::new(handler_fn);

    let wrapped_handler: Handler<TData, Err> = Box::new(move |root_ctx_data: ContextData<TData>| {
      let current_extractor = extractor_arc.clone();
      let user_sdata_handler = user_sdata_handler_arc.clone();
      let step_name_clone = step_name_for_handler.clone();

      Box::pin(async move {
        event!(Level::TRACE, step_name = %step_name_clone, "Executing wrapped on<SData> handler. Attempting extraction.");

        let any_sub_ctx_data = match current_extractor.extract_sub_context_data(root_ctx_data.clone()) {
          Ok(boxed_any) => boxed_any,
          Err(orka_extraction_err) => {
            event!(Level::ERROR, step_name = %step_name_clone, error = %orka_extraction_err, "Extractor function failed.");
            let final_err = match orka_extraction_err {
              OrkaError::HandlerError { source } => OrkaError::ExtractorFailure {
                step_name: step_name_clone.clone(),
                source,
              },
              OrkaError::ExtractorFailure { source, step_name: _ } => OrkaError::ExtractorFailure {
                step_name: step_name_clone.clone(),
                source,
              },
              other_err => other_err,
            };
            return Err(Err::from(final_err));
          }
        };

        let sub_sdata_ctx: ContextData<SData> = match downcast_context_data::<SData>(
          any_sub_ctx_data,
          current_extractor.sub_context_data_type_id(),
          &step_name_clone,
        ) {
          Ok(s_ctx_data) => s_ctx_data,
          Err(orka_downcast_err) => {
            event!(Level::ERROR, step_name = %step_name_clone, error = %orka_downcast_err, "Sub-context ContextData downcast failed.");
            return Err(Err::from(orka_downcast_err));
          }
        };
        event!(Level::TRACE, step_name = %step_name_clone, "Sub-context ContextData extraction and downcast successful.");

        // No lock guard is live across this await.
        event!(Level::TRACE, step_name = %step_name_clone, "Calling user's on<SData> handler.");
        let control = match (user_sdata_handler)(sub_sdata_ctx.clone()).await {
          Ok(control) => control,
          Err(handler_err) => {
            event!(Level::ERROR, step_name = %step_name_clone, error = %handler_err, "User's on<SData> handler failed.");
            // Deliberately skip the merge: a failed sub-handler leaves the root untouched.
            return Err(handler_err);
          }
        };

        //    No `.await` occurs while the guards taken inside are held.
        if current_extractor.has_merge() {
          event!(Level::TRACE, step_name = %step_name_clone, "Merging sub-context back into root context.");
          if let Err(merge_err) = current_extractor.merge_sub_context_data(root_ctx_data, &sub_sdata_ctx) {
            event!(Level::ERROR, step_name = %step_name_clone, error = %merge_err, "Merging sub-context back failed.");
            return Err(Err::from(merge_err));
          }
        }

        Ok(control)
      })
    });

    self.on.entry(step_name.to_string()).or_default().push(wrapped_handler);
    self.sub_handler_steps.insert(step_name.to_string());
    event!(Level::DEBUG, "on<SData> handler registered.");
    self
  }
}