1#[cfg(feature = "experimental")]
2mod nexus;
3mod options;
4mod view;
5
6#[cfg(feature = "experimental")]
7pub(crate) use nexus::NexusUnblockData;
8#[cfg(feature = "experimental")]
9pub use nexus::StartedNexusOperation;
10pub use options::{
11 ActivityCancellationType, ActivityOptions, ChildWorkflowCancellationType, ChildWorkflowOptions,
12 ContinueAsNewOptions, LocalActivityOptions, ParentClosePolicy, SignalWorkflowOptions,
13 TimerOptions, VersioningIntent, WaitConditionOptions, WorkflowIdReusePolicy,
14};
15#[cfg(feature = "experimental")]
16pub use options::{
17 ContinueAsNewVersioningBehavior, NexusOperationCancellationType, NexusOperationOptions,
18};
19pub use temporalio_common_wasm::protos::coresdk::child_workflow::StartChildWorkflowExecutionFailedCause;
20pub use view::{NamespacedWorkflowInfo, WorkflowContextView};
21
22use crate::{
23 MemoValue, WorkflowCancellationError, WorkflowCancellationToken,
24 runtime::{
25 SdkWakeGuard,
26 entry::WorkflowImplementation,
27 host::WorkflowHost,
28 mark_intercepted_future_activation,
29 model::{
30 CancelExternalWfResult, CancellableID, SignalExternalWfResult, TimerResult,
31 UnblockEvent, Unblockable, WorkflowTermination,
32 },
33 types::WorkflowInit,
34 },
35 workflow_interceptors::{
36 CancelExternalWorkflowInput, CancellableWorkflowOutboundFuture,
37 ChildWorkflowOutboundResult, ContinueAsNewInput, ScheduleActivityInput,
38 ScheduleLocalActivityInput, SignalWorkflowInput, SignalWorkflowResult,
39 SignalWorkflowTarget, StartChildWorkflowInput, StartChildWorkflowResult, StartTimerInput,
40 WorkflowCancellationHandle, WorkflowInterceptor, WorkflowInterceptorConstructor,
41 WorkflowInterceptorContext, WorkflowNext, WorkflowOutboundFuture, WorkflowOutboundValue,
42 call_cancel_external_workflow, call_continue_as_new, call_schedule_activity,
43 call_schedule_local_activity, call_signal_workflow, call_start_child_workflow,
44 call_start_timer,
45 },
46};
47use futures_channel::oneshot;
48use futures_util::{FutureExt, future::FusedFuture, task::Context};
49use rand::SeedableRng;
50use rand_pcg::Pcg64Mcg;
51use siphasher::sip::SipHasher13;
52use std::{
53 cell::{Cell, RefCell},
54 collections::{HashMap, HashSet},
55 future::{self, Future},
56 hash::Hasher,
57 marker::PhantomData,
58 pin::Pin,
59 rc::Rc,
60 sync::{
61 Arc,
62 atomic::{AtomicBool, Ordering},
63 },
64 task::{Poll, Waker},
65 time::SystemTime,
66};
67use temporalio_common_wasm::{
68 ActivityDefinition, Memo, SignalDefinition, WorkflowDefinition,
69 data_converters::{
70 ActivityExecutionDecodeHint, ChildWorkflowExecutionDecodeHint,
71 ChildWorkflowStartDecodeHint, DataConverter, GenericPayloadConverter,
72 PayloadConversionError, PayloadConverter, SerializationContext, SerializationContextData,
73 TemporalDeserializable, WorkflowSerializationContext, WorkflowSignalDecodeHint,
74 },
75 error::{
76 ActivityExecutionError, ChildWorkflowExecutionError, ChildWorkflowStartError,
77 WorkflowSignalError,
78 },
79 protos::{
80 coresdk::{
81 activity_result::{ActivityResolution, Cancellation, activity_resolution},
82 child_workflow::{ChildWorkflowResult, child_workflow_result},
83 common::NamespacedWorkflowExecution,
84 workflow_activation::{
85 InitializeWorkflow, WorkflowActivation as CoreWorkflowActivation,
86 resolve_child_workflow_execution_start::Status as ChildWorkflowStartStatus,
87 workflow_activation_job::Variant as ActivationVariant,
88 },
89 workflow_commands::{
90 CancelChildWorkflowExecution, CancelSignalWorkflow, CancelTimer,
91 ModifyWorkflowProperties, RequestCancelActivity,
92 RequestCancelExternalWorkflowExecution, RequestCancelLocalActivity,
93 RequestCancelNexusOperation, SetPatchMarker, UpsertWorkflowSearchAttributes,
94 signal_external_workflow_execution, workflow_command,
95 },
96 },
97 temporal::api::{
98 common::v1::{Memo as ProtoMemo, Payload, SearchAttributes as ProtoSearchAttributes},
99 failure::v1::{CanceledFailureInfo, Failure, failure::FailureInfo},
100 },
101 utilities::TryIntoOrNone,
102 },
103 search_attributes::{SearchAttributeUpdate, SearchAttributes},
104 worker::WorkerDeploymentVersion,
105};
106use uuid::Builder;
107
108mod private {
109 use rand::distr::{Distribution, StandardUniform};
110 use rand_pcg::Pcg64Mcg;
111
112 pub trait Sealed: Sized {
113 fn sample(rng: &mut Pcg64Mcg) -> Self;
114 }
115
116 pub(super) fn sample<T>(rng: &mut Pcg64Mcg) -> T
117 where
118 StandardUniform: Distribution<T>,
119 {
120 StandardUniform.sample(rng)
121 }
122}
123
124pub trait WorkflowRandomValue: private::Sealed + Sized {}
129
130macro_rules! impl_random_value {
131 ($($ty:ty),* $(,)?) => {
132 $(
133 impl private::Sealed for $ty {
134 fn sample(rng: &mut Pcg64Mcg) -> Self {
135 private::sample(rng)
136 }
137 }
138
139 impl WorkflowRandomValue for $ty {}
140 )*
141 };
142}
143
144impl_random_value!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
145
146#[derive(Clone)]
163pub struct WorkflowRandomStream {
164 source: WorkflowRandomStreamSource,
165 name: String,
166}
167
168#[derive(Clone)]
169enum WorkflowRandomStreamSource {
170 Workflow(Rc<RefCell<WorkflowRandomState>>),
171 System(Rc<RefCell<Pcg64Mcg>>),
172}
173
174impl WorkflowRandomStream {
175 pub fn random<T>(&self) -> T
179 where
180 T: WorkflowRandomValue,
181 {
182 match &self.source {
183 WorkflowRandomStreamSource::Workflow(random) => {
184 random.borrow_mut().named_random(&self.name)
185 }
186 WorkflowRandomStreamSource::System(random) => {
187 <T as private::Sealed>::sample(&mut random.borrow_mut())
188 }
189 }
190 }
191
192 pub fn name(&self) -> &str {
194 &self.name
195 }
196}
197
198fn system_random_stream_source() -> WorkflowRandomStreamSource {
199 #[cfg(not(target_arch = "wasm32"))]
200 let seed = rand::random();
201 #[cfg(target_arch = "wasm32")]
202 let seed = {
203 let mut hasher = std::hash::BuildHasher::build_hasher(&std::hash::RandomState::new());
208 std::hash::Hasher::write(&mut hasher, b"temporal-rust-system-random-stream");
209 std::hash::Hasher::finish(&hasher)
210 };
211
212 WorkflowRandomStreamSource::System(Rc::new(RefCell::new(Pcg64Mcg::seed_from_u64(seed))))
213}
214
215fn named_random_seed(randomness_seed: u64, name: &str) -> u64 {
216 let second_key = randomness_seed ^ u64::from_be_bytes(*b"temporal");
218 let mut hasher = SipHasher13::new_with_keys(randomness_seed, second_key);
219 hasher.write(b"temporal-rust-workflow-random-stream\0");
220 hasher.write(name.as_bytes());
221 hasher.finish()
222}
223
224#[derive(Clone, Debug)]
225pub(super) struct WorkflowRandomState {
226 random: Pcg64Mcg,
227 randomness_seed: u64,
228 named_random: HashMap<String, Pcg64Mcg>,
229}
230
231impl WorkflowRandomState {
232 fn new(randomness_seed: u64) -> Self {
233 Self {
234 random: Pcg64Mcg::seed_from_u64(randomness_seed),
235 randomness_seed,
236 named_random: HashMap::new(),
237 }
238 }
239
240 fn random<T: WorkflowRandomValue>(&mut self) -> T {
241 <T as private::Sealed>::sample(&mut self.random)
242 }
243
244 fn named_random<T: WorkflowRandomValue>(&mut self, name: &str) -> T {
245 let random = self.named_random.entry(name.to_owned()).or_insert_with(|| {
246 Pcg64Mcg::seed_from_u64(named_random_seed(self.randomness_seed, name))
247 });
248 <T as private::Sealed>::sample(random)
249 }
250
251 fn reseed(&mut self, randomness_seed: u64) {
252 self.random = Pcg64Mcg::seed_from_u64(randomness_seed);
253 self.randomness_seed = randomness_seed;
254 self.named_random.clear();
255 }
256}
257
258#[derive(Clone)]
262pub struct BaseWorkflowContext {
263 inner: Rc<WorkflowContextInner>,
264}
265
266#[derive(Clone, Debug)]
268#[non_exhaustive]
269pub struct PatchActivationInput {
270 pub workflow_info: WorkflowContextView,
272 pub patch_id: String,
274}
275
276pub type PatchActivationCallback =
278 Arc<dyn Fn(PatchActivationInput) -> bool + Send + Sync + 'static>;
279
280#[doc(hidden)]
282pub struct PatchActivationCaller {
283 callback: PatchActivationCallback,
284 workflow_info: WorkflowContextView,
285}
286
287impl PatchActivationCaller {
288 pub fn new(
290 callback: PatchActivationCallback,
291 namespace: String,
292 task_queue: String,
293 run_id: String,
294 init: InitializeWorkflow,
295 payload_converter: PayloadConverter,
296 ) -> Self {
297 Self {
298 callback,
299 workflow_info: WorkflowContextView::new(
300 namespace,
301 task_queue,
302 run_id,
303 init,
304 payload_converter,
305 false,
306 None,
307 ),
308 }
309 }
310
311 pub fn call(&self, patch_id: String) -> bool {
313 (self.callback)(PatchActivationInput {
314 workflow_info: self.workflow_info.clone(),
315 patch_id,
316 })
317 }
318}
319
320pub(crate) struct WorkflowPollWakerGuard<'a> {
321 current_waker: &'a RefCell<Option<Waker>>,
322 previous: Option<Waker>,
323}
324
325impl Drop for WorkflowPollWakerGuard<'_> {
326 fn drop(&mut self) {
327 self.current_waker.replace(self.previous.take());
328 }
329}
330
331fn outbound_type_error(
332 value: &str,
333) -> temporalio_common_wasm::data_converters::PayloadConversionError {
334 temporalio_common_wasm::data_converters::PayloadConversionError::EncodingError(Box::new(
335 std::io::Error::new(
336 std::io::ErrorKind::InvalidData,
337 format!("workflow interceptor returned the wrong concrete {value} type"),
338 ),
339 ))
340}
341
342impl BaseWorkflowContext {
343 pub(crate) fn apply_activation_context(
344 &self,
345 activation: &CoreWorkflowActivation,
346 is_replaying_history_events: bool,
347 ) {
348 let new_seed = {
349 let mut shared = self.inner.shared.borrow_mut();
350 shared.activation = activation.clone();
351 shared.is_replaying_history_events = is_replaying_history_events;
352 activation.jobs.iter().find_map(|job| match &job.variant {
353 Some(ActivationVariant::UpdateRandomSeed(attrs)) => Some(attrs.randomness_seed),
354 _ => None,
355 })
356 };
357 if let Some(seed) = new_seed {
358 self.inner.random.borrow_mut().reseed(seed);
359 }
360 }
361
362 fn random<T>(&self) -> T
363 where
364 T: WorkflowRandomValue,
365 {
366 self.inner.random.borrow_mut().random()
367 }
368
369 pub(crate) fn random_stream(&self, name: impl Into<String>) -> WorkflowRandomStream {
370 WorkflowRandomStream {
371 source: WorkflowRandomStreamSource::Workflow(self.inner.random.clone()),
372 name: name.into(),
373 }
374 }
375
376 fn uuid4(&self) -> String {
377 Builder::from_random_bytes(self.random::<u128>().to_be_bytes())
378 .into_uuid()
379 .hyphenated()
380 .to_string()
381 }
382
383 pub fn data_converter(&self) -> &DataConverter {
385 &self.inner.data_converter
386 }
387
388 pub fn workflow_id(&self) -> &str {
390 &self.inner.initial_information.workflow_id
391 }
392
393 pub fn run_id(&self) -> &str {
395 &self.inner.run_id
396 }
397
398 pub fn namespace(&self) -> &str {
400 &self.inner.namespace
401 }
402
403 pub fn task_queue(&self) -> &str {
405 &self.inner.task_queue
406 }
407
408 pub fn workflow_type(&self) -> &str {
410 &self.inner.initial_information.workflow_type
411 }
412
413 pub(crate) fn initial_headers(&self) -> HashMap<String, Payload> {
414 self.inner.initial_information.headers.clone()
415 }
416
417 pub fn workflow_time(&self) -> Option<SystemTime> {
419 self.inner
420 .shared
421 .borrow()
422 .activation
423 .timestamp
424 .try_into_or_none()
425 }
426
427 pub fn history_length(&self) -> u32 {
429 self.inner.shared.borrow().activation.history_length
430 }
431
432 pub fn search_attributes(&self) -> SearchAttributes {
434 SearchAttributes::from_proto(&self.inner.shared.borrow().search_attributes)
435 }
436
437 pub fn is_replaying(&self) -> bool {
439 self.inner.shared.borrow().activation.is_replaying
440 }
441
442 pub fn is_replaying_history_events(&self) -> bool {
444 self.inner.shared.borrow().is_replaying_history_events
445 }
446
447 fn requires_replay_safety(&self) -> bool {
448 self.inner.requires_replay_safety.get()
449 }
450
451 pub(crate) fn enter_read_only(&self) -> ReadOnlyGuard {
452 let previous = self.inner.requires_replay_safety.replace(false);
453 ReadOnlyGuard {
454 base: self.clone(),
455 previous,
456 }
457 }
458
459 pub fn payload_converter(&self) -> &PayloadConverter {
461 self.inner.data_converter.payload_converter()
462 }
463
464 pub(crate) fn construction_waker(&self) -> Waker {
465 self.inner
466 .current_waker
467 .borrow()
468 .clone()
469 .unwrap_or_else(|| Waker::noop().clone())
470 }
471
472 pub(crate) fn enter_runtime_poll<'a>(&'a self, waker: &Waker) -> WorkflowPollWakerGuard<'a> {
473 WorkflowPollWakerGuard {
474 previous: self.inner.current_waker.replace(Some(waker.clone())),
475 current_waker: &self.inner.current_waker,
476 }
477 }
478
479 pub(crate) fn notify_patch(&self, patch_id: String) {
480 self.inner
481 .shared
482 .borrow_mut()
483 .notified_patches
484 .insert(patch_id);
485 }
486
487 fn prepare_outbound_future<T>(
488 &self,
489 mut future: WorkflowOutboundFuture<T>,
490 ) -> WorkflowOutboundFuture<T> {
491 let waker = self.construction_waker();
492 let mut cx = Context::from_waker(&waker);
493 future.poll_for_construction(&mut cx);
494 future
495 }
496
497 fn prepare_cancellable_outbound_future<T>(
498 &self,
499 mut future: CancellableWorkflowOutboundFuture<T>,
500 ) -> CancellableWorkflowOutboundFuture<T> {
501 let waker = self.construction_waker();
502 let mut cx = Context::from_waker(&waker);
503 future.poll_for_construction(&mut cx);
504 future
505 }
506
507 pub(crate) fn view(&self) -> WorkflowContextView {
509 let shared = self.inner.shared.borrow();
510 let mut initial_information = self.inner.initial_information.clone();
511 if initial_information.memo.is_some() || !shared.memo.fields.is_empty() {
512 initial_information.memo = Some(shared.memo.clone());
513 }
514 WorkflowContextView::new(
515 self.inner.namespace.clone(),
516 self.inner.task_queue.clone(),
517 self.inner.run_id.clone(),
518 initial_information,
519 self.inner.data_converter.payload_converter().clone(),
520 self.requires_replay_safety(),
521 Some(self.inner.random.clone()),
522 )
523 }
524}
525
526#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
527enum PendingCommandId {
528 Timer(u32),
529 Activity(u32),
530 ChildWorkflowStart(u32),
531 ChildWorkflowComplete(u32),
532 SignalExternal(u32),
533 CancelExternal(u32),
534 NexusOpStart(u32),
535 NexusOpComplete(u32),
536}
537
538impl PendingCommandId {
539 fn from_unblock_event(event: &UnblockEvent) -> Self {
540 match event {
541 UnblockEvent::Timer(seq, _) => Self::Timer(*seq),
542 UnblockEvent::Activity(seq, _) => Self::Activity(*seq),
543 UnblockEvent::WorkflowStart(seq, _) => Self::ChildWorkflowStart(*seq),
544 UnblockEvent::WorkflowComplete(seq, _) => Self::ChildWorkflowComplete(*seq),
545 UnblockEvent::SignalExternal(seq, _) => Self::SignalExternal(*seq),
546 UnblockEvent::CancelExternal(seq, _) => Self::CancelExternal(*seq),
547 UnblockEvent::NexusOperationStart(seq, _) => Self::NexusOpStart(*seq),
548 UnblockEvent::NexusOperationComplete(seq, _) => Self::NexusOpComplete(*seq),
549 }
550 }
551}
552
553struct WorkflowRuntimeState {
554 host: Rc<dyn WorkflowHost>,
555 pending_unblocks: RefCell<HashMap<PendingCommandId, oneshot::Sender<UnblockEvent>>>,
556 forced_wft_failure: RefCell<Option<Box<dyn std::error::Error + Send + Sync>>>,
557 progress_made: Cell<bool>,
558}
559
560impl WorkflowRuntimeState {
561 fn new(host: Rc<dyn WorkflowHost>) -> Self {
562 Self {
563 host,
564 pending_unblocks: RefCell::new(HashMap::new()),
565 forced_wft_failure: RefCell::new(None),
566 progress_made: Cell::new(false),
567 }
568 }
569
570 fn register_unblocker(&self, id: PendingCommandId, unblocker: oneshot::Sender<UnblockEvent>) {
571 self.pending_unblocks.borrow_mut().insert(id, unblocker);
572 }
573
574 fn unblock(&self, event: UnblockEvent) -> Result<(), anyhow::Error> {
575 let id = PendingCommandId::from_unblock_event(&event);
576 let unblocker = self
577 .pending_unblocks
578 .borrow_mut()
579 .remove(&id)
580 .ok_or_else(|| anyhow::anyhow!("Command {id:?} not found to unblock"))?;
581 self.progress_made.set(true);
582 let _guard = SdkWakeGuard::new();
583 let _ = unblocker.send(event);
584 Ok(())
585 }
586
587 fn maybe_unblock(&self, event: UnblockEvent) -> bool {
588 let id = PendingCommandId::from_unblock_event(&event);
589 let Some(unblocker) = self.pending_unblocks.borrow_mut().remove(&id) else {
590 return false;
591 };
592 self.progress_made.set(true);
593 let _guard = SdkWakeGuard::new();
594 let _ = unblocker.send(event);
595 true
596 }
597
598 fn set_forced_wft_failure(&self, err: Box<dyn std::error::Error + Send + Sync>) {
599 *self.forced_wft_failure.borrow_mut() = Some(err);
600 self.progress_made.set(true);
601 }
602
603 fn take_forced_wft_failure(&self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
604 self.forced_wft_failure.borrow_mut().take()
605 }
606
607 fn mark_progress(&self) {
608 self.progress_made.set(true);
609 }
610
611 fn take_progress(&self) -> bool {
612 self.progress_made.replace(false)
613 }
614}
615
616struct WorkflowContextInner {
617 namespace: String,
618 task_queue: String,
619 run_id: String,
620 initial_information: InitializeWorkflow,
621 runtime: WorkflowRuntimeState,
622 cancellation_token: WorkflowCancellationToken,
623 cancelled_operations: RefCell<HashSet<CancellableSeqNum>>,
624 shared: RefCell<WorkflowContextSharedData>,
625 random: Rc<RefCell<WorkflowRandomState>>,
626 seq_nums: RefCell<WfCtxProtectedDat>,
627 data_converter: DataConverter,
628 patch_activation_callback: Option<PatchActivationCallback>,
629 state_mutated: Cell<bool>,
630 active_handlers: Cell<usize>,
631 requires_replay_safety: Cell<bool>,
632 condition_wakers: RefCell<Vec<Waker>>,
633 current_waker: RefCell<Option<Waker>>,
634 workflow_interceptors: Rc<[Arc<dyn WorkflowInterceptor>]>,
635}
636
637pub(crate) struct HandlerExecutionGuard {
638 base: BaseWorkflowContext,
639}
640
641pub(crate) struct ReadOnlyGuard {
642 base: BaseWorkflowContext,
643 previous: bool,
644}
645
646impl Drop for ReadOnlyGuard {
647 fn drop(&mut self) {
648 self.base.inner.requires_replay_safety.set(self.previous);
649 }
650}
651
652impl Drop for HandlerExecutionGuard {
653 fn drop(&mut self) {
654 let active_handlers = self.base.inner.active_handlers.get();
655 debug_assert!(active_handlers > 0, "handler execution count underflow");
656 self.base
657 .inner
658 .active_handlers
659 .set(active_handlers.saturating_sub(1));
660 if active_handlers <= 1 {
661 self.base.wake_condition_waiters();
662 }
663 }
664}
665
666#[derive(Eq, Hash, PartialEq)]
668enum CancellableSeqNum {
669 Timer(u32),
670 Activity(u32),
671 LocalActivity(u32),
672 ChildWorkflow(u32),
673 SignalExternalWorkflow(u32),
674 NexusOp(u32),
675}
676
677impl From<&CancellableID> for CancellableSeqNum {
678 fn from(value: &CancellableID) -> Self {
679 match value {
680 CancellableID::Timer(seq) => Self::Timer(*seq),
681 CancellableID::Activity(seq) => Self::Activity(*seq),
682 CancellableID::LocalActivity(seq) => Self::LocalActivity(*seq),
683 CancellableID::ChildWorkflow { seqnum, .. } => Self::ChildWorkflow(*seqnum),
684 CancellableID::SignalExternalWorkflow(seq) => Self::SignalExternalWorkflow(*seq),
685 CancellableID::NexusOp(seq) => Self::NexusOp(*seq),
686 }
687 }
688}
689
690pub struct SyncWorkflowContext<W> {
698 base: BaseWorkflowContext,
699 headers: Rc<HashMap<String, Payload>>,
701 _phantom: PhantomData<W>,
702}
703
704impl<W> Clone for SyncWorkflowContext<W> {
705 fn clone(&self) -> Self {
706 Self {
707 base: self.base.clone(),
708 headers: self.headers.clone(),
709 _phantom: PhantomData,
710 }
711 }
712}
713
714pub struct WorkflowContext<W> {
719 sync: SyncWorkflowContext<W>,
720 workflow_state: Rc<RefCell<W>>,
722}
723
724impl<W> Clone for WorkflowContext<W> {
725 fn clone(&self) -> Self {
726 Self {
727 sync: self.sync.clone(),
728 workflow_state: self.workflow_state.clone(),
729 }
730 }
731}
732
733impl BaseWorkflowContext {
734 #[doc(hidden)]
736 pub fn from_raw(
737 init: WorkflowInit,
738 data_converter: DataConverter,
739 host: Rc<dyn WorkflowHost>,
740 patch_activation_callback: Option<PatchActivationCallback>,
741 workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
742 ) -> Self {
743 let WorkflowInit {
744 namespace,
745 task_queue,
746 run_id,
747 initialize_workflow,
748 } = init;
749 let random = Rc::new(RefCell::new(WorkflowRandomState::new(
750 initialize_workflow.randomness_seed,
751 )));
752 let view = WorkflowContextView::new(
753 namespace,
754 task_queue,
755 run_id,
756 initialize_workflow,
757 data_converter.payload_converter().clone(),
758 true,
759 Some(random.clone()),
760 );
761 let workflow_interceptors = workflow_interceptor_constructors
762 .into_iter()
763 .map(|constructor| constructor.construct(&view))
764 .collect::<Vec<_>>()
765 .into();
766 let (namespace, task_queue, run_id, init_workflow_job) = view.into_parts();
767 Self {
768 inner: Rc::new(WorkflowContextInner {
769 namespace,
770 task_queue,
771 run_id,
772 shared: RefCell::new(WorkflowContextSharedData {
773 memo: init_workflow_job.memo.clone().unwrap_or_default(),
774 search_attributes: init_workflow_job
775 .search_attributes
776 .clone()
777 .unwrap_or_default(),
778 is_replaying_history_events: false,
779 changes: Default::default(),
780 activation: Default::default(),
781 current_details: Default::default(),
782 notified_patches: Default::default(),
783 }),
784 random,
785 initial_information: init_workflow_job,
786 runtime: WorkflowRuntimeState::new(host),
787 cancellation_token: WorkflowCancellationToken::new(),
788 cancelled_operations: Default::default(),
789 seq_nums: RefCell::new(WfCtxProtectedDat {
790 next_timer_sequence_number: 1,
791 next_activity_sequence_number: 1,
792 next_child_workflow_sequence_number: 1,
793 next_cancel_external_wf_sequence_number: 1,
794 next_signal_external_wf_sequence_number: 1,
795 #[cfg(feature = "experimental")]
796 next_nexus_op_sequence_number: 1,
797 }),
798 data_converter,
799 patch_activation_callback,
800 state_mutated: Cell::new(false),
801 active_handlers: Cell::new(0),
802 requires_replay_safety: Cell::new(true),
803 condition_wakers: Default::default(),
804 current_waker: RefCell::new(None),
805 workflow_interceptors,
806 }),
807 }
808 }
809
810 pub(crate) fn workflow_interceptors(&self) -> Rc<[Arc<dyn WorkflowInterceptor>]> {
811 self.inner.workflow_interceptors.clone()
812 }
813
814 pub(crate) fn take_state_mutated(&self) -> bool {
817 self.inner.state_mutated.replace(false)
818 }
819
820 pub(crate) fn set_state_mutated(&self) {
822 self.inner.state_mutated.set(true);
823 }
824
825 pub(crate) fn all_handlers_finished(&self) -> bool {
826 self.inner.active_handlers.get() == 0
827 }
828
829 pub(crate) fn track_handler(&self) -> HandlerExecutionGuard {
830 self.inner
831 .active_handlers
832 .set(self.inner.active_handlers.get() + 1);
833 HandlerExecutionGuard { base: self.clone() }
834 }
835
836 fn wake_condition_waiters(&self) {
837 let _guard = SdkWakeGuard::new();
838 for waker in self.inner.condition_wakers.borrow_mut().drain(..) {
839 waker.wake();
840 }
841 }
842
843 pub(crate) fn take_runtime_progress(&self) -> bool {
844 self.inner.runtime.take_progress()
845 }
846
847 pub(crate) fn take_forced_wft_failure(
848 &self,
849 ) -> Option<Box<dyn std::error::Error + Send + Sync>> {
850 self.inner.runtime.take_forced_wft_failure()
851 }
852
853 pub(crate) fn notify_cancel(&self, reason: String) {
854 if reason.is_empty() {
855 self.inner.cancellation_token.cancel();
856 } else {
857 self.inner.cancellation_token.cancel_with_reason(reason);
858 }
859 self.inner.runtime.mark_progress();
860 }
861
862 pub fn cancellation_token(&self) -> WorkflowCancellationToken {
864 self.inner.cancellation_token.clone()
865 }
866
867 pub(crate) fn unblock(&self, event: UnblockEvent) -> Result<(), anyhow::Error> {
868 self.inner.runtime.unblock(event)
869 }
870
871 fn cancel(&self, cancellable_id: CancellableID) {
873 if !self
874 .inner
875 .cancelled_operations
876 .borrow_mut()
877 .insert((&cancellable_id).into())
878 {
879 return;
880 }
881 match cancellable_id {
882 CancellableID::Timer(seq) => {
883 if self
884 .inner
885 .runtime
886 .maybe_unblock(UnblockEvent::Timer(seq, TimerResult::Cancelled))
887 {
888 self.inner.runtime.host.push_command(
889 workflow_command::Variant::CancelTimer(CancelTimer { seq }).into(),
890 );
891 }
892 }
893 CancellableID::Activity(seq) => {
894 self.inner.runtime.host.push_command(
895 workflow_command::Variant::RequestCancelActivity(RequestCancelActivity { seq })
896 .into(),
897 );
898 }
899 CancellableID::LocalActivity(seq) => {
900 self.inner.runtime.host.push_command(
901 workflow_command::Variant::RequestCancelLocalActivity(
902 RequestCancelLocalActivity { seq },
903 )
904 .into(),
905 );
906 }
907 CancellableID::ChildWorkflow { seqnum, reason } => {
908 self.inner.runtime.host.push_command(
909 workflow_command::Variant::CancelChildWorkflowExecution(
910 CancelChildWorkflowExecution {
911 child_workflow_seq: seqnum,
912 reason,
913 },
914 )
915 .into(),
916 );
917 }
918 CancellableID::SignalExternalWorkflow(seq) => {
919 self.inner.runtime.host.push_command(
920 workflow_command::Variant::CancelSignalWorkflow(CancelSignalWorkflow { seq })
921 .into(),
922 );
923 }
924 CancellableID::NexusOp(seq) => {
925 self.inner.runtime.host.push_command(
926 workflow_command::Variant::RequestCancelNexusOperation(
927 RequestCancelNexusOperation { seq },
928 )
929 .into(),
930 );
931 }
932 }
933 }
934
935 fn cancellation_handle(&self, cancellable_id: CancellableID) -> WorkflowCancellationHandle {
936 let base_ctx = self.clone();
937 WorkflowCancellationHandle::new(move |reason| {
938 let id = reason.map_or_else(
939 || cancellable_id.clone(),
940 |reason| cancellable_id.clone().with_reason(reason),
941 );
942 base_ctx.cancel(id);
943 })
944 }
945
946 pub fn current_details(&self) -> String {
948 self.inner.shared.borrow().current_details.clone()
949 }
950
951 pub fn timer<T: Into<TimerOptions>>(
953 &self,
954 opts: T,
955 ) -> impl CancellableFuture<Output = TimerResult> + use<T> {
956 let input = StartTimerInput::new(opts.into());
957 let base_ctx = self.clone();
958 let next = WorkflowNext::new(move |input: StartTimerInput| {
959 let mut opts = input.into_options();
960 let cancellation_token = opts
961 .cancellation_token
962 .take()
963 .unwrap_or_else(|| base_ctx.cancellation_token());
964 let seq = base_ctx.inner.seq_nums.borrow_mut().next_timer_seq();
965 let (cmd, unblocker) =
966 CancellableWFCommandFut::new(CancellableID::Timer(seq), base_ctx.clone());
967 base_ctx
968 .inner
969 .runtime
970 .register_unblocker(PendingCommandId::Timer(seq), unblocker);
971 base_ctx
972 .inner
973 .runtime
974 .host
975 .push_command(opts.into_command(seq));
976 CancellableWorkflowOutboundFuture::new(
977 cmd,
978 base_ctx.cancellation_handle(CancellableID::Timer(seq)),
979 )
980 .with_cancellation_token(cancellation_token)
981 });
982 let interceptors = self.inner.workflow_interceptors.clone();
983 let future = call_start_timer(
984 interceptors,
985 WorkflowInterceptorContext::new(self.clone()),
986 input,
987 next,
988 );
989 self.prepare_cancellable_outbound_future(future)
990 }
991
992 #[allow(clippy::result_large_err)]
994 pub fn execute_activity<AD: ActivityDefinition>(
995 &self,
996 activity: AD,
997 input: impl Into<AD::Input>,
998 opts: ActivityOptions,
999 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1000 where
1001 AD::Output: TemporalDeserializable,
1002 {
1003 let input =
1004 ScheduleActivityInput::new(activity.name().to_string(), Box::new(input.into()), opts);
1005 let base_ctx = self.clone();
1006 let next = WorkflowNext::new(move |input: ScheduleActivityInput| {
1007 let (activity_type, input, headers, mut opts) = input.into_parts();
1008 let input = match input.downcast::<AD::Input>() {
1009 Ok(input) => *input,
1010 Err(_) => {
1011 return CancellableWorkflowOutboundFuture::new(
1012 async {
1013 Err(ActivityExecutionError::Serialization(outbound_type_error(
1014 "activity input",
1015 )))
1016 },
1017 WorkflowCancellationHandle::noop(),
1018 );
1019 }
1020 };
1021 let payload_converter = base_ctx.inner.data_converter.payload_converter();
1022 let context_data =
1023 SerializationContextData::Workflow(WorkflowSerializationContext::new());
1024 let ctx = SerializationContext::new(&context_data, payload_converter);
1025 match payload_converter.to_payloads(&ctx, &input) {
1026 Ok(payloads) => {
1027 let cancellation_token = opts
1028 .cancellation_token
1029 .take()
1030 .unwrap_or_else(|| base_ctx.cancellation_token());
1031 let seq = base_ctx.inner.seq_nums.borrow_mut().next_activity_seq();
1032 let (cmd, unblocker) = CancellableWFCommandFut::new(
1033 CancellableID::Activity(seq),
1034 base_ctx.clone(),
1035 );
1036 base_ctx
1037 .inner
1038 .runtime
1039 .register_unblocker(PendingCommandId::Activity(seq), unblocker);
1040 if opts.task_queue.is_none() {
1041 opts.task_queue = Some(base_ctx.inner.task_queue.clone());
1042 }
1043 base_ctx.inner.runtime.host.push_command(opts.into_command(
1044 seq,
1045 activity_type,
1046 payloads,
1047 headers,
1048 ));
1049 CancellableWorkflowOutboundFuture::new(
1050 ActivityFut::running(cmd, base_ctx.inner.data_converter.clone()),
1051 base_ctx.cancellation_handle(CancellableID::Activity(seq)),
1052 )
1053 .with_cancellation_token(cancellation_token)
1054 }
1055 Err(err) => CancellableWorkflowOutboundFuture::new(
1056 ActivityFut::<future::Ready<ActivityResolution>, AD::Output>::eager(err.into()),
1057 WorkflowCancellationHandle::noop(),
1058 ),
1059 }
1060 .map(|result| result.map(|output| Box::new(output) as Box<dyn WorkflowOutboundValue>))
1061 });
1062 let interceptors = self.inner.workflow_interceptors.clone();
1063 let future = call_schedule_activity(
1064 interceptors,
1065 WorkflowInterceptorContext::new(self.clone()),
1066 input,
1067 next,
1068 )
1069 .map(|result| {
1070 result.and_then(|output| {
1071 output
1072 .downcast::<AD::Output>()
1073 .map(|output| *output)
1074 .map_err(|_| {
1075 ActivityExecutionError::Serialization(outbound_type_error(
1076 "activity output",
1077 ))
1078 })
1079 })
1080 });
1081 self.prepare_cancellable_outbound_future(future)
1082 }
1083
1084 #[allow(clippy::result_large_err)]
1086 pub fn execute_local_activity<AD: ActivityDefinition>(
1087 &self,
1088 activity: AD,
1089 input: impl Into<AD::Input>,
1090 opts: LocalActivityOptions,
1091 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1092 where
1093 AD::Output: TemporalDeserializable,
1094 {
1095 let input = ScheduleLocalActivityInput::new(
1096 activity.name().to_string(),
1097 Box::new(input.into()),
1098 opts,
1099 );
1100 let base_ctx = self.clone();
1101 let next = WorkflowNext::new(move |input: ScheduleLocalActivityInput| {
1102 let (activity_type, input, headers, mut opts) = input.into_parts();
1103 let input = match input.downcast::<AD::Input>() {
1104 Ok(input) => *input,
1105 Err(_) => {
1106 return CancellableWorkflowOutboundFuture::new(
1107 async {
1108 Err(ActivityExecutionError::Serialization(outbound_type_error(
1109 "local activity input",
1110 )))
1111 },
1112 WorkflowCancellationHandle::noop(),
1113 );
1114 }
1115 };
1116 let payload_converter = base_ctx.inner.data_converter.payload_converter();
1117 let context_data =
1118 SerializationContextData::Workflow(WorkflowSerializationContext::new());
1119 let ctx = SerializationContext::new(&context_data, payload_converter);
1120 match payload_converter.to_payloads(&ctx, &input) {
1121 Ok(payloads) => {
1122 let cancellation_token = opts
1123 .cancellation_token
1124 .take()
1125 .unwrap_or_else(|| base_ctx.cancellation_token());
1126 let future = LATimerBackoffFut::new(
1127 activity_type,
1128 payloads,
1129 headers,
1130 opts,
1131 cancellation_token.clone(),
1132 base_ctx.clone(),
1133 );
1134 cancellable_outbound(ActivityFut::running(
1135 future,
1136 base_ctx.inner.data_converter.clone(),
1137 ))
1138 .with_cancellation_token(cancellation_token)
1139 }
1140 Err(err) => CancellableWorkflowOutboundFuture::new(
1141 ActivityFut::<future::Ready<ActivityResolution>, AD::Output>::eager(err.into()),
1142 WorkflowCancellationHandle::noop(),
1143 ),
1144 }
1145 .map(|result| result.map(|output| Box::new(output) as Box<dyn WorkflowOutboundValue>))
1146 });
1147 let interceptors = self.inner.workflow_interceptors.clone();
1148 let future = call_schedule_local_activity(
1149 interceptors,
1150 WorkflowInterceptorContext::new(self.clone()),
1151 input,
1152 next,
1153 )
1154 .map(|result| {
1155 result.and_then(|output| {
1156 output
1157 .downcast::<AD::Output>()
1158 .map(|output| *output)
1159 .map_err(|_| {
1160 ActivityExecutionError::Serialization(outbound_type_error(
1161 "local activity output",
1162 ))
1163 })
1164 })
1165 });
1166 self.prepare_cancellable_outbound_future(future)
1167 }
1168
1169 pub(crate) fn start_child_workflow<WD: WorkflowDefinition + 'static>(
1171 &self,
1172 workflow: WD,
1173 input: impl Into<WD::Input>,
1174 opts: ChildWorkflowOptions,
1175 ) -> impl CancellableFutureWithReason<
1176 Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
1177 >
1178 where
1179 WD::Output: TemporalDeserializable,
1180 {
1181 let input =
1182 StartChildWorkflowInput::new(workflow.name().to_string(), Box::new(input.into()), opts);
1183 let base_ctx = self.clone();
1184 let next = WorkflowNext::new(move |input: StartChildWorkflowInput| {
1185 let (workflow_type, input, headers, mut opts) = input.into_parts();
1186 let input = match input.downcast::<WD::Input>() {
1187 Ok(input) => *input,
1188 Err(_) => {
1189 return CancellableWorkflowOutboundFuture::new(
1190 async {
1191 Err(ChildWorkflowStartError::Serialization(outbound_type_error(
1192 "child workflow input",
1193 )))
1194 },
1195 WorkflowCancellationHandle::noop(),
1196 );
1197 }
1198 };
1199 let payload_converter = base_ctx.inner.data_converter.payload_converter();
1200 let context_data =
1201 SerializationContextData::Workflow(WorkflowSerializationContext::new());
1202 let ctx = SerializationContext::new(&context_data, payload_converter);
1203 let payloads = match payload_converter.to_payloads(&ctx, &input) {
1204 Ok(payloads) => payloads,
1205 Err(err) => {
1206 return CancellableWorkflowOutboundFuture::new(
1207 ChildWorkflowStartFut::<future::Ready<PendingChildWorkflow<WD>>, WD>::eager(
1208 err.into(),
1209 ),
1210 WorkflowCancellationHandle::noop(),
1211 );
1212 }
1213 };
1214 let workflow_id = opts
1215 .workflow_id
1216 .take()
1217 .filter(|id| !id.is_empty())
1218 .unwrap_or_else(|| base_ctx.uuid4());
1219 let cancellation_token = opts
1220 .cancellation_token
1221 .take()
1222 .unwrap_or_else(|| base_ctx.cancellation_token());
1223
1224 let child_seq = base_ctx
1225 .inner
1226 .seq_nums
1227 .borrow_mut()
1228 .next_child_workflow_seq();
1229 let (result_cmd, unblocker) = CancellableWFCommandFut::new(
1233 CancellableID::ChildWorkflow {
1234 seqnum: child_seq,
1235 reason: String::new(),
1236 },
1237 base_ctx.clone(),
1238 );
1239 base_ctx.inner.runtime.register_unblocker(
1240 PendingCommandId::ChildWorkflowComplete(child_seq),
1241 unblocker,
1242 );
1243 base_ctx.inner.runtime.host.push_command(opts.into_command(
1244 child_seq,
1245 workflow_type,
1246 payloads,
1247 headers,
1248 workflow_id.clone(),
1249 ));
1250
1251 let result_future =
1252 cancellable_outbound_with_reason(ChildWorkflowFut::<_, WD::Output>::Running {
1253 inner: result_cmd,
1254 data_converter: base_ctx.inner.data_converter.clone(),
1255 _phantom: PhantomData,
1256 })
1257 .map(|result| {
1258 result.map(|output| Box::new(output) as Box<dyn WorkflowOutboundValue>)
1259 })
1260 .with_cancellation_token(cancellation_token);
1261
1262 let common = ChildWfCommon {
1263 workflow_id: workflow_id.clone(),
1264 child_seq,
1265 result_future,
1266 base_ctx: base_ctx.clone(),
1267 };
1268
1269 let (cmd, unblocker) =
1270 CancellableWFCommandFut::<PendingChildWorkflow<WD>, ChildWfCommon>::new_with_dat(
1271 CancellableID::ChildWorkflow {
1272 seqnum: child_seq,
1273 reason: String::new(),
1274 },
1275 common,
1276 base_ctx.clone(),
1277 );
1278 base_ctx
1279 .inner
1280 .runtime
1281 .register_unblocker(PendingCommandId::ChildWorkflowStart(child_seq), unblocker);
1282
1283 cancellable_outbound_with_reason(ChildWorkflowStartFut::Running(cmd))
1284 });
1285 let interceptors = self.inner.workflow_interceptors.clone();
1286 let future = call_start_child_workflow(
1287 interceptors,
1288 WorkflowInterceptorContext::new(self.clone()),
1289 input,
1290 next,
1291 )
1292 .map(|result| result.map(StartChildWorkflowOutput::into_started));
1293 self.prepare_cancellable_outbound_future(future)
1294 }
1295
1296 fn local_activity_no_timer_retry(
1298 self,
1299 activity_type: String,
1300 arguments: Vec<Payload>,
1301 headers: HashMap<String, Payload>,
1302 opts: LocalActivityOptions,
1303 ) -> impl CancellableFuture<Output = ActivityResolution> {
1304 let seq = self.inner.seq_nums.borrow_mut().next_activity_seq();
1305 let (cmd, unblocker) =
1306 CancellableWFCommandFut::new(CancellableID::LocalActivity(seq), self.clone());
1307 self.inner
1308 .runtime
1309 .register_unblocker(PendingCommandId::Activity(seq), unblocker);
1310 self.inner.runtime.host.push_command(opts.into_command(
1311 seq,
1312 activity_type,
1313 arguments,
1314 headers,
1315 ));
1316 cmd
1317 }
1318
1319 fn signal_workflow<S: SignalDefinition + 'static>(
1320 &self,
1321 target: SignalWorkflowTarget,
1322 signal: S,
1323 input: S::Input,
1324 options: SignalWorkflowOptions,
1325 ) -> CancellableWorkflowOutboundFuture<SignalWorkflowResult> {
1326 let input = SignalWorkflowInput::new(
1327 S::name(&signal).to_string(),
1328 target,
1329 Box::new(input),
1330 options,
1331 );
1332 let base_ctx = self.clone();
1333 let next = WorkflowNext::new(move |input: SignalWorkflowInput| {
1334 let (signal_name, target, input, headers, mut options) = input.into_parts();
1335 let cancellation_token = options
1336 .cancellation_token
1337 .take()
1338 .unwrap_or_else(|| base_ctx.cancellation_token());
1339 let input = match input.downcast::<S::Input>() {
1340 Ok(input) => *input,
1341 Err(_) => {
1342 return CancellableWorkflowOutboundFuture::new(
1343 async {
1344 Err(WorkflowSignalError::Serialization(outbound_type_error(
1345 "signal input",
1346 )))
1347 },
1348 WorkflowCancellationHandle::noop(),
1349 );
1350 }
1351 };
1352 let payload_converter = base_ctx.data_converter().payload_converter();
1353 let context_data =
1354 SerializationContextData::Workflow(WorkflowSerializationContext::new());
1355 let ctx = SerializationContext::new(&context_data, payload_converter);
1356 let payloads = match payload_converter.to_payloads(&ctx, &input) {
1357 Ok(payloads) => payloads,
1358 Err(err) => {
1359 return CancellableWorkflowOutboundFuture::new(
1360 async move { Err(err.into()) },
1361 WorkflowCancellationHandle::noop(),
1362 );
1363 }
1364 };
1365 let target = match target {
1366 SignalWorkflowTarget::Child { workflow_id } => {
1367 signal_external_workflow_execution::Target::ChildWorkflowId(workflow_id)
1368 }
1369 SignalWorkflowTarget::External {
1370 namespace,
1371 workflow_id,
1372 run_id,
1373 } => signal_external_workflow_execution::Target::WorkflowExecution(
1374 NamespacedWorkflowExecution {
1375 namespace,
1376 workflow_id,
1377 run_id: run_id.unwrap_or_default(),
1378 },
1379 ),
1380 };
1381 let seq = base_ctx
1382 .inner
1383 .seq_nums
1384 .borrow_mut()
1385 .next_signal_external_wf_seq();
1386 let (cmd, unblocker) = CancellableWFCommandFut::new(
1387 CancellableID::SignalExternalWorkflow(seq),
1388 base_ctx.clone(),
1389 );
1390 base_ctx
1391 .inner
1392 .runtime
1393 .register_unblocker(PendingCommandId::SignalExternal(seq), unblocker);
1394 base_ctx
1395 .inner
1396 .runtime
1397 .host
1398 .push_command(options.into_command(seq, signal_name, payloads, headers, target));
1399 cancellable_outbound(SignalChildFut::Running {
1400 inner: cmd,
1401 data_converter: base_ctx.data_converter().clone(),
1402 })
1403 .with_cancellation_token(cancellation_token)
1404 });
1405 let interceptors = self.inner.workflow_interceptors.clone();
1406 let future = call_signal_workflow(
1407 interceptors,
1408 WorkflowInterceptorContext::new(self.clone()),
1409 input,
1410 next,
1411 );
1412 self.prepare_cancellable_outbound_future(future)
1413 }
1414
1415 pub(crate) fn external_workflow(
1416 &self,
1417 workflow_id: impl Into<String>,
1418 run_id: Option<String>,
1419 ) -> ExternalWorkflowHandle {
1420 ExternalWorkflowHandle {
1421 workflow_id: workflow_id.into(),
1422 run_id,
1423 namespace: self.inner.namespace.clone(),
1424 base_ctx: self.clone(),
1425 }
1426 }
1427
1428 fn cancel_external_workflow(
1429 &self,
1430 input: CancelExternalWorkflowInput,
1431 ) -> WorkflowOutboundFuture<CancelExternalWfResult> {
1432 let base_ctx = self.clone();
1433 let next = WorkflowNext::new(move |input: CancelExternalWorkflowInput| {
1434 let seq = base_ctx
1435 .inner
1436 .seq_nums
1437 .borrow_mut()
1438 .next_cancel_external_wf_seq();
1439 let (cmd, unblocker) = WFCommandFut::new();
1440 base_ctx
1441 .inner
1442 .runtime
1443 .register_unblocker(PendingCommandId::CancelExternal(seq), unblocker);
1444 base_ctx.inner.runtime.host.push_command(
1445 workflow_command::Variant::RequestCancelExternalWorkflowExecution(
1446 RequestCancelExternalWorkflowExecution {
1447 seq,
1448 workflow_execution: Some(NamespacedWorkflowExecution {
1449 namespace: base_ctx.inner.namespace.clone(),
1450 workflow_id: input.workflow_id,
1451 run_id: input.run_id.unwrap_or_default(),
1452 }),
1453 reason: input.reason.unwrap_or_default(),
1454 },
1455 )
1456 .into(),
1457 );
1458 WorkflowOutboundFuture::new(cmd)
1459 });
1460 let interceptors = self.inner.workflow_interceptors.clone();
1461 let future = call_cancel_external_workflow(
1462 interceptors,
1463 WorkflowInterceptorContext::new(self.clone()),
1464 input,
1465 next,
1466 );
1467 self.prepare_outbound_future(future)
1468 }
1469}
1470
1471impl<W> SyncWorkflowContext<W> {
1472 pub fn workflow_id(&self) -> &str {
1474 &self.base.inner.initial_information.workflow_id
1475 }
1476
1477 pub fn run_id(&self) -> &str {
1479 &self.base.inner.run_id
1480 }
1481
1482 pub fn namespace(&self) -> &str {
1484 &self.base.inner.namespace
1485 }
1486
1487 pub fn task_queue(&self) -> &str {
1489 &self.base.inner.task_queue
1490 }
1491
1492 pub fn workflow_time(&self) -> Option<SystemTime> {
1494 self.base
1495 .inner
1496 .shared
1497 .borrow()
1498 .activation
1499 .timestamp
1500 .try_into_or_none()
1501 }
1502
1503 pub fn history_length(&self) -> u32 {
1505 self.base.inner.shared.borrow().activation.history_length
1506 }
1507
1508 pub fn current_deployment_version(&self) -> Option<WorkerDeploymentVersion> {
1512 self.base
1513 .inner
1514 .shared
1515 .borrow()
1516 .activation
1517 .clone()
1518 .deployment_version_for_current_task
1519 .map(Into::into)
1520 }
1521
1522 pub fn search_attributes(&self) -> SearchAttributes {
1524 SearchAttributes::from_proto(&self.base.inner.shared.borrow().search_attributes)
1525 }
1526
1527 pub fn memo(&self) -> Memo {
1529 Memo::from_raw(
1530 Some(self.base.inner.shared.borrow().memo.clone()),
1531 self.payload_converter().clone(),
1532 SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1533 )
1534 }
1535
1536 pub fn random<T>(&self) -> T
1541 where
1542 T: WorkflowRandomValue,
1543 {
1544 self.base.random()
1545 }
1546
1547 pub fn uuid4(&self) -> String {
1551 self.base.uuid4()
1552 }
1553
1554 pub fn random_stream(&self, name: impl Into<String>) -> WorkflowRandomStream {
1570 self.base.random_stream(name)
1571 }
1572
1573 pub fn is_replaying(&self) -> bool {
1575 self.base.inner.shared.borrow().activation.is_replaying
1576 }
1577
1578 pub fn is_replaying_history_events(&self) -> bool {
1580 self.base.inner.shared.borrow().is_replaying_history_events
1581 }
1582
1583 pub fn all_handlers_finished(&self) -> bool {
1587 self.base.all_handlers_finished()
1588 }
1589
1590 pub fn continue_as_new_suggested(&self) -> bool {
1592 self.base
1593 .inner
1594 .shared
1595 .borrow()
1596 .activation
1597 .continue_as_new_suggested
1598 }
1599
1600 #[cfg(feature = "experimental")]
1604 pub fn target_worker_deployment_version_changed(&self) -> bool {
1605 self.base
1606 .inner
1607 .shared
1608 .borrow()
1609 .activation
1610 .target_worker_deployment_version_changed
1611 }
1612
1613 pub fn headers(&self) -> &HashMap<String, Payload> {
1618 &self.headers
1619 }
1620
1621 pub fn payload_converter(&self) -> &PayloadConverter {
1623 self.base.inner.data_converter.payload_converter()
1624 }
1625
1626 pub fn info(&self) -> WorkflowContextView {
1628 self.view()
1629 }
1630
1631 pub fn cancellation_token(&self) -> WorkflowCancellationToken {
1633 self.base.cancellation_token()
1634 }
1635
1636 pub fn cancelled(&self) -> impl FusedFuture<Output = Option<String>> + '_ {
1638 let token = self.cancellation_token();
1639 async move {
1640 token.cancelled().await;
1641 token.reason()
1642 }
1643 .fuse()
1644 }
1645
1646 pub fn continue_as_new(
1651 &self,
1652 input: <W::Run as WorkflowDefinition>::Input,
1653 opts: ContinueAsNewOptions,
1654 ) -> Result<std::convert::Infallible, WorkflowTermination>
1655 where
1656 W: WorkflowImplementation,
1657 {
1658 let input = ContinueAsNewInput::new(Box::new(input), opts);
1659 let base_ctx = self.base.clone();
1660 let workflow_type = base_ctx.workflow_type().to_string();
1661 let next = WorkflowNext::new(move |input: ContinueAsNewInput| {
1662 let (input, headers, opts) = input.into_parts();
1663 let input = match input.downcast::<<W::Run as WorkflowDefinition>::Input>() {
1664 Ok(input) => input,
1665 Err(_) => return Err(outbound_type_error("continue-as-new input").into()),
1666 };
1667 let pc = base_ctx.data_converter().payload_converter();
1668 let context_data =
1669 SerializationContextData::Workflow(WorkflowSerializationContext::new());
1670 let ctx = SerializationContext::new(&context_data, pc);
1671 let arguments = pc
1672 .to_payloads(&ctx, &*input)
1673 .map_err(WorkflowTermination::from)?;
1674 let request = opts.into_request(workflow_type, arguments, headers, pc)?;
1675 Err(WorkflowTermination::continue_as_new(request))
1676 });
1677 let interceptors = self.base.inner.workflow_interceptors.clone();
1678 call_continue_as_new(
1679 interceptors,
1680 crate::workflow_interceptors::SyncWorkflowInterceptorContext::new(self.base.clone()),
1681 input,
1682 next,
1683 )
1684 }
1685
1686 pub fn timer<T: Into<TimerOptions>>(
1688 &self,
1689 opts: T,
1690 ) -> impl CancellableFuture<Output = TimerResult> {
1691 self.base.timer(opts)
1692 }
1693
1694 pub fn execute_activity<AD: ActivityDefinition>(
1696 &self,
1697 activity: AD,
1698 input: impl Into<AD::Input>,
1699 opts: ActivityOptions,
1700 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1701 where
1702 AD::Output: TemporalDeserializable,
1703 {
1704 self.base.execute_activity(activity, input, opts)
1705 }
1706
1707 #[deprecated(note = "use `execute_activity` instead")]
1711 pub fn start_activity<AD: ActivityDefinition>(
1712 &self,
1713 activity: AD,
1714 input: impl Into<AD::Input>,
1715 opts: ActivityOptions,
1716 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1717 where
1718 AD::Output: TemporalDeserializable,
1719 {
1720 self.execute_activity(activity, input, opts)
1721 }
1722
1723 pub fn execute_local_activity<AD: ActivityDefinition>(
1725 &self,
1726 activity: AD,
1727 input: impl Into<AD::Input>,
1728 opts: LocalActivityOptions,
1729 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1730 where
1731 AD::Output: TemporalDeserializable,
1732 {
1733 self.base.execute_local_activity(activity, input, opts)
1734 }
1735
1736 #[deprecated(note = "use `execute_local_activity` instead")]
1740 pub fn start_local_activity<AD: ActivityDefinition>(
1741 &self,
1742 activity: AD,
1743 input: impl Into<AD::Input>,
1744 opts: LocalActivityOptions,
1745 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
1746 where
1747 AD::Output: TemporalDeserializable,
1748 {
1749 self.execute_local_activity(activity, input, opts)
1750 }
1751
1752 pub fn start_child_workflow<WD: WorkflowDefinition + 'static>(
1755 &self,
1756 workflow: WD,
1757 input: impl Into<WD::Input>,
1758 opts: ChildWorkflowOptions,
1759 ) -> impl CancellableFutureWithReason<
1760 Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
1761 >
1762 where
1763 WD::Output: TemporalDeserializable,
1764 {
1765 self.base.start_child_workflow(workflow, input, opts)
1766 }
1767
1768 #[deprecated(note = "use `start_child_workflow` instead")]
1770 pub fn child_workflow<WD: WorkflowDefinition + 'static>(
1771 &self,
1772 workflow: WD,
1773 input: impl Into<WD::Input>,
1774 opts: ChildWorkflowOptions,
1775 ) -> impl CancellableFutureWithReason<
1776 Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
1777 >
1778 where
1779 WD::Output: TemporalDeserializable,
1780 {
1781 self.start_child_workflow(workflow, input, opts)
1782 }
1783
1784 pub fn patched(&self, patch_id: &str) -> bool {
1790 self.patch_impl(patch_id, false)
1791 }
1792
1793 pub fn deprecate_patch(&self, patch_id: &str) -> bool {
1796 self.patch_impl(patch_id, true)
1797 }
1798
1799 fn patch_impl(&self, patch_id: &str, deprecated: bool) -> bool {
1800 if let Some(present) = self.base.inner.shared.borrow().changes.get(patch_id) {
1801 return *present;
1802 }
1803
1804 let shared = self.base.inner.shared.borrow();
1805 let replaying = shared.activation.is_replaying;
1806 let notified = shared.notified_patches.contains(patch_id);
1807 drop(shared);
1808
1809 let res = if deprecated || replaying || notified {
1811 !replaying || notified
1812 } else if let Some(callback) = &self.base.inner.patch_activation_callback {
1813 let _read_only = self.base.enter_read_only();
1814 callback(PatchActivationInput {
1815 workflow_info: self.base.view(),
1816 patch_id: patch_id.to_string(),
1817 })
1818 } else {
1819 true
1820 };
1821
1822 if res {
1823 self.base.inner.runtime.host.push_command(
1824 workflow_command::Variant::SetPatchMarker(SetPatchMarker {
1825 patch_id: patch_id.to_string(),
1826 deprecated,
1827 })
1828 .into(),
1829 );
1830 }
1831
1832 self.base
1833 .inner
1834 .shared
1835 .borrow_mut()
1836 .changes
1837 .insert(patch_id.to_string(), res);
1838
1839 res
1840 }
1841
1842 pub fn external_workflow(
1844 &self,
1845 workflow_id: impl Into<String>,
1846 run_id: Option<String>,
1847 ) -> ExternalWorkflowHandle {
1848 self.base.external_workflow(workflow_id, run_id)
1849 }
1850
1851 pub fn upsert_search_attributes(
1857 &self,
1858 updates: impl IntoIterator<Item = SearchAttributeUpdate>,
1859 ) {
1860 let updates: Vec<SearchAttributeUpdate> = updates.into_iter().collect();
1863
1864 {
1867 let mut shared = self.base.inner.shared.borrow_mut();
1868 let mut attrs = SearchAttributes::from_proto(&shared.search_attributes);
1869 for update in updates.iter().cloned() {
1870 attrs.apply(update);
1871 }
1872 shared.search_attributes = attrs.into_proto();
1873 }
1874
1875 let proto = SearchAttributes::updates_to_proto(updates);
1876 self.base.inner.runtime.host.push_command(
1877 workflow_command::Variant::UpsertWorkflowSearchAttributes(
1878 UpsertWorkflowSearchAttributes {
1879 search_attributes: Some(proto),
1880 },
1881 )
1882 .into(),
1883 );
1884 }
1885
1886 pub fn upsert_memo<K>(
1888 &self,
1889 updates: impl IntoIterator<Item = (K, Option<MemoValue>)>,
1890 ) -> Result<(), PayloadConversionError>
1891 where
1892 K: Into<String>,
1893 {
1894 let payload_converter = self.payload_converter();
1895 let context_data = SerializationContextData::Workflow(WorkflowSerializationContext::new());
1896 let context = SerializationContext::new(&context_data, payload_converter);
1897 let mut fields = HashMap::new();
1898 let mut local_updates = Vec::new();
1899 for (key, value) in updates {
1900 let key = key.into();
1901 let (command_payload, local_payload) = match value {
1902 Some(value) => {
1903 let payload = payload_converter.to_payload(&context, &value)?;
1904 (payload.clone(), Some(payload))
1905 }
1906 None => (
1907 payload_converter.to_payload(&context, &MemoValue::new(()))?,
1908 None,
1909 ),
1910 };
1911 fields.insert(key.clone(), command_payload);
1912 local_updates.push((key, local_payload));
1913 }
1914 {
1915 let mut shared = self.base.inner.shared.borrow_mut();
1916 for (key, payload) in local_updates {
1917 match payload {
1918 Some(payload) => {
1919 shared.memo.fields.insert(key, payload);
1920 }
1921 None => {
1922 shared.memo.fields.remove(&key);
1923 }
1924 }
1925 }
1926 }
1927 self.base.inner.runtime.host.push_command(
1928 workflow_command::Variant::ModifyWorkflowProperties(ModifyWorkflowProperties {
1929 upserted_memo: Some(ProtoMemo { fields }),
1930 })
1931 .into(),
1932 );
1933 Ok(())
1934 }
1935
1936 pub fn set_current_details(&self, details: impl Into<String>) {
1941 let details = details.into();
1942 self.base.inner.shared.borrow_mut().current_details = details.clone();
1943 self.base.inner.runtime.host.set_current_details(details);
1944 }
1945
1946 pub fn force_task_fail(&self, with: impl Into<Box<dyn std::error::Error + Send + Sync>>) {
1948 self.base.inner.runtime.set_forced_wft_failure(with.into());
1949 }
1950
1951 pub(crate) fn view(&self) -> WorkflowContextView {
1953 self.base.view()
1954 }
1955}
1956
1957impl<W> WorkflowContext<W> {
1958 pub(crate) fn from_base(base: BaseWorkflowContext, workflow_state: Rc<RefCell<W>>) -> Self {
1960 Self {
1961 sync: SyncWorkflowContext {
1962 base,
1963 headers: Rc::new(HashMap::new()),
1964 _phantom: PhantomData,
1965 },
1966 workflow_state,
1967 }
1968 }
1969
1970 pub(crate) fn with_headers(&self, headers: HashMap<String, Payload>) -> Self {
1972 Self {
1973 sync: SyncWorkflowContext {
1974 base: self.sync.base.clone(),
1975 headers: Rc::new(headers),
1976 _phantom: PhantomData,
1977 },
1978 workflow_state: self.workflow_state.clone(),
1979 }
1980 }
1981
1982 pub(crate) fn sync_context(&self) -> SyncWorkflowContext<W> {
1984 self.sync.clone()
1985 }
1986
1987 pub(crate) fn view(&self) -> WorkflowContextView {
1989 self.sync.view()
1990 }
1991
1992 pub fn workflow_id(&self) -> &str {
1996 self.sync.workflow_id()
1997 }
1998
1999 pub fn run_id(&self) -> &str {
2001 self.sync.run_id()
2002 }
2003
2004 pub fn namespace(&self) -> &str {
2006 self.sync.namespace()
2007 }
2008
2009 pub fn task_queue(&self) -> &str {
2011 self.sync.task_queue()
2012 }
2013
2014 pub fn workflow_time(&self) -> Option<SystemTime> {
2016 self.sync.workflow_time()
2017 }
2018
2019 pub fn history_length(&self) -> u32 {
2021 self.sync.history_length()
2022 }
2023
2024 pub fn current_deployment_version(&self) -> Option<WorkerDeploymentVersion> {
2028 self.sync.current_deployment_version()
2029 }
2030
2031 pub fn search_attributes(&self) -> SearchAttributes {
2033 self.sync.search_attributes()
2034 }
2035
2036 pub fn memo(&self) -> Memo {
2038 self.sync.memo()
2039 }
2040
2041 pub fn random<T>(&self) -> T
2045 where
2046 T: WorkflowRandomValue,
2047 {
2048 self.sync.random()
2049 }
2050
2051 pub fn uuid4(&self) -> String {
2055 self.sync.uuid4()
2056 }
2057
2058 pub fn random_stream(&self, name: impl Into<String>) -> WorkflowRandomStream {
2062 self.sync.random_stream(name)
2063 }
2064
2065 pub fn is_replaying(&self) -> bool {
2067 self.sync.is_replaying()
2068 }
2069
2070 pub fn is_replaying_history_events(&self) -> bool {
2072 self.sync.is_replaying_history_events()
2073 }
2074
2075 pub fn all_handlers_finished(&self) -> bool {
2094 self.sync.all_handlers_finished()
2095 }
2096
2097 pub fn continue_as_new_suggested(&self) -> bool {
2099 self.sync.continue_as_new_suggested()
2100 }
2101
2102 #[cfg(feature = "experimental")]
2106 pub fn target_worker_deployment_version_changed(&self) -> bool {
2107 self.sync.target_worker_deployment_version_changed()
2108 }
2109
2110 pub fn headers(&self) -> &HashMap<String, Payload> {
2112 self.sync.headers()
2113 }
2114
2115 pub fn payload_converter(&self) -> &PayloadConverter {
2117 self.sync.payload_converter()
2118 }
2119
2120 pub fn info(&self) -> WorkflowContextView {
2122 self.sync.info()
2123 }
2124
2125 pub fn cancellation_token(&self) -> WorkflowCancellationToken {
2127 self.sync.cancellation_token()
2128 }
2129
2130 pub fn cancelled(&self) -> impl FusedFuture<Output = Option<String>> + '_ {
2132 self.sync.cancelled()
2133 }
2134
2135 pub fn timer<T: Into<TimerOptions>>(
2137 &self,
2138 opts: T,
2139 ) -> impl CancellableFuture<Output = TimerResult> {
2140 self.sync.timer(opts)
2141 }
2142
2143 pub fn execute_activity<AD: ActivityDefinition>(
2145 &self,
2146 activity: AD,
2147 input: impl Into<AD::Input>,
2148 opts: ActivityOptions,
2149 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
2150 where
2151 AD::Output: TemporalDeserializable,
2152 {
2153 self.sync.execute_activity(activity, input, opts)
2154 }
2155
2156 #[deprecated(note = "use `execute_activity` instead")]
2160 pub fn start_activity<AD: ActivityDefinition>(
2161 &self,
2162 activity: AD,
2163 input: impl Into<AD::Input>,
2164 opts: ActivityOptions,
2165 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
2166 where
2167 AD::Output: TemporalDeserializable,
2168 {
2169 self.execute_activity(activity, input, opts)
2170 }
2171
2172 pub fn execute_local_activity<AD: ActivityDefinition>(
2174 &self,
2175 activity: AD,
2176 input: impl Into<AD::Input>,
2177 opts: LocalActivityOptions,
2178 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
2179 where
2180 AD::Output: TemporalDeserializable,
2181 {
2182 self.sync.execute_local_activity(activity, input, opts)
2183 }
2184
2185 #[deprecated(note = "use `execute_local_activity` instead")]
2189 pub fn start_local_activity<AD: ActivityDefinition>(
2190 &self,
2191 activity: AD,
2192 input: impl Into<AD::Input>,
2193 opts: LocalActivityOptions,
2194 ) -> impl CancellableFuture<Output = Result<AD::Output, ActivityExecutionError>>
2195 where
2196 AD::Output: TemporalDeserializable,
2197 {
2198 self.execute_local_activity(activity, input, opts)
2199 }
2200
2201 pub fn start_child_workflow<WD: WorkflowDefinition + 'static>(
2203 &self,
2204 workflow: WD,
2205 input: impl Into<WD::Input>,
2206 opts: ChildWorkflowOptions,
2207 ) -> impl CancellableFutureWithReason<
2208 Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
2209 >
2210 where
2211 WD::Output: TemporalDeserializable,
2212 {
2213 self.sync.start_child_workflow(workflow, input, opts)
2214 }
2215
2216 #[deprecated(note = "use `start_child_workflow` instead")]
2218 pub fn child_workflow<WD: WorkflowDefinition + 'static>(
2219 &self,
2220 workflow: WD,
2221 input: impl Into<WD::Input>,
2222 opts: ChildWorkflowOptions,
2223 ) -> impl CancellableFutureWithReason<
2224 Output = Result<StartedChildWorkflow<WD>, ChildWorkflowStartError>,
2225 >
2226 where
2227 WD::Output: TemporalDeserializable,
2228 {
2229 self.start_child_workflow(workflow, input, opts)
2230 }
2231
2232 pub fn patched(&self, patch_id: &str) -> bool {
2234 self.sync.patched(patch_id)
2235 }
2236
2237 pub fn deprecate_patch(&self, patch_id: &str) -> bool {
2240 self.sync.deprecate_patch(patch_id)
2241 }
2242
2243 pub fn external_workflow(
2245 &self,
2246 workflow_id: impl Into<String>,
2247 run_id: Option<String>,
2248 ) -> ExternalWorkflowHandle {
2249 self.sync.external_workflow(workflow_id, run_id)
2250 }
2251
2252 pub fn upsert_search_attributes(
2254 &self,
2255 updates: impl IntoIterator<Item = SearchAttributeUpdate>,
2256 ) {
2257 self.sync.upsert_search_attributes(updates)
2258 }
2259
2260 pub fn upsert_memo<K>(
2262 &self,
2263 updates: impl IntoIterator<Item = (K, Option<MemoValue>)>,
2264 ) -> Result<(), PayloadConversionError>
2265 where
2266 K: Into<String>,
2267 {
2268 self.sync.upsert_memo(updates)
2269 }
2270
2271 pub fn set_current_details(&self, details: impl Into<String>) {
2275 self.sync.set_current_details(details)
2276 }
2277
2278 pub fn force_task_fail(&self, with: impl Into<Box<dyn std::error::Error + Send + Sync>>) {
2280 self.sync.force_task_fail(with)
2281 }
2282
2283 pub fn state<R>(&self, f: impl FnOnce(&W) -> R) -> R {
2288 f(&*self.workflow_state.borrow())
2289 }
2290
2291 pub fn state_mut<R>(&self, f: impl FnOnce(&mut W) -> R) -> R {
2300 let result = f(&mut *self.workflow_state.borrow_mut());
2301 self.sync.base.wake_condition_waiters();
2302 self.sync.base.set_state_mutated();
2303 result
2304 }
2305
2306 pub fn continue_as_new(
2311 &self,
2312 input: <W::Run as WorkflowDefinition>::Input,
2313 opts: ContinueAsNewOptions,
2314 ) -> Result<std::convert::Infallible, WorkflowTermination>
2315 where
2316 W: WorkflowImplementation,
2317 {
2318 self.sync.continue_as_new(input, opts)
2319 }
2320
2321 pub fn wait_condition<'a>(
2327 &'a self,
2328 condition: impl FnMut(&W) -> bool + 'a,
2329 ) -> impl FusedFuture<Output = Result<(), WorkflowCancellationError>> + 'a {
2330 self.wait_condition_with_options(condition, Default::default())
2331 }
2332
2333 pub fn wait_condition_with_options<'a>(
2335 &'a self,
2336 mut condition: impl FnMut(&W) -> bool + 'a,
2337 options: WaitConditionOptions,
2338 ) -> impl FusedFuture<Output = Result<(), WorkflowCancellationError>> + 'a {
2339 let token = options
2340 .cancellation_token
2341 .unwrap_or_else(|| self.cancellation_token());
2342 let wait_token = token.clone();
2343 let mut cancelled = Box::pin(async move {
2344 wait_token.cancelled().await;
2345 });
2346 future::poll_fn(move |cx: &mut Context<'_>| {
2347 if condition(&*self.workflow_state.borrow()) {
2348 Poll::Ready(Ok(()))
2349 } else if cancelled.as_mut().poll(cx).is_ready() {
2350 Poll::Ready(Err(WorkflowCancellationError::new(token.reason())))
2351 } else {
2352 self.sync
2353 .base
2354 .inner
2355 .condition_wakers
2356 .borrow_mut()
2357 .push(cx.waker().clone());
2358 Poll::Pending
2359 }
2360 })
2361 .fuse()
2362 }
2363}
2364
2365struct WfCtxProtectedDat {
2366 next_timer_sequence_number: u32,
2367 next_activity_sequence_number: u32,
2368 next_child_workflow_sequence_number: u32,
2369 next_cancel_external_wf_sequence_number: u32,
2370 next_signal_external_wf_sequence_number: u32,
2371 #[cfg(feature = "experimental")]
2372 next_nexus_op_sequence_number: u32,
2373}
2374
2375impl WfCtxProtectedDat {
2376 fn next_timer_seq(&mut self) -> u32 {
2377 let seq = self.next_timer_sequence_number;
2378 self.next_timer_sequence_number += 1;
2379 seq
2380 }
2381 fn next_activity_seq(&mut self) -> u32 {
2382 let seq = self.next_activity_sequence_number;
2383 self.next_activity_sequence_number += 1;
2384 seq
2385 }
2386 fn next_child_workflow_seq(&mut self) -> u32 {
2387 let seq = self.next_child_workflow_sequence_number;
2388 self.next_child_workflow_sequence_number += 1;
2389 seq
2390 }
2391 fn next_cancel_external_wf_seq(&mut self) -> u32 {
2392 let seq = self.next_cancel_external_wf_sequence_number;
2393 self.next_cancel_external_wf_sequence_number += 1;
2394 seq
2395 }
2396 fn next_signal_external_wf_seq(&mut self) -> u32 {
2397 let seq = self.next_signal_external_wf_sequence_number;
2398 self.next_signal_external_wf_sequence_number += 1;
2399 seq
2400 }
2401}
2402
2403#[derive(Clone, Debug)]
2404struct WorkflowContextSharedData {
2405 changes: HashMap<String, bool>,
2407 notified_patches: HashSet<String>,
2409 activation: CoreWorkflowActivation,
2410 memo: ProtoMemo,
2411 is_replaying_history_events: bool,
2412 search_attributes: ProtoSearchAttributes,
2413 current_details: String,
2415}
2416
2417pub trait CancellableFuture: FusedFuture {
2420 fn cancel(&self);
2422}
2423
2424pub trait CancellableFutureWithReason: CancellableFuture {
2426 fn cancel_with_reason(&self, reason: String);
2428}
2429
2430fn cancellable_outbound<T: 'static>(
2431 future: impl CancellableFuture<Output = T> + 'static,
2432) -> CancellableWorkflowOutboundFuture<T> {
2433 let future = Rc::new(RefCell::new(Box::pin(future)));
2434 let polled = future.clone();
2435 let cancellation = WorkflowCancellationHandle::new(move |_| {
2436 future.borrow().as_ref().get_ref().cancel();
2437 });
2438 CancellableWorkflowOutboundFuture::new(
2439 future::poll_fn(move |cx| polled.borrow_mut().as_mut().poll(cx)),
2440 cancellation,
2441 )
2442}
2443
2444fn cancellable_outbound_with_reason<T: 'static>(
2445 future: impl CancellableFutureWithReason<Output = T> + 'static,
2446) -> CancellableWorkflowOutboundFuture<T> {
2447 let future = Rc::new(RefCell::new(Box::pin(future)));
2448 let polled = future.clone();
2449 let cancellation = WorkflowCancellationHandle::new(move |reason| {
2450 let future = future.borrow();
2451 let future = future.as_ref().get_ref();
2452 if let Some(reason) = reason {
2453 future.cancel_with_reason(reason);
2454 } else {
2455 future.cancel();
2456 }
2457 });
2458 CancellableWorkflowOutboundFuture::new(
2459 future::poll_fn(move |cx| polled.borrow_mut().as_mut().poll(cx)),
2460 cancellation,
2461 )
2462}
2463
2464pub(crate) struct WFCommandFut<T, D> {
2465 _unused: PhantomData<T>,
2466 result_rx: oneshot::Receiver<UnblockEvent>,
2467 other_dat: Option<D>,
2468}
2469impl<T> WFCommandFut<T, ()> {
2470 fn new() -> (Self, oneshot::Sender<UnblockEvent>) {
2471 Self::new_with_dat(())
2472 }
2473}
2474
2475impl<T, D> WFCommandFut<T, D> {
2476 fn new_with_dat(other_dat: D) -> (Self, oneshot::Sender<UnblockEvent>) {
2477 let (tx, rx) = oneshot::channel();
2478 (
2479 Self {
2480 _unused: PhantomData,
2481 result_rx: rx,
2482 other_dat: Some(other_dat),
2483 },
2484 tx,
2485 )
2486 }
2487}
2488
2489impl<T, D> Unpin for WFCommandFut<T, D> where T: Unblockable<OtherDat = D> {}
2490impl<T, D> Future for WFCommandFut<T, D>
2491where
2492 T: Unblockable<OtherDat = D>,
2493{
2494 type Output = T;
2495
2496 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2497 let poll = self.result_rx.poll_unpin(cx).map(|x| {
2498 let od = self
2499 .other_dat
2500 .take()
2501 .expect("Other data must exist when resolving command future");
2502 Unblockable::unblock(x.unwrap(), od)
2503 });
2504 if poll.is_pending() {
2505 mark_intercepted_future_activation();
2506 }
2507 poll
2508 }
2509}
2510impl<T, D> FusedFuture for WFCommandFut<T, D>
2511where
2512 T: Unblockable<OtherDat = D>,
2513{
2514 fn is_terminated(&self) -> bool {
2515 self.other_dat.is_none()
2516 }
2517}
2518
2519struct CancellableWFCommandFut<T, D> {
2520 cmd_fut: WFCommandFut<T, D>,
2521 cancellable_id: CancellableID,
2522 base_ctx: BaseWorkflowContext,
2523}
2524impl<T> CancellableWFCommandFut<T, ()> {
2525 fn new(
2526 cancellable_id: CancellableID,
2527 base_ctx: BaseWorkflowContext,
2528 ) -> (Self, oneshot::Sender<UnblockEvent>) {
2529 Self::new_with_dat(cancellable_id, (), base_ctx)
2530 }
2531}
2532impl<T, D> CancellableWFCommandFut<T, D> {
2533 fn new_with_dat(
2534 cancellable_id: CancellableID,
2535 other_dat: D,
2536 base_ctx: BaseWorkflowContext,
2537 ) -> (Self, oneshot::Sender<UnblockEvent>) {
2538 let (cmd_fut, sender) = WFCommandFut::new_with_dat(other_dat);
2539 (
2540 Self {
2541 cmd_fut,
2542 cancellable_id,
2543 base_ctx,
2544 },
2545 sender,
2546 )
2547 }
2548}
2549impl<T, D> Unpin for CancellableWFCommandFut<T, D> where T: Unblockable<OtherDat = D> {}
2550impl<T, D> Future for CancellableWFCommandFut<T, D>
2551where
2552 T: Unblockable<OtherDat = D>,
2553{
2554 type Output = T;
2555
2556 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2557 self.cmd_fut.poll_unpin(cx)
2558 }
2559}
2560impl<T, D> FusedFuture for CancellableWFCommandFut<T, D>
2561where
2562 T: Unblockable<OtherDat = D>,
2563{
2564 fn is_terminated(&self) -> bool {
2565 self.cmd_fut.is_terminated()
2566 }
2567}
2568
2569impl<T, D> CancellableFuture for CancellableWFCommandFut<T, D>
2570where
2571 T: Unblockable<OtherDat = D>,
2572{
2573 fn cancel(&self) {
2574 self.base_ctx.cancel(self.cancellable_id.clone());
2575 }
2576}
2577impl<T, D> CancellableFutureWithReason for CancellableWFCommandFut<T, D>
2578where
2579 T: Unblockable<OtherDat = D>,
2580{
2581 fn cancel_with_reason(&self, reason: String) {
2582 self.base_ctx
2583 .cancel(self.cancellable_id.clone().with_reason(reason));
2584 }
2585}
2586
2587struct LATimerBackoffFut {
2588 la_opts: LocalActivityOptions,
2589 activity_type: String,
2590 arguments: Vec<Payload>,
2591 headers: HashMap<String, Payload>,
2592 current_fut: Pin<Box<dyn CancellableFuture<Output = ActivityResolution> + Unpin>>,
2593 timer_fut: Option<Pin<Box<dyn CancellableFuture<Output = TimerResult> + Unpin>>>,
2594 cancellation_token: WorkflowCancellationToken,
2595 base_ctx: BaseWorkflowContext,
2596 next_attempt: u32,
2597 next_sched_time: Option<prost_types::Timestamp>,
2598 did_cancel: AtomicBool,
2599 terminated: bool,
2600}
2601impl LATimerBackoffFut {
2602 fn new(
2603 activity_type: String,
2604 arguments: Vec<Payload>,
2605 headers: HashMap<String, Payload>,
2606 opts: LocalActivityOptions,
2607 cancellation_token: WorkflowCancellationToken,
2608 base_ctx: BaseWorkflowContext,
2609 ) -> Self {
2610 let current_fut = Box::pin(base_ctx.clone().local_activity_no_timer_retry(
2611 activity_type.clone(),
2612 arguments.clone(),
2613 headers.clone(),
2614 opts.clone(),
2615 ));
2616 Self {
2617 la_opts: opts,
2618 activity_type,
2619 arguments,
2620 headers,
2621 current_fut,
2622 timer_fut: None,
2623 cancellation_token,
2624 base_ctx,
2625 next_attempt: 1,
2626 next_sched_time: None,
2627 did_cancel: AtomicBool::new(false),
2628 terminated: false,
2629 }
2630 }
2631}
2632impl Unpin for LATimerBackoffFut {}
2633impl Future for LATimerBackoffFut {
2634 type Output = ActivityResolution;
2635
2636 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2637 if let Some(tf) = self.timer_fut.as_mut() {
2639 return match tf.poll_unpin(cx) {
2640 Poll::Ready(tr) => {
2641 self.timer_fut = None;
2642 if let TimerResult::Fired = tr {
2644 let mut opts = self.la_opts.clone();
2645 opts.attempt = Some(self.next_attempt);
2646 opts.original_schedule_time
2647 .clone_from(&self.next_sched_time);
2648 self.current_fut =
2649 Box::pin(self.base_ctx.clone().local_activity_no_timer_retry(
2650 self.activity_type.clone(),
2651 self.arguments.clone(),
2652 self.headers.clone(),
2653 opts,
2654 ));
2655 Poll::Pending
2656 } else {
2657 self.terminated = true;
2658 Poll::Ready(ActivityResolution {
2659 status: Some(activity_resolution::Status::Cancelled(Cancellation {
2660 failure: Some(Failure {
2661 message: "Activity cancelled".to_owned(),
2662 failure_info: Some(FailureInfo::CanceledFailureInfo(
2663 CanceledFailureInfo::default(),
2664 )),
2665 ..Default::default()
2666 }),
2667 })),
2668 })
2669 }
2670 }
2671 Poll::Pending => Poll::Pending,
2672 };
2673 }
2674 let poll_res = self.current_fut.poll_unpin(cx);
2675 if let Poll::Ready(ref r) = poll_res
2676 && let Some(activity_resolution::Status::Backoff(b)) = r.status.as_ref()
2677 {
2678 if self.did_cancel.load(Ordering::Acquire) {
2682 self.terminated = true;
2683 return Poll::Ready(ActivityResolution {
2684 status: Some(activity_resolution::Status::Cancelled(Cancellation {
2685 failure: Some(Failure {
2686 message: "Activity cancelled".to_owned(),
2687 failure_info: Some(FailureInfo::CanceledFailureInfo(
2688 CanceledFailureInfo::default(),
2689 )),
2690 ..Default::default()
2691 }),
2692 })),
2693 });
2694 }
2695
2696 let timer_f = self.base_ctx.timer(TimerOptions {
2697 duration: b
2698 .backoff_duration
2699 .expect("Duration is set")
2700 .try_into()
2701 .expect("duration converts ok"),
2702 cancellation_token: Some(self.cancellation_token.clone()),
2703 summary: None,
2704 #[cfg(feature = "experimental")]
2705 event_group_markers: self.la_opts.event_group_markers.clone(),
2706 });
2707 self.timer_fut = Some(Box::pin(timer_f));
2708 self.next_attempt = b.attempt;
2709 self.next_sched_time.clone_from(&b.original_schedule_time);
2710 return Poll::Pending;
2711 }
2712 if poll_res.is_ready() {
2713 self.terminated = true;
2714 }
2715 poll_res
2716 }
2717}
2718impl FusedFuture for LATimerBackoffFut {
2719 fn is_terminated(&self) -> bool {
2720 self.terminated
2721 }
2722}
2723impl CancellableFuture for LATimerBackoffFut {
2724 fn cancel(&self) {
2725 self.did_cancel.store(true, Ordering::Release);
2726 if let Some(tf) = self.timer_fut.as_ref() {
2727 tf.cancel();
2728 }
2729 self.current_fut.cancel();
2730 }
2731}
2732
2733enum ActivityFut<F, Output> {
2735 Errored {
2737 error: Option<Box<ActivityExecutionError>>,
2738 _phantom: PhantomData<Output>,
2739 },
2740 Running {
2742 inner: F,
2743 data_converter: DataConverter,
2744 _phantom: PhantomData<Output>,
2745 },
2746 Terminated,
2747}
2748
2749impl<F, Output> ActivityFut<F, Output> {
2750 fn eager(err: ActivityExecutionError) -> Self {
2751 Self::Errored {
2752 error: Some(Box::new(err)),
2753 _phantom: PhantomData,
2754 }
2755 }
2756
2757 fn running(inner: F, data_converter: DataConverter) -> Self {
2758 Self::Running {
2759 inner,
2760 data_converter,
2761 _phantom: PhantomData,
2762 }
2763 }
2764}
2765
2766impl<F, Output> Unpin for ActivityFut<F, Output> where F: Unpin {}
2767
2768impl<F, Output> Future for ActivityFut<F, Output>
2769where
2770 F: Future<Output = ActivityResolution> + Unpin,
2771 Output: TemporalDeserializable + 'static,
2772{
2773 type Output = Result<Output, ActivityExecutionError>;
2774
2775 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2776 let this = self.get_mut();
2777 let poll =
2778 match this {
2779 ActivityFut::Errored { error, .. } => {
2780 Poll::Ready(Err(*error.take().expect("polled after completion")))
2781 }
2782 ActivityFut::Running {
2783 inner,
2784 data_converter,
2785 ..
2786 } => match Pin::new(inner).poll(cx) {
2787 Poll::Pending => Poll::Pending,
2788 Poll::Ready(resolution) => Poll::Ready({
2789 let status = resolution.status.ok_or_else(|| {
2790 data_converter
2791 .to_error(
2792 &SerializationContextData::Workflow(
2793 WorkflowSerializationContext::new(),
2794 ),
2795 Failure {
2796 message: "Activity completed without a status".to_string(),
2797 ..Default::default()
2798 },
2799 ActivityExecutionDecodeHint::new(false),
2800 )
2801 .expect("synthetic activity failure should decode")
2802 })?;
2803
2804 match status {
2805 activity_resolution::Status::Completed(success) => {
2806 let payload = success.result.unwrap_or_default();
2807 let context_data = SerializationContextData::Workflow(
2808 WorkflowSerializationContext::new(),
2809 );
2810 let ctx = SerializationContext::new(
2811 &context_data,
2812 data_converter.payload_converter(),
2813 );
2814 data_converter
2815 .payload_converter()
2816 .from_payload::<Output>(&ctx, payload)
2817 .map_err(ActivityExecutionError::Serialization)
2818 }
2819 activity_resolution::Status::Failed(f) => Err(data_converter
2820 .to_error(
2821 &SerializationContextData::Workflow(
2822 WorkflowSerializationContext::new(),
2823 ),
2824 f.failure.unwrap_or_default(),
2825 ActivityExecutionDecodeHint::new(false),
2826 )?),
2827 activity_resolution::Status::Cancelled(c) => Err(data_converter
2828 .to_error(
2829 &SerializationContextData::Workflow(
2830 WorkflowSerializationContext::new(),
2831 ),
2832 c.failure.unwrap_or_default(),
2833 ActivityExecutionDecodeHint::new(true),
2834 )?),
2835 activity_resolution::Status::Backoff(_) => {
2836 panic!("DoBackoff should be handled by LATimerBackoffFut")
2837 }
2838 }
2839 }),
2840 },
2841 ActivityFut::Terminated => panic!("polled after termination"),
2842 };
2843 if poll.is_ready() {
2844 *this = ActivityFut::Terminated;
2845 }
2846 poll
2847 }
2848}
2849
2850impl<F, Output> FusedFuture for ActivityFut<F, Output>
2851where
2852 F: Future<Output = ActivityResolution> + Unpin,
2853 Output: TemporalDeserializable + 'static,
2854{
2855 fn is_terminated(&self) -> bool {
2856 matches!(self, ActivityFut::Terminated)
2857 }
2858}
2859
2860impl<F, Output> CancellableFuture for ActivityFut<F, Output>
2861where
2862 F: CancellableFuture<Output = ActivityResolution> + Unpin,
2863 Output: TemporalDeserializable + 'static,
2864{
2865 fn cancel(&self) {
2866 if let ActivityFut::Running { inner, .. } = self {
2867 inner.cancel()
2868 }
2869 }
2870}
2871
2872pub(crate) struct ChildWfCommon {
2873 workflow_id: String,
2874 child_seq: u32,
2875 result_future: CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2876 base_ctx: BaseWorkflowContext,
2877}
2878
2879#[derive(derive_more::Debug)]
2883pub(crate) struct PendingChildWorkflow<WD: WorkflowDefinition> {
2884 pub(crate) status: ChildWorkflowStartStatus,
2885 #[debug(skip)]
2886 pub(crate) common: ChildWfCommon,
2887 pub(crate) _phantom: PhantomData<WD>,
2888}
2889
2890#[derive(derive_more::Debug)]
2892pub struct StartChildWorkflowOutput {
2893 pub run_id: String,
2895 #[debug(skip)]
2896 result_future: CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2897 workflow_id: String,
2898 child_seq: u32,
2899 #[debug(skip)]
2900 base_ctx: BaseWorkflowContext,
2901}
2902
2903impl StartChildWorkflowOutput {
2904 pub fn map_result(
2906 mut self,
2907 map: impl FnOnce(
2908 CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2909 ) -> CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2910 ) -> Self {
2911 self.result_future = map(self.result_future);
2912 self
2913 }
2914
2915 fn into_started<WD: WorkflowDefinition>(self) -> StartedChildWorkflow<WD> {
2916 StartedChildWorkflow {
2917 run_id: self.run_id,
2918 result_future: self.result_future,
2919 workflow_id: self.workflow_id,
2920 child_seq: self.child_seq,
2921 base_ctx: self.base_ctx,
2922 _phantom: PhantomData,
2923 }
2924 }
2925}
2926
2927#[derive(derive_more::Debug)]
2929pub struct StartedChildWorkflow<WD: WorkflowDefinition> {
2930 pub run_id: String,
2932 #[debug(skip)]
2933 result_future: CancellableWorkflowOutboundFuture<ChildWorkflowOutboundResult>,
2934 workflow_id: String,
2935 child_seq: u32,
2936 #[debug(skip)]
2937 base_ctx: BaseWorkflowContext,
2938 _phantom: PhantomData<WD>,
2939}
2940
2941enum ChildWorkflowFut<F, Output> {
2944 Running {
2945 inner: F,
2946 data_converter: DataConverter,
2947 _phantom: PhantomData<Output>,
2948 },
2949 Terminated,
2950}
2951
2952impl<F, Output> Unpin for ChildWorkflowFut<F, Output> where F: Unpin {}
2953
2954impl<F, Output> Future for ChildWorkflowFut<F, Output>
2955where
2956 F: Future<Output = ChildWorkflowResult> + Unpin,
2957 Output: TemporalDeserializable + 'static,
2958{
2959 type Output = Result<Output, ChildWorkflowExecutionError>;
2960
2961 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2962 let this = self.get_mut();
2963 let poll = match this {
2964 ChildWorkflowFut::Running {
2965 inner,
2966 data_converter,
2967 ..
2968 } => match Pin::new(inner).poll(cx) {
2969 Poll::Pending => Poll::Pending,
2970 Poll::Ready(result) => Poll::Ready({
2971 let status = result.status.ok_or_else(|| {
2972 data_converter
2973 .to_error(
2974 &SerializationContextData::Workflow(
2975 WorkflowSerializationContext::new(),
2976 ),
2977 Failure {
2978 message: "Child workflow completed without a status"
2979 .to_string(),
2980 ..Default::default()
2981 },
2982 ChildWorkflowExecutionDecodeHint::default(),
2983 )
2984 .expect("synthetic child workflow failure should decode")
2985 })?;
2986 match status {
2987 child_workflow_result::Status::Completed(success) => {
2988 let payloads = success.result.into_iter().collect();
2989 let context_data = SerializationContextData::Workflow(
2990 WorkflowSerializationContext::new(),
2991 );
2992 let ctx = SerializationContext::new(
2993 &context_data,
2994 data_converter.payload_converter(),
2995 );
2996 data_converter
2997 .payload_converter()
2998 .from_payloads::<Output>(&ctx, payloads)
2999 .map_err(ChildWorkflowExecutionError::Serialization)
3000 }
3001 child_workflow_result::Status::Failed(f) => {
3002 Err(data_converter.to_error(
3003 &SerializationContextData::Workflow(
3004 WorkflowSerializationContext::new(),
3005 ),
3006 f.failure.unwrap_or_default(),
3007 ChildWorkflowExecutionDecodeHint::default(),
3008 )?)
3009 }
3010 child_workflow_result::Status::Cancelled(c) => Err(data_converter
3011 .to_error(
3012 &SerializationContextData::Workflow(
3013 WorkflowSerializationContext::new(),
3014 ),
3015 c.failure.unwrap_or_default(),
3016 ChildWorkflowExecutionDecodeHint::default(),
3017 )?),
3018 }
3019 }),
3020 },
3021 ChildWorkflowFut::Terminated => panic!("polled after termination"),
3022 };
3023 if poll.is_ready() {
3024 *this = ChildWorkflowFut::Terminated;
3025 }
3026 poll
3027 }
3028}
3029
3030impl<F, Output> FusedFuture for ChildWorkflowFut<F, Output>
3031where
3032 F: Future<Output = ChildWorkflowResult> + Unpin,
3033 Output: TemporalDeserializable + 'static,
3034{
3035 fn is_terminated(&self) -> bool {
3036 matches!(self, ChildWorkflowFut::Terminated)
3037 }
3038}
3039
3040impl<F, Output> CancellableFutureWithReason for ChildWorkflowFut<F, Output>
3041where
3042 F: CancellableFutureWithReason<Output = ChildWorkflowResult> + Unpin,
3043 Output: TemporalDeserializable + 'static,
3044{
3045 fn cancel_with_reason(&self, reason: String) {
3046 if let ChildWorkflowFut::Running { inner, .. } = self {
3047 inner.cancel_with_reason(reason)
3048 }
3049 }
3050}
3051
3052impl<F, Output> CancellableFuture for ChildWorkflowFut<F, Output>
3053where
3054 F: CancellableFutureWithReason<Output = ChildWorkflowResult> + Unpin,
3055 Output: TemporalDeserializable + 'static,
3056{
3057 fn cancel(&self) {
3058 if let ChildWorkflowFut::Running { inner, .. } = self {
3059 inner.cancel()
3060 }
3061 }
3062}
3063
3064enum ChildWorkflowStartFut<F, WD: WorkflowDefinition> {
3067 Errored {
3069 error: Option<Box<ChildWorkflowStartError>>,
3070 _phantom: PhantomData<WD>,
3071 },
3072 Running(F),
3073 Terminated,
3074}
3075
3076impl<F, WD: WorkflowDefinition> ChildWorkflowStartFut<F, WD> {
3077 fn eager(err: ChildWorkflowStartError) -> Self {
3078 Self::Errored {
3079 error: Some(Box::new(err)),
3080 _phantom: PhantomData,
3081 }
3082 }
3083}
3084
3085impl<F, WD: WorkflowDefinition> Unpin for ChildWorkflowStartFut<F, WD> where F: Unpin {}
3086
3087impl<F, WD> Future for ChildWorkflowStartFut<F, WD>
3088where
3089 F: Future<Output = PendingChildWorkflow<WD>> + Unpin,
3090 WD: WorkflowDefinition,
3091{
3092 type Output = StartChildWorkflowResult;
3093
3094 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
3095 let this = self.get_mut();
3096 let poll =
3097 match this {
3098 ChildWorkflowStartFut::Errored { error, .. } => {
3099 Poll::Ready(Err(*error.take().expect("polled after completion")))
3100 }
3101 ChildWorkflowStartFut::Running(inner) => match Pin::new(inner).poll(cx) {
3102 Poll::Pending => Poll::Pending,
3103 Poll::Ready(pending) => Poll::Ready(match pending.status {
3104 ChildWorkflowStartStatus::Succeeded(s) => {
3105 let ChildWfCommon {
3106 workflow_id,
3107 child_seq,
3108 result_future,
3109 base_ctx,
3110 } = pending.common;
3111 Ok(StartChildWorkflowOutput {
3112 run_id: s.run_id,
3113 result_future,
3114 workflow_id,
3115 child_seq,
3116 base_ctx,
3117 })
3118 }
3119 ChildWorkflowStartStatus::Failed(f) => {
3120 let mut result_future = pending.common.result_future;
3121 result_future.unregister_cancellation();
3122 Err(ChildWorkflowStartError::StartFailed {
3123 workflow_id: f.workflow_id,
3124 workflow_type: f.workflow_type,
3125 cause: StartChildWorkflowExecutionFailedCause::try_from(f.cause)
3126 .unwrap_or(StartChildWorkflowExecutionFailedCause::Unspecified),
3127 })
3128 }
3129 ChildWorkflowStartStatus::Cancelled(c) => {
3130 let ChildWfCommon {
3131 mut result_future,
3132 base_ctx,
3133 ..
3134 } = pending.common;
3135 result_future.unregister_cancellation();
3136 Err(base_ctx.data_converter().to_error(
3137 &SerializationContextData::Workflow(
3138 WorkflowSerializationContext::new(),
3139 ),
3140 c.failure.unwrap_or_default(),
3141 ChildWorkflowStartDecodeHint::default(),
3142 )?)
3143 }
3144 }),
3145 },
3146 ChildWorkflowStartFut::Terminated => panic!("polled after termination"),
3147 };
3148 if poll.is_ready() {
3149 *this = ChildWorkflowStartFut::Terminated;
3150 }
3151 poll
3152 }
3153}
3154
3155impl<F, WD> FusedFuture for ChildWorkflowStartFut<F, WD>
3156where
3157 F: Future<Output = PendingChildWorkflow<WD>> + Unpin,
3158 WD: WorkflowDefinition,
3159{
3160 fn is_terminated(&self) -> bool {
3161 matches!(self, ChildWorkflowStartFut::Terminated)
3162 }
3163}
3164
3165impl<F, WD> CancellableFuture for ChildWorkflowStartFut<F, WD>
3166where
3167 F: CancellableFutureWithReason<Output = PendingChildWorkflow<WD>> + Unpin,
3168 WD: WorkflowDefinition,
3169{
3170 fn cancel(&self) {
3171 if let ChildWorkflowStartFut::Running(inner) = self {
3172 inner.cancel()
3173 }
3174 }
3175}
3176
3177impl<F, WD> CancellableFutureWithReason for ChildWorkflowStartFut<F, WD>
3178where
3179 F: CancellableFutureWithReason<Output = PendingChildWorkflow<WD>> + Unpin,
3180 WD: WorkflowDefinition,
3181{
3182 fn cancel_with_reason(&self, reason: String) {
3183 if let ChildWorkflowStartFut::Running(inner) = self {
3184 inner.cancel_with_reason(reason)
3185 }
3186 }
3187}
3188
3189enum SignalChildFut<F> {
3191 Running {
3192 inner: F,
3193 data_converter: DataConverter,
3194 },
3195 Terminated,
3196}
3197
3198impl<F> Unpin for SignalChildFut<F> where F: Unpin {}
3199
3200impl<F> Future for SignalChildFut<F>
3201where
3202 F: Future<Output = SignalExternalWfResult> + Unpin,
3203{
3204 type Output = Result<(), WorkflowSignalError>;
3205
3206 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
3207 let this = self.get_mut();
3208 let poll = match this {
3209 SignalChildFut::Running {
3210 inner,
3211 data_converter,
3212 } => match Pin::new(inner).poll(cx) {
3213 Poll::Pending => Poll::Pending,
3214 Poll::Ready(Ok(_)) => Poll::Ready(Ok(())),
3215 Poll::Ready(Err(failure)) => Poll::Ready(Err(data_converter.to_error(
3216 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
3217 failure,
3218 WorkflowSignalDecodeHint::default(),
3219 )?)),
3220 },
3221 SignalChildFut::Terminated => panic!("polled after termination"),
3222 };
3223 if poll.is_ready() {
3224 *this = SignalChildFut::Terminated;
3225 }
3226 poll
3227 }
3228}
3229
3230impl<F> FusedFuture for SignalChildFut<F>
3231where
3232 F: Future<Output = SignalExternalWfResult> + Unpin,
3233{
3234 fn is_terminated(&self) -> bool {
3235 matches!(self, SignalChildFut::Terminated)
3236 }
3237}
3238
3239impl<F> CancellableFuture for SignalChildFut<F>
3240where
3241 F: CancellableFuture<Output = SignalExternalWfResult> + Unpin,
3242{
3243 fn cancel(&self) {
3244 if let SignalChildFut::Running { inner, .. } = self {
3245 inner.cancel()
3246 }
3247 }
3248}
3249
3250impl<WD: WorkflowDefinition> StartedChildWorkflow<WD>
3251where
3252 WD::Output: TemporalDeserializable + 'static,
3253{
3254 pub fn result(
3257 self,
3258 ) -> impl CancellableFutureWithReason<Output = Result<WD::Output, ChildWorkflowExecutionError>>
3259 {
3260 self.result_future.map(|result| {
3261 result.and_then(|output| {
3262 output
3263 .downcast::<WD::Output>()
3264 .map(|output| *output)
3265 .map_err(|_| {
3266 ChildWorkflowExecutionError::Serialization(outbound_type_error(
3267 "child workflow output",
3268 ))
3269 })
3270 })
3271 })
3272 }
3273
3274 pub fn cancel(&self, reason: String) {
3276 self.base_ctx.cancel(CancellableID::ChildWorkflow {
3277 seqnum: self.child_seq,
3278 reason,
3279 });
3280 }
3281
3282 pub fn signal<S: SignalDefinition<Workflow = WD> + 'static>(
3286 &self,
3287 signal: S,
3288 input: S::Input,
3289 options: SignalWorkflowOptions,
3290 ) -> impl CancellableFuture<Output = Result<(), WorkflowSignalError>> + 'static {
3291 self.base_ctx.signal_workflow(
3292 SignalWorkflowTarget::Child {
3293 workflow_id: self.workflow_id.clone(),
3294 },
3295 signal,
3296 input,
3297 options,
3298 )
3299 }
3300}
3301
3302#[derive(derive_more::Debug)]
3308pub struct ExternalWorkflowHandle {
3309 workflow_id: String,
3310 run_id: Option<String>,
3311 namespace: String,
3312 #[debug(skip)]
3313 base_ctx: BaseWorkflowContext,
3314}
3315
3316impl ExternalWorkflowHandle {
3317 pub fn workflow_id(&self) -> &str {
3319 &self.workflow_id
3320 }
3321
3322 pub fn run_id(&self) -> Option<&str> {
3324 self.run_id.as_deref()
3325 }
3326
3327 pub fn signal<S: SignalDefinition + 'static>(
3331 &self,
3332 signal: S,
3333 input: S::Input,
3334 options: SignalWorkflowOptions,
3335 ) -> impl CancellableFuture<Output = Result<(), WorkflowSignalError>> + 'static {
3336 self.base_ctx.signal_workflow(
3337 SignalWorkflowTarget::External {
3338 namespace: self.namespace.clone(),
3339 workflow_id: self.workflow_id.clone(),
3340 run_id: self.run_id.clone(),
3341 },
3342 signal,
3343 input,
3344 options,
3345 )
3346 }
3347
3348 pub fn cancel(
3350 &self,
3351 reason: Option<String>,
3352 ) -> impl FusedFuture<Output = CancelExternalWfResult> {
3353 self.base_ctx
3354 .cancel_external_workflow(CancelExternalWorkflowInput {
3355 workflow_id: self.workflow_id.clone(),
3356 run_id: self.run_id.clone(),
3357 reason,
3358 })
3359 }
3360}
3361
3362#[cfg(test)]
3363mod tests {
3364 use super::*;
3365 use crate::MemoValues;
3366 use std::{
3367 collections::HashMap,
3368 sync::{
3369 Mutex,
3370 atomic::{AtomicUsize, Ordering as AtomicOrdering},
3371 },
3372 task::Wake,
3373 time::Duration,
3374 };
3375 use temporalio_common_wasm::{
3376 data_converters::{TemporalDeserializable, TemporalSerializable},
3377 error::OutgoingWorkflowError,
3378 protos::{
3379 coresdk::{
3380 AsJsonPayloadExt, FromJsonPayloadExt,
3381 common::VersioningIntent as ProtoVersioningIntent,
3382 workflow_activation::{UpdateRandomSeed, WorkflowActivationJob},
3383 workflow_commands::WorkflowCommand,
3384 },
3385 temporal::api::{
3386 common::v1::Payload,
3387 enums::v1::ContinueAsNewVersioningBehavior as ProtoContinueAsNewVersioningBehavior,
3388 },
3389 },
3390 };
3391 use temporalio_macros::{workflow, workflow_methods};
3392
3393 #[derive(Default)]
3394 struct NoopHost;
3395
3396 struct CountingWake(Arc<AtomicUsize>);
3397
3398 impl Wake for CountingWake {
3399 fn wake(self: Arc<Self>) {
3400 self.wake_by_ref();
3401 }
3402
3403 fn wake_by_ref(self: &Arc<Self>) {
3404 self.0.fetch_add(1, AtomicOrdering::Relaxed);
3405 }
3406 }
3407
3408 impl WorkflowHost for NoopHost {
3409 fn set_current_details(&self, _details: String) {}
3410 fn push_command(&self, _command: WorkflowCommand) {}
3411 }
3412
3413 #[derive(Default)]
3414 struct RecordingHost {
3415 commands: Rc<RefCell<Vec<WorkflowCommand>>>,
3416 }
3417
3418 impl WorkflowHost for RecordingHost {
3419 fn set_current_details(&self, _details: String) {}
3420
3421 fn push_command(&self, command: WorkflowCommand) {
3422 self.commands.borrow_mut().push(command);
3423 }
3424 }
3425
3426 #[derive(Debug)]
3427 struct FailingMemoValue;
3428
3429 impl TemporalSerializable for FailingMemoValue {
3430 fn to_payload(
3431 &self,
3432 _ctx: &temporalio_common_wasm::data_converters::SerializationContext<'_>,
3433 ) -> Result<Payload, temporalio_common_wasm::data_converters::PayloadConversionError>
3434 {
3435 Err(
3436 temporalio_common_wasm::data_converters::PayloadConversionError::EncodingError(
3437 std::io::Error::other("memo serialization failure").into(),
3438 ),
3439 )
3440 }
3441 }
3442
3443 #[workflow]
3444 #[derive(Default)]
3445 struct TestWorkflow;
3446
3447 #[workflow_methods]
3448 impl TestWorkflow {
3449 #[run]
3450 async fn run(_ctx: &mut WorkflowContext<Self>, _input: u8) -> crate::WorkflowResult<()> {
3451 unreachable!("test workflow run should not be polled")
3452 }
3453
3454 #[signal]
3455 fn test_signal(&mut self, _ctx: &mut SyncWorkflowContext<Self>, _input: String) {
3456 unreachable!("test workflow signal should not be dispatched")
3457 }
3458 }
3459
3460 fn test_context() -> WorkflowContext<TestWorkflow> {
3461 test_context_with_seed(0)
3462 }
3463
3464 fn test_context_with_seed(randomness_seed: u64) -> WorkflowContext<TestWorkflow> {
3465 let init = InitializeWorkflow {
3466 workflow_type: TestWorkflow.name().to_string(),
3467 randomness_seed,
3468 ..Default::default()
3469 };
3470 let init = WorkflowInit {
3471 namespace: "default".to_string(),
3472 task_queue: "orig-task-queue".to_string(),
3473 run_id: "run-id".to_string(),
3474 initialize_workflow: init,
3475 };
3476 let base = BaseWorkflowContext::from_raw(
3477 init,
3478 DataConverter::default(),
3479 Rc::new(NoopHost),
3480 None,
3481 Vec::new(),
3482 );
3483 WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)))
3484 }
3485
3486 fn patch_test_context(
3487 callback: Option<PatchActivationCallback>,
3488 ) -> (
3489 BaseWorkflowContext,
3490 WorkflowContext<TestWorkflow>,
3491 Rc<RefCell<Vec<WorkflowCommand>>>,
3492 ) {
3493 let init = InitializeWorkflow {
3494 workflow_id: "workflow-id".to_string(),
3495 workflow_type: TestWorkflow.name().to_string(),
3496 ..Default::default()
3497 };
3498 let host = Rc::new(RecordingHost::default());
3499 let commands = host.commands.clone();
3500 let init = WorkflowInit {
3501 namespace: "default".to_string(),
3502 task_queue: "task-queue".to_string(),
3503 run_id: "run-id".to_string(),
3504 initialize_workflow: init,
3505 };
3506 let base = BaseWorkflowContext::from_raw(
3507 init,
3508 DataConverter::default(),
3509 host,
3510 callback,
3511 Vec::new(),
3512 );
3513
3514 let ctx = WorkflowContext::from_base(base.clone(), Rc::new(RefCell::new(TestWorkflow)));
3515 (base, ctx, commands)
3516 }
3517
3518 struct ShortCircuitFirstTimer {
3519 calls: AtomicUsize,
3520 }
3521
3522 impl WorkflowInterceptor for ShortCircuitFirstTimer {
3523 fn start_timer(
3524 &self,
3525 _ctx: WorkflowInterceptorContext,
3526 input: StartTimerInput,
3527 next: WorkflowNext<
3528 'static,
3529 StartTimerInput,
3530 CancellableWorkflowOutboundFuture<TimerResult>,
3531 >,
3532 ) -> CancellableWorkflowOutboundFuture<TimerResult> {
3533 if self.calls.fetch_add(1, Ordering::Relaxed) == 0 {
3534 CancellableWorkflowOutboundFuture::new(
3535 async { TimerResult::Cancelled },
3536 WorkflowCancellationHandle::new(|_| {}),
3537 )
3538 } else {
3539 next.run(input)
3540 }
3541 }
3542 }
3543
3544 #[test]
3545 fn short_circuited_outbound_call_does_not_consume_sequence_number() {
3546 let host = Rc::new(RecordingHost::default());
3547 let init = InitializeWorkflow {
3548 workflow_type: TestWorkflow.name().to_string(),
3549 ..Default::default()
3550 };
3551 let init = WorkflowInit {
3552 namespace: "default".to_string(),
3553 task_queue: "task-queue".to_string(),
3554 run_id: "run-id".to_string(),
3555 initialize_workflow: init,
3556 };
3557 let base = BaseWorkflowContext::from_raw(
3558 init,
3559 DataConverter::default(),
3560 host.clone(),
3561 None,
3562 vec![WorkflowInterceptorConstructor::new(|_| {
3563 ShortCircuitFirstTimer {
3564 calls: AtomicUsize::new(0),
3565 }
3566 })],
3567 );
3568
3569 let first = base.timer(Duration::from_secs(1));
3570 assert_eq!(first.now_or_never(), Some(TimerResult::Cancelled));
3571 let _second = base.timer(Duration::from_secs(1));
3572
3573 let commands = host.commands.borrow();
3574 assert_eq!(commands.len(), 1);
3575 let Some(workflow_command::Variant::StartTimer(timer)) = &commands[0].variant else {
3576 panic!("expected start timer command");
3577 };
3578 assert_eq!(timer.seq, 1);
3579 }
3580
3581 #[cfg(feature = "experimental")]
3582 mod experimental_operation_tests {
3583 use super::*;
3584 use temporalio_common_wasm::protos::{
3585 coresdk::workflow_activation::{
3586 ResolveChildWorkflowExecutionStartSuccess, resolve_nexus_operation_start,
3587 },
3588 temporal::api::sdk::v1::{EventGroupMarker, event_group_marker},
3589 };
3590
3591 struct TestActivity;
3592
3593 impl ActivityDefinition for TestActivity {
3594 type Input = ();
3595 type Output = ();
3596
3597 fn name(&self) -> &str {
3598 "test_activity"
3599 }
3600 }
3601
3602 #[test]
3603 fn custom_token_cancels_command_backed_operations() {
3604 let host = Rc::new(RecordingHost::default());
3605 let init = WorkflowInit {
3606 namespace: "default".to_string(),
3607 task_queue: "task-queue".to_string(),
3608 run_id: "run-id".to_string(),
3609 initialize_workflow: InitializeWorkflow {
3610 workflow_type: TestWorkflow.name().to_string(),
3611 ..Default::default()
3612 },
3613 };
3614 let base = BaseWorkflowContext::from_raw(
3615 init,
3616 DataConverter::default(),
3617 host.clone(),
3618 None,
3619 Vec::new(),
3620 );
3621 let token = WorkflowCancellationToken::new();
3622
3623 let timer = base.timer(TimerOptions {
3624 duration: Duration::from_secs(1),
3625 cancellation_token: Some(token.clone()),
3626 summary: None,
3627 event_group_markers: vec![],
3628 });
3629
3630 let mut activity_options =
3631 ActivityOptions::start_to_close_timeout(Duration::from_secs(1));
3632 activity_options.cancellation_token = Some(token.clone());
3633 let activity = base.execute_activity(TestActivity, (), activity_options);
3634
3635 let mut local_activity_options = LocalActivityOptions {
3636 schedule_to_close_timeout: Some(Duration::from_secs(1)),
3637 ..Default::default()
3638 };
3639 local_activity_options.cancellation_token = Some(token.clone());
3640 let local_activity =
3641 base.execute_local_activity(TestActivity, (), local_activity_options);
3642
3643 let child_options = ChildWorkflowOptions {
3644 cancellation_token: Some(token.clone()),
3645 ..Default::default()
3646 };
3647 let child = base.start_child_workflow(TestWorkflow::run, 1, child_options);
3648
3649 let signal = base.external_workflow("external", None).signal(
3650 TestWorkflow::test_signal,
3651 "input".to_string(),
3652 SignalWorkflowOptions::builder()
3653 .cancellation_token(token.clone())
3654 .build(),
3655 );
3656
3657 let nexus_options = NexusOperationOptions::builder()
3658 .endpoint("endpoint")
3659 .service("service")
3660 .operation("operation")
3661 .cancellation_token(token.clone())
3662 .build();
3663 let nexus = base.start_nexus_operation(nexus_options);
3664
3665 token.cancel_with_reason("group cancelled");
3666 timer.cancel();
3667 activity.cancel();
3668 local_activity.cancel();
3669 child.cancel_with_reason("explicit cancellation".to_string());
3670 signal.cancel();
3671 nexus.cancel();
3672
3673 let commands = host.commands.borrow();
3674 assert_eq!(
3675 commands
3676 .iter()
3677 .filter(|command| matches!(
3678 &command.variant,
3679 Some(workflow_command::Variant::CancelTimer(_))
3680 ))
3681 .count(),
3682 1
3683 );
3684 assert_eq!(
3685 commands
3686 .iter()
3687 .filter(|command| matches!(
3688 &command.variant,
3689 Some(workflow_command::Variant::RequestCancelActivity(_))
3690 ))
3691 .count(),
3692 1
3693 );
3694 assert_eq!(
3695 commands
3696 .iter()
3697 .filter(|command| matches!(
3698 &command.variant,
3699 Some(workflow_command::Variant::RequestCancelLocalActivity(_))
3700 ))
3701 .count(),
3702 1
3703 );
3704 let child_cancellations = commands
3705 .iter()
3706 .filter_map(|command| match &command.variant {
3707 Some(workflow_command::Variant::CancelChildWorkflowExecution(cancel)) => {
3708 Some(cancel)
3709 }
3710 _ => None,
3711 })
3712 .collect::<Vec<_>>();
3713 assert_eq!(child_cancellations.len(), 1);
3714 assert_eq!(child_cancellations[0].reason, "group cancelled");
3715 assert_eq!(
3716 commands
3717 .iter()
3718 .filter(|command| matches!(
3719 &command.variant,
3720 Some(workflow_command::Variant::CancelSignalWorkflow(_))
3721 ))
3722 .count(),
3723 1
3724 );
3725 assert_eq!(
3726 commands
3727 .iter()
3728 .filter(|command| matches!(
3729 &command.variant,
3730 Some(workflow_command::Variant::RequestCancelNexusOperation(_))
3731 ))
3732 .count(),
3733 1
3734 );
3735 }
3736
3737 #[test]
3738 fn child_and_nexus_tokens_remain_active_after_start() {
3739 let host = Rc::new(RecordingHost::default());
3740 let init = WorkflowInit {
3741 namespace: "default".to_string(),
3742 task_queue: "task-queue".to_string(),
3743 run_id: "run-id".to_string(),
3744 initialize_workflow: InitializeWorkflow {
3745 workflow_type: TestWorkflow.name().to_string(),
3746 ..Default::default()
3747 },
3748 };
3749 let base = BaseWorkflowContext::from_raw(
3750 init,
3751 DataConverter::default(),
3752 host.clone(),
3753 None,
3754 Vec::new(),
3755 );
3756
3757 let child_token = WorkflowCancellationToken::new();
3758 let child_options = ChildWorkflowOptions {
3759 cancellation_token: Some(child_token.clone()),
3760 ..Default::default()
3761 };
3762 let child = base.start_child_workflow(TestWorkflow::run, 1, child_options);
3763 base.unblock(UnblockEvent::WorkflowStart(
3764 1,
3765 Box::new(ChildWorkflowStartStatus::Succeeded(
3766 ResolveChildWorkflowExecutionStartSuccess {
3767 run_id: "child-run".to_string(),
3768 },
3769 )),
3770 ))
3771 .unwrap();
3772 let started_child = child
3773 .now_or_never()
3774 .expect("child start should resolve")
3775 .unwrap();
3776 child_token.cancel();
3777 started_child.cancel("explicit cancellation".to_string());
3778
3779 let nexus_token = WorkflowCancellationToken::new();
3780 let nexus_options = NexusOperationOptions::builder()
3781 .endpoint("endpoint")
3782 .service("service")
3783 .operation("operation")
3784 .cancellation_token(nexus_token.clone())
3785 .build();
3786 let nexus = base.start_nexus_operation(nexus_options);
3787 base.unblock(UnblockEvent::NexusOperationStart(
3788 1,
3789 Box::new(resolve_nexus_operation_start::Status::OperationToken(
3790 "operation-token".to_string(),
3791 )),
3792 ))
3793 .unwrap();
3794 let started_nexus = nexus
3795 .now_or_never()
3796 .expect("Nexus start should resolve")
3797 .unwrap();
3798 nexus_token.cancel();
3799 started_nexus.cancel();
3800
3801 let commands = host.commands.borrow();
3802 assert_eq!(
3803 commands
3804 .iter()
3805 .filter(|command| matches!(
3806 &command.variant,
3807 Some(workflow_command::Variant::CancelChildWorkflowExecution(_))
3808 ))
3809 .count(),
3810 1
3811 );
3812 assert_eq!(
3813 commands
3814 .iter()
3815 .filter(|command| matches!(
3816 &command.variant,
3817 Some(workflow_command::Variant::RequestCancelNexusOperation(_))
3818 ))
3819 .count(),
3820 1
3821 );
3822 }
3823
3824 #[test]
3825 fn local_activity_token_cancels_retry_backoff_timer() {
3826 let host = Rc::new(RecordingHost::default());
3827 let init = WorkflowInit {
3828 namespace: "default".to_string(),
3829 task_queue: "task-queue".to_string(),
3830 run_id: "run-id".to_string(),
3831 initialize_workflow: InitializeWorkflow {
3832 workflow_type: TestWorkflow.name().to_string(),
3833 ..Default::default()
3834 },
3835 };
3836 let base = BaseWorkflowContext::from_raw(
3837 init,
3838 DataConverter::default(),
3839 host.clone(),
3840 None,
3841 Vec::new(),
3842 );
3843 let token = WorkflowCancellationToken::new();
3844 let marker = EventGroupMarker {
3845 variant: Some(event_group_marker::Variant::Label(
3846 event_group_marker::Label {
3847 id: "la-group".to_string(),
3848 label: Some("la-group".as_json_payload().unwrap()),
3849 },
3850 )),
3851 };
3852 let mut options = LocalActivityOptions {
3853 schedule_to_close_timeout: Some(Duration::from_secs(10)),
3854 event_group_markers: vec![marker.clone()],
3855 ..Default::default()
3856 };
3857 options.cancellation_token = Some(token.clone());
3858 let activity = base.execute_local_activity(TestActivity, (), options);
3859 futures_util::pin_mut!(activity);
3860 base.unblock(UnblockEvent::Activity(
3861 1,
3862 Box::new(ActivityResolution {
3863 status: Some(activity_resolution::Status::Backoff(
3864 temporalio_common_wasm::protos::coresdk::activity_result::DoBackoff {
3865 attempt: 2,
3866 backoff_duration: Some(Duration::from_secs(5).try_into().unwrap()),
3867 original_schedule_time: None,
3868 },
3869 )),
3870 }),
3871 ))
3872 .unwrap();
3873
3874 assert!(activity.as_mut().now_or_never().is_none());
3875 token.cancel();
3876
3877 let commands = host.commands.borrow();
3878 assert!(commands.iter().any(|command| matches!(
3879 &command.variant,
3880 Some(workflow_command::Variant::CancelTimer(_))
3881 )));
3882
3883 let start_timer = commands
3884 .iter()
3885 .find(|command| {
3886 matches!(
3887 &command.variant,
3888 Some(workflow_command::Variant::StartTimer(_))
3889 )
3890 })
3891 .expect("backoff StartTimer is issued");
3892 assert_eq!(start_timer.event_group_markers, [marker]);
3893 }
3894 }
3895
3896 #[test]
3897 fn patch_activation_callback_activates_and_memoizes() {
3898 let calls = Arc::new(AtomicUsize::new(0));
3899 let input = Arc::new(Mutex::new(None));
3900 let callback_calls = calls.clone();
3901 let callback_input = input.clone();
3902 let callback: PatchActivationCallback = Arc::new(move |value| {
3903 assert!(matches!(
3904 value.workflow_info.random_stream("plugin").source,
3905 WorkflowRandomStreamSource::System(_)
3906 ));
3907 callback_calls.fetch_add(1, AtomicOrdering::Relaxed);
3908 *callback_input.lock().unwrap() = Some((
3909 value.workflow_info.workflow_id().to_string(),
3910 value.workflow_info.run_id().to_string(),
3911 value.patch_id,
3912 ));
3913 true
3914 });
3915 let (_, ctx, commands) = patch_test_context(Some(callback));
3916
3917 assert!(ctx.patched("my-patch"));
3918 assert!(ctx.patched("my-patch"));
3919 assert_eq!(calls.load(AtomicOrdering::Relaxed), 1);
3920 assert_eq!(commands.borrow().len(), 1);
3921 let input = input.lock().unwrap();
3922 let input = input.as_ref().unwrap();
3923 assert_eq!(input.0, "workflow-id");
3924 assert_eq!(input.1, "run-id");
3925 assert_eq!(input.2, "my-patch");
3926 }
3927
3928 #[test]
3929 fn patch_activation_callback_can_decline_and_memoizes() {
3930 let calls = Arc::new(AtomicUsize::new(0));
3931 let callback_calls = calls.clone();
3932 let callback: PatchActivationCallback = Arc::new(move |_| {
3933 callback_calls.fetch_add(1, AtomicOrdering::Relaxed);
3934 false
3935 });
3936 let (_, ctx, commands) = patch_test_context(Some(callback));
3937
3938 assert!(!ctx.patched("my-patch"));
3939 assert!(!ctx.patched("my-patch"));
3940 assert_eq!(calls.load(AtomicOrdering::Relaxed), 1);
3941 assert!(commands.borrow().is_empty());
3942 }
3943
3944 #[test]
3945 fn patch_activation_callback_bypasses_history_and_deprecation() {
3946 let callback: PatchActivationCallback = Arc::new(|_| panic!("callback must not run"));
3947
3948 let (base, ctx, commands) = patch_test_context(Some(callback.clone()));
3949 base.apply_activation_context(
3950 &CoreWorkflowActivation {
3951 is_replaying: true,
3952 ..Default::default()
3953 },
3954 true,
3955 );
3956 assert!(!ctx.patched("replay-patch"));
3957 assert!(commands.borrow().is_empty());
3958
3959 let (base, ctx, commands) = patch_test_context(Some(callback.clone()));
3960 base.apply_activation_context(
3961 &CoreWorkflowActivation {
3962 is_replaying: true,
3963 ..Default::default()
3964 },
3965 true,
3966 );
3967 base.notify_patch("existing-patch".to_string());
3968 assert!(ctx.patched("existing-patch"));
3969 assert_eq!(commands.borrow().len(), 1);
3970
3971 let (_, ctx, commands) = patch_test_context(Some(callback));
3972 assert!(ctx.deprecate_patch("deprecated-patch"));
3973 assert_eq!(commands.borrow().len(), 1);
3974 }
3975
3976 #[test]
3977 fn patch_activation_defaults_to_active() {
3978 let (_, ctx, commands) = patch_test_context(None);
3979
3980 assert!(ctx.patched("my-patch"));
3981 assert_eq!(commands.borrow().len(), 1);
3982 }
3983
3984 #[test]
3985 fn random_is_deterministic_for_supported_numeric_types() {
3986 let first = test_context_with_seed(42);
3987 let second = test_context_with_seed(42);
3988
3989 assert_eq!(first.random::<u8>(), second.random::<u8>());
3990 assert_eq!(first.random::<i64>(), second.random::<i64>());
3991 assert_eq!(first.random::<u128>(), second.random::<u128>());
3992 assert_eq!(first.random::<f32>(), second.random::<f32>());
3993 assert_eq!(first.random::<f64>(), second.random::<f64>());
3994 assert_eq!(first.uuid4(), second.uuid4());
3995 }
3996
3997 #[test]
3998 fn random_is_reseeded_by_activation() {
3999 let ctx = test_context_with_seed(123);
4000 let expected = ctx.random::<u64>();
4001 let activation = CoreWorkflowActivation {
4002 jobs: vec![WorkflowActivationJob {
4003 variant: Some(ActivationVariant::UpdateRandomSeed(UpdateRandomSeed {
4004 randomness_seed: 123,
4005 })),
4006 }],
4007 ..Default::default()
4008 };
4009
4010 ctx.sync.base.apply_activation_context(&activation, false);
4011
4012 assert_eq!(ctx.random::<u64>(), expected);
4013 }
4014
4015 #[test]
4016 fn named_random_lookup_continues_the_same_stream() {
4017 let ctx = test_context_with_seed(42);
4018 let first_lookup = ctx.random_stream("orders");
4019 let first = first_lookup.random::<u64>();
4020 let second = ctx.random_stream("orders").random::<u64>();
4021
4022 let expected = test_context_with_seed(42).random_stream("orders");
4023 assert_eq!(first, expected.random::<u64>());
4024 assert_eq!(second, expected.random::<u64>());
4025 }
4026
4027 #[test]
4028 fn named_random_sequence_is_stable() {
4029 let stream = test_context_with_seed(42).random_stream("example.com/orders");
4030
4031 assert_eq!(stream.random::<u64>(), 18_054_372_068_998_079_507);
4033 }
4034
4035 #[test]
4036 fn named_random_streams_are_isolated() {
4037 let ctx = test_context_with_seed(42);
4038 let alpha = ctx.random_stream("alpha");
4039 let first_alpha = alpha.random::<u64>();
4040 let _ = ctx.random_stream("beta").random::<u64>();
4041 let second_alpha = alpha.random::<u64>();
4042
4043 let expected_ctx = test_context_with_seed(42);
4044 let expected_alpha = expected_ctx.random_stream("alpha");
4045 assert_eq!(first_alpha, expected_alpha.random::<u64>());
4046 assert_eq!(second_alpha, expected_alpha.random::<u64>());
4047 assert_ne!(
4048 test_context_with_seed(42)
4049 .random_stream("alpha")
4050 .random::<u64>(),
4051 test_context_with_seed(42)
4052 .random_stream("beta")
4053 .random::<u64>()
4054 );
4055 }
4056
4057 #[test]
4058 fn named_random_does_not_advance_default_randomness() {
4059 let ctx = test_context_with_seed(42);
4060 let first = ctx.random::<u64>();
4061 let _ = ctx.random_stream("plugin").random::<u64>();
4062 let second = ctx.random::<u64>();
4063
4064 let expected = test_context_with_seed(42);
4065 assert_eq!(first, expected.random::<u64>());
4066 assert_eq!(second, expected.random::<u64>());
4067 }
4068
4069 #[test]
4070 fn interceptor_context_shares_named_random_stream_state() {
4071 let ctx = test_context_with_seed(42);
4072 let first = ctx.random_stream("plugin").random::<u64>();
4073 let interceptor_ctx =
4074 crate::workflow_interceptors::WorkflowInterceptorContext::new(ctx.sync.base.clone());
4075 let second = interceptor_ctx.random_stream("plugin").random::<u64>();
4076
4077 let expected = test_context_with_seed(42).random_stream("plugin");
4078 assert_eq!(first, expected.random::<u64>());
4079 assert_eq!(second, expected.random::<u64>());
4080 }
4081
4082 #[test]
4083 fn replay_safe_context_view_shares_workflow_randomness() {
4084 let ctx = test_context_with_seed(42);
4085 let first = ctx.sync.base.view().random_stream("plugin").random::<u64>();
4086 let second = ctx.random_stream("plugin").random::<u64>();
4087
4088 let expected = test_context_with_seed(42).random_stream("plugin");
4089 assert_eq!(first, expected.random::<u64>());
4090 assert_eq!(second, expected.random::<u64>());
4091 }
4092
4093 #[test]
4094 fn read_only_context_view_does_not_advance_workflow_randomness() {
4095 let ctx = test_context_with_seed(42);
4096 let expected = test_context_with_seed(42)
4097 .random_stream("plugin")
4098 .random::<u64>();
4099
4100 {
4101 let _read_only = ctx.sync.base.enter_read_only();
4102 let _ = ctx.sync.base.view().random_stream("plugin").random::<u64>();
4103 }
4104
4105 assert_eq!(ctx.random_stream("plugin").random::<u64>(), expected);
4106 }
4107
4108 #[test]
4109 fn nested_read_only_scopes_restore_replay_safety() {
4110 let ctx = test_context_with_seed(42);
4111 assert!(ctx.sync.base.requires_replay_safety());
4112
4113 {
4114 let _outer = ctx.sync.base.enter_read_only();
4115 assert!(!ctx.sync.base.requires_replay_safety());
4116 {
4117 let _inner = ctx.sync.base.enter_read_only();
4118 assert!(!ctx.sync.base.requires_replay_safety());
4119 }
4120 assert!(!ctx.sync.base.requires_replay_safety());
4121 }
4122
4123 assert!(ctx.sync.base.requires_replay_safety());
4124 }
4125
4126 #[test]
4127 fn named_random_streams_are_reseeded_by_activation() {
4128 let ctx = test_context_with_seed(123);
4129 let stream = ctx.random_stream("orders");
4130 let _ = stream.random::<u64>();
4131 let activation = CoreWorkflowActivation {
4132 jobs: vec![WorkflowActivationJob {
4133 variant: Some(ActivationVariant::UpdateRandomSeed(UpdateRandomSeed {
4134 randomness_seed: 456,
4135 })),
4136 }],
4137 ..Default::default()
4138 };
4139
4140 ctx.sync.base.apply_activation_context(&activation, false);
4141
4142 let expected = test_context_with_seed(456).random_stream("orders");
4143 assert_eq!(stream.random::<u64>(), expected.random::<u64>());
4144 }
4145
4146 #[cfg(feature = "experimental")]
4147 mod experimental_interceptor_tests {
4148 use super::*;
4149 use crate::workflow_interceptors::StartNexusOperationInput;
4150
4151 struct MutatingRemainingOutboundInterceptor;
4152
4153 impl WorkflowInterceptor for MutatingRemainingOutboundInterceptor {
4154 fn signal_workflow(
4155 &self,
4156 _ctx: WorkflowInterceptorContext,
4157 mut input: SignalWorkflowInput,
4158 next: WorkflowNext<
4159 'static,
4160 SignalWorkflowInput,
4161 CancellableWorkflowOutboundFuture<SignalWorkflowResult>,
4162 >,
4163 ) -> CancellableWorkflowOutboundFuture<SignalWorkflowResult> {
4164 *input.signal_name_mut() = "mutated-signal".to_string();
4165 *input.input_mut::<String>().unwrap() = "mutated-input".to_string();
4166 *input.target_mut() = SignalWorkflowTarget::External {
4167 namespace: "mutated-namespace".to_string(),
4168 workflow_id: "mutated-workflow".to_string(),
4169 run_id: Some("mutated-run".to_string()),
4170 };
4171 input
4172 .headers_mut()
4173 .insert("signal-header".to_string(), Payload::default());
4174 next.run(input)
4175 }
4176
4177 fn cancel_external_workflow(
4178 &self,
4179 _ctx: WorkflowInterceptorContext,
4180 mut input: CancelExternalWorkflowInput,
4181 next: WorkflowNext<
4182 'static,
4183 CancelExternalWorkflowInput,
4184 WorkflowOutboundFuture<CancelExternalWfResult>,
4185 >,
4186 ) -> WorkflowOutboundFuture<CancelExternalWfResult> {
4187 input.workflow_id = "mutated-cancel-workflow".to_string();
4188 input.run_id = Some("mutated-cancel-run".to_string());
4189 input.reason = Some("mutated-reason".to_string());
4190 next.run(input)
4191 }
4192
4193 fn continue_as_new(
4194 &self,
4195 _ctx: crate::workflow_interceptors::SyncWorkflowInterceptorContext,
4196 mut input: ContinueAsNewInput,
4197 next: WorkflowNext<
4198 'static,
4199 ContinueAsNewInput,
4200 crate::workflow_interceptors::ContinueAsNewResult,
4201 >,
4202 ) -> crate::workflow_interceptors::ContinueAsNewResult {
4203 *input.input_mut::<u8>().unwrap() = 42;
4204 input.options_mut().workflow_type = Some("mutated-workflow-type".to_string());
4205 input.headers_mut().insert(
4206 "continue-header".to_string(),
4207 Payload::from(b"continue-header-value".as_slice()),
4208 );
4209 next.run(input)
4210 }
4211
4212 fn start_nexus_operation(
4213 &self,
4214 _ctx: WorkflowInterceptorContext,
4215 mut input: StartNexusOperationInput,
4216 next: WorkflowNext<
4217 'static,
4218 StartNexusOperationInput,
4219 CancellableWorkflowOutboundFuture<
4220 crate::workflow_interceptors::StartNexusOperationResult,
4221 >,
4222 >,
4223 ) -> CancellableWorkflowOutboundFuture<
4224 crate::workflow_interceptors::StartNexusOperationResult,
4225 > {
4226 input.options_mut().endpoint = "mutated-endpoint".to_string();
4227 input.options_mut().service = "mutated-service".to_string();
4228 input.options_mut().operation = "mutated-operation".to_string();
4229 next.run(input)
4230 }
4231 }
4232
4233 #[test]
4234 fn outbound_interceptors_mutate_signal_cancel_continue_as_new_and_nexus() {
4235 let host = Rc::new(RecordingHost::default());
4236 let init = InitializeWorkflow {
4237 workflow_type: TestWorkflow.name().to_string(),
4238 ..Default::default()
4239 };
4240 let init = WorkflowInit {
4241 namespace: "default".to_string(),
4242 task_queue: "task-queue".to_string(),
4243 run_id: "run-id".to_string(),
4244 initialize_workflow: init,
4245 };
4246 let base = BaseWorkflowContext::from_raw(
4247 init,
4248 DataConverter::default(),
4249 host.clone(),
4250 None,
4251 vec![WorkflowInterceptorConstructor::new(|_| {
4252 MutatingRemainingOutboundInterceptor
4253 })],
4254 );
4255 let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4256
4257 let signal = ctx
4258 .external_workflow("original-workflow", Some("original-run".to_string()))
4259 .signal(
4260 TestWorkflow::test_signal,
4261 "original-input".to_string(),
4262 Default::default(),
4263 );
4264 let cancel_target =
4265 ctx.external_workflow("cancel-workflow", Some("cancel-run".to_string()));
4266 let cancel = cancel_target.cancel(Some("original-reason".to_string()));
4267 let termination = ctx
4268 .continue_as_new(7, ContinueAsNewOptions::default())
4269 .expect_err("continue_as_new should terminate the workflow");
4270 let sync_ctx = ctx.sync_context();
4271 let nexus = sync_ctx.start_nexus_operation(
4272 NexusOperationOptions::builder()
4273 .endpoint("original-endpoint")
4274 .service("original-service")
4275 .operation("original-operation")
4276 .build(),
4277 );
4278 drop((signal, cancel, nexus));
4279
4280 let WorkflowTermination::ContinueAsNew(continue_as_new) = termination else {
4281 panic!("expected continue-as-new termination")
4282 };
4283 assert_eq!(continue_as_new.workflow_type, "mutated-workflow-type");
4284 assert_eq!(
4285 continue_as_new.arguments,
4286 vec![42u8.as_json_payload().unwrap()]
4287 );
4288 assert!(continue_as_new.headers.contains_key("continue-header"));
4289
4290 let commands = host.commands.borrow();
4291 assert_eq!(commands.len(), 3);
4292 let Some(workflow_command::Variant::SignalExternalWorkflowExecution(signal)) =
4293 &commands[0].variant
4294 else {
4295 panic!("expected signal command")
4296 };
4297 assert_eq!(signal.signal_name, "mutated-signal");
4298 assert_eq!(
4299 signal.args,
4300 vec!["mutated-input".to_string().as_json_payload().unwrap()]
4301 );
4302 assert!(signal.headers.contains_key("signal-header"));
4303 let Some(signal_external_workflow_execution::Target::WorkflowExecution(target)) =
4304 &signal.target
4305 else {
4306 panic!("expected external workflow signal target")
4307 };
4308 assert_eq!(target.namespace, "mutated-namespace");
4309 assert_eq!(target.workflow_id, "mutated-workflow");
4310 assert_eq!(target.run_id, "mutated-run");
4311
4312 let Some(workflow_command::Variant::RequestCancelExternalWorkflowExecution(cancel)) =
4313 &commands[1].variant
4314 else {
4315 panic!("expected external cancellation command")
4316 };
4317 let target = cancel.workflow_execution.as_ref().unwrap();
4318 assert_eq!(target.workflow_id, "mutated-cancel-workflow");
4319 assert_eq!(target.run_id, "mutated-cancel-run");
4320 assert_eq!(cancel.reason, "mutated-reason");
4321
4322 let Some(workflow_command::Variant::ScheduleNexusOperation(nexus)) =
4323 &commands[2].variant
4324 else {
4325 panic!("expected Nexus operation command")
4326 };
4327 assert_eq!(nexus.endpoint, "mutated-endpoint");
4328 assert_eq!(nexus.service, "mutated-service");
4329 assert_eq!(nexus.operation, "mutated-operation");
4330 }
4331 }
4332
4333 struct HeaderAddingContinueAsNewInterceptor;
4334
4335 impl WorkflowInterceptor for HeaderAddingContinueAsNewInterceptor {
4336 fn continue_as_new(
4337 &self,
4338 _ctx: crate::workflow_interceptors::SyncWorkflowInterceptorContext,
4339 mut input: ContinueAsNewInput,
4340 next: WorkflowNext<
4341 'static,
4342 ContinueAsNewInput,
4343 crate::workflow_interceptors::ContinueAsNewResult,
4344 >,
4345 ) -> crate::workflow_interceptors::ContinueAsNewResult {
4346 input.headers_mut().insert(
4347 "continue-header".to_string(),
4348 Payload::from(b"continue-header-value".as_slice()),
4349 );
4350 next.run(input)
4351 }
4352 }
4353
4354 #[test]
4355 fn continue_as_new_interceptor_header_reaches_proto_command() {
4356 let init = InitializeWorkflow {
4357 workflow_type: TestWorkflow.name().to_string(),
4358 ..Default::default()
4359 };
4360 let init = WorkflowInit {
4361 namespace: "default".to_string(),
4362 task_queue: "task-queue".to_string(),
4363 run_id: "run-id".to_string(),
4364 initialize_workflow: init,
4365 };
4366 let base = BaseWorkflowContext::from_raw(
4367 init,
4368 DataConverter::default(),
4369 Rc::new(NoopHost),
4370 None,
4371 vec![WorkflowInterceptorConstructor::new(|_| {
4372 HeaderAddingContinueAsNewInterceptor
4373 })],
4374 );
4375 let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4376
4377 let termination = ctx
4378 .continue_as_new(7, ContinueAsNewOptions::default())
4379 .expect_err("continue_as_new should terminate the workflow");
4380 let WorkflowTermination::ContinueAsNew(proto_command) = termination else {
4381 panic!("expected continue-as-new termination")
4382 };
4383
4384 assert_eq!(
4385 proto_command.headers,
4386 HashMap::from([(
4387 "continue-header".to_string(),
4388 Payload::from(b"continue-header-value".as_slice()),
4389 )])
4390 );
4391 }
4392
4393 #[test]
4394 fn construction_waker_uses_runtime_poll_waker() {
4395 let base = test_context().sync.base;
4396 let wakes = Arc::new(AtomicUsize::new(0));
4397 let waker = Waker::from(Arc::new(CountingWake(wakes.clone())));
4398 let _guard = base.enter_runtime_poll(&waker);
4399 base.construction_waker().wake_by_ref();
4400 assert_eq!(wakes.load(AtomicOrdering::Relaxed), 1);
4401 }
4402
4403 #[test]
4404 fn workflow_context_continue_as_new_serializes_input_and_defaults() {
4405 let ctx = test_context();
4406
4407 let termination = ctx
4408 .continue_as_new(7, ContinueAsNewOptions::default())
4409 .expect_err("continue_as_new should terminate the workflow");
4410 assert!(
4411 matches!(termination, WorkflowTermination::ContinueAsNew(_)),
4412 "expected continue-as-new termination, got {termination:?}"
4413 );
4414 let WorkflowTermination::ContinueAsNew(cmd) = termination else {
4415 unreachable!()
4416 };
4417
4418 assert_eq!(
4419 *cmd,
4420 crate::runtime::types::ContinueAsNewRequest {
4421 workflow_type: TestWorkflow.name().to_string(),
4422 task_queue: String::new(),
4423 arguments: vec![7u8.as_json_payload().unwrap()],
4424 workflow_run_timeout: None,
4425 workflow_task_timeout: None,
4426 backoff_start_interval: None,
4427 memo: HashMap::new(),
4428 headers: HashMap::new(),
4429 search_attributes: None,
4430 retry_policy: None,
4431 versioning_intent: ProtoVersioningIntent::Unspecified.into(),
4432 initial_versioning_behavior: ProtoContinueAsNewVersioningBehavior::Unspecified
4433 .into(),
4434 }
4435 );
4436 }
4437
4438 #[cfg(feature = "experimental")]
4439 mod experimental_continue_as_new_tests {
4440 use super::*;
4441 use temporalio_common_wasm::{
4442 RetryPolicy, protos::temporal::api::common::v1::RetryPolicy as ProtoRetryPolicy,
4443 };
4444
4445 #[test]
4446 fn sync_workflow_context_continue_as_new_applies_options() {
4447 let ctx = test_context();
4448 let sync = ctx.sync_context();
4449 let mut memo = MemoValues::new();
4450 memo.insert("memo-key", "memo-value".to_string());
4451 let mut proto_search_attributes = ProtoSearchAttributes::default();
4452 proto_search_attributes.indexed_fields.insert(
4453 "CustomKeywordField".to_string(),
4454 Payload::from(b"value".as_slice()),
4455 );
4456 let search_attributes = SearchAttributes::from_proto(&proto_search_attributes);
4457
4458 let termination = sync
4459 .continue_as_new(
4460 11,
4461 ContinueAsNewOptions {
4462 workflow_type: Some("next-workflow".to_string()),
4463 task_queue: Some("next-task-queue".to_string()),
4464 run_timeout: Some(Duration::from_secs(10)),
4465 task_timeout: Some(Duration::from_secs(3)),
4466 backoff_start_interval: Some(Duration::from_secs(4)),
4467 memo: Some(memo.clone()),
4468 search_attributes: Some(search_attributes.clone()),
4469 retry_policy: Some(RetryPolicy::builder().maximum_attempts(5).build()),
4470 versioning_intent: Some(ProtoVersioningIntent::Compatible.into()),
4471 initial_versioning_behavior: Some(
4472 ContinueAsNewVersioningBehavior::UseRampingVersion,
4473 ),
4474 },
4475 )
4476 .expect_err("continue_as_new should terminate the workflow");
4477 assert!(
4478 matches!(termination, WorkflowTermination::ContinueAsNew(_)),
4479 "expected continue-as-new termination, got {termination:?}"
4480 );
4481 let WorkflowTermination::ContinueAsNew(cmd) = termination else {
4482 unreachable!()
4483 };
4484
4485 assert_eq!(
4486 *cmd,
4487 crate::runtime::types::ContinueAsNewRequest {
4488 workflow_type: "next-workflow".to_string(),
4489 task_queue: "next-task-queue".to_string(),
4490 arguments: vec![11u8.as_json_payload().unwrap()],
4491 workflow_run_timeout: Some(Duration::from_secs(10).try_into().unwrap()),
4492 workflow_task_timeout: Some(Duration::from_secs(3).try_into().unwrap()),
4493 backoff_start_interval: Some(Duration::from_secs(4).try_into().unwrap()),
4494 memo: HashMap::from([(
4495 "memo-key".to_string(),
4496 "memo-value".as_json_payload().unwrap(),
4497 )]),
4498 headers: HashMap::new(),
4499 search_attributes: Some(proto_search_attributes),
4500 retry_policy: Some(ProtoRetryPolicy {
4501 initial_interval: Some(Duration::from_secs(1).try_into().unwrap()),
4502 backoff_coefficient: 2.0,
4503 maximum_attempts: 5,
4504 ..Default::default()
4505 }),
4506 versioning_intent: ProtoVersioningIntent::Compatible.into(),
4507 initial_versioning_behavior:
4508 ProtoContinueAsNewVersioningBehavior::UseRampingVersion as i32,
4509 }
4510 );
4511 }
4512
4513 #[test]
4514 fn workflow_context_continue_as_new_applies_auto_upgrade_versioning_behavior() {
4515 let ctx = test_context();
4516
4517 let termination = ctx
4518 .continue_as_new(
4519 13,
4520 ContinueAsNewOptions {
4521 initial_versioning_behavior: Some(
4522 ContinueAsNewVersioningBehavior::AutoUpgrade,
4523 ),
4524 ..Default::default()
4525 },
4526 )
4527 .expect_err("continue_as_new should terminate the workflow");
4528 let WorkflowTermination::ContinueAsNew(cmd) = termination else {
4529 unreachable!()
4530 };
4531
4532 assert_eq!(
4533 cmd.initial_versioning_behavior,
4534 ProtoContinueAsNewVersioningBehavior::AutoUpgrade as i32
4535 );
4536 }
4537 }
4538
4539 #[test]
4540 fn continue_as_new_preserves_explicit_empty_search_attributes() {
4541 let ctx = test_context();
4542 let sync = ctx.sync_context();
4543
4544 let termination = sync
4545 .continue_as_new(
4546 11,
4547 ContinueAsNewOptions {
4548 search_attributes: Some(SearchAttributes::default()),
4549 ..Default::default()
4550 },
4551 )
4552 .expect_err("continue_as_new should terminate the workflow");
4553 let WorkflowTermination::ContinueAsNew(cmd) = termination else {
4554 unreachable!()
4555 };
4556
4557 assert_eq!(
4558 cmd.search_attributes,
4559 Some(ProtoSearchAttributes::default())
4560 );
4561 }
4562
4563 #[test]
4564 fn continue_as_new_preserves_input_serialization_errors() {
4565 #[derive(Debug)]
4566 struct FailingInput;
4567
4568 impl TemporalSerializable for FailingInput {
4569 fn to_payload(
4570 &self,
4571 _ctx: &temporalio_common_wasm::data_converters::SerializationContext<'_>,
4572 ) -> Result<Payload, temporalio_common_wasm::data_converters::PayloadConversionError>
4573 {
4574 Err(
4575 temporalio_common_wasm::data_converters::PayloadConversionError::EncodingError(
4576 std::io::Error::other("serialization failure").into(),
4577 ),
4578 )
4579 }
4580 }
4581
4582 impl TemporalDeserializable for FailingInput {
4583 fn from_payload(
4584 _ctx: &temporalio_common_wasm::data_converters::SerializationContext<'_>,
4585 _payload: Payload,
4586 ) -> Result<Self, temporalio_common_wasm::data_converters::PayloadConversionError>
4587 {
4588 unreachable!("test input is only serialized")
4589 }
4590 }
4591
4592 #[workflow]
4593 #[derive(Default)]
4594 struct FailingWorkflow;
4595
4596 #[workflow_methods]
4597 impl FailingWorkflow {
4598 #[run]
4599 async fn run(
4600 _ctx: &mut WorkflowContext<Self>,
4601 _input: FailingInput,
4602 ) -> crate::WorkflowResult<()> {
4603 unreachable!("test workflow run should not be polled")
4604 }
4605 }
4606
4607 let init = InitializeWorkflow {
4608 workflow_type: "failing-workflow".to_string(),
4609 ..Default::default()
4610 };
4611 let init = WorkflowInit {
4612 namespace: "default".to_string(),
4613 task_queue: "orig-task-queue".to_string(),
4614 run_id: "run-id".to_string(),
4615 initialize_workflow: init,
4616 };
4617 let base = BaseWorkflowContext::from_raw(
4618 init,
4619 DataConverter::default(),
4620 Rc::new(NoopHost),
4621 None,
4622 Vec::new(),
4623 );
4624 let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(FailingWorkflow)));
4625
4626 let termination = ctx
4627 .continue_as_new(FailingInput, ContinueAsNewOptions::default())
4628 .expect_err("input serialization should fail");
4629 let WorkflowTermination::Failed(OutgoingWorkflowError::PayloadConversion(err)) =
4630 termination
4631 else {
4632 panic!("expected a payload conversion failure");
4633 };
4634 assert_eq!(err.to_string(), "Encoding error: serialization failure");
4635 }
4636
4637 #[test]
4638 fn continue_as_new_preserves_memo_serialization_errors() {
4639 let ctx = test_context();
4640 let mut memo = MemoValues::new();
4641 memo.insert("invalid", FailingMemoValue);
4642
4643 let termination = ctx
4644 .continue_as_new(
4645 7,
4646 ContinueAsNewOptions {
4647 memo: Some(memo),
4648 ..Default::default()
4649 },
4650 )
4651 .expect_err("memo serialization should fail");
4652 let WorkflowTermination::Failed(OutgoingWorkflowError::PayloadConversion(err)) =
4653 termination
4654 else {
4655 panic!("expected a payload conversion failure");
4656 };
4657 assert_eq!(
4658 err.to_string(),
4659 "Encoding error: memo serialization failure"
4660 );
4661 }
4662
4663 #[test]
4664 fn upsert_search_attributes_updates_local_state() {
4665 use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4666
4667 const K: SearchAttributeKey<i64> = SearchAttributeKey::int("my_int");
4668
4669 let ctx = test_context();
4670 assert!(ctx.search_attributes().is_empty());
4671
4672 ctx.upsert_search_attributes([K.value_set(42)]);
4673 let attrs = ctx.search_attributes();
4674 assert_eq!(attrs.get(&K), Some(42));
4675 }
4676
4677 #[test]
4678 fn upsert_memo_updates_local_state_and_encodes_removals() {
4679 let init = InitializeWorkflow {
4680 workflow_type: TestWorkflow.name().to_string(),
4681 memo: Some(ProtoMemo {
4682 fields: HashMap::from([("old".to_string(), "before".as_json_payload().unwrap())]),
4683 }),
4684 ..Default::default()
4685 };
4686 let host = Rc::new(RecordingHost::default());
4687 let init = WorkflowInit {
4688 namespace: "default".to_string(),
4689 task_queue: "orig-task-queue".to_string(),
4690 run_id: "run-id".to_string(),
4691 initialize_workflow: init,
4692 };
4693 let base = BaseWorkflowContext::from_raw(
4694 init,
4695 DataConverter::default(),
4696 host.clone(),
4697 None,
4698 Vec::new(),
4699 );
4700 let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4701
4702 assert_eq!(
4703 ctx.memo().get::<String>("old").unwrap(),
4704 Some("before".to_string())
4705 );
4706 ctx.upsert_memo([("new", Some(MemoValue::new(42_u32))), ("old", None)])
4707 .unwrap();
4708
4709 let current = ctx.memo();
4710 assert_eq!(current.get::<u32>("new").unwrap(), Some(42));
4711 assert_eq!(current.get::<String>("old").unwrap(), None);
4712 let view = ctx.view();
4713 assert_eq!(view.memo().get::<u32>("new").unwrap(), Some(42));
4714 assert_eq!(
4715 view.memo().raw(),
4716 view.raw()
4717 .memo
4718 .as_ref()
4719 .expect("view memo should be present")
4720 );
4721
4722 let commands = host.commands.borrow();
4723 let [command] = commands.as_slice() else {
4724 panic!("expected one modify-properties command");
4725 };
4726 let Some(workflow_command::Variant::ModifyWorkflowProperties(command)) = &command.variant
4727 else {
4728 panic!("expected a modify-properties command");
4729 };
4730 let fields = &command.upserted_memo.as_ref().unwrap().fields;
4731 let payload_converter = PayloadConverter::default();
4732 let removal_payload = payload_converter
4733 .to_payload(
4734 &SerializationContext::new(
4735 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
4736 &payload_converter,
4737 ),
4738 &MemoValue::new(()),
4739 )
4740 .unwrap();
4741 assert_eq!(fields.get("old"), Some(&removal_payload));
4742 assert_eq!(
4743 u32::from_json_payload(fields.get("new").unwrap()).unwrap(),
4744 42
4745 );
4746 }
4747
4748 #[test]
4749 fn upsert_memo_conversion_failure_does_not_mutate_or_emit_command() {
4750 let host = Rc::new(RecordingHost::default());
4751 let init = InitializeWorkflow {
4752 workflow_type: TestWorkflow.name().to_string(),
4753 ..Default::default()
4754 };
4755 let init = WorkflowInit {
4756 namespace: "default".to_string(),
4757 task_queue: "orig-task-queue".to_string(),
4758 run_id: "run-id".to_string(),
4759 initialize_workflow: init,
4760 };
4761 let base = BaseWorkflowContext::from_raw(
4762 init,
4763 DataConverter::default(),
4764 host.clone(),
4765 None,
4766 Vec::new(),
4767 );
4768 let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4769 let err = ctx
4770 .upsert_memo([
4771 ("valid", Some(MemoValue::new("value".to_string()))),
4772 ("invalid", Some(MemoValue::new(FailingMemoValue))),
4773 ])
4774 .unwrap_err();
4775
4776 assert_eq!(
4777 err.to_string(),
4778 "Encoding error: memo serialization failure"
4779 );
4780 assert_eq!(ctx.memo().get::<String>("valid").unwrap(), None);
4781 assert!(host.commands.borrow().is_empty());
4782 }
4783
4784 #[test]
4785 fn upsert_search_attributes_unset_removes_from_local_state() {
4786 use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4787
4788 const K: SearchAttributeKey<String> = SearchAttributeKey::keyword("my_kw");
4789
4790 let ctx = test_context();
4791 ctx.upsert_search_attributes([K.value_set("hello".into())]);
4793 assert_eq!(ctx.search_attributes().get(&K), Some("hello".into()));
4794
4795 ctx.upsert_search_attributes([K.value_unset()]);
4796 assert!(!ctx.search_attributes().contains_key(&K));
4797 assert!(ctx.search_attributes().is_empty());
4798 }
4799
4800 #[test]
4801 fn upsert_search_attributes_multiple_updates_last_wins() {
4802 use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4803
4804 const K: SearchAttributeKey<i64> = SearchAttributeKey::int("counter");
4805
4806 let ctx = test_context();
4807 ctx.upsert_search_attributes([K.value_set(1), K.value_set(2)]);
4808 assert_eq!(ctx.search_attributes().get(&K), Some(2));
4809 }
4810
4811 #[test]
4812 fn upsert_search_attributes_merges_with_initial() {
4813 use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4814
4815 const A: SearchAttributeKey<i64> = SearchAttributeKey::int("attr_a");
4816 const B: SearchAttributeKey<String> = SearchAttributeKey::keyword("attr_b");
4817
4818 let init_sa = SearchAttributes::new([A.value_set(1)]).into_proto();
4820 let init = InitializeWorkflow {
4821 workflow_type: TestWorkflow.name().to_string(),
4822 search_attributes: Some(init_sa),
4823 ..Default::default()
4824 };
4825 let init = WorkflowInit {
4826 namespace: "default".to_string(),
4827 task_queue: "tq".to_string(),
4828 run_id: "run-id".to_string(),
4829 initialize_workflow: init,
4830 };
4831 let base = BaseWorkflowContext::from_raw(
4832 init,
4833 DataConverter::default(),
4834 Rc::new(NoopHost),
4835 None,
4836 Vec::new(),
4837 );
4838 let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4839
4840 assert_eq!(ctx.search_attributes().get(&A), Some(1));
4841
4842 ctx.upsert_search_attributes([B.value_set("hello".into())]);
4844 assert_eq!(ctx.search_attributes().get(&A), Some(1));
4845 assert_eq!(ctx.search_attributes().get(&B), Some("hello".into()));
4846 }
4847
4848 #[test]
4849 fn view_search_attributes_returns_typed() {
4850 use temporalio_common_wasm::search_attributes::SearchAttributeKey;
4851
4852 const K: SearchAttributeKey<bool> = SearchAttributeKey::bool("active");
4853
4854 let init_sa = SearchAttributes::new([K.value_set(true)]).into_proto();
4855 let init = InitializeWorkflow {
4856 workflow_type: TestWorkflow.name().to_string(),
4857 search_attributes: Some(init_sa),
4858 ..Default::default()
4859 };
4860 let init = WorkflowInit {
4861 namespace: "default".to_string(),
4862 task_queue: "tq".to_string(),
4863 run_id: "run-id".to_string(),
4864 initialize_workflow: init,
4865 };
4866 let base = BaseWorkflowContext::from_raw(
4867 init,
4868 DataConverter::default(),
4869 Rc::new(NoopHost),
4870 None,
4871 Vec::new(),
4872 );
4873 let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4874
4875 let view = ctx.view();
4876 let sa = view
4877 .search_attributes()
4878 .expect("should have search attributes");
4879 assert_eq!(sa.get(&K), Some(true));
4880 }
4881
4882 #[test]
4883 fn workflow_info_retains_raw_initialization() {
4884 let init = InitializeWorkflow {
4885 workflow_type: TestWorkflow.name().to_string(),
4886 identity: "raw-only-identity".to_owned(),
4887 ..Default::default()
4888 };
4889 let expected = init.clone();
4890 let init = WorkflowInit {
4891 namespace: "default".to_string(),
4892 task_queue: "tq".to_string(),
4893 run_id: "run-id".to_string(),
4894 initialize_workflow: init,
4895 };
4896 let base = BaseWorkflowContext::from_raw(
4897 init,
4898 DataConverter::default(),
4899 Rc::new(NoopHost),
4900 None,
4901 Vec::new(),
4902 );
4903 let ctx = WorkflowContext::from_base(base, Rc::new(RefCell::new(TestWorkflow)));
4904 let info = ctx.info();
4905
4906 assert_eq!(info.raw().identity, "raw-only-identity");
4907 assert_eq!(info.raw(), &expected);
4908 assert_eq!(info.into_raw(), expected);
4909 }
4910}