use crate::{
Worker, WorkerOptions, WorkerRunError,
interceptors::{self, Next, WithWorkflowReplayWorkerInput, WorkerInterceptor},
plugins::WorkerPlugin,
runtime::WorkflowErrorType,
workflow_interceptors::WorkflowInterceptorConstructor,
workflow_registry::{WorkflowDefinitions, WorkflowRegistrationError},
};
use futures_util::{future::LocalBoxFuture, stream};
use parking_lot::Mutex;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use temporalio_client::{ClientOptions, PluginApplyError, WorkflowHistory};
use temporalio_common::{
WorkflowDefinition,
data_converters::DataConverter,
protos::{
coresdk::workflow_activation::{
WorkflowActivation, remove_from_cache::EvictionReason,
workflow_activation_job::Variant as ActivationVariant,
},
temporal::api::history::v1::History,
},
};
use temporalio_sdk_core::{
init_replay_worker,
replay::{HistoryForReplay, ReplayWorkerInput},
};
use temporalio_workflow::{PatchActivationCallback, runtime::entry::WorkflowImplementation};
#[cfg(feature = "wasm-workflows")]
use crate::WasmWorkflowComponent;
const DEFAULT_REPLAY_NAMESPACE: &str = "ReplayNamespace";
const DEFAULT_REPLAY_TASK_QUEUE: &str = "ReplayTaskQueue";
const DEFAULT_REPLAY_WORKFLOW_ID: &str = "replay-workflow";
#[derive(bon::Builder, Clone)]
#[builder(start_fn = new, on(String, into), state_mod(vis = "pub"))]
#[non_exhaustive]
pub struct WorkflowReplayerOptions {
#[builder(field)]
pub(super) workflows: WorkflowDefinitions,
#[builder(field)]
pub(super) worker_interceptors: Vec<Arc<dyn WorkerInterceptor>>,
#[builder(field)]
pub(super) workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
#[builder(field)]
pub(super) worker_plugins: Vec<Arc<dyn WorkerPlugin>>,
#[cfg(feature = "wasm-workflows")]
#[builder(field)]
pub(super) wasm_workflow_components: Vec<WasmWorkflowComponent>,
#[builder(default = DEFAULT_REPLAY_NAMESPACE.to_owned())]
pub namespace: String,
#[builder(default = DEFAULT_REPLAY_TASK_QUEUE.to_owned())]
pub task_queue: String,
#[builder(default)]
pub data_converter: DataConverter,
#[builder(default)]
pub workflow_failure_errors: HashSet<WorkflowErrorType>,
#[builder(default)]
pub workflow_types_to_failure_errors: HashMap<String, HashSet<WorkflowErrorType>>,
#[builder(default = true)]
pub detect_nondeterministic_futures: bool,
pub patch_activation_callback: Option<PatchActivationCallback>,
}
impl<S: workflow_replayer_options_builder::State> WorkflowReplayerOptionsBuilder<S> {
pub fn worker_plugin<P: WorkerPlugin>(mut self, plugin: P) -> Self {
self.worker_plugins.push(Arc::new(plugin));
self
}
pub fn worker_interceptor<I: WorkerInterceptor + 'static>(mut self, interceptor: I) -> Self {
self.worker_interceptors.push(Arc::new(interceptor));
self
}
pub fn workflow_interceptor(mut self, constructor: WorkflowInterceptorConstructor) -> Self {
self.workflow_interceptor_constructors.push(constructor);
self
}
pub fn register_workflow<W>(mut self) -> Result<Self, WorkflowRegistrationError>
where
W: WorkflowImplementation,
<W::Run as WorkflowDefinition>::Input: Send,
{
self.workflows.register_workflow::<W>()?;
Ok(self)
}
pub fn register_workflow_with_factory<W, F>(
mut self,
factory: F,
) -> Result<Self, WorkflowRegistrationError>
where
W: WorkflowImplementation,
<W::Run as WorkflowDefinition>::Input: Send,
F: Fn() -> W + Send + Sync + 'static,
{
self.workflows
.register_workflow_run_with_factory::<W, F>(factory)?;
Ok(self)
}
pub fn register_workflow_interceptors(
mut self,
constructors: Vec<WorkflowInterceptorConstructor>,
) -> Self {
self.workflow_interceptor_constructors = constructors;
self
}
pub fn workflow_interceptor_constructors_mut(
&mut self,
) -> &mut Vec<WorkflowInterceptorConstructor> {
&mut self.workflow_interceptor_constructors
}
#[cfg(feature = "wasm-workflows")]
pub fn register_wasm_workflow(mut self, component: WasmWorkflowComponent) -> Self {
self.wasm_workflow_components.push(component);
self
}
}
impl WorkflowReplayerOptions {
pub fn worker_interceptor<I: WorkerInterceptor + 'static>(
&mut self,
interceptor: I,
) -> &mut Self {
self.worker_interceptors.push(Arc::new(interceptor));
self
}
pub fn workflow_interceptor(
&mut self,
constructor: WorkflowInterceptorConstructor,
) -> &mut Self {
self.workflow_interceptor_constructors.push(constructor);
self
}
pub fn register_workflow<W>(&mut self) -> Result<&mut Self, WorkflowRegistrationError>
where
W: WorkflowImplementation,
<W::Run as WorkflowDefinition>::Input: Send,
{
self.workflows.register_workflow::<W>()?;
Ok(self)
}
pub fn register_workflow_with_factory<W, F>(
&mut self,
factory: F,
) -> Result<&mut Self, WorkflowRegistrationError>
where
W: WorkflowImplementation,
<W::Run as WorkflowDefinition>::Input: Send,
F: Fn() -> W + Send + Sync + 'static,
{
self.workflows
.register_workflow_run_with_factory::<W, F>(factory)?;
Ok(self)
}
pub fn register_workflow_interceptors(
&mut self,
constructors: Vec<WorkflowInterceptorConstructor>,
) -> &mut Self {
self.workflow_interceptor_constructors = constructors;
self
}
#[cfg(feature = "wasm-workflows")]
pub fn register_wasm_workflow(&mut self, component: WasmWorkflowComponent) -> &mut Self {
self.wasm_workflow_components.push(component);
self
}
pub fn workflows(&self) -> WorkflowDefinitions {
self.workflows.clone()
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum WorkflowReplayFailure {
#[error("invalid workflow history: {message}")]
InvalidHistory {
message: String,
},
#[error("workflow replay was nondeterministic: {message}")]
Nondeterminism {
message: String,
},
#[error("workflow task failed during replay: {message}")]
WorkflowTaskFailure {
message: String,
},
#[error("workflow replay failed internally ({reason}): {message}")]
Internal {
reason: String,
message: String,
},
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct WorkflowReplayResult {
pub history: WorkflowHistory,
pub replay_failure: Option<WorkflowReplayFailure>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorkflowReplayError {
#[error(transparent)]
Worker(#[from] WorkflowReplayWorkerError),
#[error(transparent)]
Replay(#[from] WorkflowReplayFailure),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorkflowReplayWorkerError {
#[error(transparent)]
Plugin(#[from] PluginApplyError),
#[error("at least one workflow must be registered for replay")]
NoWorkflowsRegistered,
#[error("workflow replay initialization failed: {message}")]
Initialization {
message: String,
},
#[error("workflow replay worker failed: {0}")]
Run(#[source] WorkerRunError),
#[error("workflow replay failed internally: {message}")]
Internal {
message: String,
},
}
pub struct WorkflowReplayer {
options: WorkflowReplayerOptions,
}
impl WorkflowReplayer {
pub fn new(mut options: WorkflowReplayerOptions) -> Result<Self, WorkflowReplayError> {
crate::plugins::apply_workflow_replayer_plugins(&mut options)
.map_err(WorkflowReplayWorkerError::Plugin)?;
if options.workflows.is_empty() {
return Err(WorkflowReplayWorkerError::NoWorkflowsRegistered.into());
}
Ok(Self { options })
}
pub fn options(&self) -> &WorkflowReplayerOptions {
&self.options
}
pub async fn replay_workflow(
&self,
history: WorkflowHistory,
) -> Result<(), WorkflowReplayError> {
let mut results = self.replay_workflows([history]).await?;
let result = results
.pop()
.ok_or_else(|| WorkflowReplayWorkerError::Internal {
message: "replay produced no result for its history".to_owned(),
})?;
match result.replay_failure {
Some(failure) => Err(failure.into()),
None => Ok(()),
}
}
pub async fn replay_workflows(
&self,
histories: impl IntoIterator<Item = WorkflowHistory>,
) -> Result<Vec<WorkflowReplayResult>, WorkflowReplayError> {
self.replay_workflows_internal(histories.into_iter().collect())
.await
}
async fn replay_workflows_internal(
&self,
histories: Vec<WorkflowHistory>,
) -> Result<Vec<WorkflowReplayResult>, WorkflowReplayError> {
if histories.is_empty() {
return Ok(Vec::new());
}
let mut results = histories
.into_iter()
.map(|history| WorkflowReplayResult {
history,
replay_failure: None,
})
.collect::<Vec<_>>();
let core_histories: Vec<_> = results
.iter()
.map(|result| {
HistoryForReplay::new(
History {
events: result.history.events().to_vec(),
},
result
.history
.workflow_id()
.unwrap_or(DEFAULT_REPLAY_WORKFLOW_ID),
)
})
.collect();
let recorded_outcomes = Arc::new(Mutex::new(Vec::new()));
let observer = ReplayOutcomeInterceptor {
outcomes: recorded_outcomes.clone(),
};
let worker_options = self.replay_worker_options(observer);
let core_options = worker_options
.to_core_options(self.options.namespace.clone(), String::new())
.map_err(|message| WorkflowReplayWorkerError::Initialization { message })?;
let core_worker = init_replay_worker(ReplayWorkerInput::new(
core_options,
stream::iter(core_histories),
))
.map_err(|error| WorkflowReplayWorkerError::Initialization {
message: error.to_string(),
})?;
let client_options = ClientOptions::new(self.options.namespace.clone())
.data_converter(self.options.data_converter.clone())
.build();
let mut worker = Worker::new_from_core_options_prepared(
Arc::new(core_worker),
client_options,
worker_options,
)
.map_err(|error| WorkflowReplayWorkerError::Initialization {
message: error.to_string(),
})?;
let worker_interceptors = worker.worker_interceptors();
if let Err(source) = interceptors::call_with_workflow_replay_worker(
&worker_interceptors,
WithWorkflowReplayWorkerInput::new(&mut worker),
Next::new(
|input: WithWorkflowReplayWorkerInput<'_>| -> LocalBoxFuture<'_, Result<(), _>> {
Box::pin(async move { input.worker.run_inner().await })
},
),
)
.await
{
let core_worker = worker.core_worker();
core_worker.initiate_shutdown();
core_worker.shutdown().await;
return Err(WorkflowReplayWorkerError::Run(source).into());
}
let outcomes = std::mem::take(&mut *recorded_outcomes.lock());
for (index, replay_failure) in outcomes.into_iter().enumerate() {
results[index].replay_failure = replay_failure;
}
Ok(results)
}
fn replay_worker_options(&self, observer: ReplayOutcomeInterceptor) -> WorkerOptions {
let worker_interceptors = std::iter::once(Arc::new(observer) as Arc<dyn WorkerInterceptor>)
.chain(self.options.worker_interceptors.iter().cloned())
.collect();
let worker_options = WorkerOptions::new(self.options.task_queue.clone())
.with_workflows(self.options.workflows.clone())
.with_worker_interceptors(worker_interceptors)
.with_workflow_interceptor_constructors(
self.options.workflow_interceptor_constructors.clone(),
)
.with_worker_plugins(self.options.worker_plugins.clone())
.workflow_failure_errors(self.options.workflow_failure_errors.clone())
.workflow_types_to_failure_errors(self.options.workflow_types_to_failure_errors.clone())
.detect_nondeterministic_futures(self.options.detect_nondeterministic_futures)
.maybe_patch_activation_callback(self.options.patch_activation_callback.clone());
#[cfg(feature = "wasm-workflows")]
let worker_options = worker_options
.with_wasm_workflow_components(self.options.wasm_workflow_components.clone());
worker_options.build()
}
}
struct ReplayOutcomeInterceptor {
outcomes: Arc<Mutex<Vec<Option<WorkflowReplayFailure>>>>,
}
#[async_trait::async_trait(?Send)]
impl WorkerInterceptor for ReplayOutcomeInterceptor {
async fn on_workflow_activation(
&self,
activation: &WorkflowActivation,
) -> Result<(), anyhow::Error> {
let Some(remove) = activation.jobs.iter().find_map(|job| match &job.variant {
Some(ActivationVariant::RemoveFromCache(remove)) => Some(remove),
_ => None,
}) else {
return Ok(());
};
let reason = remove.reason();
let failure = match reason {
EvictionReason::CacheFull | EvictionReason::LangRequested => None,
EvictionReason::Nondeterminism => Some(WorkflowReplayFailure::Nondeterminism {
message: remove.message.clone(),
}),
EvictionReason::LangFail => Some(WorkflowReplayFailure::WorkflowTaskFailure {
message: remove.message.clone(),
}),
reason => Some(WorkflowReplayFailure::Internal {
reason: format!("{reason:?}"),
message: remove.message.clone(),
}),
};
self.outcomes.lock().push(failure);
Ok(())
}
}