Skip to main content

temporalio_sdk/
workflow_replayer.rs

1use crate::{
2    Worker, WorkerOptions, WorkerRunError,
3    interceptors::{self, Next, WithWorkflowReplayWorkerInput, WorkerInterceptor},
4    plugins::WorkerPlugin,
5    runtime::WorkflowErrorType,
6    workflow_interceptors::WorkflowInterceptorConstructor,
7    workflow_registry::{WorkflowDefinitions, WorkflowRegistrationError},
8};
9use futures_util::{future::LocalBoxFuture, stream};
10use parking_lot::Mutex;
11use std::{
12    collections::{HashMap, HashSet},
13    sync::Arc,
14};
15use temporalio_client::{ClientOptions, PluginApplyError, WorkflowHistory};
16use temporalio_common::{
17    WorkflowDefinition,
18    data_converters::DataConverter,
19    protos::{
20        coresdk::workflow_activation::{
21            WorkflowActivation, remove_from_cache::EvictionReason,
22            workflow_activation_job::Variant as ActivationVariant,
23        },
24        temporal::api::history::v1::History,
25    },
26};
27use temporalio_sdk_core::{
28    init_replay_worker,
29    replay::{HistoryForReplay, ReplayWorkerInput},
30};
31use temporalio_workflow::{PatchActivationCallback, runtime::entry::WorkflowImplementation};
32
33#[cfg(feature = "wasm-workflows")]
34use crate::WasmWorkflowComponent;
35
36const DEFAULT_REPLAY_NAMESPACE: &str = "ReplayNamespace";
37const DEFAULT_REPLAY_TASK_QUEUE: &str = "ReplayTaskQueue";
38const DEFAULT_REPLAY_WORKFLOW_ID: &str = "replay-workflow";
39
40/// Options for constructing a workflow replayer.
41#[derive(bon::Builder, Clone)]
42#[builder(start_fn = new, on(String, into), state_mod(vis = "pub"))]
43#[non_exhaustive]
44pub struct WorkflowReplayerOptions {
45    #[builder(field)]
46    pub(super) workflows: WorkflowDefinitions,
47
48    #[builder(field)]
49    pub(super) worker_interceptors: Vec<Arc<dyn WorkerInterceptor>>,
50
51    #[builder(field)]
52    pub(super) workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
53
54    #[builder(field)]
55    pub(super) worker_plugins: Vec<Arc<dyn WorkerPlugin>>,
56
57    #[cfg(feature = "wasm-workflows")]
58    #[builder(field)]
59    pub(super) wasm_workflow_components: Vec<WasmWorkflowComponent>,
60
61    /// Namespace exposed to workflow code during replay.
62    #[builder(default = DEFAULT_REPLAY_NAMESPACE.to_owned())]
63    pub namespace: String,
64
65    /// Task queue exposed to workflow code during replay.
66    #[builder(default = DEFAULT_REPLAY_TASK_QUEUE.to_owned())]
67    pub task_queue: String,
68
69    /// Data converter used for workflow payloads.
70    #[builder(default)]
71    pub data_converter: DataConverter,
72
73    /// Worker-level workflow errors that should fail workflow executions.
74    #[builder(default)]
75    pub workflow_failure_errors: HashSet<WorkflowErrorType>,
76
77    /// Per-workflow-type errors that should fail workflow executions.
78    #[builder(default)]
79    pub workflow_types_to_failure_errors: HashMap<String, HashSet<WorkflowErrorType>>,
80
81    /// Whether to detect nondeterministic future usage in workflow code.
82    #[builder(default = true)]
83    pub detect_nondeterministic_futures: bool,
84
85    /// Callback controlling first non-replay patch decisions.
86    pub patch_activation_callback: Option<PatchActivationCallback>,
87}
88
89impl<S: workflow_replayer_options_builder::State> WorkflowReplayerOptionsBuilder<S> {
90    /// Register a worker plugin with this replayer.
91    ///
92    /// **Experimental:** This API may change or be removed.
93    pub fn worker_plugin<P: WorkerPlugin>(mut self, plugin: P) -> Self {
94        self.worker_plugins.push(Arc::new(plugin));
95        self
96    }
97
98    /// Append a worker interceptor used during replay.
99    pub fn worker_interceptor<I: WorkerInterceptor + 'static>(mut self, interceptor: I) -> Self {
100        self.worker_interceptors.push(Arc::new(interceptor));
101        self
102    }
103
104    /// Append a workflow interceptor constructor used during replay.
105    pub fn workflow_interceptor(mut self, constructor: WorkflowInterceptorConstructor) -> Self {
106        self.workflow_interceptor_constructors.push(constructor);
107        self
108    }
109
110    /// Register a workflow implementation for replay.
111    pub fn register_workflow<W>(mut self) -> Result<Self, WorkflowRegistrationError>
112    where
113        W: WorkflowImplementation,
114        <W::Run as WorkflowDefinition>::Input: Send,
115    {
116        self.workflows.register_workflow::<W>()?;
117        Ok(self)
118    }
119
120    /// Register a workflow using a custom instance factory.
121    pub fn register_workflow_with_factory<W, F>(
122        mut self,
123        factory: F,
124    ) -> Result<Self, WorkflowRegistrationError>
125    where
126        W: WorkflowImplementation,
127        <W::Run as WorkflowDefinition>::Input: Send,
128        F: Fn() -> W + Send + Sync + 'static,
129    {
130        self.workflows
131            .register_workflow_run_with_factory::<W, F>(factory)?;
132        Ok(self)
133    }
134
135    /// Set the ordered constructors used to create workflow interceptors for each workflow instance.
136    ///
137    /// This replaces any previously configured workflow interceptor constructors.
138    pub fn register_workflow_interceptors(
139        mut self,
140        constructors: Vec<WorkflowInterceptorConstructor>,
141    ) -> Self {
142        self.workflow_interceptor_constructors = constructors;
143        self
144    }
145
146    /// Get a mutable reference to the workflow interceptor constructors list.
147    pub fn workflow_interceptor_constructors_mut(
148        &mut self,
149    ) -> &mut Vec<WorkflowInterceptorConstructor> {
150        &mut self.workflow_interceptor_constructors
151    }
152
153    /// Register a prebuilt WASM workflow component for replay.
154    #[cfg(feature = "wasm-workflows")]
155    pub fn register_wasm_workflow(mut self, component: WasmWorkflowComponent) -> Self {
156        self.wasm_workflow_components.push(component);
157        self
158    }
159}
160
161impl WorkflowReplayerOptions {
162    /// Append a worker interceptor used during replay.
163    pub fn worker_interceptor<I: WorkerInterceptor + 'static>(
164        &mut self,
165        interceptor: I,
166    ) -> &mut Self {
167        self.worker_interceptors.push(Arc::new(interceptor));
168        self
169    }
170
171    /// Append a workflow interceptor constructor used during replay.
172    pub fn workflow_interceptor(
173        &mut self,
174        constructor: WorkflowInterceptorConstructor,
175    ) -> &mut Self {
176        self.workflow_interceptor_constructors.push(constructor);
177        self
178    }
179
180    /// Register a workflow implementation for replay.
181    pub fn register_workflow<W>(&mut self) -> Result<&mut Self, WorkflowRegistrationError>
182    where
183        W: WorkflowImplementation,
184        <W::Run as WorkflowDefinition>::Input: Send,
185    {
186        self.workflows.register_workflow::<W>()?;
187        Ok(self)
188    }
189
190    /// Register a workflow using a custom instance factory.
191    pub fn register_workflow_with_factory<W, F>(
192        &mut self,
193        factory: F,
194    ) -> Result<&mut Self, WorkflowRegistrationError>
195    where
196        W: WorkflowImplementation,
197        <W::Run as WorkflowDefinition>::Input: Send,
198        F: Fn() -> W + Send + Sync + 'static,
199    {
200        self.workflows
201            .register_workflow_run_with_factory::<W, F>(factory)?;
202        Ok(self)
203    }
204
205    /// Set the ordered constructors used to create workflow interceptors for each workflow instance.
206    ///
207    /// This replaces any previously configured workflow interceptor constructors.
208    pub fn register_workflow_interceptors(
209        &mut self,
210        constructors: Vec<WorkflowInterceptorConstructor>,
211    ) -> &mut Self {
212        self.workflow_interceptor_constructors = constructors;
213        self
214    }
215
216    /// Register a prebuilt WASM workflow component for replay.
217    #[cfg(feature = "wasm-workflows")]
218    pub fn register_wasm_workflow(&mut self, component: WasmWorkflowComponent) -> &mut Self {
219        self.wasm_workflow_components.push(component);
220        self
221    }
222
223    /// Returns all the registered workflows by cloning the current set.
224    pub fn workflows(&self) -> WorkflowDefinitions {
225        self.workflows.clone()
226    }
227}
228
229/// A failure attributable to one workflow history during replay.
230#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
231#[non_exhaustive]
232pub enum WorkflowReplayFailure {
233    /// The history could not be replayed because it was malformed or incomplete.
234    #[error("invalid workflow history: {message}")]
235    InvalidHistory {
236        /// Validation failure details.
237        message: String,
238    },
239    /// Workflow code was incompatible with recorded history.
240    #[error("workflow replay was nondeterministic: {message}")]
241    Nondeterminism {
242        /// Nondeterminism details reported by Core.
243        message: String,
244    },
245    /// Language workflow execution failed while processing a task.
246    #[error("workflow task failed during replay: {message}")]
247    WorkflowTaskFailure {
248        /// Workflow task failure details.
249        message: String,
250    },
251    /// Replay ended for an unexpected internal reason.
252    #[error("workflow replay failed internally ({reason}): {message}")]
253    Internal {
254        /// Core eviction reason.
255        reason: String,
256        /// Eviction details reported by Core.
257        message: String,
258    },
259}
260
261/// Outcome of replaying one workflow history.
262#[derive(Clone, Debug)]
263#[non_exhaustive]
264pub struct WorkflowReplayResult {
265    /// History supplied to the replayer.
266    pub history: WorkflowHistory,
267    /// Replay failure, or `None` when the workflow code is compatible with the history.
268    pub replay_failure: Option<WorkflowReplayFailure>,
269}
270
271/// Error replaying a workflow history.
272#[derive(Debug, thiserror::Error)]
273#[non_exhaustive]
274pub enum WorkflowReplayError {
275    /// The replay worker could not be created or run.
276    #[error(transparent)]
277    Worker(#[from] WorkflowReplayWorkerError),
278    /// A single-history replay failed.
279    #[error(transparent)]
280    Replay(#[from] WorkflowReplayFailure),
281}
282
283/// Error creating or running the worker used for replay.
284#[derive(Debug, thiserror::Error)]
285#[non_exhaustive]
286pub enum WorkflowReplayWorkerError {
287    /// A plugin failed while configuring replay options.
288    #[error(transparent)]
289    Plugin(#[from] PluginApplyError),
290    /// No workflow definitions were registered after plugin configuration.
291    #[error("at least one workflow must be registered for replay")]
292    NoWorkflowsRegistered,
293    /// The replay worker could not be initialized.
294    #[error("workflow replay initialization failed: {message}")]
295    Initialization {
296        /// Initialization failure details.
297        message: String,
298    },
299    /// The replay worker stopped before producing trustworthy results.
300    #[error("workflow replay worker failed: {0}")]
301    Run(#[source] WorkerRunError),
302    /// Replay completed without producing the expected outcomes.
303    #[error("workflow replay failed internally: {message}")]
304    Internal {
305        /// Failure details.
306        message: String,
307    },
308}
309
310/// Replays workflow histories against registered workflow implementations.
311pub struct WorkflowReplayer {
312    options: WorkflowReplayerOptions,
313}
314
315impl WorkflowReplayer {
316    /// Construct a replayer and apply its worker plugins.
317    pub fn new(mut options: WorkflowReplayerOptions) -> Result<Self, WorkflowReplayError> {
318        crate::plugins::apply_workflow_replayer_plugins(&mut options)
319            .map_err(WorkflowReplayWorkerError::Plugin)?;
320        if options.workflows.is_empty() {
321            return Err(WorkflowReplayWorkerError::NoWorkflowsRegistered.into());
322        }
323        Ok(Self { options })
324    }
325
326    /// Return the configured replay options.
327    pub fn options(&self) -> &WorkflowReplayerOptions {
328        &self.options
329    }
330
331    /// Replay one history and return an error if it is incompatible with workflow code.
332    pub async fn replay_workflow(
333        &self,
334        history: WorkflowHistory,
335    ) -> Result<(), WorkflowReplayError> {
336        let mut results = self.replay_workflows([history]).await?;
337        let result = results
338            .pop()
339            .ok_or_else(|| WorkflowReplayWorkerError::Internal {
340                message: "replay produced no result for its history".to_owned(),
341            })?;
342        match result.replay_failure {
343            Some(failure) => Err(failure.into()),
344            None => Ok(()),
345        }
346    }
347
348    /// Replay histories using one replay worker and return outcomes in input order.
349    pub async fn replay_workflows(
350        &self,
351        histories: impl IntoIterator<Item = WorkflowHistory>,
352    ) -> Result<Vec<WorkflowReplayResult>, WorkflowReplayError> {
353        self.replay_workflows_internal(histories.into_iter().collect())
354            .await
355    }
356
357    async fn replay_workflows_internal(
358        &self,
359        histories: Vec<WorkflowHistory>,
360    ) -> Result<Vec<WorkflowReplayResult>, WorkflowReplayError> {
361        if histories.is_empty() {
362            return Ok(Vec::new());
363        }
364
365        let mut results = histories
366            .into_iter()
367            .map(|history| WorkflowReplayResult {
368                history,
369                replay_failure: None,
370            })
371            .collect::<Vec<_>>();
372        let core_histories: Vec<_> = results
373            .iter()
374            .map(|result| {
375                HistoryForReplay::new(
376                    History {
377                        events: result.history.events().to_vec(),
378                    },
379                    result
380                        .history
381                        .workflow_id()
382                        .unwrap_or(DEFAULT_REPLAY_WORKFLOW_ID),
383                )
384            })
385            .collect();
386
387        let recorded_outcomes = Arc::new(Mutex::new(Vec::new()));
388        let observer = ReplayOutcomeInterceptor {
389            outcomes: recorded_outcomes.clone(),
390        };
391        let worker_options = self.replay_worker_options(observer);
392
393        let core_options = worker_options
394            .to_core_options(self.options.namespace.clone(), String::new())
395            .map_err(|message| WorkflowReplayWorkerError::Initialization { message })?;
396        let core_worker = init_replay_worker(ReplayWorkerInput::new(
397            core_options,
398            stream::iter(core_histories),
399        ))
400        .map_err(|error| WorkflowReplayWorkerError::Initialization {
401            message: error.to_string(),
402        })?;
403        let client_options = ClientOptions::new(self.options.namespace.clone())
404            .data_converter(self.options.data_converter.clone())
405            .build();
406        let mut worker = Worker::new_from_core_options_prepared(
407            Arc::new(core_worker),
408            client_options,
409            worker_options,
410        )
411        .map_err(|error| WorkflowReplayWorkerError::Initialization {
412            message: error.to_string(),
413        })?;
414
415        let worker_interceptors = worker.worker_interceptors();
416        if let Err(source) = interceptors::call_with_workflow_replay_worker(
417            &worker_interceptors,
418            WithWorkflowReplayWorkerInput::new(&mut worker),
419            Next::new(
420                |input: WithWorkflowReplayWorkerInput<'_>| -> LocalBoxFuture<'_, Result<(), _>> {
421                    Box::pin(async move { input.worker.run_inner().await })
422                },
423            ),
424        )
425        .await
426        {
427            let core_worker = worker.core_worker();
428            core_worker.initiate_shutdown();
429            core_worker.shutdown().await;
430            return Err(WorkflowReplayWorkerError::Run(source).into());
431        }
432
433        let outcomes = std::mem::take(&mut *recorded_outcomes.lock());
434
435        for (index, replay_failure) in outcomes.into_iter().enumerate() {
436            results[index].replay_failure = replay_failure;
437        }
438        Ok(results)
439    }
440
441    fn replay_worker_options(&self, observer: ReplayOutcomeInterceptor) -> WorkerOptions {
442        let worker_interceptors = std::iter::once(Arc::new(observer) as Arc<dyn WorkerInterceptor>)
443            .chain(self.options.worker_interceptors.iter().cloned())
444            .collect();
445        let worker_options = WorkerOptions::new(self.options.task_queue.clone())
446            .with_workflows(self.options.workflows.clone())
447            .with_worker_interceptors(worker_interceptors)
448            .with_workflow_interceptor_constructors(
449                self.options.workflow_interceptor_constructors.clone(),
450            )
451            .with_worker_plugins(self.options.worker_plugins.clone())
452            .workflow_failure_errors(self.options.workflow_failure_errors.clone())
453            .workflow_types_to_failure_errors(self.options.workflow_types_to_failure_errors.clone())
454            .detect_nondeterministic_futures(self.options.detect_nondeterministic_futures)
455            .maybe_patch_activation_callback(self.options.patch_activation_callback.clone());
456        #[cfg(feature = "wasm-workflows")]
457        let worker_options = worker_options
458            .with_wasm_workflow_components(self.options.wasm_workflow_components.clone());
459        worker_options.build()
460    }
461}
462
463struct ReplayOutcomeInterceptor {
464    outcomes: Arc<Mutex<Vec<Option<WorkflowReplayFailure>>>>,
465}
466
467#[async_trait::async_trait(?Send)]
468impl WorkerInterceptor for ReplayOutcomeInterceptor {
469    async fn on_workflow_activation(
470        &self,
471        activation: &WorkflowActivation,
472    ) -> Result<(), anyhow::Error> {
473        let Some(remove) = activation.jobs.iter().find_map(|job| match &job.variant {
474            Some(ActivationVariant::RemoveFromCache(remove)) => Some(remove),
475            _ => None,
476        }) else {
477            return Ok(());
478        };
479        let reason = remove.reason();
480        let failure = match reason {
481            EvictionReason::CacheFull | EvictionReason::LangRequested => None,
482            EvictionReason::Nondeterminism => Some(WorkflowReplayFailure::Nondeterminism {
483                message: remove.message.clone(),
484            }),
485            EvictionReason::LangFail => Some(WorkflowReplayFailure::WorkflowTaskFailure {
486                message: remove.message.clone(),
487            }),
488            reason => Some(WorkflowReplayFailure::Internal {
489                reason: format!("{reason:?}"),
490                message: remove.message.clone(),
491            }),
492        };
493        self.outcomes.lock().push(failure);
494        Ok(())
495    }
496}