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#[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 #[builder(default = DEFAULT_REPLAY_NAMESPACE.to_owned())]
67 pub namespace: String,
68
69 #[builder(default = DEFAULT_REPLAY_TASK_QUEUE.to_owned())]
71 pub task_queue: String,
72
73 #[builder(default)]
75 pub data_converter: DataConverter,
76
77 #[builder(default)]
79 pub workflow_failure_errors: HashSet<WorkflowErrorType>,
80
81 #[builder(default)]
83 pub workflow_types_to_failure_errors: HashMap<String, HashSet<WorkflowErrorType>>,
84
85 #[builder(default = true)]
87 pub detect_nondeterministic_futures: bool,
88}
89
90impl<S: workflow_replayer_options_builder::State> WorkflowReplayerOptionsBuilder<S> {
91 #[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 #[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 pub fn workflow_interceptor(mut self, constructor: WorkflowInterceptorConstructor) -> Self {
109 self.workflow_interceptor_constructors.push(constructor);
110 self
111 }
112
113 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 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 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 pub fn workflow_interceptor_constructors_mut(
151 &mut self,
152 ) -> &mut Vec<WorkflowInterceptorConstructor> {
153 &mut self.workflow_interceptor_constructors
154 }
155
156 #[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 #[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 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 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 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 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 #[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 pub fn workflows(&self) -> WorkflowDefinitions {
229 self.workflows.clone()
230 }
231}
232
233#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
235#[non_exhaustive]
236pub enum WorkflowReplayFailure {
237 #[error("invalid workflow history: {message}")]
239 InvalidHistory {
240 message: String,
242 },
243 #[error("workflow replay was nondeterministic: {message}")]
245 Nondeterminism {
246 message: String,
248 },
249 #[error("workflow task failed during replay: {message}")]
251 WorkflowTaskFailure {
252 message: String,
254 },
255 #[error("workflow replay failed internally ({reason}): {message}")]
257 Internal {
258 reason: String,
260 message: String,
262 },
263}
264
265#[derive(Clone, Debug)]
267pub struct ReplayHistory {
268 events: Vec<HistoryEvent>,
269 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 pub fn events(&self) -> &[HistoryEvent] {
283 &self.events
284 }
285
286 pub fn workflow_id(&self) -> Option<&str> {
288 self.workflow_id.as_deref()
289 }
290}
291
292#[derive(Clone, Debug)]
294#[non_exhaustive]
295pub struct WorkflowReplayResult {
296 pub history: ReplayHistory,
298 pub replay_failure: Option<WorkflowReplayFailure>,
300}
301
302#[derive(Debug, thiserror::Error)]
304#[non_exhaustive]
305pub enum WorkflowReplayError {
306 #[error(transparent)]
308 History(#[from] WorkflowInteractionError),
309 #[error(transparent)]
311 Worker(#[from] WorkflowReplayWorkerError),
312 #[error(transparent)]
314 Replay(#[from] WorkflowReplayFailure),
315}
316
317#[derive(Debug, thiserror::Error)]
319#[non_exhaustive]
320pub enum WorkflowReplayWorkerError {
321 #[cfg(feature = "experimental")]
323 #[error(transparent)]
324 Plugin(#[from] PluginApplyError),
325 #[error("at least one workflow must be registered for replay")]
327 NoWorkflowsRegistered,
328 #[error("workflow replay initialization failed: {message}")]
330 Initialization {
331 message: String,
333 },
334 #[error("workflow replay worker failed: {0}")]
336 Run(#[source] WorkerRunError),
337 #[error("workflow replay failed internally: {message}")]
339 Internal {
340 message: String,
342 },
343}
344
345pub struct WorkflowReplayer {
347 options: WorkflowReplayerOptions,
348}
349
350impl WorkflowReplayer {
351 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 pub fn options(&self) -> &WorkflowReplayerOptions {
366 &self.options
367 }
368
369 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 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}