Skip to main content

temporalio_sdk/
workflow_replayer.rs

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