#[cfg(feature = "experimental")]
mod nexus;
mod options;
mod view;
#[cfg(feature = "experimental")]
pub(crate) use nexus::NexusUnblockData;
#[cfg(feature = "experimental")]
pub use nexus::StartedNexusOperation;
pub use options::{
ActivityCancellationType, ActivityOptions, ChildWorkflowCancellationType, ChildWorkflowOptions,
ContinueAsNewOptions, LocalActivityOptions, ParentClosePolicy, SignalWorkflowOptions,
TimerOptions, VersioningIntent, WaitConditionOptions, WorkflowIdReusePolicy,
};
#[cfg(feature = "experimental")]
pub use options::{
ContinueAsNewVersioningBehavior, NexusOperationCancellationType, NexusOperationOptions,
};
pub use temporalio_common_wasm::error::StartChildWorkflowExecutionFailedCause;
pub use view::{NamespacedWorkflowInfo, WorkflowContextView};
use crate::{
MemoValue, WorkflowCancellationError, WorkflowCancellationToken,
runtime::{
SdkWakeGuard,
entry::WorkflowImplementation,
host::WorkflowHost,
mark_intercepted_future_activation,
model::{
CancelExternalWfResult, CancellableID, SignalExternalWfResult, TimerResult,
UnblockEvent, Unblockable, WorkflowTermination,
},
types::WorkflowInit,
},
workflow_interceptors::{
CancelExternalWorkflowInput, CancelExternalWorkflowResult,
CancellableWorkflowOutboundFuture, ChildWorkflowOutboundResult, ContinueAsNewInput,
ScheduleActivityInput, ScheduleLocalActivityInput, SignalWorkflowInput,
SignalWorkflowResult, SignalWorkflowTarget, StartChildWorkflowInput,
StartChildWorkflowResult, StartTimerInput, WorkflowCancellationHandle, WorkflowInterceptor,
WorkflowInterceptorConstructor, WorkflowInterceptorContext, WorkflowNext,
WorkflowOutboundFuture, WorkflowOutboundValue, call_cancel_external_workflow,
call_continue_as_new, call_schedule_activity, call_schedule_local_activity,
call_signal_workflow, call_start_child_workflow, call_start_timer,
},
};
use futures_channel::oneshot;
use futures_util::{FutureExt, future::FusedFuture, task::Context};
use rand::SeedableRng;
use rand_pcg::Pcg64Mcg;
use siphasher::sip::SipHasher13;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
collections::{HashMap, HashSet},
fmt,
future::{self, Future},
hash::Hasher,
marker::PhantomData,
pin::Pin,
rc::Rc,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
task::{Poll, Waker},
time::SystemTime,
};
use temporalio_common_wasm::{
ActivityDefinition, Memo, SignalDefinition, WorkflowDefinition,
data_converters::{
ActivityExecutionDecodeHint, CancelExternalWorkflowDecodeHint,
ChildWorkflowExecutionDecodeHint, ChildWorkflowStartDecodeHint, DataConverter,
GenericPayloadConverter, PayloadConversionError, PayloadConverter, SerializationContext,
SerializationContextData, TemporalDeserializable, WorkflowSerializationContext,
WorkflowSignalDecodeHint,
},
error::{
ActivityExecutionError, ChildWorkflowExecutionError, ChildWorkflowStartError,
WorkflowSignalError,
},
protos::{
coresdk::{
activity_result::{ActivityResolution, Cancellation, activity_resolution},
child_workflow::{
ChildWorkflowResult,
StartChildWorkflowExecutionFailedCause as ProtoStartChildCause,
child_workflow_result,
},
common::NamespacedWorkflowExecution,
workflow_activation::{
InitializeWorkflow, WorkflowActivation as CoreWorkflowActivation,
resolve_child_workflow_execution_start::Status as ChildWorkflowStartStatus,
workflow_activation_job::Variant as ActivationVariant,
},
workflow_commands::{
CancelChildWorkflowExecution, CancelSignalWorkflow, CancelTimer,
ModifyWorkflowProperties, RequestCancelActivity,
RequestCancelExternalWorkflowExecution, RequestCancelLocalActivity,
RequestCancelNexusOperation, SetPatchMarker, UpsertWorkflowSearchAttributes,
signal_external_workflow_execution, workflow_command,
},
},
temporal::api::{
common::v1::{Memo as ProtoMemo, Payload, SearchAttributes as ProtoSearchAttributes},
failure::v1::{CanceledFailureInfo, Failure, failure::FailureInfo},
},
utilities::TryIntoOrNone,
},
search_attributes::{SearchAttributeUpdate, SearchAttributes},
worker::WorkerDeploymentVersion,
};
use uuid::Builder;
mod private {
use rand::distr::{Distribution, StandardUniform};
use rand_pcg::Pcg64Mcg;
pub trait Sealed: Sized {
fn sample(rng: &mut Pcg64Mcg) -> Self;
}
pub(super) fn sample<T>(rng: &mut Pcg64Mcg) -> T
where
StandardUniform: Distribution<T>,
{
StandardUniform.sample(rng)
}
}
pub trait WorkflowRandomValue: private::Sealed + Sized {}
macro_rules! impl_random_value {
($($ty:ty),* $(,)?) => {
$(
impl private::Sealed for $ty {
fn sample(rng: &mut Pcg64Mcg) -> Self {
private::sample(rng)
}
}
impl WorkflowRandomValue for $ty {}
)*
};
}
impl_random_value!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
#[derive(Clone)]
pub struct WorkflowRandomStream {
source: WorkflowRandomStreamSource,
name: String,
}
#[derive(Clone)]
enum WorkflowRandomStreamSource {
Workflow(Rc<RefCell<WorkflowRandomState>>),
System(Rc<RefCell<Pcg64Mcg>>),
}
impl WorkflowRandomStream {
pub fn random<T>(&self) -> T
where
T: WorkflowRandomValue,
{
match &self.source {
WorkflowRandomStreamSource::Workflow(random) => {
random.borrow_mut().named_random(&self.name)
}
WorkflowRandomStreamSource::System(random) => {
<T as private::Sealed>::sample(&mut random.borrow_mut())
}
}
}
pub fn name(&self) -> &str {
&self.name
}
}
fn system_random_stream_source() -> WorkflowRandomStreamSource {
#[cfg(not(target_arch = "wasm32"))]
let seed = rand::random();
#[cfg(target_arch = "wasm32")]
let seed = {
let mut hasher = std::hash::BuildHasher::build_hasher(&std::hash::RandomState::new());
std::hash::Hasher::write(&mut hasher, b"temporal-rust-system-random-stream");
std::hash::Hasher::finish(&hasher)
};
WorkflowRandomStreamSource::System(Rc::new(RefCell::new(Pcg64Mcg::seed_from_u64(seed))))
}
fn named_random_seed(randomness_seed: u64, name: &str) -> u64 {
let second_key = randomness_seed ^ u64::from_be_bytes(*b"temporal");
let mut hasher = SipHasher13::new_with_keys(randomness_seed, second_key);
hasher.write(b"temporal-rust-workflow-random-stream\0");
hasher.write(name.as_bytes());
hasher.finish()
}
#[derive(Clone, Debug)]
pub(super) struct WorkflowRandomState {
random: Pcg64Mcg,
randomness_seed: u64,
named_random: HashMap<String, Pcg64Mcg>,
}
impl WorkflowRandomState {
fn new(randomness_seed: u64) -> Self {
Self {
random: Pcg64Mcg::seed_from_u64(randomness_seed),
randomness_seed,
named_random: HashMap::new(),
}
}
fn random<T: WorkflowRandomValue>(&mut self) -> T {
<T as private::Sealed>::sample(&mut self.random)
}
fn named_random<T: WorkflowRandomValue>(&mut self, name: &str) -> T {
let random = self.named_random.entry(name.to_owned()).or_insert_with(|| {
Pcg64Mcg::seed_from_u64(named_random_seed(self.randomness_seed, name))
});
<T as private::Sealed>::sample(random)
}
fn reseed(&mut self, randomness_seed: u64) {
self.random = Pcg64Mcg::seed_from_u64(randomness_seed);
self.randomness_seed = randomness_seed;
self.named_random.clear();
}
}
#[derive(Clone)]
pub struct BaseWorkflowContext {
inner: Rc<WorkflowContextInner>,
}
pub trait WorkflowContextKey: 'static {
type Value: 'static;
}
type WorkflowContextValues = Rc<HashMap<TypeId, Rc<dyn Any>>>;
#[derive(Clone, Default)]
pub(super) struct WorkflowContextValueStore {
current: Rc<RefCell<WorkflowContextValues>>,
}
impl fmt::Debug for WorkflowContextValueStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WorkflowContextValueStore")
.finish_non_exhaustive()
}
}
impl WorkflowContextValueStore {
pub(super) fn context_value<K: WorkflowContextKey>(&self) -> Option<Rc<K::Value>> {
self.current
.borrow()
.get(&TypeId::of::<K>())
.cloned()
.and_then(|value| value.downcast().ok())
}
}
#[must_use = "futures do nothing unless polled"]
pub struct WorkflowContextFuture<F> {
base: BaseWorkflowContext,
values: WorkflowContextValues,
inner: Pin<Box<F>>,
}
impl<F: Future> Future for WorkflowContextFuture<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let _guard = this.base.install_context_values(this.values.clone());
this.inner.as_mut().poll(cx)
}
}
struct WorkflowContextRestoreGuard {
base: BaseWorkflowContext,
previous: Option<WorkflowContextValues>,
}
impl Drop for WorkflowContextRestoreGuard {
fn drop(&mut self) {
self.base
.inner
.context_values
.current
.replace(self.previous.take().expect("context is restored once"));
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct PatchActivationInput {
pub workflow_info: WorkflowContextView,
pub patch_id: String,
}
pub type PatchActivationCallback =
Arc<dyn Fn(PatchActivationInput) -> bool + Send + Sync + 'static>;
#[doc(hidden)]
pub struct PatchActivationCaller {
callback: PatchActivationCallback,
workflow_info: WorkflowContextView,
}
impl PatchActivationCaller {
pub fn new(
callback: PatchActivationCallback,
namespace: String,
task_queue: String,
run_id: String,
init: InitializeWorkflow,
payload_converter: PayloadConverter,
) -> Self {
Self {
callback,
workflow_info: WorkflowContextView::new(
namespace,
task_queue,
run_id,
init,
payload_converter,
false,
None,
),
}
}
pub fn call(&self, patch_id: String) -> bool {
(self.callback)(PatchActivationInput {
workflow_info: self.workflow_info.clone(),
patch_id,
})
}
}
pub(crate) struct WorkflowPollWakerGuard<'a> {
current_waker: &'a RefCell<Option<Waker>>,
previous: Option<Waker>,
}
impl Drop for WorkflowPollWakerGuard<'_> {
fn drop(&mut self) {
self.current_waker.replace(self.previous.take());
}
}
fn outbound_type_error(
value: &str,
) -> temporalio_common_wasm::data_converters::PayloadConversionError {
temporalio_common_wasm::data_converters::PayloadConversionError::EncodingError(Box::new(
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("workflow interceptor returned the wrong concrete {value} type"),
),
))
}
impl BaseWorkflowContext {
pub(crate) fn apply_activation_context(
&self,
activation: &CoreWorkflowActivation,
is_replaying_history_events: bool,
) {
let new_seed = {
let mut shared = self.inner.shared.borrow_mut();
shared.activation = activation.clone();
shared.is_replaying_history_events = is_replaying_history_events;
activation.jobs.iter().find_map(|job| match &job.variant {
Some(ActivationVariant::UpdateRandomSeed(attrs)) => Some(attrs.randomness_seed),
_ => None,
})
};
if let Some(seed) = new_seed {
self.inner.random.borrow_mut().reseed(seed);
}
}
fn random<T>(&self) -> T
where
T: WorkflowRandomValue,
{
self.inner.random.borrow_mut().random()
}
pub(crate) fn random_stream(&self, name: impl Into<String>) -> WorkflowRandomStream {
WorkflowRandomStream {
source: WorkflowRandomStreamSource::Workflow(self.inner.random.clone()),
name: name.into(),
}
}
fn uuid4(&self) -> String {
Builder::from_random_bytes(self.random::<u128>().to_be_bytes())
.into_uuid()
.hyphenated()
.to_string()
}
pub fn data_converter(&self) -> &DataConverter {
&self.inner.data_converter
}
pub fn workflow_id(&self) -> &str {
&self.inner.initial_information.workflow_id
}
pub fn run_id(&self) -> &str {
&self.inner.run_id
}
pub fn namespace(&self) -> &str {
&self.inner.namespace
}
pub fn task_queue(&self) -> &str {
&self.inner.task_queue
}
pub fn workflow_type(&self) -> &str {
&self.inner.initial_information.workflow_type
}
pub(crate) fn initial_headers(&self) -> HashMap<String, Payload> {
self.inner.initial_information.headers.clone()
}
pub fn workflow_time(&self) -> Option<SystemTime> {
self.inner
.shared
.borrow()
.activation
.timestamp
.try_into_or_none()
}
pub fn history_length(&self) -> u32 {
self.inner.shared.borrow().activation.history_length
}
pub fn search_attributes(&self) -> SearchAttributes {
SearchAttributes::from_proto(&self.inner.shared.borrow().search_attributes)
}
pub fn is_replaying(&self) -> bool {
self.inner.shared.borrow().activation.is_replaying
}
pub fn is_replaying_history_events(&self) -> bool {
self.inner.shared.borrow().is_replaying_history_events
}
fn requires_replay_safety(&self) -> bool {
self.inner.requires_replay_safety.get()
}
pub(crate) fn enter_read_only(&self) -> ReadOnlyGuard {
let previous = self.inner.requires_replay_safety.replace(false);
ReadOnlyGuard {
base: self.clone(),
previous,
}
}
pub fn payload_converter(&self) -> &PayloadConverter {
self.inner.data_converter.payload_converter()
}
pub(crate) fn construction_waker(&self) -> Waker {
self.inner
.current_waker
.borrow()
.clone()
.unwrap_or_else(|| Waker::noop().clone())
}
pub(crate) fn enter_runtime_poll<'a>(&'a self, waker: &Waker) -> WorkflowPollWakerGuard<'a> {
WorkflowPollWakerGuard {
previous: self.inner.current_waker.replace(Some(waker.clone())),
current_waker: &self.inner.current_waker,
}
}
pub(crate) fn notify_patch(&self, patch_id: String) {
self.inner
.shared
.borrow_mut()
.notified_patches
.insert(patch_id);
}
fn prepare_outbound_future<T>(
&self,
mut future: WorkflowOutboundFuture<T>,
) -> WorkflowOutboundFuture<T> {
let waker = self.construction_waker();
let mut cx = Context::from_waker(&waker);
future.poll_for_construction(&mut cx);
future
}
fn prepare_cancellable_outbound_future<T>(
&self,
mut future: CancellableWorkflowOutboundFuture<T>,
) -> CancellableWorkflowOutboundFuture<T> {
let waker = self.construction_waker();
let mut cx = Context::from_waker(&waker);
future.poll_for_construction(&mut cx);
future
}
pub(crate) fn view(&self) -> WorkflowContextView {
let shared = self.inner.shared.borrow();
let mut initial_information = self.inner.initial_information.clone();
if initial_information.memo.is_some() || !shared.memo.fields.is_empty() {
initial_information.memo = Some(shared.memo.clone());
}
WorkflowContextView::new(
self.inner.namespace.clone(),
self.inner.task_queue.clone(),
self.inner.run_id.clone(),
initial_information,
self.inner.data_converter.payload_converter().clone(),
self.requires_replay_safety(),
Some(self.inner.random.clone()),
)
.with_context_values(self.inner.context_values.clone())
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
enum PendingCommandId {
Timer(u32),
Activity(u32),
ChildWorkflowStart(u32),
ChildWorkflowComplete(u32),
SignalExternal(u32),
CancelExternal(u32),
NexusOpStart(u32),
NexusOpComplete(u32),
}
impl PendingCommandId {
fn from_unblock_event(event: &UnblockEvent) -> Self {
match event {
UnblockEvent::Timer(seq, _) => Self::Timer(*seq),
UnblockEvent::Activity(seq, _) => Self::Activity(*seq),
UnblockEvent::WorkflowStart(seq, _) => Self::ChildWorkflowStart(*seq),
UnblockEvent::WorkflowComplete(seq, _) => Self::ChildWorkflowComplete(*seq),
UnblockEvent::SignalExternal(seq, _) => Self::SignalExternal(*seq),
UnblockEvent::CancelExternal(seq, _) => Self::CancelExternal(*seq),
UnblockEvent::NexusOperationStart(seq, _) => Self::NexusOpStart(*seq),
UnblockEvent::NexusOperationComplete(seq, _) => Self::NexusOpComplete(*seq),
}
}
}
struct WorkflowRuntimeState {
host: Rc<dyn WorkflowHost>,
pending_unblocks: RefCell<HashMap<PendingCommandId, oneshot::Sender<UnblockEvent>>>,
forced_wft_failure: RefCell<Option<Box<dyn std::error::Error + Send + Sync>>>,
progress_made: Cell<bool>,
}
impl WorkflowRuntimeState {
fn new(host: Rc<dyn WorkflowHost>) -> Self {
Self {
host,
pending_unblocks: RefCell::new(HashMap::new()),
forced_wft_failure: RefCell::new(None),
progress_made: Cell::new(false),
}
}
fn register_unblocker(&self, id: PendingCommandId, unblocker: oneshot::Sender<UnblockEvent>) {
self.pending_unblocks.borrow_mut().insert(id, unblocker);
}
fn unblock(&self, event: UnblockEvent) -> Result<(), anyhow::Error> {
let id = PendingCommandId::from_unblock_event(&event);
let unblocker = self
.pending_unblocks
.borrow_mut()
.remove(&id)
.ok_or_else(|| anyhow::anyhow!("Command {id:?} not found to unblock"))?;
self.progress_made.set(true);
let _guard = SdkWakeGuard::new();
let _ = unblocker.send(event);
Ok(())
}
fn maybe_unblock(&self, event: UnblockEvent) -> bool {
let id = PendingCommandId::from_unblock_event(&event);
let Some(unblocker) = self.pending_unblocks.borrow_mut().remove(&id) else {
return false;
};
self.progress_made.set(true);
let _guard = SdkWakeGuard::new();
let _ = unblocker.send(event);
true
}
fn set_forced_wft_failure(&self, err: Box<dyn std::error::Error + Send + Sync>) {
*self.forced_wft_failure.borrow_mut() = Some(err);
self.progress_made.set(true);
}
fn take_forced_wft_failure(&self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
self.forced_wft_failure.borrow_mut().take()
}
fn mark_progress(&self) {
self.progress_made.set(true);
}
fn take_progress(&self) -> bool {
self.progress_made.replace(false)
}
}
struct WorkflowContextInner {
namespace: String,
task_queue: String,
run_id: String,
initial_information: InitializeWorkflow,
runtime: WorkflowRuntimeState,
cancellation_token: WorkflowCancellationToken,
cancelled_operations: RefCell<HashSet<CancellableSeqNum>>,
shared: RefCell<WorkflowContextSharedData>,
random: Rc<RefCell<WorkflowRandomState>>,
seq_nums: RefCell<WfCtxProtectedDat>,
data_converter: DataConverter,
patch_activation_callback: Option<PatchActivationCallback>,
state_mutated: Cell<bool>,
active_handlers: Cell<usize>,
requires_replay_safety: Cell<bool>,
condition_wakers: RefCell<Vec<Waker>>,
current_waker: RefCell<Option<Waker>>,
context_values: WorkflowContextValueStore,
workflow_interceptors: Rc<[Arc<dyn WorkflowInterceptor>]>,
}
pub(crate) struct HandlerExecutionGuard {
base: BaseWorkflowContext,
}
pub(crate) struct ReadOnlyGuard {
base: BaseWorkflowContext,
previous: bool,
}
impl Drop for ReadOnlyGuard {
fn drop(&mut self) {
self.base.inner.requires_replay_safety.set(self.previous);
}
}
impl Drop for HandlerExecutionGuard {
fn drop(&mut self) {
let active_handlers = self.base.inner.active_handlers.get();
debug_assert!(active_handlers > 0, "handler execution count underflow");
self.base
.inner
.active_handlers
.set(active_handlers.saturating_sub(1));
if active_handlers <= 1 {
self.base.wake_condition_waiters();
}
}
}
#[derive(Eq, Hash, PartialEq)]
enum CancellableSeqNum {
Timer(u32),
Activity(u32),
LocalActivity(u32),
ChildWorkflow(u32),
SignalExternalWorkflow(u32),
NexusOp(u32),
}
impl From<&CancellableID> for CancellableSeqNum {
fn from(value: &CancellableID) -> Self {
match value {
CancellableID::Timer(seq) => Self::Timer(*seq),
CancellableID::Activity(seq) => Self::Activity(*seq),
CancellableID::LocalActivity(seq) => Self::LocalActivity(*seq),
CancellableID::ChildWorkflow { seqnum, .. } => Self::ChildWorkflow(*seqnum),
CancellableID::SignalExternalWorkflow(seq) => Self::SignalExternalWorkflow(*seq),
CancellableID::NexusOp(seq) => Self::NexusOp(*seq),
}
}
}
pub struct SyncWorkflowContext<W> {
base: BaseWorkflowContext,
headers: Rc<HashMap<String, Payload>>,
_phantom: PhantomData<W>,
}
impl<W> Clone for SyncWorkflowContext<W> {
fn clone(&self) -> Self {
Self {
base: self.base.clone(),
headers: self.headers.clone(),
_phantom: PhantomData,
}
}
}
pub struct WorkflowContext<W> {
sync: SyncWorkflowContext<W>,
workflow_state: Rc<RefCell<W>>,
}
impl<W> Clone for WorkflowContext<W> {
fn clone(&self) -> Self {
Self {
sync: self.sync.clone(),
workflow_state: self.workflow_state.clone(),
}
}
}
impl BaseWorkflowContext {
#[doc(hidden)]
pub fn from_raw(
init: WorkflowInit,
data_converter: DataConverter,
host: Rc<dyn WorkflowHost>,
patch_activation_callback: Option<PatchActivationCallback>,
workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
) -> Self {
let WorkflowInit {
namespace,
task_queue,
run_id,
initialize_workflow,
} = init;
let random = Rc::new(RefCell::new(WorkflowRandomState::new(
initialize_workflow.randomness_seed,
)));
let context_values = WorkflowContextValueStore::default();
let view = WorkflowContextView::new(
namespace,
task_queue,
run_id,
initialize_workflow,
data_converter.payload_converter().clone(),
true,
Some(random.clone()),
)
.with_context_values(context_values.clone());
let workflow_interceptors = workflow_interceptor_constructors
.into_iter()
.map(|constructor| constructor.construct(&view))
.collect::<Vec<_>>()
.into();
let (namespace, task_queue, run_id, init_workflow_job) = view.into_parts();
Self {
inner: Rc::new(WorkflowContextInner {
namespace,
task_queue,
run_id,
shared: RefCell::new(WorkflowContextSharedData {
memo: init_workflow_job.memo.clone().unwrap_or_default(),
search_attributes: init_workflow_job
.search_attributes
.clone()
.unwrap_or_default(),
is_replaying_history_events: false,
changes: Default::default(),
activation: Default::default(),
current_details: Default::default(),
notified_patches: Default::default(),
}),
random,
initial_information: init_workflow_job,
runtime: WorkflowRuntimeState::new(host),
cancellation_token: WorkflowCancellationToken::new(),
cancelled_operations: Default::default(),
seq_nums: RefCell::new(WfCtxProtectedDat {
next_timer_sequence_number: 1,
next_activity_sequence_number: 1,
next_child_workflow_sequence_number: 1,
next_cancel_external_wf_sequence_number: 1,
next_signal_external_wf_sequence_number: 1,
#[cfg(feature = "experimental")]
next_nexus_op_sequence_number: 1,
}),
data_converter,
patch_activation_callback,
state_mutated: Cell::new(false),
active_handlers: Cell::new(0),
requires_replay_safety: Cell::new(true),
condition_wakers: Default::default(),
current_waker: RefCell::new(None),
context_values,
workflow_interceptors,
}),
}
}
pub(crate) fn context_value<K: WorkflowContextKey>(&self) -> Option<Rc<K::Value>> {
self.inner.context_values.context_value::<K>()
}
fn context_values_with<K: WorkflowContextKey>(&self, value: K::Value) -> WorkflowContextValues {
let mut values = self.inner.context_values.current.borrow().as_ref().clone();
values.insert(TypeId::of::<K>(), Rc::new(value));
Rc::new(values)
}
pub(crate) fn with_context_value<K: WorkflowContextKey, F: Future>(
&self,
value: K::Value,
future: F,
) -> WorkflowContextFuture<F> {
WorkflowContextFuture {
base: self.clone(),
values: self.context_values_with::<K>(value),
inner: Box::pin(future),
}
}
pub(crate) fn with_context_value_sync<K: WorkflowContextKey, R>(
&self,
value: K::Value,
f: impl FnOnce() -> R,
) -> R {
let values = self.context_values_with::<K>(value);
let _guard = self.install_context_values(values);
f()
}
fn install_context_values(&self, values: WorkflowContextValues) -> WorkflowContextRestoreGuard {
let previous = self.inner.context_values.current.replace(values);
WorkflowContextRestoreGuard {
base: self.clone(),
previous: Some(previous),
}
}
pub(crate) fn workflow_interceptors(&self) -> Rc<[Arc<dyn WorkflowInterceptor>]> {
self.inner.workflow_interceptors.clone()
}
pub(crate) fn take_state_mutated(&self) -> bool {
self.inner.state_mutated.replace(false)
}
pub(crate) fn set_state_mutated(&self) {
self.inner.state_mutated.set(true);
}
pub(crate) fn all_handlers_finished(&self) -> bool {
self.inner.active_handlers.get() == 0
}
pub(crate) fn track_handler(&self) -> HandlerExecutionGuard {
self.inner
.active_handlers
.set(self.inner.active_handlers.get() + 1);
HandlerExecutionGuard { base: self.clone() }
}
fn wake_condition_waiters(&self) {
let _guard = SdkWakeGuard::new();
for waker in self.inner.condition_wakers.borrow_mut().drain(..) {
waker.wake();
}
}
pub(crate) fn take_runtime_progress(&self) -> bool {
self.inner.runtime.take_progress()
}
pub(crate) fn take_forced_wft_failure(
&self,
) -> Option<Box<dyn std::error::Error + Send + Sync>> {
self.inner.runtime.take_forced_wft_failure()
}
pub(crate) fn notify_cancel(&self, reason: String) {
if reason.is_empty() {
self.inner.cancellation_token.cancel();
} else {
self.inner.cancellation_token.cancel_with_reason(reason);
}
self.inner.runtime.mark_progress();
}
pub fn cancellation_token(&self) -> WorkflowCancellationToken {
self.inner.cancellation_token.clone()
}
pub(crate) fn unblock(&self, event: UnblockEvent) -> Result<(), anyhow::Error> {
self.inner.runtime.unblock(event)
}
fn cancel(&self, cancellable_id: CancellableID) {
if !self
.inner
.cancelled_operations
.borrow_mut()
.insert((&cancellable_id).into())
{
return;
}
match cancellable_id {
CancellableID::Timer(seq) => {
if self
.inner
.runtime
.maybe_unblock(UnblockEvent::Timer(seq, TimerResult::Cancelled))
{
self.inner.runtime.host.push_command(
workflow_command::Variant::CancelTimer(CancelTimer { seq }).into(),
);
}
}
CancellableID::Activity(seq) => {
self.inner.runtime.host.push_command(
workflow_command::Variant::RequestCancelActivity(RequestCancelActivity { seq })
.into(),
);
}
CancellableID::LocalActivity(seq) => {
self.inner.runtime.host.push_command(
workflow_command::Variant::RequestCancelLocalActivity(
RequestCancelLocalActivity { seq },
)
.into(),
);
}
CancellableID::ChildWorkflow { seqnum, reason } => {
self.inner.runtime.host.push_command(
workflow_command::Variant::CancelChildWorkflowExecution(
CancelChildWorkflowExecution {
child_workflow_seq: seqnum,
reason,
},
)
.into(),
);
}
CancellableID::SignalExternalWorkflow(seq) => {
self.inner.runtime.host.push_command(
workflow_command::Variant::CancelSignalWorkflow(CancelSignalWorkflow { seq })
.into(),
);
}
CancellableID::NexusOp(seq) => {
self.inner.runtime.host.push_command(
workflow_command::Variant::RequestCancelNexusOperation(
RequestCancelNexusOperation { seq },
)
.into(),
);
}
}
}
fn cancellation_handle(&self, cancellable_id: CancellableID) -> WorkflowCancellationHandle {
let base_ctx = self.clone();
WorkflowCancellationHandle::new(move |reason| {
let id = reason.map_or_else(
|| cancellable_id.clone(),
|reason| cancellable_id.clone().with_reason(reason),
);
base_ctx.cancel(id);
})
}
pub fn current_details(&self) -> String {
self.inner.shared.borrow().current_details.clone()
}
pub fn timer<T: Into<TimerOptions>>(
&self,
opts: T,
) -> impl CancellableFuture<Output = TimerResult> + use<T> {
let input = StartTimerInput::new(opts.into());
let base_ctx = self.clone();
let next = WorkflowNext::new(move |input: StartTimerInput| {
let mut opts = input.into_options();
let cancellation_token = opts
.cancellation_token
.take()
.unwrap_or_else(|| base_ctx.cancellation_token());
let seq = base_ctx.inner.seq_nums.borrow_mut().next_timer_seq();
let (cmd, unblocker) =
CancellableWFCommandFut::new(CancellableID::Timer(seq), base_ctx.clone());
base_ctx
.inner
.runtime
.register_unblocker(PendingCommandId::Timer(seq), unblocker);
base_ctx
.inner
.runtime
.host
.push_command(opts.into_command(seq));
CancellableWorkflowOutboundFuture::new(
cmd,
base_ctx.cancellation_handle(CancellableID::Timer(seq)),
)
.with_cancellation_token(cancellation_token)
});
let interceptors = self.inner.workflow_interceptors.clone();
let future = call_start_timer(
interceptors,
WorkflowInterceptorContext::new(self.clone()),
input,
next,
);
self.prepare_cancellable_outbound_future(future)
}
#[allow(clippy::result_large_err)]
pub fn execute_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: ActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
let input =
ScheduleActivityInput::new(activity.name().to_string(), Box::new(input.into()), opts);
let base_ctx = self.clone();
let next = WorkflowNext::new(move |input: ScheduleActivityInput| {
let (activity_type, input, headers, mut opts) = input.into_parts();
let input = match input.downcast::<AD::Input>() {
Ok(input) => *input,
Err(_) => {
return CancellableWorkflowOutboundFuture::new(
async {
Err(ActivityExecutionError::Serialization(outbound_type_error(
"activity input",
)))
},
WorkflowCancellationHandle::noop(),
);
}
};
let payload_converter = base_ctx.inner.data_converter.payload_converter();
let context_data =
SerializationContextData::Workflow(WorkflowSerializationContext::new());
let ctx = SerializationContext::new(&context_data, payload_converter);
match payload_converter.to_payloads(&ctx, &input) {
Ok(payloads) => {
let cancellation_token = opts
.cancellation_token
.take()
.unwrap_or_else(|| base_ctx.cancellation_token());
let seq = base_ctx.inner.seq_nums.borrow_mut().next_activity_seq();
let (cmd, unblocker) = CancellableWFCommandFut::new(
CancellableID::Activity(seq),
base_ctx.clone(),
);
base_ctx
.inner
.runtime
.register_unblocker(PendingCommandId::Activity(seq), unblocker);
if opts.task_queue.is_none() {
opts.task_queue = Some(base_ctx.inner.task_queue.clone());
}
base_ctx.inner.runtime.host.push_command(opts.into_command(
seq,
activity_type,
payloads,
headers,
));
CancellableWorkflowOutboundFuture::new(
ActivityFut::running(cmd, base_ctx.inner.data_converter.clone()),
base_ctx.cancellation_handle(CancellableID::Activity(seq)),
)
.with_cancellation_token(cancellation_token)
}
Err(err) => CancellableWorkflowOutboundFuture::new(
ActivityFut::<future::Ready<ActivityResolution>, AD::Output>::eager(err.into()),
WorkflowCancellationHandle::noop(),
),
}
.map(|result| result.map(|output| Box::new(output) as Box<dyn WorkflowOutboundValue>))
});
let interceptors = self.inner.workflow_interceptors.clone();
let future = call_schedule_activity(
interceptors,
WorkflowInterceptorContext::new(self.clone()),
input,
next,
)
.map(|result| {
result.and_then(|output| {
output
.downcast::<AD::Output>()
.map(|output| *output)
.map_err(|_| {
ActivityExecutionError::Serialization(outbound_type_error(
"activity output",
))
})
})
});
self.prepare_cancellable_outbound_future(future)
}
#[allow(clippy::result_large_err)]
pub fn execute_local_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: LocalActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
let input = ScheduleLocalActivityInput::new(
activity.name().to_string(),
Box::new(input.into()),
opts,
);
let base_ctx = self.clone();
let next = WorkflowNext::new(move |input: ScheduleLocalActivityInput| {
let (activity_type, input, headers, mut opts) = input.into_parts();
let input = match input.downcast::<AD::Input>() {
Ok(input) => *input,
Err(_) => {
return CancellableWorkflowOutboundFuture::new(
async {
Err(ActivityExecutionError::Serialization(outbound_type_error(
"local activity input",
)))
},
WorkflowCancellationHandle::noop(),
);
}
};
let payload_converter = base_ctx.inner.data_converter.payload_converter();
let context_data =
SerializationContextData::Workflow(WorkflowSerializationContext::new());
let ctx = SerializationContext::new(&context_data, payload_converter);
match payload_converter.to_payloads(&ctx, &input) {
Ok(payloads) => {
let cancellation_token = opts
.cancellation_token
.take()
.unwrap_or_else(|| base_ctx.cancellation_token());
let future = LATimerBackoffFut::new(
activity_type,
payloads,
headers,
opts,
cancellation_token.clone(),
base_ctx.clone(),
);
cancellable_outbound(ActivityFut::running(
future,
base_ctx.inner.data_converter.clone(),
))
.with_cancellation_token(cancellation_token)
}
Err(err) => CancellableWorkflowOutboundFuture::new(
ActivityFut::<future::Ready<ActivityResolution>, AD::Output>::eager(err.into()),
WorkflowCancellationHandle::noop(),
),
}
.map(|result| result.map(|output| Box::new(output) as Box<dyn WorkflowOutboundValue>))
});
let interceptors = self.inner.workflow_interceptors.clone();
let future = call_schedule_local_activity(
interceptors,
WorkflowInterceptorContext::new(self.clone()),
input,
next,
)
.map(|result| {
result.and_then(|output| {
output
.downcast::<AD::Output>()
.map(|output| *output)
.map_err(|_| {
ActivityExecutionError::Serialization(outbound_type_error(
"local activity output",
))
})
})
});
self.prepare_cancellable_outbound_future(future)
}
pub(crate) fn start_child_workflow<WD: WorkflowDefinition + 'static>(
&self,
workflow: WD,
input: impl Into<WD::Input>,
opts: ChildWorkflowOptions,
) -> impl CancellableFutureWithReason<
Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
>
where
WD::Output: TemporalDeserializable,
{
let input =
StartChildWorkflowInput::new(workflow.name().to_string(), Box::new(input.into()), opts);
let base_ctx = self.clone();
let next = WorkflowNext::new(move |input: StartChildWorkflowInput| {
let (workflow_type, input, headers, mut opts) = input.into_parts();
let input = match input.downcast::<WD::Input>() {
Ok(input) => *input,
Err(_) => {
return CancellableWorkflowOutboundFuture::new(
async {
Err(ChildWorkflowStartError::Serialization(outbound_type_error(
"child workflow input",
)))
},
WorkflowCancellationHandle::noop(),
);
}
};
let payload_converter = base_ctx.inner.data_converter.payload_converter();
let context_data =
SerializationContextData::Workflow(WorkflowSerializationContext::new());
let ctx = SerializationContext::new(&context_data, payload_converter);
let payloads = match payload_converter.to_payloads(&ctx, &input) {
Ok(payloads) => payloads,
Err(err) => {
return CancellableWorkflowOutboundFuture::new(
ChildWorkflowStartFut::<future::Ready<PendingChildWorkflow<WD>>, WD>::eager(
err.into(),
),
WorkflowCancellationHandle::noop(),
);
}
};
let workflow_id = opts
.workflow_id
.take()
.filter(|id| !id.is_empty())
.unwrap_or_else(|| base_ctx.uuid4());
let cancellation_token = opts
.cancellation_token
.take()
.unwrap_or_else(|| base_ctx.cancellation_token());
let child_seq = base_ctx
.inner
.seq_nums
.borrow_mut()
.next_child_workflow_seq();
let (result_cmd, unblocker) = CancellableWFCommandFut::new(
CancellableID::ChildWorkflow {
seqnum: child_seq,
reason: String::new(),
},
base_ctx.clone(),
);
base_ctx.inner.runtime.register_unblocker(
PendingCommandId::ChildWorkflowComplete(child_seq),
unblocker,
);
base_ctx.inner.runtime.host.push_command(opts.into_command(
child_seq,
workflow_type,
payloads,
headers,
workflow_id.clone(),
));
let result_future =
cancellable_outbound_with_reason(ChildWorkflowFut::<_, WD::Output>::Running {
inner: result_cmd,
data_converter: base_ctx.inner.data_converter.clone(),
_phantom: PhantomData,
})
.map(|result| {
result.map(|output| Box::new(output) as Box<dyn WorkflowOutboundValue>)
})
.with_cancellation_token(cancellation_token);
let common = ChildWfCommon {
workflow_id: workflow_id.clone(),
child_seq,
result_future,
base_ctx: base_ctx.clone(),
};
let (cmd, unblocker) =
CancellableWFCommandFut::<PendingChildWorkflow<WD>, ChildWfCommon>::new_with_dat(
CancellableID::ChildWorkflow {
seqnum: child_seq,
reason: String::new(),
},
common,
base_ctx.clone(),
);
base_ctx
.inner
.runtime
.register_unblocker(PendingCommandId::ChildWorkflowStart(child_seq), unblocker);
cancellable_outbound_with_reason(ChildWorkflowStartFut::Running(cmd))
});
let interceptors = self.inner.workflow_interceptors.clone();
let future = call_start_child_workflow(
interceptors,
WorkflowInterceptorContext::new(self.clone()),
input,
next,
)
.map(|result| result.map(StartChildWorkflowOutput::into_started));
self.prepare_cancellable_outbound_future(future)
}
fn local_activity_no_timer_retry(
self,
activity_type: String,
arguments: Vec<Payload>,
headers: HashMap<String, Payload>,
opts: LocalActivityOptions,
) -> impl CancellableFuture<Output = ActivityResolution> {
let seq = self.inner.seq_nums.borrow_mut().next_activity_seq();
let (cmd, unblocker) =
CancellableWFCommandFut::new(CancellableID::LocalActivity(seq), self.clone());
self.inner
.runtime
.register_unblocker(PendingCommandId::Activity(seq), unblocker);
self.inner.runtime.host.push_command(opts.into_command(
seq,
activity_type,
arguments,
headers,
));
cmd
}
fn signal_workflow<S: SignalDefinition + 'static>(
&self,
target: SignalWorkflowTarget,
signal: S,
input: S::Input,
options: SignalWorkflowOptions,
) -> CancellableWorkflowOutboundFuture<SignalWorkflowResult> {
let input = SignalWorkflowInput::new(
S::name(&signal).to_string(),
target,
Box::new(input),
options,
);
let base_ctx = self.clone();
let next = WorkflowNext::new(move |input: SignalWorkflowInput| {
let (signal_name, target, input, headers, mut options) = input.into_parts();
let cancellation_token = options
.cancellation_token
.take()
.unwrap_or_else(|| base_ctx.cancellation_token());
let input = match input.downcast::<S::Input>() {
Ok(input) => *input,
Err(_) => {
return CancellableWorkflowOutboundFuture::new(
async {
Err(WorkflowSignalError::Serialization(outbound_type_error(
"signal input",
)))
},
WorkflowCancellationHandle::noop(),
);
}
};
let payload_converter = base_ctx.data_converter().payload_converter();
let context_data =
SerializationContextData::Workflow(WorkflowSerializationContext::new());
let ctx = SerializationContext::new(&context_data, payload_converter);
let payloads = match payload_converter.to_payloads(&ctx, &input) {
Ok(payloads) => payloads,
Err(err) => {
return CancellableWorkflowOutboundFuture::new(
async move { Err(err.into()) },
WorkflowCancellationHandle::noop(),
);
}
};
let target = match target {
SignalWorkflowTarget::Child { workflow_id } => {
signal_external_workflow_execution::Target::ChildWorkflowId(workflow_id)
}
SignalWorkflowTarget::External {
namespace,
workflow_id,
run_id,
} => signal_external_workflow_execution::Target::WorkflowExecution(
NamespacedWorkflowExecution {
namespace,
workflow_id,
run_id: run_id.unwrap_or_default(),
},
),
};
let seq = base_ctx
.inner
.seq_nums
.borrow_mut()
.next_signal_external_wf_seq();
let (cmd, unblocker) = CancellableWFCommandFut::new(
CancellableID::SignalExternalWorkflow(seq),
base_ctx.clone(),
);
base_ctx
.inner
.runtime
.register_unblocker(PendingCommandId::SignalExternal(seq), unblocker);
base_ctx
.inner
.runtime
.host
.push_command(options.into_command(seq, signal_name, payloads, headers, target));
cancellable_outbound(SignalChildFut::Running {
inner: cmd,
data_converter: base_ctx.data_converter().clone(),
})
.with_cancellation_token(cancellation_token)
});
let interceptors = self.inner.workflow_interceptors.clone();
let future = call_signal_workflow(
interceptors,
WorkflowInterceptorContext::new(self.clone()),
input,
next,
);
self.prepare_cancellable_outbound_future(future)
}
pub(crate) fn external_workflow(
&self,
workflow_id: impl Into<String>,
run_id: Option<String>,
) -> ExternalWorkflowHandle {
ExternalWorkflowHandle {
workflow_id: workflow_id.into(),
run_id,
namespace: self.inner.namespace.clone(),
base_ctx: self.clone(),
}
}
fn cancel_external_workflow(
&self,
input: CancelExternalWorkflowInput,
) -> WorkflowOutboundFuture<CancelExternalWorkflowResult> {
let base_ctx = self.clone();
let next = WorkflowNext::new(move |input: CancelExternalWorkflowInput| {
let seq = base_ctx
.inner
.seq_nums
.borrow_mut()
.next_cancel_external_wf_seq();
let (cmd, unblocker) = WFCommandFut::<CancelExternalWfResult, ()>::new();
base_ctx
.inner
.runtime
.register_unblocker(PendingCommandId::CancelExternal(seq), unblocker);
base_ctx.inner.runtime.host.push_command(
workflow_command::Variant::RequestCancelExternalWorkflowExecution(
RequestCancelExternalWorkflowExecution {
seq,
workflow_execution: Some(NamespacedWorkflowExecution {
namespace: base_ctx.inner.namespace.clone(),
workflow_id: input.workflow_id,
run_id: input.run_id.unwrap_or_default(),
}),
reason: input.reason.unwrap_or_default(),
},
)
.into(),
);
let data_converter = base_ctx.data_converter().clone();
WorkflowOutboundFuture::new(async move {
match cmd.await {
Ok(_) => Ok(()),
Err(error) => {
let context =
SerializationContextData::Workflow(WorkflowSerializationContext::new());
Err(data_converter.to_error(
&context,
error.failure,
CancelExternalWorkflowDecodeHint::new(error.cause),
)?)
}
}
})
});
let interceptors = self.inner.workflow_interceptors.clone();
let future = call_cancel_external_workflow(
interceptors,
WorkflowInterceptorContext::new(self.clone()),
input,
next,
);
self.prepare_outbound_future(future)
}
}
impl<W> SyncWorkflowContext<W> {
pub fn context_value<K: WorkflowContextKey>(&self) -> Option<Rc<K::Value>> {
self.base.context_value::<K>()
}
pub fn with_context_value_sync<K: WorkflowContextKey, R>(
&self,
value: K::Value,
f: impl FnOnce() -> R,
) -> R {
self.base.with_context_value_sync::<K, R>(value, f)
}
pub fn workflow_id(&self) -> &str {
&self.base.inner.initial_information.workflow_id
}
pub fn run_id(&self) -> &str {
&self.base.inner.run_id
}
pub fn namespace(&self) -> &str {
&self.base.inner.namespace
}
pub fn task_queue(&self) -> &str {
&self.base.inner.task_queue
}
pub fn workflow_time(&self) -> Option<SystemTime> {
self.base
.inner
.shared
.borrow()
.activation
.timestamp
.try_into_or_none()
}
pub fn history_length(&self) -> u32 {
self.base.inner.shared.borrow().activation.history_length
}
pub fn current_deployment_version(&self) -> Option<WorkerDeploymentVersion> {
self.base
.inner
.shared
.borrow()
.activation
.clone()
.deployment_version_for_current_task
.map(Into::into)
}
pub fn search_attributes(&self) -> SearchAttributes {
SearchAttributes::from_proto(&self.base.inner.shared.borrow().search_attributes)
}
pub fn memo(&self) -> Memo {
Memo::from_raw(
Some(self.base.inner.shared.borrow().memo.clone()),
self.payload_converter().clone(),
SerializationContextData::Workflow(WorkflowSerializationContext::new()),
)
}
pub fn random<T>(&self) -> T
where
T: WorkflowRandomValue,
{
self.base.random()
}
pub fn uuid4(&self) -> String {
self.base.uuid4()
}
pub fn random_stream(&self, name: impl Into<String>) -> WorkflowRandomStream {
self.base.random_stream(name)
}
pub fn is_replaying(&self) -> bool {
self.base.inner.shared.borrow().activation.is_replaying
}
pub fn is_replaying_history_events(&self) -> bool {
self.base.inner.shared.borrow().is_replaying_history_events
}
pub fn all_handlers_finished(&self) -> bool {
self.base.all_handlers_finished()
}
pub fn continue_as_new_suggested(&self) -> bool {
self.base
.inner
.shared
.borrow()
.activation
.continue_as_new_suggested
}
#[cfg(feature = "experimental")]
pub fn target_worker_deployment_version_changed(&self) -> bool {
self.base
.inner
.shared
.borrow()
.activation
.target_worker_deployment_version_changed
}
pub fn headers(&self) -> &HashMap<String, Payload> {
&self.headers
}
pub fn payload_converter(&self) -> &PayloadConverter {
self.base.inner.data_converter.payload_converter()
}
pub fn info(&self) -> WorkflowContextView {
self.view()
}
pub fn cancellation_token(&self) -> WorkflowCancellationToken {
self.base.cancellation_token()
}
pub fn cancelled(&self) -> impl FusedFuture<Output = Option<String>> + '_ {
let token = self.cancellation_token();
async move {
token.cancelled().await;
token.reason()
}
.fuse()
}
pub fn continue_as_new(
&self,
input: <W::Run as WorkflowDefinition>::Input,
opts: ContinueAsNewOptions,
) -> Result<std::convert::Infallible, WorkflowTermination>
where
W: WorkflowImplementation,
{
let input = ContinueAsNewInput::new(Box::new(input), opts);
let base_ctx = self.base.clone();
let workflow_type = base_ctx.workflow_type().to_string();
let next = WorkflowNext::new(move |input: ContinueAsNewInput| {
let (input, headers, opts) = input.into_parts();
let input = match input.downcast::<<W::Run as WorkflowDefinition>::Input>() {
Ok(input) => input,
Err(_) => return Err(outbound_type_error("continue-as-new input").into()),
};
let pc = base_ctx.data_converter().payload_converter();
let context_data =
SerializationContextData::Workflow(WorkflowSerializationContext::new());
let ctx = SerializationContext::new(&context_data, pc);
let arguments = pc
.to_payloads(&ctx, &*input)
.map_err(WorkflowTermination::from)?;
let request = opts.into_request(workflow_type, arguments, headers, pc)?;
Err(WorkflowTermination::continue_as_new(request))
});
let interceptors = self.base.inner.workflow_interceptors.clone();
call_continue_as_new(
interceptors,
crate::workflow_interceptors::SyncWorkflowInterceptorContext::new(self.base.clone()),
input,
next,
)
}
pub fn timer<T: Into<TimerOptions>>(
&self,
opts: T,
) -> impl CancellableFuture<Output = TimerResult> {
self.base.timer(opts)
}
pub fn execute_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: ActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
self.base.execute_activity(activity, input, opts)
}
#[deprecated(note = "use `execute_activity` instead")]
pub fn start_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: ActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
self.execute_activity(activity, input, opts)
}
pub fn execute_local_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: LocalActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
self.base.execute_local_activity(activity, input, opts)
}
#[deprecated(note = "use `execute_local_activity` instead")]
pub fn start_local_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: LocalActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
self.execute_local_activity(activity, input, opts)
}
pub fn start_child_workflow<WD: WorkflowDefinition + 'static>(
&self,
workflow: WD,
input: impl Into<WD::Input>,
opts: ChildWorkflowOptions,
) -> impl CancellableFutureWithReason<
Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
>
where
WD::Output: TemporalDeserializable,
{
self.base.start_child_workflow(workflow, input, opts)
}
#[deprecated(note = "use `start_child_workflow` instead")]
pub fn child_workflow<WD: WorkflowDefinition + 'static>(
&self,
workflow: WD,
input: impl Into<WD::Input>,
opts: ChildWorkflowOptions,
) -> impl CancellableFutureWithReason<
Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
>
where
WD::Output: TemporalDeserializable,
{
self.start_child_workflow(workflow, input, opts)
}
pub fn patched(&self, patch_id: &str) -> bool {
self.patch_impl(patch_id, false)
}
pub fn deprecate_patch(&self, patch_id: &str) -> bool {
self.patch_impl(patch_id, true)
}
fn patch_impl(&self, patch_id: &str, deprecated: bool) -> bool {
if let Some(present) = self.base.inner.shared.borrow().changes.get(patch_id) {
return *present;
}
let shared = self.base.inner.shared.borrow();
let replaying = shared.activation.is_replaying;
let notified = shared.notified_patches.contains(patch_id);
drop(shared);
let res = if deprecated || replaying || notified {
!replaying || notified
} else if let Some(callback) = &self.base.inner.patch_activation_callback {
let _read_only = self.base.enter_read_only();
callback(PatchActivationInput {
workflow_info: self.base.view(),
patch_id: patch_id.to_string(),
})
} else {
true
};
if res {
self.base.inner.runtime.host.push_command(
workflow_command::Variant::SetPatchMarker(SetPatchMarker {
patch_id: patch_id.to_string(),
deprecated,
})
.into(),
);
}
self.base
.inner
.shared
.borrow_mut()
.changes
.insert(patch_id.to_string(), res);
res
}
pub fn external_workflow(
&self,
workflow_id: impl Into<String>,
run_id: Option<String>,
) -> ExternalWorkflowHandle {
self.base.external_workflow(workflow_id, run_id)
}
pub fn upsert_search_attributes(
&self,
updates: impl IntoIterator<Item = SearchAttributeUpdate>,
) {
let updates: Vec<SearchAttributeUpdate> = updates.into_iter().collect();
{
let mut shared = self.base.inner.shared.borrow_mut();
let mut attrs = SearchAttributes::from_proto(&shared.search_attributes);
for update in updates.iter().cloned() {
attrs.apply(update);
}
shared.search_attributes = attrs.into_proto();
}
let proto = SearchAttributes::updates_to_proto(updates);
self.base.inner.runtime.host.push_command(
workflow_command::Variant::UpsertWorkflowSearchAttributes(
UpsertWorkflowSearchAttributes {
search_attributes: Some(proto),
},
)
.into(),
);
}
pub fn upsert_memo<K>(
&self,
updates: impl IntoIterator<Item = (K, Option<MemoValue>)>,
) -> Result<(), PayloadConversionError>
where
K: Into<String>,
{
let payload_converter = self.payload_converter();
let context_data = SerializationContextData::Workflow(WorkflowSerializationContext::new());
let context = SerializationContext::new(&context_data, payload_converter);
let mut fields = HashMap::new();
let mut local_updates = Vec::new();
for (key, value) in updates {
let key = key.into();
let (command_payload, local_payload) = match value {
Some(value) => {
let payload = payload_converter.to_payload(&context, &value)?;
(payload.clone(), Some(payload))
}
None => (
payload_converter.to_payload(&context, &MemoValue::new(()))?,
None,
),
};
fields.insert(key.clone(), command_payload);
local_updates.push((key, local_payload));
}
{
let mut shared = self.base.inner.shared.borrow_mut();
for (key, payload) in local_updates {
match payload {
Some(payload) => {
shared.memo.fields.insert(key, payload);
}
None => {
shared.memo.fields.remove(&key);
}
}
}
}
self.base.inner.runtime.host.push_command(
workflow_command::Variant::ModifyWorkflowProperties(ModifyWorkflowProperties {
upserted_memo: Some(ProtoMemo { fields }),
})
.into(),
);
Ok(())
}
pub fn set_current_details(&self, details: impl Into<String>) {
let details = details.into();
self.base.inner.shared.borrow_mut().current_details = details.clone();
self.base.inner.runtime.host.set_current_details(details);
}
pub fn force_task_fail(&self, with: impl Into<Box<dyn std::error::Error + Send + Sync>>) {
self.base.inner.runtime.set_forced_wft_failure(with.into());
}
pub(crate) fn view(&self) -> WorkflowContextView {
self.base.view()
}
}
impl<W> WorkflowContext<W> {
pub(crate) fn from_base(base: BaseWorkflowContext, workflow_state: Rc<RefCell<W>>) -> Self {
Self {
sync: SyncWorkflowContext {
base,
headers: Rc::new(HashMap::new()),
_phantom: PhantomData,
},
workflow_state,
}
}
pub(crate) fn with_headers(&self, headers: HashMap<String, Payload>) -> Self {
Self {
sync: SyncWorkflowContext {
base: self.sync.base.clone(),
headers: Rc::new(headers),
_phantom: PhantomData,
},
workflow_state: self.workflow_state.clone(),
}
}
pub(crate) fn sync_context(&self) -> SyncWorkflowContext<W> {
self.sync.clone()
}
pub(crate) fn view(&self) -> WorkflowContextView {
self.sync.view()
}
pub fn context_value<K: WorkflowContextKey>(&self) -> Option<Rc<K::Value>> {
self.sync.context_value::<K>()
}
pub fn with_context_value<K: WorkflowContextKey, F: Future>(
&self,
value: K::Value,
future: F,
) -> WorkflowContextFuture<F> {
self.sync.base.with_context_value::<K, F>(value, future)
}
pub fn with_context_value_sync<K: WorkflowContextKey, R>(
&self,
value: K::Value,
f: impl FnOnce() -> R,
) -> R {
self.sync.with_context_value_sync::<K, R>(value, f)
}
pub fn workflow_id(&self) -> &str {
self.sync.workflow_id()
}
pub fn run_id(&self) -> &str {
self.sync.run_id()
}
pub fn namespace(&self) -> &str {
self.sync.namespace()
}
pub fn task_queue(&self) -> &str {
self.sync.task_queue()
}
pub fn workflow_time(&self) -> Option<SystemTime> {
self.sync.workflow_time()
}
pub fn history_length(&self) -> u32 {
self.sync.history_length()
}
pub fn current_deployment_version(&self) -> Option<WorkerDeploymentVersion> {
self.sync.current_deployment_version()
}
pub fn search_attributes(&self) -> SearchAttributes {
self.sync.search_attributes()
}
pub fn memo(&self) -> Memo {
self.sync.memo()
}
pub fn random<T>(&self) -> T
where
T: WorkflowRandomValue,
{
self.sync.random()
}
pub fn uuid4(&self) -> String {
self.sync.uuid4()
}
pub fn random_stream(&self, name: impl Into<String>) -> WorkflowRandomStream {
self.sync.random_stream(name)
}
pub fn is_replaying(&self) -> bool {
self.sync.is_replaying()
}
pub fn is_replaying_history_events(&self) -> bool {
self.sync.is_replaying_history_events()
}
pub fn all_handlers_finished(&self) -> bool {
self.sync.all_handlers_finished()
}
pub fn continue_as_new_suggested(&self) -> bool {
self.sync.continue_as_new_suggested()
}
#[cfg(feature = "experimental")]
pub fn target_worker_deployment_version_changed(&self) -> bool {
self.sync.target_worker_deployment_version_changed()
}
pub fn headers(&self) -> &HashMap<String, Payload> {
self.sync.headers()
}
pub fn payload_converter(&self) -> &PayloadConverter {
self.sync.payload_converter()
}
pub fn info(&self) -> WorkflowContextView {
self.sync.info()
}
pub fn cancellation_token(&self) -> WorkflowCancellationToken {
self.sync.cancellation_token()
}
pub fn cancelled(&self) -> impl FusedFuture<Output = Option<String>> + '_ {
self.sync.cancelled()
}
pub fn timer<T: Into<TimerOptions>>(
&self,
opts: T,
) -> impl CancellableFuture<Output = TimerResult> {
self.sync.timer(opts)
}
pub fn execute_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: ActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
self.sync.execute_activity(activity, input, opts)
}
#[deprecated(note = "use `execute_activity` instead")]
pub fn start_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: ActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
self.execute_activity(activity, input, opts)
}
pub fn execute_local_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: LocalActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
self.sync.execute_local_activity(activity, input, opts)
}
#[deprecated(note = "use `execute_local_activity` instead")]
pub fn start_local_activity<AD: ActivityDefinition>(
&self,
activity: AD,
input: impl Into<AD::Input>,
opts: LocalActivityOptions,
) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
where
AD::Output: TemporalDeserializable,
{
self.execute_local_activity(activity, input, opts)
}
pub fn start_child_workflow<WD: WorkflowDefinition + 'static>(
&self,
workflow: WD,
input: impl Into<WD::Input>,
opts: ChildWorkflowOptions,
) -> impl CancellableFutureWithReason<
Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
>
where
WD::Output: TemporalDeserializable,
{
self.sync.start_child_workflow(workflow, input, opts)
}
#[deprecated(note = "use `start_child_workflow` instead")]
pub fn child_workflow<WD: WorkflowDefinition + 'static>(
&self,
workflow: WD,
input: impl Into<WD::Input>,
opts: ChildWorkflowOptions,
) -> impl CancellableFutureWithReason<
Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
>
where
WD::Output: TemporalDeserializable,
{
self.start_child_workflow(workflow, input, opts)
}
pub fn patched(&self, patch_id: &str) -> bool {
self.sync.patched(patch_id)
}
pub fn deprecate_patch(&self, patch_id: &str) -> bool {
self.sync.deprecate_patch(patch_id)
}
pub fn external_workflow(
&self,
workflow_id: impl Into<String>,
run_id: Option<String>,
) -> ExternalWorkflowHandle {
self.sync.external_workflow(workflow_id, run_id)
}
pub fn upsert_search_attributes(
&self,
updates: impl IntoIterator<Item = SearchAttributeUpdate>,
) {
self.sync.upsert_search_attributes(updates)
}
pub fn upsert_memo<K>(
&self,
updates: impl IntoIterator<Item = (K, Option<MemoValue>)>,
) -> Result<(), PayloadConversionError>
where
K: Into<String>,
{
self.sync.upsert_memo(updates)
}
pub fn set_current_details(&self, details: impl Into<String>) {
self.sync.set_current_details(details)
}
pub fn force_task_fail(&self, with: impl Into<Box<dyn std::error::Error + Send + Sync>>) {
self.sync.force_task_fail(with)
}
pub fn state<R>(&self, f: impl FnOnce(&W) -> R) -> R {
f(&*self.workflow_state.borrow())
}
pub fn state_mut<R>(&self, f: impl FnOnce(&mut W) -> R) -> R {
let result = f(&mut *self.workflow_state.borrow_mut());
self.sync.base.wake_condition_waiters();
self.sync.base.set_state_mutated();
result
}
pub fn continue_as_new(
&self,
input: <W::Run as WorkflowDefinition>::Input,
opts: ContinueAsNewOptions,
) -> Result<std::convert::Infallible, WorkflowTermination>
where
W: WorkflowImplementation,
{
self.sync.continue_as_new(input, opts)
}
pub fn wait_condition<'a>(
&'a self,
condition: impl FnMut(&W) -> bool + 'a,
) -> impl FusedFuture<Output = Result<(), WorkflowCancellationError>> + 'a {
self.wait_condition_with_options(condition, Default::default())
}
pub fn wait_condition_with_options<'a>(
&'a self,
mut condition: impl FnMut(&W) -> bool + 'a,
options: WaitConditionOptions,
) -> impl FusedFuture<Output = Result<(), WorkflowCancellationError>> + 'a {
let token = options
.cancellation_token
.unwrap_or_else(|| self.cancellation_token());
let wait_token = token.clone();
let mut cancelled = Box::pin(async move {
wait_token.cancelled().await;
});
future::poll_fn(move |cx: &mut Context<'_>| {
if condition(&*self.workflow_state.borrow()) {
Poll::Ready(Ok(()))
} else if cancelled.as_mut().poll(cx).is_ready() {
Poll::Ready(Err(WorkflowCancellationError::new(token.reason())))
} else {
self.sync
.base
.inner
.condition_wakers
.borrow_mut()
.push(cx.waker().clone());
Poll::Pending
}
})
.fuse()
}
}
struct WfCtxProtectedDat {
next_timer_sequence_number: u32,
next_activity_sequence_number: u32,
next_child_workflow_sequence_number: u32,
next_cancel_external_wf_sequence_number: u32,
next_signal_external_wf_sequence_number: u32,
#[cfg(feature = "experimental")]
next_nexus_op_sequence_number: u32,
}
impl WfCtxProtectedDat {
fn next_timer_seq(&mut self) -> u32 {
let seq = self.next_timer_sequence_number;
self.next_timer_sequence_number += 1;
seq
}
fn next_activity_seq(&mut self) -> u32 {
let seq = self.next_activity_sequence_number;
self.next_activity_sequence_number += 1;
seq
}
fn next_child_workflow_seq(&mut self) -> u32 {
let seq = self.next_child_workflow_sequence_number;
self.next_child_workflow_sequence_number += 1;
seq
}
fn next_cancel_external_wf_seq(&mut self) -> u32 {
let seq = self.next_cancel_external_wf_sequence_number;
self.next_cancel_external_wf_sequence_number += 1;
seq
}
fn next_signal_external_wf_seq(&mut self) -> u32 {
let seq = self.next_signal_external_wf_sequence_number;
self.next_signal_external_wf_sequence_number += 1;
seq
}
}
#[derive(Clone, Debug)]
struct WorkflowContextSharedData {
changes: HashMap<String, bool>,
notified_patches: HashSet<String>,
activation: CoreWorkflowActivation,
memo: ProtoMemo,
is_replaying_history_events: bool,
search_attributes: ProtoSearchAttributes,
current_details: String,
}
pub trait CancellableFuture: FusedFuture {
fn cancel(&self);
}
pub trait CancellableFutureWithReason: CancellableFuture {
fn cancel_with_reason(&self, reason: String);
}
fn cancellable_outbound<T: 'static>(
future: impl CancellableFuture<Output = T> + 'static,
) -> CancellableWorkflowOutboundFuture<T> {
let future = Rc::new(RefCell::new(Box::pin(future)));
let polled = future.clone();
let cancellation = WorkflowCancellationHandle::new(move |_| {
future.borrow().as_ref().get_ref().cancel();
});
CancellableWorkflowOutboundFuture::new(
future::poll_fn(move |cx| polled.borrow_mut().as_mut().poll(cx)),
cancellation,
)
}
fn cancellable_outbound_with_reason<T: 'static>(
future: impl CancellableFutureWithReason<Output = T> + 'static,
) -> CancellableWorkflowOutboundFuture<T> {
let future = Rc::new(RefCell::new(Box::pin(future)));
let polled = future.clone();
let cancellation = WorkflowCancellationHandle::new(move |reason| {
let future = future.borrow();
let future = future.as_ref().get_ref();
if let Some(reason) = reason {
future.cancel_with_reason(reason);
} else {
future.cancel();
}
});
CancellableWorkflowOutboundFuture::new(
future::poll_fn(move |cx| polled.borrow_mut().as_mut().poll(cx)),
cancellation,
)
}
pub(crate) struct WFCommandFut<T, D> {
_unused: PhantomData<T>,
result_rx: oneshot::Receiver<UnblockEvent>,
other_dat: Option<D>,
}
impl<T> WFCommandFut<T, ()> {
fn new() -> (Self, oneshot::Sender<UnblockEvent>) {
Self::new_with_dat(())
}
}
impl<T, D> WFCommandFut<T, D> {
fn new_with_dat(other_dat: D) -> (Self, oneshot::Sender<UnblockEvent>) {
let (tx, rx) = oneshot::channel();
(
Self {
_unused: PhantomData,
result_rx: rx,
other_dat: Some(other_dat),
},
tx,
)
}
}
impl<T, D> Unpin for WFCommandFut<T, D> where T: Unblockable<OtherDat = D> {}
impl<T, D> Future for WFCommandFut<T, D>
where
T: Unblockable<OtherDat = D>,
{
type Output = T;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let poll = self.result_rx.poll_unpin(cx).map(|x| {
let od = self
.other_dat
.take()
.expect("Other data must exist when resolving command future");
Unblockable::unblock(x.unwrap(), od)
});
if poll.is_pending() {
mark_intercepted_future_activation();
}
poll
}
}
impl<T, D> FusedFuture for WFCommandFut<T, D>
where
T: Unblockable<OtherDat = D>,
{
fn is_terminated(&self) -> bool {
self.other_dat.is_none()
}
}
struct CancellableWFCommandFut<T, D> {
cmd_fut: WFCommandFut<T, D>,
cancellable_id: CancellableID,
base_ctx: BaseWorkflowContext,
}
impl<T> CancellableWFCommandFut<T, ()> {
fn new(
cancellable_id: CancellableID,
base_ctx: BaseWorkflowContext,
) -> (Self, oneshot::Sender<UnblockEvent>) {
Self::new_with_dat(cancellable_id, (), base_ctx)
}
}
impl<T, D> CancellableWFCommandFut<T, D> {
fn new_with_dat(
cancellable_id: CancellableID,
other_dat: D,
base_ctx: BaseWorkflowContext,
) -> (Self, oneshot::Sender<UnblockEvent>) {
let (cmd_fut, sender) = WFCommandFut::new_with_dat(other_dat);
(
Self {
cmd_fut,
cancellable_id,
base_ctx,
},
sender,
)
}
}
impl<T, D> Unpin for CancellableWFCommandFut<T, D> where T: Unblockable<OtherDat = D> {}
impl<T, D> Future for CancellableWFCommandFut<T, D>
where
T: Unblockable<OtherDat = D>,
{
type Output = T;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.cmd_fut.poll_unpin(cx)
}
}
impl<T, D> FusedFuture for CancellableWFCommandFut<T, D>
where
T: Unblockable<OtherDat = D>,
{
fn is_terminated(&self) -> bool {
self.cmd_fut.is_terminated()
}
}
impl<T, D> CancellableFuture for CancellableWFCommandFut<T, D>
where
T: Unblockable<OtherDat = D>,
{
fn cancel(&self) {
self.base_ctx.cancel(self.cancellable_id.clone());
}
}
impl<T, D> CancellableFutureWithReason for CancellableWFCommandFut<T, D>
where
T: Unblockable<OtherDat = D>,
{
fn cancel_with_reason(&self, reason: String) {
self.base_ctx
.cancel(self.cancellable_id.clone().with_reason(reason));
}
}
struct LATimerBackoffFut {
la_opts: LocalActivityOptions,
activity_type: String,
arguments: Vec<Payload>,
headers: HashMap<String, Payload>,
current_fut: Pin<Box<dyn CancellableFuture<Output = ActivityResolution> + Unpin>>,
timer_fut: Option<Pin<Box<dyn CancellableFuture<Output = TimerResult> + Unpin>>>,
cancellation_token: WorkflowCancellationToken,
base_ctx: BaseWorkflowContext,
next_attempt: u32,
next_sched_time: Option<prost_types::Timestamp>,
did_cancel: AtomicBool,
terminated: bool,
}
impl LATimerBackoffFut {
fn new(
activity_type: String,
arguments: Vec<Payload>,
headers: HashMap<String, Payload>,
opts: LocalActivityOptions,
cancellation_token: WorkflowCancellationToken,
base_ctx: BaseWorkflowContext,
) -> Self {
let current_fut = Box::pin(base_ctx.clone().local_activity_no_timer_retry(
activity_type.clone(),
arguments.clone(),
headers.clone(),
opts.clone(),
));
Self {
la_opts: opts,
activity_type,
arguments,
headers,
current_fut,
timer_fut: None,
cancellation_token,
base_ctx,
next_attempt: 1,
next_sched_time: None,
did_cancel: AtomicBool::new(false),
terminated: false,
}
}
}
impl Unpin for LATimerBackoffFut {}
impl Future for LATimerBackoffFut {
type Output = ActivityResolution;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(tf) = self.timer_fut.as_mut() {
return match tf.poll_unpin(cx) {
Poll::Ready(tr) => {
self.timer_fut = None;
if let TimerResult::Fired = tr {
let mut opts = self.la_opts.clone();
opts.attempt = Some(self.next_attempt);
opts.original_schedule_time
.clone_from(&self.next_sched_time);
self.current_fut =
Box::pin(self.base_ctx.clone().local_activity_no_timer_retry(
self.activity_type.clone(),
self.arguments.clone(),
self.headers.clone(),
opts,
));
Poll::Pending
} else {
self.terminated = true;
Poll::Ready(ActivityResolution {
status: Some(activity_resolution::Status::Cancelled(Cancellation {
failure: Some(Failure {
message: "Activity cancelled".to_owned(),
failure_info: Some(FailureInfo::CanceledFailureInfo(
CanceledFailureInfo::default(),
)),
..Default::default()
}),
})),
})
}
}
Poll::Pending => Poll::Pending,
};
}
let poll_res = self.current_fut.poll_unpin(cx);
if let Poll::Ready(ref r) = poll_res
&& let Some(activity_resolution::Status::Backoff(b)) = r.status.as_ref()
{
if self.did_cancel.load(Ordering::Acquire) {
self.terminated = true;
return Poll::Ready(ActivityResolution {
status: Some(activity_resolution::Status::Cancelled(Cancellation {
failure: Some(Failure {
message: "Activity cancelled".to_owned(),
failure_info: Some(FailureInfo::CanceledFailureInfo(
CanceledFailureInfo::default(),
)),
..Default::default()
}),
})),
});
}
let timer_f = self.base_ctx.timer(TimerOptions {
duration: b
.backoff_duration
.expect("Duration is set")
.try_into()
.expect("duration converts ok"),
cancellation_token: Some(self.cancellation_token.clone()),
summary: None,
#[cfg(feature = "experimental")]
event_group_markers: self.la_opts.event_group_markers.clone(),
});
self.timer_fut = Some(Box::pin(timer_f));
self.next_attempt = b.attempt;
self.next_sched_time.clone_from(&b.original_schedule_time);
return Poll::Pending;
}
if poll_res.is_ready() {
self.terminated = true;
}
poll_res
}
}
impl FusedFuture for LATimerBackoffFut {
fn is_terminated(&self) -> bool {
self.terminated
}
}
impl CancellableFuture for LATimerBackoffFut {
fn cancel(&self) {
self.did_cancel.store(true, Ordering::Release);
if let Some(tf) = self.timer_fut.as_ref() {
tf.cancel();
}
self.current_fut.cancel();
}
}
enum ActivityFut<F, Output> {
Errored {
error: Option<Box<ActivityExecutionError>>,
_phantom: PhantomData<Output>,
},
Running {
inner: F,
data_converter: DataConverter,
_phantom: PhantomData<Output>,
},
Terminated,
}
impl<F, Output> ActivityFut<F, Output> {
fn eager(err: ActivityExecutionError) -> Self {
Self::Errored {
error: Some(Box::new(err)),
_phantom: PhantomData,
}
}
fn running(inner: F, data_converter: DataConverter) -> Self {
Self::Running {
inner,
data_converter,
_phantom: PhantomData,
}
}
}
impl<F, Output> Unpin for ActivityFut<F, Output> where F: Unpin {}
impl<F, Output> Future for ActivityFut<F, Output>
where
F: Future<Output = ActivityResolution> + Unpin,
Output: TemporalDeserializable + 'static,
{
type Output = Result<Output, ActivityExecutionError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let poll =
match this {
ActivityFut::Errored { error, .. } => {
Poll::Ready(Err(*error.take().expect("polled after completion")))
}
ActivityFut::Running {
inner,
data_converter,
..
} => match Pin::new(inner).poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(resolution) => Poll::Ready({
let status = resolution.status.ok_or_else(|| {
data_converter
.to_error(
&SerializationContextData::Workflow(
WorkflowSerializationContext::new(),
),
Failure {
message: "Activity completed without a status".to_string(),
..Default::default()
},
ActivityExecutionDecodeHint::new(false),
)
.expect("synthetic activity failure should decode")
})?;
match status {
activity_resolution::Status::Completed(success) => {
let payload = success.result.unwrap_or_default();
let context_data = SerializationContextData::Workflow(
WorkflowSerializationContext::new(),
);
let ctx = SerializationContext::new(
&context_data,
data_converter.payload_converter(),
);
data_converter
.payload_converter()
.from_payload::<Output>(&ctx, payload)
.map_err(ActivityExecutionError::Serialization)
}
activity_resolution::Status::Failed(f) => Err(data_converter
.to_error(
&SerializationContextData::Workflow(
WorkflowSerializationContext::new(),
),
f.failure.unwrap_or_default(),
ActivityExecutionDecodeHint::new(false),
)?),
activity_resolution::Status::Cancelled(c) => Err(data_converter
.to_error(
&SerializationContextData::Workflow(
WorkflowSerializationContext::new(),
),
c.failure.unwrap_or_default(),
ActivityExecutionDecodeHint::new(true),
)?),
activity_resolution::Status::Backoff(_) => {
panic!("DoBackoff should be handled by LATimerBackoffFut")
}
}
}),
},
ActivityFut::Terminated => panic!("polled after termination"),
};
if poll.is_ready() {
*this = ActivityFut::Terminated;
}
poll
}
}
impl<F, Output> FusedFuture for ActivityFut<F, Output>
where
F: Future<Output = ActivityResolution> + Unpin,
Output: TemporalDeserializable + 'static,
{
fn is_terminated(&self) -> bool {
matches!(self, ActivityFut::Terminated)
}
}
impl<F, Output> CancellableFuture for ActivityFut<F, Output>
where
F: CancellableFuture<Output = ActivityResolution> + Unpin,
Output: TemporalDeserializable + 'static,
{
fn cancel(&self) {
if let ActivityFut::Running { inner, .. } = self {
inner.cancel()
}
}
}
pub(crate) struct ChildWfCommon {
workflow_id: String,
child_seq: u32,
result_future: CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
base_ctx: BaseWorkflowContext,
}
#[derive(derive_more::Debug)]
pub(crate) struct PendingChildWorkflow<WD: WorkflowDefinition> {
pub(crate) status: ChildWorkflowStartStatus,
#[debug(skip)]
pub(crate) common: ChildWfCommon,
pub(crate) _phantom: PhantomData<WD>,
}
#[derive(derive_more::Debug)]
pub struct StartChildWorkflowOutput {
pub run_id: String,
#[debug(skip)]
result_future: CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
workflow_id: String,
child_seq: u32,
#[debug(skip)]
base_ctx: BaseWorkflowContext,
}
impl StartChildWorkflowOutput {
pub fn map_result(
mut self,
map: impl FnOnce(
CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
) -> CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
) -> Self {
self.result_future = map(self.result_future);
self
}
fn into_started<WD: WorkflowDefinition>(self) -> StartedChildWorkflow<WD> {
StartedChildWorkflow {
run_id: self.run_id,
result_future: self.result_future,
workflow_id: self.workflow_id,
child_seq: self.child_seq,
base_ctx: self.base_ctx,
_phantom: PhantomData,
}
}
}
#[derive(derive_more::Debug)]
pub struct StartedChildWorkflow<WD: WorkflowDefinition> {
pub run_id: String,
#[debug(skip)]
result_future: CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
workflow_id: String,
child_seq: u32,
#[debug(skip)]
base_ctx: BaseWorkflowContext,
_phantom: PhantomData<WD>,
}
enum ChildWorkflowFut<F, Output> {
Running {
inner: F,
data_converter: DataConverter,
_phantom: PhantomData<Output>,
},
Terminated,
}
impl<F, Output> Unpin for ChildWorkflowFut<F, Output> where F: Unpin {}
impl<F, Output> Future for ChildWorkflowFut<F, Output>
where
F: Future<Output = ChildWorkflowResult> + Unpin,
Output: TemporalDeserializable + 'static,
{
type Output = Result<Output, ChildWorkflowExecutionError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let poll = match this {
ChildWorkflowFut::Running {
inner,
data_converter,
..
} => match Pin::new(inner).poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(result) => Poll::Ready({
let status = result.status.ok_or_else(|| {
data_converter
.to_error(
&SerializationContextData::Workflow(
WorkflowSerializationContext::new(),
),
Failure {
message: "Child workflow completed without a status"
.to_string(),
..Default::default()
},
ChildWorkflowExecutionDecodeHint::default(),
)
.expect("synthetic child workflow failure should decode")
})?;
match status {
child_workflow_result::Status::Completed(success) => {
let payloads = success.result.into_iter().collect();
let context_data = SerializationContextData::Workflow(
WorkflowSerializationContext::new(),
);
let ctx = SerializationContext::new(
&context_data,
data_converter.payload_converter(),
);
data_converter
.payload_converter()
.from_payloads::<Output>(&ctx, payloads)
.map_err(ChildWorkflowExecutionError::Serialization)
}
child_workflow_result::Status::Failed(f) => {
Err(data_converter.to_error(
&SerializationContextData::Workflow(
WorkflowSerializationContext::new(),
),
f.failure.unwrap_or_default(),
ChildWorkflowExecutionDecodeHint::default(),
)?)
}
child_workflow_result::Status::Cancelled(c) => Err(data_converter
.to_error(
&SerializationContextData::Workflow(
WorkflowSerializationContext::new(),
),
c.failure.unwrap_or_default(),
ChildWorkflowExecutionDecodeHint::default(),
)?),
}
}),
},
ChildWorkflowFut::Terminated => panic!("polled after termination"),
};
if poll.is_ready() {
*this = ChildWorkflowFut::Terminated;
}
poll
}
}
impl<F, Output> FusedFuture for ChildWorkflowFut<F, Output>
where
F: Future<Output = ChildWorkflowResult> + Unpin,
Output: TemporalDeserializable + 'static,
{
fn is_terminated(&self) -> bool {
matches!(self, ChildWorkflowFut::Terminated)
}
}
impl<F, Output> CancellableFutureWithReason for ChildWorkflowFut<F, Output>
where
F: CancellableFutureWithReason<Output = ChildWorkflowResult> + Unpin,
Output: TemporalDeserializable + 'static,
{
fn cancel_with_reason(&self, reason: String) {
if let ChildWorkflowFut::Running { inner, .. } = self {
inner.cancel_with_reason(reason)
}
}
}
impl<F, Output> CancellableFuture for ChildWorkflowFut<F, Output>
where
F: CancellableFutureWithReason<Output = ChildWorkflowResult> + Unpin,
Output: TemporalDeserializable + 'static,
{
fn cancel(&self) {
if let ChildWorkflowFut::Running { inner, .. } = self {
inner.cancel()
}
}
}
enum ChildWorkflowStartFut<F, WD: WorkflowDefinition> {
Errored {
error: Option<Box<ChildWorkflowStartError>>,
_phantom: PhantomData<WD>,
},
Running(F),
Terminated,
}
impl<F, WD: WorkflowDefinition> ChildWorkflowStartFut<F, WD> {
fn eager(err: ChildWorkflowStartError) -> Self {
Self::Errored {
error: Some(Box::new(err)),
_phantom: PhantomData,
}
}
}
impl<F, WD: WorkflowDefinition> Unpin for ChildWorkflowStartFut<F, WD> where F: Unpin {}
impl<F, WD> Future for ChildWorkflowStartFut<F, WD>
where
F: Future<Output = PendingChildWorkflow<WD>> + Unpin,
WD: WorkflowDefinition,
{
type Output = StartChildWorkflowResult;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let poll = match this {
ChildWorkflowStartFut::Errored { error, .. } => {
Poll::Ready(Err(*error.take().expect("polled after completion")))
}
ChildWorkflowStartFut::Running(inner) => {
match Pin::new(inner).poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(pending) => Poll::Ready(match pending.status {
ChildWorkflowStartStatus::Succeeded(s) => {
let ChildWfCommon {
workflow_id,
child_seq,
result_future,
base_ctx,
} = pending.common;
Ok(StartChildWorkflowOutput {
run_id: s.run_id,
result_future,
workflow_id,
child_seq,
base_ctx,
})
}
ChildWorkflowStartStatus::Failed(f) => {
let mut result_future = pending.common.result_future;
result_future.unregister_cancellation();
Err(ChildWorkflowStartError::StartFailed {
workflow_id: f.workflow_id,
workflow_type: f.workflow_type,
cause: match f.cause {
cause if cause == ProtoStartChildCause::Unspecified as i32 => {
StartChildWorkflowExecutionFailedCause::Unspecified
}
cause
if cause
== ProtoStartChildCause::WorkflowAlreadyExists as i32 =>
{
StartChildWorkflowExecutionFailedCause::WorkflowAlreadyExists
}
_ => StartChildWorkflowExecutionFailedCause::Unknown,
},
})
}
ChildWorkflowStartStatus::Cancelled(c) => {
let ChildWfCommon {
mut result_future,
base_ctx,
..
} = pending.common;
result_future.unregister_cancellation();
Err(base_ctx.data_converter().to_error(
&SerializationContextData::Workflow(
WorkflowSerializationContext::new(),
),
c.failure.unwrap_or_default(),
ChildWorkflowStartDecodeHint::default(),
)?)
}
}),
}
}
ChildWorkflowStartFut::Terminated => panic!("polled after termination"),
};
if poll.is_ready() {
*this = ChildWorkflowStartFut::Terminated;
}
poll
}
}
impl<F, WD> FusedFuture for ChildWorkflowStartFut<F, WD>
where
F: Future<Output = PendingChildWorkflow<WD>> + Unpin,
WD: WorkflowDefinition,
{
fn is_terminated(&self) -> bool {
matches!(self, ChildWorkflowStartFut::Terminated)
}
}
impl<F, WD> CancellableFuture for ChildWorkflowStartFut<F, WD>
where
F: CancellableFutureWithReason<Output = PendingChildWorkflow<WD>> + Unpin,
WD: WorkflowDefinition,
{
fn cancel(&self) {
if let ChildWorkflowStartFut::Running(inner) = self {
inner.cancel()
}
}
}
impl<F, WD> CancellableFutureWithReason for ChildWorkflowStartFut<F, WD>
where
F: CancellableFutureWithReason<Output = PendingChildWorkflow<WD>> + Unpin,
WD: WorkflowDefinition,
{
fn cancel_with_reason(&self, reason: String) {
if let ChildWorkflowStartFut::Running(inner) = self {
inner.cancel_with_reason(reason)
}
}
}
enum SignalChildFut<F> {
Running {
inner: F,
data_converter: DataConverter,
},
Terminated,
}
impl<F> Unpin for SignalChildFut<F> where F: Unpin {}
impl<F> Future for SignalChildFut<F>
where
F: Future<Output = SignalExternalWfResult> + Unpin,
{
type Output = Result<(), WorkflowSignalError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let poll = match this {
SignalChildFut::Running {
inner,
data_converter,
} => match Pin::new(inner).poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Ok(_)) => Poll::Ready(Ok(())),
Poll::Ready(Err(error)) => Poll::Ready(Err(data_converter.to_error(
&SerializationContextData::Workflow(WorkflowSerializationContext::new()),
error.failure,
WorkflowSignalDecodeHint::new(error.cause),
)?)),
},
SignalChildFut::Terminated => panic!("polled after termination"),
};
if poll.is_ready() {
*this = SignalChildFut::Terminated;
}
poll
}
}
impl<F> FusedFuture for SignalChildFut<F>
where
F: Future<Output = SignalExternalWfResult> + Unpin,
{
fn is_terminated(&self) -> bool {
matches!(self, SignalChildFut::Terminated)
}
}
impl<F> CancellableFuture for SignalChildFut<F>
where
F: CancellableFuture<Output = SignalExternalWfResult> + Unpin,
{
fn cancel(&self) {
if let SignalChildFut::Running { inner, .. } = self {
inner.cancel()
}
}
}
impl<WD: WorkflowDefinition> StartedChildWorkflow<WD>
where
WD::Output: TemporalDeserializable + 'static,
{
pub fn result(
self,
) -> impl CancellableFutureWithReason<Output = Result<WD::Output, ChildWorkflowExecutionError>>
{
self.result_future.map(|result| {
result.and_then(|output| {
output
.downcast::<WD::Output>()
.map(|output| *output)
.map_err(|_| {
ChildWorkflowExecutionError::Serialization(outbound_type_error(
"child workflow output",
))
})
})
})
}
pub fn cancel(&self, reason: String) {
self.base_ctx.cancel(CancellableID::ChildWorkflow {
seqnum: self.child_seq,
reason,
});
}
pub fn signal<S: SignalDefinition<Workflow = WD> + 'static>(
&self,
signal: S,
input: S::Input,
options: SignalWorkflowOptions,
) -> impl CancellableFuture<Output = Result<(), WorkflowSignalError>> + 'static {
self.base_ctx.signal_workflow(
SignalWorkflowTarget::Child {
workflow_id: self.workflow_id.clone(),
},
signal,
input,
options,
)
}
}
#[derive(derive_more::Debug)]
pub struct ExternalWorkflowHandle {
workflow_id: String,
run_id: Option<String>,
namespace: String,
#[debug(skip)]
base_ctx: BaseWorkflowContext,
}
impl ExternalWorkflowHandle {
pub fn workflow_id(&self) -> &str {
&self.workflow_id
}
pub fn run_id(&self) -> Option<&str> {
self.run_id.as_deref()
}
pub fn signal<S: SignalDefinition + 'static>(
&self,
signal: S,
input: S::Input,
options: SignalWorkflowOptions,
) -> impl CancellableFuture<Output = Result<(), WorkflowSignalError>> + 'static {
self.base_ctx.signal_workflow(
SignalWorkflowTarget::External {
namespace: self.namespace.clone(),
workflow_id: self.workflow_id.clone(),
run_id: self.run_id.clone(),
},
signal,
input,
options,
)
}
pub fn cancel(
&self,
reason: Option<String>,
) -> impl FusedFuture<Output = CancelExternalWorkflowResult> {
self.base_ctx
.cancel_external_workflow(CancelExternalWorkflowInput {
workflow_id: self.workflow_id.clone(),
run_id: self.run_id.clone(),
reason,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemoValues;
use std::{
collections::HashMap,
sync::{
Mutex,
atomic::{AtomicUsize, Ordering as AtomicOrdering},
},
task::Wake,
time::Duration,
};
use temporalio_common_wasm::{
data_converters::{TemporalDeserializable, TemporalSerializable},
error::OutgoingWorkflowError,
protos::{
coresdk::{
AsJsonPayloadExt, FromJsonPayloadExt,
common::VersioningIntent as ProtoVersioningIntent,
workflow_activation::{UpdateRandomSeed, WorkflowActivationJob},
workflow_commands::WorkflowCommand,
},
temporal::api::{
common::v1::Payload,
enums::v1::ContinueAsNewVersioningBehavior as ProtoContinueAsNewVersioningBehavior,
},
},
};
use temporalio_macros::{workflow, workflow_methods};
#[derive(Default)]
struct NoopHost;
struct CountingWake(Arc<AtomicUsize>);
impl Wake for CountingWake {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.fetch_add(1, AtomicOrdering::Relaxed);
}
}
impl WorkflowHost for NoopHost {
fn set_current_details(&self, _details: String) {}
fn push_command(&self, _command: WorkflowCommand) {}
}
#[derive(Default)]
struct RecordingHost {
commands: Rc<RefCell<Vec<WorkflowCommand>>>,
}
impl WorkflowHost for RecordingHost {
fn set_current_details(&self, _details: String) {}
fn push_command(&self, command: WorkflowCommand) {
self.commands.borrow_mut().push(command);
}
}
#[derive(Debug)]
struct FailingMemoValue;
impl TemporalSerializable for FailingMemoValue {
fn to_payload(
&self,
_ctx: &temporalio_common_wasm::data_converters::SerializationContext<'_>,
) -> Result<Payload, temporalio_common_wasm::data_converters::PayloadConversionError>
{
Err(
temporalio_common_wasm::data_converters::PayloadConversionError::EncodingError(
std::io::Error::other("memo serialization failure").into(),
),
)
}
}
#[workflow]
#[derive(Default)]
struct TestWorkflow;
#[workflow_methods]
impl TestWorkflow {
#[run]
async fn run(_ctx: &mut WorkflowContext<Self>, _input: u8) -> crate::WorkflowResult<()> {
unreachable!("test workflow run should not be polled")
}
#[signal]
fn test_signal(&mut self, _ctx: &mut SyncWorkflowContext<Self>, _input: String) {
unreachable!("test workflow signal should not be dispatched")
}
}
fn test_context() -> WorkflowContext<TestWorkflow> {
test_context_with_seed(0)
}
fn test_context_with_seed(randomness_seed: u64) -> WorkflowContext<TestWorkflow> {
let init = InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
randomness_seed,
..Default::default()
};
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "orig-task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
Rc::new(NoopHost),
None,
Vec::new(),
);
WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)))
}
fn patch_test_context(
callback: Option<PatchActivationCallback>,
) -> (
BaseWorkflowContext,
WorkflowContext<TestWorkflow>,
Rc<RefCell<Vec<WorkflowCommand>>>,
) {
let init = InitializeWorkflow {
workflow_id: "workflow-id".to_string(),
workflow_type: TestWorkflow.name().to_string(),
..Default::default()
};
let host = Rc::new(RecordingHost::default());
let commands = host.commands.clone();
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
host,
callback,
Vec::new(),
);
let ctx = WorkflowContext::from_base(base.clone(), Rc::new(RefCell::new(TestWorkflow)));
(base, ctx, commands)
}
struct ShortCircuitFirstTimer {
calls: AtomicUsize,
}
impl WorkflowInterceptor for ShortCircuitFirstTimer {
fn start_timer(
&self,
_ctx: WorkflowInterceptorContext,
input: StartTimerInput,
next: WorkflowNext<
'static,
StartTimerInput,
CancellableWorkflowOutboundFuture<TimerResult>,
>,
) -> CancellableWorkflowOutboundFuture<TimerResult> {
if self.calls.fetch_add(1, Ordering::Relaxed) == 0 {
CancellableWorkflowOutboundFuture::new(
async { TimerResult::Cancelled },
WorkflowCancellationHandle::new(|_| {}),
)
} else {
next.run(input)
}
}
}
#[test]
fn short_circuited_outbound_call_does_not_consume_sequence_number() {
let host = Rc::new(RecordingHost::default());
let init = InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
..Default::default()
};
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
host.clone(),
None,
vec![WorkflowInterceptorConstructor::new(|_| {
ShortCircuitFirstTimer {
calls: AtomicUsize::new(0),
}
})],
);
let first = base.timer(Duration::from_secs(1));
assert_eq!(first.now_or_never(), Some(TimerResult::Cancelled));
let _second = base.timer(Duration::from_secs(1));
let commands = host.commands.borrow();
assert_eq!(commands.len(), 1);
let Some(workflow_command::Variant::StartTimer(timer)) = &commands[0].variant else {
panic!("expected start timer command");
};
assert_eq!(timer.seq, 1);
}
#[cfg(feature = "experimental")]
mod experimental_operation_tests {
use super::*;
use temporalio_common_wasm::protos::{
coresdk::workflow_activation::{
ResolveChildWorkflowExecutionStartSuccess, resolve_nexus_operation_start,
},
temporal::api::sdk::v1::{EventGroupMarker, event_group_marker},
};
struct TestActivity;
impl ActivityDefinition for TestActivity {
type Input = ();
type Output = ();
fn name(&self) -> &str {
"test_activity"
}
}
#[test]
fn custom_token_cancels_command_backed_operations() {
let host = Rc::new(RecordingHost::default());
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
..Default::default()
},
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
host.clone(),
None,
Vec::new(),
);
let token = WorkflowCancellationToken::new();
let timer = base.timer(TimerOptions {
duration: Duration::from_secs(1),
cancellation_token: Some(token.clone()),
summary: None,
event_group_markers: vec![],
});
let mut activity_options =
ActivityOptions::start_to_close_timeout(Duration::from_secs(1));
activity_options.cancellation_token = Some(token.clone());
let activity = base.execute_activity(TestActivity, (), activity_options);
let mut local_activity_options = LocalActivityOptions {
schedule_to_close_timeout: Some(Duration::from_secs(1)),
..Default::default()
};
local_activity_options.cancellation_token = Some(token.clone());
let local_activity =
base.execute_local_activity(TestActivity, (), local_activity_options);
let child_options = ChildWorkflowOptions {
cancellation_token: Some(token.clone()),
..Default::default()
};
let child = base.start_child_workflow(TestWorkflow::run, 1, child_options);
let signal = base.external_workflow("external", None).signal(
TestWorkflow::test_signal,
"input".to_string(),
SignalWorkflowOptions::builder()
.cancellation_token(token.clone())
.build(),
);
let nexus_options = NexusOperationOptions::builder()
.endpoint("endpoint")
.service("service")
.operation("operation")
.cancellation_token(token.clone())
.build();
let nexus = base.start_nexus_operation(nexus_options);
token.cancel_with_reason("group cancelled");
timer.cancel();
activity.cancel();
local_activity.cancel();
child.cancel_with_reason("explicit cancellation".to_string());
signal.cancel();
nexus.cancel();
let commands = host.commands.borrow();
assert_eq!(
commands
.iter()
.filter(|command| matches!(
&command.variant,
Some(workflow_command::Variant::CancelTimer(_))
))
.count(),
1
);
assert_eq!(
commands
.iter()
.filter(|command| matches!(
&command.variant,
Some(workflow_command::Variant::RequestCancelActivity(_))
))
.count(),
1
);
assert_eq!(
commands
.iter()
.filter(|command| matches!(
&command.variant,
Some(workflow_command::Variant::RequestCancelLocalActivity(_))
))
.count(),
1
);
let child_cancellations = commands
.iter()
.filter_map(|command| match &command.variant {
Some(workflow_command::Variant::CancelChildWorkflowExecution(cancel)) => {
Some(cancel)
}
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(child_cancellations.len(), 1);
assert_eq!(child_cancellations[0].reason, "group cancelled");
assert_eq!(
commands
.iter()
.filter(|command| matches!(
&command.variant,
Some(workflow_command::Variant::CancelSignalWorkflow(_))
))
.count(),
1
);
assert_eq!(
commands
.iter()
.filter(|command| matches!(
&command.variant,
Some(workflow_command::Variant::RequestCancelNexusOperation(_))
))
.count(),
1
);
}
#[test]
fn child_and_nexus_tokens_remain_active_after_start() {
let host = Rc::new(RecordingHost::default());
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
..Default::default()
},
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
host.clone(),
None,
Vec::new(),
);
let child_token = WorkflowCancellationToken::new();
let child_options = ChildWorkflowOptions {
cancellation_token: Some(child_token.clone()),
..Default::default()
};
let child = base.start_child_workflow(TestWorkflow::run, 1, child_options);
base.unblock(UnblockEvent::WorkflowStart(
1,
Box::new(ChildWorkflowStartStatus::Succeeded(
ResolveChildWorkflowExecutionStartSuccess {
run_id: "child-run".to_string(),
},
)),
))
.unwrap();
let started_child = child
.now_or_never()
.expect("child start should resolve")
.unwrap();
child_token.cancel();
started_child.cancel("explicit cancellation".to_string());
let nexus_token = WorkflowCancellationToken::new();
let nexus_options = NexusOperationOptions::builder()
.endpoint("endpoint")
.service("service")
.operation("operation")
.cancellation_token(nexus_token.clone())
.build();
let nexus = base.start_nexus_operation(nexus_options);
base.unblock(UnblockEvent::NexusOperationStart(
1,
Box::new(resolve_nexus_operation_start::Status::OperationToken(
"operation-token".to_string(),
)),
))
.unwrap();
let started_nexus = nexus
.now_or_never()
.expect("Nexus start should resolve")
.unwrap();
nexus_token.cancel();
started_nexus.cancel();
let commands = host.commands.borrow();
assert_eq!(
commands
.iter()
.filter(|command| matches!(
&command.variant,
Some(workflow_command::Variant::CancelChildWorkflowExecution(_))
))
.count(),
1
);
assert_eq!(
commands
.iter()
.filter(|command| matches!(
&command.variant,
Some(workflow_command::Variant::RequestCancelNexusOperation(_))
))
.count(),
1
);
}
#[test]
fn local_activity_token_cancels_retry_backoff_timer() {
let host = Rc::new(RecordingHost::default());
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
..Default::default()
},
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
host.clone(),
None,
Vec::new(),
);
let token = WorkflowCancellationToken::new();
let marker = EventGroupMarker {
variant: Some(event_group_marker::Variant::Label(
event_group_marker::Label {
id: "la-group".to_string(),
label: Some("la-group".as_json_payload().unwrap()),
},
)),
};
let mut options = LocalActivityOptions {
schedule_to_close_timeout: Some(Duration::from_secs(10)),
event_group_markers: vec![marker.clone()],
..Default::default()
};
options.cancellation_token = Some(token.clone());
let activity = base.execute_local_activity(TestActivity, (), options);
futures_util::pin_mut!(activity);
base.unblock(UnblockEvent::Activity(
1,
Box::new(ActivityResolution {
status: Some(activity_resolution::Status::Backoff(
temporalio_common_wasm::protos::coresdk::activity_result::DoBackoff {
attempt: 2,
backoff_duration: Some(Duration::from_secs(5).try_into().unwrap()),
original_schedule_time: None,
},
)),
}),
))
.unwrap();
assert!(activity.as_mut().now_or_never().is_none());
token.cancel();
let commands = host.commands.borrow();
assert!(commands.iter().any(|command| matches!(
&command.variant,
Some(workflow_command::Variant::CancelTimer(_))
)));
let start_timer = commands
.iter()
.find(|command| {
matches!(
&command.variant,
Some(workflow_command::Variant::StartTimer(_))
)
})
.expect("backoff StartTimer is issued");
assert_eq!(start_timer.event_group_markers, [marker]);
}
}
#[test]
fn patch_activation_callback_activates_and_memoizes() {
let calls = Arc::new(AtomicUsize::new(0));
let input = Arc::new(Mutex::new(None));
let callback_calls = calls.clone();
let callback_input = input.clone();
let callback: PatchActivationCallback = Arc::new(move |value| {
assert!(matches!(
value.workflow_info.random_stream("plugin").source,
WorkflowRandomStreamSource::System(_)
));
callback_calls.fetch_add(1, AtomicOrdering::Relaxed);
*callback_input.lock().unwrap() = Some((
value.workflow_info.workflow_id().to_string(),
value.workflow_info.run_id().to_string(),
value.patch_id,
));
true
});
let (_, ctx, commands) = patch_test_context(Some(callback));
assert!(ctx.patched("my-patch"));
assert!(ctx.patched("my-patch"));
assert_eq!(calls.load(AtomicOrdering::Relaxed), 1);
assert_eq!(commands.borrow().len(), 1);
let input = input.lock().unwrap();
let input = input.as_ref().unwrap();
assert_eq!(input.0, "workflow-id");
assert_eq!(input.1, "run-id");
assert_eq!(input.2, "my-patch");
}
#[test]
fn patch_activation_callback_can_decline_and_memoizes() {
let calls = Arc::new(AtomicUsize::new(0));
let callback_calls = calls.clone();
let callback: PatchActivationCallback = Arc::new(move |_| {
callback_calls.fetch_add(1, AtomicOrdering::Relaxed);
false
});
let (_, ctx, commands) = patch_test_context(Some(callback));
assert!(!ctx.patched("my-patch"));
assert!(!ctx.patched("my-patch"));
assert_eq!(calls.load(AtomicOrdering::Relaxed), 1);
assert!(commands.borrow().is_empty());
}
#[test]
fn patch_activation_callback_bypasses_history_and_deprecation() {
let callback: PatchActivationCallback = Arc::new(|_| panic!("callback must not run"));
let (base, ctx, commands) = patch_test_context(Some(callback.clone()));
base.apply_activation_context(
&CoreWorkflowActivation {
is_replaying: true,
..Default::default()
},
true,
);
assert!(!ctx.patched("replay-patch"));
assert!(commands.borrow().is_empty());
let (base, ctx, commands) = patch_test_context(Some(callback.clone()));
base.apply_activation_context(
&CoreWorkflowActivation {
is_replaying: true,
..Default::default()
},
true,
);
base.notify_patch("existing-patch".to_string());
assert!(ctx.patched("existing-patch"));
assert_eq!(commands.borrow().len(), 1);
let (_, ctx, commands) = patch_test_context(Some(callback));
assert!(ctx.deprecate_patch("deprecated-patch"));
assert_eq!(commands.borrow().len(), 1);
}
#[test]
fn patch_activation_defaults_to_active() {
let (_, ctx, commands) = patch_test_context(None);
assert!(ctx.patched("my-patch"));
assert_eq!(commands.borrow().len(), 1);
}
#[test]
fn random_is_deterministic_for_supported_numeric_types() {
let first = test_context_with_seed(42);
let second = test_context_with_seed(42);
assert_eq!(first.random::<u8>(), second.random::<u8>());
assert_eq!(first.random::<i64>(), second.random::<i64>());
assert_eq!(first.random::<u128>(), second.random::<u128>());
assert_eq!(first.random::<f32>(), second.random::<f32>());
assert_eq!(first.random::<f64>(), second.random::<f64>());
assert_eq!(first.uuid4(), second.uuid4());
}
#[test]
fn random_is_reseeded_by_activation() {
let ctx = test_context_with_seed(123);
let expected = ctx.random::<u64>();
let activation = CoreWorkflowActivation {
jobs: vec![WorkflowActivationJob {
variant: Some(ActivationVariant::UpdateRandomSeed(UpdateRandomSeed {
randomness_seed: 123,
})),
}],
..Default::default()
};
ctx.sync.base.apply_activation_context(&activation, false);
assert_eq!(ctx.random::<u64>(), expected);
}
#[test]
fn named_random_lookup_continues_the_same_stream() {
let ctx = test_context_with_seed(42);
let first_lookup = ctx.random_stream("orders");
let first = first_lookup.random::<u64>();
let second = ctx.random_stream("orders").random::<u64>();
let expected = test_context_with_seed(42).random_stream("orders");
assert_eq!(first, expected.random::<u64>());
assert_eq!(second, expected.random::<u64>());
}
#[test]
fn named_random_sequence_is_stable() {
let stream = test_context_with_seed(42).random_stream("example.com/orders");
assert_eq!(stream.random::<u64>(), 18_054_372_068_998_079_507);
}
#[test]
fn named_random_streams_are_isolated() {
let ctx = test_context_with_seed(42);
let alpha = ctx.random_stream("alpha");
let first_alpha = alpha.random::<u64>();
let _ = ctx.random_stream("beta").random::<u64>();
let second_alpha = alpha.random::<u64>();
let expected_ctx = test_context_with_seed(42);
let expected_alpha = expected_ctx.random_stream("alpha");
assert_eq!(first_alpha, expected_alpha.random::<u64>());
assert_eq!(second_alpha, expected_alpha.random::<u64>());
assert_ne!(
test_context_with_seed(42)
.random_stream("alpha")
.random::<u64>(),
test_context_with_seed(42)
.random_stream("beta")
.random::<u64>()
);
}
#[test]
fn named_random_does_not_advance_default_randomness() {
let ctx = test_context_with_seed(42);
let first = ctx.random::<u64>();
let _ = ctx.random_stream("plugin").random::<u64>();
let second = ctx.random::<u64>();
let expected = test_context_with_seed(42);
assert_eq!(first, expected.random::<u64>());
assert_eq!(second, expected.random::<u64>());
}
#[test]
fn interceptor_context_shares_named_random_stream_state() {
let ctx = test_context_with_seed(42);
let first = ctx.random_stream("plugin").random::<u64>();
let interceptor_ctx =
crate::workflow_interceptors::WorkflowInterceptorContext::new(ctx.sync.base.clone());
let second = interceptor_ctx.random_stream("plugin").random::<u64>();
let expected = test_context_with_seed(42).random_stream("plugin");
assert_eq!(first, expected.random::<u64>());
assert_eq!(second, expected.random::<u64>());
}
#[test]
fn replay_safe_context_view_shares_workflow_randomness() {
let ctx = test_context_with_seed(42);
let first = ctx.sync.base.view().random_stream("plugin").random::<u64>();
let second = ctx.random_stream("plugin").random::<u64>();
let expected = test_context_with_seed(42).random_stream("plugin");
assert_eq!(first, expected.random::<u64>());
assert_eq!(second, expected.random::<u64>());
}
#[test]
fn read_only_context_view_does_not_advance_workflow_randomness() {
let ctx = test_context_with_seed(42);
let expected = test_context_with_seed(42)
.random_stream("plugin")
.random::<u64>();
{
let _read_only = ctx.sync.base.enter_read_only();
let _ = ctx.sync.base.view().random_stream("plugin").random::<u64>();
}
assert_eq!(ctx.random_stream("plugin").random::<u64>(), expected);
}
#[test]
fn nested_read_only_scopes_restore_replay_safety() {
let ctx = test_context_with_seed(42);
assert!(ctx.sync.base.requires_replay_safety());
{
let _outer = ctx.sync.base.enter_read_only();
assert!(!ctx.sync.base.requires_replay_safety());
{
let _inner = ctx.sync.base.enter_read_only();
assert!(!ctx.sync.base.requires_replay_safety());
}
assert!(!ctx.sync.base.requires_replay_safety());
}
assert!(ctx.sync.base.requires_replay_safety());
}
#[test]
fn named_random_streams_are_reseeded_by_activation() {
let ctx = test_context_with_seed(123);
let stream = ctx.random_stream("orders");
let _ = stream.random::<u64>();
let activation = CoreWorkflowActivation {
jobs: vec![WorkflowActivationJob {
variant: Some(ActivationVariant::UpdateRandomSeed(UpdateRandomSeed {
randomness_seed: 456,
})),
}],
..Default::default()
};
ctx.sync.base.apply_activation_context(&activation, false);
let expected = test_context_with_seed(456).random_stream("orders");
assert_eq!(stream.random::<u64>(), expected.random::<u64>());
}
#[cfg(feature = "experimental")]
mod experimental_interceptor_tests {
use super::*;
use crate::workflow_interceptors::StartNexusOperationInput;
struct MutatingRemainingOutboundInterceptor;
impl WorkflowInterceptor for MutatingRemainingOutboundInterceptor {
fn signal_workflow(
&self,
_ctx: WorkflowInterceptorContext,
mut input: SignalWorkflowInput,
next: WorkflowNext<
'static,
SignalWorkflowInput,
CancellableWorkflowOutboundFuture<SignalWorkflowResult>,
>,
) -> CancellableWorkflowOutboundFuture<SignalWorkflowResult> {
*input.signal_name_mut() = "mutated-signal".to_string();
*input.input_mut::<String>().unwrap() = "mutated-input".to_string();
*input.target_mut() = SignalWorkflowTarget::External {
namespace: "mutated-namespace".to_string(),
workflow_id: "mutated-workflow".to_string(),
run_id: Some("mutated-run".to_string()),
};
input
.headers_mut()
.insert("signal-header".to_string(), Payload::default());
next.run(input)
}
fn cancel_external_workflow(
&self,
_ctx: WorkflowInterceptorContext,
mut input: CancelExternalWorkflowInput,
next: WorkflowNext<
'static,
CancelExternalWorkflowInput,
WorkflowOutboundFuture<CancelExternalWorkflowResult>,
>,
) -> WorkflowOutboundFuture<CancelExternalWorkflowResult> {
input.workflow_id = "mutated-cancel-workflow".to_string();
input.run_id = Some("mutated-cancel-run".to_string());
input.reason = Some("mutated-reason".to_string());
next.run(input)
}
fn continue_as_new(
&self,
_ctx: crate::workflow_interceptors::SyncWorkflowInterceptorContext,
mut input: ContinueAsNewInput,
next: WorkflowNext<
'static,
ContinueAsNewInput,
crate::workflow_interceptors::ContinueAsNewResult,
>,
) -> crate::workflow_interceptors::ContinueAsNewResult {
*input.input_mut::<u8>().unwrap() = 42;
input.options_mut().workflow_type = Some("mutated-workflow-type".to_string());
input.headers_mut().insert(
"continue-header".to_string(),
Payload::from(b"continue-header-value".as_slice()),
);
next.run(input)
}
fn start_nexus_operation(
&self,
_ctx: WorkflowInterceptorContext,
mut input: StartNexusOperationInput,
next: WorkflowNext<
'static,
StartNexusOperationInput,
CancellableWorkflowOutboundFuture<
crate::workflow_interceptors::StartNexusOperationResult,
>,
>,
) -> CancellableWorkflowOutboundFuture<
crate::workflow_interceptors::StartNexusOperationResult,
> {
input.options_mut().endpoint = "mutated-endpoint".to_string();
input.options_mut().service = "mutated-service".to_string();
input.options_mut().operation = "mutated-operation".to_string();
next.run(input)
}
}
#[test]
fn outbound_interceptors_mutate_signal_cancel_continue_as_new_and_nexus() {
let host = Rc::new(RecordingHost::default());
let init = InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
..Default::default()
};
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
host.clone(),
None,
vec![WorkflowInterceptorConstructor::new(|_| {
MutatingRemainingOutboundInterceptor
})],
);
let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
let signal = ctx
.external_workflow("original-workflow", Some("original-run".to_string()))
.signal(
TestWorkflow::test_signal,
"original-input".to_string(),
Default::default(),
);
let cancel_target =
ctx.external_workflow("cancel-workflow", Some("cancel-run".to_string()));
let cancel = cancel_target.cancel(Some("original-reason".to_string()));
let termination = ctx
.continue_as_new(7, ContinueAsNewOptions::default())
.expect_err("continue_as_new should terminate the workflow");
let sync_ctx = ctx.sync_context();
let nexus = sync_ctx.start_nexus_operation(
NexusOperationOptions::builder()
.endpoint("original-endpoint")
.service("original-service")
.operation("original-operation")
.build(),
);
drop((signal, cancel, nexus));
let WorkflowTermination::ContinueAsNew(continue_as_new) = termination else {
panic!("expected continue-as-new termination")
};
assert_eq!(continue_as_new.workflow_type, "mutated-workflow-type");
assert_eq!(
continue_as_new.arguments,
vec![42u8.as_json_payload().unwrap()]
);
assert!(continue_as_new.headers.contains_key("continue-header"));
let commands = host.commands.borrow();
assert_eq!(commands.len(), 3);
let Some(workflow_command::Variant::SignalExternalWorkflowExecution(signal)) =
&commands[0].variant
else {
panic!("expected signal command")
};
assert_eq!(signal.signal_name, "mutated-signal");
assert_eq!(
signal.args,
vec!["mutated-input".to_string().as_json_payload().unwrap()]
);
assert!(signal.headers.contains_key("signal-header"));
let Some(signal_external_workflow_execution::Target::WorkflowExecution(target)) =
&signal.target
else {
panic!("expected external workflow signal target")
};
assert_eq!(target.namespace, "mutated-namespace");
assert_eq!(target.workflow_id, "mutated-workflow");
assert_eq!(target.run_id, "mutated-run");
let Some(workflow_command::Variant::RequestCancelExternalWorkflowExecution(cancel)) =
&commands[1].variant
else {
panic!("expected external cancellation command")
};
let target = cancel.workflow_execution.as_ref().unwrap();
assert_eq!(target.workflow_id, "mutated-cancel-workflow");
assert_eq!(target.run_id, "mutated-cancel-run");
assert_eq!(cancel.reason, "mutated-reason");
let Some(workflow_command::Variant::ScheduleNexusOperation(nexus)) =
&commands[2].variant
else {
panic!("expected Nexus operation command")
};
assert_eq!(nexus.endpoint, "mutated-endpoint");
assert_eq!(nexus.service, "mutated-service");
assert_eq!(nexus.operation, "mutated-operation");
}
}
struct HeaderAddingContinueAsNewInterceptor;
impl WorkflowInterceptor for HeaderAddingContinueAsNewInterceptor {
fn continue_as_new(
&self,
_ctx: crate::workflow_interceptors::SyncWorkflowInterceptorContext,
mut input: ContinueAsNewInput,
next: WorkflowNext<
'static,
ContinueAsNewInput,
crate::workflow_interceptors::ContinueAsNewResult,
>,
) -> crate::workflow_interceptors::ContinueAsNewResult {
input.headers_mut().insert(
"continue-header".to_string(),
Payload::from(b"continue-header-value".as_slice()),
);
next.run(input)
}
}
#[test]
fn continue_as_new_interceptor_header_reaches_proto_command() {
let init = InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
..Default::default()
};
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
Rc::new(NoopHost),
None,
vec![WorkflowInterceptorConstructor::new(|_| {
HeaderAddingContinueAsNewInterceptor
})],
);
let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
let termination = ctx
.continue_as_new(7, ContinueAsNewOptions::default())
.expect_err("continue_as_new should terminate the workflow");
let WorkflowTermination::ContinueAsNew(proto_command) = termination else {
panic!("expected continue-as-new termination")
};
assert_eq!(
proto_command.headers,
HashMap::from([(
"continue-header".to_string(),
Payload::from(b"continue-header-value".as_slice()),
)])
);
}
#[test]
fn construction_waker_uses_runtime_poll_waker() {
let base = test_context().sync.base;
let wakes = Arc::new(AtomicUsize::new(0));
let waker = Waker::from(Arc::new(CountingWake(wakes.clone())));
let _guard = base.enter_runtime_poll(&waker);
base.construction_waker().wake_by_ref();
assert_eq!(wakes.load(AtomicOrdering::Relaxed), 1);
}
#[test]
fn workflow_context_continue_as_new_serializes_input_and_defaults() {
let ctx = test_context();
let termination = ctx
.continue_as_new(7, ContinueAsNewOptions::default())
.expect_err("continue_as_new should terminate the workflow");
assert!(
matches!(termination, WorkflowTermination::ContinueAsNew(_)),
"expected continue-as-new termination, got {termination:?}"
);
let WorkflowTermination::ContinueAsNew(cmd) = termination else {
unreachable!()
};
assert_eq!(
*cmd,
crate::runtime::types::ContinueAsNewRequest {
workflow_type: TestWorkflow.name().to_string(),
task_queue: String::new(),
arguments: vec![7u8.as_json_payload().unwrap()],
workflow_run_timeout: None,
workflow_task_timeout: None,
backoff_start_interval: None,
memo: HashMap::new(),
headers: HashMap::new(),
search_attributes: None,
retry_policy: None,
versioning_intent: ProtoVersioningIntent::Unspecified.into(),
initial_versioning_behavior: ProtoContinueAsNewVersioningBehavior::Unspecified
.into(),
}
);
}
#[cfg(feature = "experimental")]
mod experimental_continue_as_new_tests {
use super::*;
use temporalio_common_wasm::{
RetryPolicy, protos::temporal::api::common::v1::RetryPolicy as ProtoRetryPolicy,
};
#[test]
fn sync_workflow_context_continue_as_new_applies_options() {
let ctx = test_context();
let sync = ctx.sync_context();
let mut memo = MemoValues::new();
memo.insert("memo-key", "memo-value".to_string());
let mut proto_search_attributes = ProtoSearchAttributes::default();
proto_search_attributes.indexed_fields.insert(
"CustomKeywordField".to_string(),
Payload::from(b"value".as_slice()),
);
let search_attributes = SearchAttributes::from_proto(&proto_search_attributes);
let termination = sync
.continue_as_new(
11,
ContinueAsNewOptions {
workflow_type: Some("next-workflow".to_string()),
task_queue: Some("next-task-queue".to_string()),
run_timeout: Some(Duration::from_secs(10)),
task_timeout: Some(Duration::from_secs(3)),
backoff_start_interval: Some(Duration::from_secs(4)),
memo: Some(memo.clone()),
search_attributes: Some(search_attributes.clone()),
retry_policy: Some(RetryPolicy::builder().maximum_attempts(5).build()),
versioning_intent: Some(ProtoVersioningIntent::Compatible.into()),
initial_versioning_behavior: Some(
ContinueAsNewVersioningBehavior::UseRampingVersion,
),
},
)
.expect_err("continue_as_new should terminate the workflow");
assert!(
matches!(termination, WorkflowTermination::ContinueAsNew(_)),
"expected continue-as-new termination, got {termination:?}"
);
let WorkflowTermination::ContinueAsNew(cmd) = termination else {
unreachable!()
};
assert_eq!(
*cmd,
crate::runtime::types::ContinueAsNewRequest {
workflow_type: "next-workflow".to_string(),
task_queue: "next-task-queue".to_string(),
arguments: vec![11u8.as_json_payload().unwrap()],
workflow_run_timeout: Some(Duration::from_secs(10).try_into().unwrap()),
workflow_task_timeout: Some(Duration::from_secs(3).try_into().unwrap()),
backoff_start_interval: Some(Duration::from_secs(4).try_into().unwrap()),
memo: HashMap::from([(
"memo-key".to_string(),
"memo-value".as_json_payload().unwrap(),
)]),
headers: HashMap::new(),
search_attributes: Some(proto_search_attributes),
retry_policy: Some(ProtoRetryPolicy {
initial_interval: Some(Duration::from_secs(1).try_into().unwrap()),
backoff_coefficient: 2.0,
maximum_attempts: 5,
..Default::default()
}),
versioning_intent: ProtoVersioningIntent::Compatible.into(),
initial_versioning_behavior:
ProtoContinueAsNewVersioningBehavior::UseRampingVersion as i32,
}
);
}
#[test]
fn workflow_context_continue_as_new_applies_auto_upgrade_versioning_behavior() {
let ctx = test_context();
let termination = ctx
.continue_as_new(
13,
ContinueAsNewOptions {
initial_versioning_behavior: Some(
ContinueAsNewVersioningBehavior::AutoUpgrade,
),
..Default::default()
},
)
.expect_err("continue_as_new should terminate the workflow");
let WorkflowTermination::ContinueAsNew(cmd) = termination else {
unreachable!()
};
assert_eq!(
cmd.initial_versioning_behavior,
ProtoContinueAsNewVersioningBehavior::AutoUpgrade as i32
);
}
}
#[test]
fn continue_as_new_preserves_explicit_empty_search_attributes() {
let ctx = test_context();
let sync = ctx.sync_context();
let termination = sync
.continue_as_new(
11,
ContinueAsNewOptions {
search_attributes: Some(SearchAttributes::default()),
..Default::default()
},
)
.expect_err("continue_as_new should terminate the workflow");
let WorkflowTermination::ContinueAsNew(cmd) = termination else {
unreachable!()
};
assert_eq!(
cmd.search_attributes,
Some(ProtoSearchAttributes::default())
);
}
#[test]
fn continue_as_new_preserves_input_serialization_errors() {
#[derive(Debug)]
struct FailingInput;
impl TemporalSerializable for FailingInput {
fn to_payload(
&self,
_ctx: &temporalio_common_wasm::data_converters::SerializationContext<'_>,
) -> Result<Payload, temporalio_common_wasm::data_converters::PayloadConversionError>
{
Err(
temporalio_common_wasm::data_converters::PayloadConversionError::EncodingError(
std::io::Error::other("serialization failure").into(),
),
)
}
}
impl TemporalDeserializable for FailingInput {
fn from_payload(
_ctx: &temporalio_common_wasm::data_converters::SerializationContext<'_>,
_payload: Payload,
) -> Result<Self, temporalio_common_wasm::data_converters::PayloadConversionError>
{
unreachable!("test input is only serialized")
}
}
#[workflow]
#[derive(Default)]
struct FailingWorkflow;
#[workflow_methods]
impl FailingWorkflow {
#[run]
async fn run(
_ctx: &mut WorkflowContext<Self>,
_input: FailingInput,
) -> crate::WorkflowResult<()> {
unreachable!("test workflow run should not be polled")
}
}
let init = InitializeWorkflow {
workflow_type: "failing-workflow".to_string(),
..Default::default()
};
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "orig-task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
Rc::new(NoopHost),
None,
Vec::new(),
);
let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(FailingWorkflow)));
let termination = ctx
.continue_as_new(FailingInput, ContinueAsNewOptions::default())
.expect_err("input serialization should fail");
let WorkflowTermination::Failed(OutgoingWorkflowError::PayloadConversion(err)) =
termination
else {
panic!("expected a payload conversion failure");
};
assert_eq!(err.to_string(), "Encoding error: serialization failure");
}
#[test]
fn continue_as_new_preserves_memo_serialization_errors() {
let ctx = test_context();
let mut memo = MemoValues::new();
memo.insert("invalid", FailingMemoValue);
let termination = ctx
.continue_as_new(
7,
ContinueAsNewOptions {
memo: Some(memo),
..Default::default()
},
)
.expect_err("memo serialization should fail");
let WorkflowTermination::Failed(OutgoingWorkflowError::PayloadConversion(err)) =
termination
else {
panic!("expected a payload conversion failure");
};
assert_eq!(
err.to_string(),
"Encoding error: memo serialization failure"
);
}
#[test]
fn upsert_search_attributes_updates_local_state() {
use temporalio_common_wasm::search_attributes::SearchAttributeKey;
const K: SearchAttributeKey<i64> = SearchAttributeKey::int("my_int");
let ctx = test_context();
assert!(ctx.search_attributes().is_empty());
ctx.upsert_search_attributes([K.value_set(42)]);
let attrs = ctx.search_attributes();
assert_eq!(attrs.get(&K), Some(42));
}
#[test]
fn upsert_memo_updates_local_state_and_encodes_removals() {
let init = InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
memo: Some(ProtoMemo {
fields: HashMap::from([("old".to_string(), "before".as_json_payload().unwrap())]),
}),
..Default::default()
};
let host = Rc::new(RecordingHost::default());
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "orig-task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
host.clone(),
None,
Vec::new(),
);
let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
assert_eq!(
ctx.memo().get::<String>("old").unwrap(),
Some("before".to_string())
);
ctx.upsert_memo([("new", Some(MemoValue::new(42_u32))), ("old", None)])
.unwrap();
let current = ctx.memo();
assert_eq!(current.get::<u32>("new").unwrap(), Some(42));
assert_eq!(current.get::<String>("old").unwrap(), None);
let view = ctx.view();
assert_eq!(view.memo().get::<u32>("new").unwrap(), Some(42));
assert_eq!(
view.memo().raw(),
view.raw()
.memo
.as_ref()
.expect("view memo should be present")
);
let commands = host.commands.borrow();
let [command] = commands.as_slice() else {
panic!("expected one modify-properties command");
};
let Some(workflow_command::Variant::ModifyWorkflowProperties(command)) = &command.variant
else {
panic!("expected a modify-properties command");
};
let fields = &command.upserted_memo.as_ref().unwrap().fields;
let payload_converter = PayloadConverter::default();
let removal_payload = payload_converter
.to_payload(
&SerializationContext::new(
&SerializationContextData::Workflow(WorkflowSerializationContext::new()),
&payload_converter,
),
&MemoValue::new(()),
)
.unwrap();
assert_eq!(fields.get("old"), Some(&removal_payload));
assert_eq!(
u32::from_json_payload(fields.get("new").unwrap()).unwrap(),
42
);
}
#[test]
fn upsert_memo_conversion_failure_does_not_mutate_or_emit_command() {
let host = Rc::new(RecordingHost::default());
let init = InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
..Default::default()
};
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "orig-task-queue".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
host.clone(),
None,
Vec::new(),
);
let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
let err = ctx
.upsert_memo([
("valid", Some(MemoValue::new("value".to_string()))),
("invalid", Some(MemoValue::new(FailingMemoValue))),
])
.unwrap_err();
assert_eq!(
err.to_string(),
"Encoding error: memo serialization failure"
);
assert_eq!(ctx.memo().get::<String>("valid").unwrap(), None);
assert!(host.commands.borrow().is_empty());
}
#[test]
fn upsert_search_attributes_unset_removes_from_local_state() {
use temporalio_common_wasm::search_attributes::SearchAttributeKey;
const K: SearchAttributeKey<String> = SearchAttributeKey::keyword("my_kw");
let ctx = test_context();
ctx.upsert_search_attributes([K.value_set("hello".into())]);
assert_eq!(ctx.search_attributes().get(&K), Some("hello".into()));
ctx.upsert_search_attributes([K.value_unset()]);
assert!(!ctx.search_attributes().contains_key(&K));
assert!(ctx.search_attributes().is_empty());
}
#[test]
fn upsert_search_attributes_multiple_updates_last_wins() {
use temporalio_common_wasm::search_attributes::SearchAttributeKey;
const K: SearchAttributeKey<i64> = SearchAttributeKey::int("counter");
let ctx = test_context();
ctx.upsert_search_attributes([K.value_set(1), K.value_set(2)]);
assert_eq!(ctx.search_attributes().get(&K), Some(2));
}
#[test]
fn upsert_search_attributes_merges_with_initial() {
use temporalio_common_wasm::search_attributes::SearchAttributeKey;
const A: SearchAttributeKey<i64> = SearchAttributeKey::int("attr_a");
const B: SearchAttributeKey<String> = SearchAttributeKey::keyword("attr_b");
let init_sa = SearchAttributes::new([A.value_set(1)]).into_proto();
let init = InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
search_attributes: Some(init_sa),
..Default::default()
};
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "tq".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
Rc::new(NoopHost),
None,
Vec::new(),
);
let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
assert_eq!(ctx.search_attributes().get(&A), Some(1));
ctx.upsert_search_attributes([B.value_set("hello".into())]);
assert_eq!(ctx.search_attributes().get(&A), Some(1));
assert_eq!(ctx.search_attributes().get(&B), Some("hello".into()));
}
#[test]
fn view_search_attributes_returns_typed() {
use temporalio_common_wasm::search_attributes::SearchAttributeKey;
const K: SearchAttributeKey<bool> = SearchAttributeKey::bool("active");
let init_sa = SearchAttributes::new([K.value_set(true)]).into_proto();
let init = InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
search_attributes: Some(init_sa),
..Default::default()
};
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "tq".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
Rc::new(NoopHost),
None,
Vec::new(),
);
let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
let view = ctx.view();
let sa = view
.search_attributes()
.expect("should have search attributes");
assert_eq!(sa.get(&K), Some(true));
}
#[test]
fn workflow_info_retains_raw_initialization() {
let init = InitializeWorkflow {
workflow_type: TestWorkflow.name().to_string(),
identity: "raw-only-identity".to_owned(),
..Default::default()
};
let expected = init.clone();
let init = WorkflowInit {
namespace: "default".to_string(),
task_queue: "tq".to_string(),
run_id: "run-id".to_string(),
initialize_workflow: init,
};
let base = BaseWorkflowContext::from_raw(
init,
DataConverter::default(),
Rc::new(NoopHost),
None,
Vec::new(),
);
let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
let info = ctx.info();
assert_eq!(info.raw().identity, "raw-only-identity");
assert_eq!(info.raw(), &expected);
assert_eq!(info.into_raw(), expected);
}
#[test]
fn async_context_values_survive_suspension_and_isolate_concurrent_branches() {
struct Label;
impl WorkflowContextKey for Label {
type Value = &'static str;
}
let ctx = test_context();
let first_poll = Rc::new(Cell::new(true));
let second_poll = Rc::new(Cell::new(true));
let first_ctx = ctx.clone();
let first_poll_in_future = first_poll.clone();
let first = ctx.with_context_value::<Label, _>(
"first",
future::poll_fn(move |_| {
assert_eq!(
first_ctx.context_value::<Label>().as_deref(),
Some(&"first")
);
if first_poll_in_future.replace(false) {
Poll::Pending
} else {
Poll::Ready(())
}
}),
);
let second_ctx = ctx.clone();
let second_poll_in_future = second_poll.clone();
let second = ctx.with_context_value::<Label, _>(
"second",
future::poll_fn(move |_| {
assert_eq!(
second_ctx.context_value::<Label>().as_deref(),
Some(&"second")
);
if second_poll_in_future.replace(false) {
Poll::Pending
} else {
Poll::Ready(())
}
}),
);
let mut joined = Box::pin(futures_util::future::join(first, second));
let waker = futures_util::task::noop_waker();
let mut poll_ctx = Context::from_waker(&waker);
assert!(joined.as_mut().poll(&mut poll_ctx).is_pending());
assert!(ctx.context_value::<Label>().is_none());
assert!(joined.as_mut().poll(&mut poll_ctx).is_ready());
assert!(ctx.context_value::<Label>().is_none());
let mut dropped =
Box::pin(ctx.with_context_value::<Label, _>("dropped", future::pending::<()>()));
assert!(dropped.as_mut().poll(&mut poll_ctx).is_pending());
assert!(ctx.context_value::<Label>().is_none());
drop(dropped);
assert!(ctx.context_value::<Label>().is_none());
}
#[test]
fn context_scopes_inherit_shadow_and_restore_after_panic() {
struct Label;
struct OtherLabel;
struct Count;
impl WorkflowContextKey for Label {
type Value = &'static str;
}
impl WorkflowContextKey for OtherLabel {
type Value = &'static str;
}
impl WorkflowContextKey for Count {
type Value = u32;
}
let ctx = test_context();
ctx.with_context_value_sync::<Label, _>("outer", || {
assert_eq!(ctx.context_value::<Label>().as_deref(), Some(&"outer"));
assert!(ctx.context_value::<OtherLabel>().is_none());
ctx.with_context_value_sync::<Count, _>(7, || {
assert_eq!(ctx.context_value::<Label>().as_deref(), Some(&"outer"));
assert_eq!(ctx.context_value::<Count>().as_deref(), Some(&7));
ctx.with_context_value_sync::<Label, _>("inner", || {
assert_eq!(ctx.context_value::<Label>().as_deref(), Some(&"inner"));
});
assert_eq!(ctx.context_value::<Label>().as_deref(), Some(&"outer"));
});
assert!(ctx.context_value::<Count>().is_none());
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ctx.with_context_value_sync::<Label, _>("panic", || panic!("test panic"));
}));
assert!(result.is_err());
assert_eq!(ctx.context_value::<Label>().as_deref(), Some(&"outer"));
});
assert!(ctx.context_value::<Label>().is_none());
let panic_ctx = ctx.clone();
let mut panic_future = Box::pin(ctx.with_context_value::<Label, _>(
"async-panic",
async move {
assert_eq!(
panic_ctx.context_value::<Label>().as_deref(),
Some(&"async-panic")
);
panic!("async test panic");
},
));
let waker = futures_util::task::noop_waker();
let mut poll_ctx = Context::from_waker(&waker);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
panic_future.as_mut().poll(&mut poll_ctx)
}));
assert!(result.is_err());
assert!(ctx.context_value::<Label>().is_none());
}
}