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#[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 #[builder(default = DEFAULT_REPLAY_NAMESPACE.to_owned())]
63 pub namespace: String,
64
65 #[builder(default = DEFAULT_REPLAY_TASK_QUEUE.to_owned())]
67 pub task_queue: String,
68
69 #[builder(default)]
71 pub data_converter: DataConverter,
72
73 #[builder(default)]
75 pub workflow_failure_errors: HashSet<WorkflowErrorType>,
76
77 #[builder(default)]
79 pub workflow_types_to_failure_errors: HashMap<String, HashSet<WorkflowErrorType>>,
80
81 #[builder(default = true)]
83 pub detect_nondeterministic_futures: bool,
84
85 pub patch_activation_callback: Option<PatchActivationCallback>,
87}
88
89impl<S: workflow_replayer_options_builder::State> WorkflowReplayerOptionsBuilder<S> {
90 pub fn worker_plugin<P: WorkerPlugin>(mut self, plugin: P) -> Self {
94 self.worker_plugins.push(Arc::new(plugin));
95 self
96 }
97
98 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 pub fn workflow_interceptor(mut self, constructor: WorkflowInterceptorConstructor) -> Self {
106 self.workflow_interceptor_constructors.push(constructor);
107 self
108 }
109
110 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 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 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 pub fn workflow_interceptor_constructors_mut(
148 &mut self,
149 ) -> &mut Vec<WorkflowInterceptorConstructor> {
150 &mut self.workflow_interceptor_constructors
151 }
152
153 #[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 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 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 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 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 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 #[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 pub fn workflows(&self) -> WorkflowDefinitions {
225 self.workflows.clone()
226 }
227}
228
229#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
231#[non_exhaustive]
232pub enum WorkflowReplayFailure {
233 #[error("invalid workflow history: {message}")]
235 InvalidHistory {
236 message: String,
238 },
239 #[error("workflow replay was nondeterministic: {message}")]
241 Nondeterminism {
242 message: String,
244 },
245 #[error("workflow task failed during replay: {message}")]
247 WorkflowTaskFailure {
248 message: String,
250 },
251 #[error("workflow replay failed internally ({reason}): {message}")]
253 Internal {
254 reason: String,
256 message: String,
258 },
259}
260
261#[derive(Clone, Debug)]
263#[non_exhaustive]
264pub struct WorkflowReplayResult {
265 pub history: WorkflowHistory,
267 pub replay_failure: Option<WorkflowReplayFailure>,
269}
270
271#[derive(Debug, thiserror::Error)]
273#[non_exhaustive]
274pub enum WorkflowReplayError {
275 #[error(transparent)]
277 Worker(#[from] WorkflowReplayWorkerError),
278 #[error(transparent)]
280 Replay(#[from] WorkflowReplayFailure),
281}
282
283#[derive(Debug, thiserror::Error)]
285#[non_exhaustive]
286pub enum WorkflowReplayWorkerError {
287 #[error(transparent)]
289 Plugin(#[from] PluginApplyError),
290 #[error("at least one workflow must be registered for replay")]
292 NoWorkflowsRegistered,
293 #[error("workflow replay initialization failed: {message}")]
295 Initialization {
296 message: String,
298 },
299 #[error("workflow replay worker failed: {0}")]
301 Run(#[source] WorkerRunError),
302 #[error("workflow replay failed internally: {message}")]
304 Internal {
305 message: String,
307 },
308}
309
310pub struct WorkflowReplayer {
312 options: WorkflowReplayerOptions,
313}
314
315impl WorkflowReplayer {
316 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 pub fn options(&self) -> &WorkflowReplayerOptions {
328 &self.options
329 }
330
331 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 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}