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