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