1use crate::{
2 BackfillScheduleInput, Client, CreateScheduleInput, CreateScheduleOutput, DeleteScheduleInput,
3 DescribeScheduleInput, DescribeScheduleOutput, ListSchedulesPageInput, ListSchedulesPageOutput,
4 NamespacedClient, Next, PauseScheduleInput, RpcOptions, SendScheduleUpdateInput,
5 TriggerScheduleInput, UnpauseScheduleInput, UpdateScheduleInput, grpc::WorkflowService,
6 interceptors,
7};
8use futures_util::{FutureExt, future::BoxFuture, stream};
9use std::{
10 collections::VecDeque,
11 pin::Pin,
12 sync::Arc,
13 task::{Context, Poll},
14 time::{Duration, SystemTime},
15};
16use temporalio_common::{
17 HasWorkflowDefinition,
18 data_converters::{
19 DataConverter, PayloadConversionError, SerializationContextData, TemporalDeserializable,
20 TemporalSerializable,
21 },
22 payload_visitor::decode_payloads,
23 protos::{
24 coresdk::IntoPayloadsExt,
25 proto_ts_to_system_time,
26 temporal::api::{
27 common::v1 as common_proto, schedule::v1 as schedule_proto,
28 taskqueue::v1 as taskqueue_proto, workflow::v1 as workflow_proto,
29 workflowservice::v1::*,
30 },
31 },
32 search_attributes::SearchAttributes,
33};
34use tonic::IntoRequest;
35use uuid::Uuid;
36
37#[derive(Debug, thiserror::Error)]
39#[non_exhaustive]
40pub enum ScheduleError {
41 #[error("Server error: {0}")]
43 Rpc(#[from] tonic::Status),
44 #[error("Payload conversion error: {0}")]
46 PayloadConversion(#[from] PayloadConversionError),
47 #[error("Malformed schedule description for schedule ID '{schedule_id}': {reason}")]
49 MalformedDescription {
50 schedule_id: String,
52 reason: String,
54 },
55}
56
57trait SerializableScheduleInput: Send + Sync {
58 fn to_payloads<'a>(
59 &'a self,
60 dc: &'a DataConverter,
61 context: &'a SerializationContextData,
62 ) -> BoxFuture<'a, Result<Vec<common_proto::Payload>, PayloadConversionError>>;
63}
64
65impl<T> SerializableScheduleInput for T
66where
67 T: TemporalSerializable + Send + Sync + 'static,
68{
69 fn to_payloads<'a>(
70 &'a self,
71 dc: &'a DataConverter,
72 context: &'a SerializationContextData,
73 ) -> BoxFuture<'a, Result<Vec<common_proto::Payload>, PayloadConversionError>> {
74 dc.to_payloads(context, self).boxed()
75 }
76}
77
78#[derive(derive_more::Debug, Clone)]
80pub struct ScheduleWorkflowInput {
81 repr: ScheduleWorkflowInputRepr,
82}
83
84#[derive(derive_more::Debug, Clone)]
85enum ScheduleWorkflowInputRepr {
86 #[debug("Deferred(...)")]
87 Deferred(#[debug(skip)] Arc<dyn SerializableScheduleInput>),
88}
89
90impl ScheduleWorkflowInput {
91 fn new_deferred<T>(val: T) -> Self
92 where
93 T: SerializableScheduleInput + 'static,
94 {
95 Self {
96 repr: ScheduleWorkflowInputRepr::Deferred(Arc::new(val)),
97 }
98 }
99
100 pub(crate) async fn into_payloads(
101 self,
102 dc: &DataConverter,
103 ) -> Result<Vec<common_proto::Payload>, PayloadConversionError> {
104 let ScheduleWorkflowInputRepr::Deferred(v) = self.repr;
105 v.to_payloads(dc, &SerializationContextData::Workflow).await
106 }
107}
108
109#[derive(Debug, Clone, bon::Builder)]
111#[builder(on(String, into))]
112#[non_exhaustive]
113pub struct CreateScheduleOptions {
114 pub action: ScheduleAction,
116 pub spec: ScheduleSpec,
118 #[builder(default)]
120 pub trigger_immediately: bool,
121 #[builder(default)]
124 pub overlap_policy: ScheduleOverlapPolicy,
125 #[builder(default)]
127 pub paused: bool,
128 #[builder(default)]
130 pub note: String,
131 #[builder(default)]
133 pub rpc_options: RpcOptions,
134}
135
136#[derive(derive_more::Debug, Clone)]
139#[non_exhaustive]
140pub enum ScheduleAction {
141 StartWorkflow {
143 workflow_type: String,
145 task_queue: String,
147 workflow_id: String,
149 input: Option<ScheduleWorkflowInput>,
151 },
152}
153
154impl ScheduleAction {
155 pub fn start_workflow<W>(
157 workflow: W,
158 input: W::Input,
159 task_queue: impl Into<String>,
160 workflow_id: impl Into<String>,
161 ) -> Self
162 where
163 W: HasWorkflowDefinition,
164 W::Input: TemporalSerializable + Send + Sync + 'static,
165 {
166 Self::StartWorkflow {
167 workflow_type: workflow.name().to_string(),
168 task_queue: task_queue.into(),
169 workflow_id: workflow_id.into(),
170 input: Some(ScheduleWorkflowInput::new_deferred(input)),
171 }
172 }
173
174 pub(crate) async fn into_proto(
175 self,
176 dc: &DataConverter,
177 ) -> Result<schedule_proto::ScheduleAction, PayloadConversionError> {
178 match self {
179 Self::StartWorkflow {
180 workflow_type,
181 task_queue,
182 workflow_id,
183 input,
184 } => {
185 let input = if let Some(wi) = input {
186 wi.into_payloads(dc).await?.into_payloads()
187 } else {
188 None
189 };
190 Ok(schedule_proto::ScheduleAction {
191 action: Some(schedule_proto::schedule_action::Action::StartWorkflow(
192 workflow_proto::NewWorkflowExecutionInfo {
193 workflow_id,
194 workflow_type: Some(common_proto::WorkflowType {
195 name: workflow_type,
196 }),
197 task_queue: Some(taskqueue_proto::TaskQueue {
198 name: task_queue,
199 ..Default::default()
200 }),
201 input,
202 ..Default::default()
203 },
204 )),
205 })
206 }
207 }
208 }
209}
210
211#[derive(Debug, Clone, Default, PartialEq, bon::Builder)]
216#[builder(on(String, into))]
217pub struct ScheduleSpec {
218 #[builder(default)]
220 pub intervals: Vec<ScheduleIntervalSpec>,
221 #[builder(default)]
223 pub calendars: Vec<ScheduleCalendarSpec>,
224 #[builder(default)]
226 pub exclude_calendars: Vec<ScheduleCalendarSpec>,
227 #[builder(default)]
229 pub cron_strings: Vec<String>,
230 #[builder(default)]
232 pub timezone_name: String,
233 pub start_time: Option<SystemTime>,
235 pub end_time: Option<SystemTime>,
237 pub jitter: Option<Duration>,
239}
240
241impl ScheduleSpec {
242 pub fn from_interval(every: Duration) -> Self {
244 Self {
245 intervals: vec![every.into()],
246 ..Default::default()
247 }
248 }
249
250 pub fn from_calendar(calendar: ScheduleCalendarSpec) -> Self {
252 Self {
253 calendars: vec![calendar],
254 ..Default::default()
255 }
256 }
257
258 pub(crate) fn into_proto(self) -> schedule_proto::ScheduleSpec {
259 #[allow(deprecated)]
260 schedule_proto::ScheduleSpec {
261 interval: self.intervals.into_iter().map(Into::into).collect(),
262 calendar: self.calendars.into_iter().map(Into::into).collect(),
263 exclude_calendar: self.exclude_calendars.into_iter().map(Into::into).collect(),
264 cron_string: self.cron_strings,
265 timezone_name: self.timezone_name,
266 start_time: self.start_time.map(Into::into),
267 end_time: self.end_time.map(Into::into),
268 jitter: self.jitter.and_then(|d| d.try_into().ok()),
269 ..Default::default()
270 }
271 }
272}
273
274#[derive(Debug, Clone, PartialEq)]
276#[non_exhaustive]
277pub struct ScheduleIntervalSpec {
278 pub every: Duration,
280 pub offset: Option<Duration>,
282}
283
284impl ScheduleIntervalSpec {
285 pub fn new(every: Duration, offset: Option<Duration>) -> Self {
287 Self { every, offset }
288 }
289}
290
291impl From<Duration> for ScheduleIntervalSpec {
292 fn from(every: Duration) -> Self {
293 Self {
294 every,
295 offset: None,
296 }
297 }
298}
299
300impl From<ScheduleIntervalSpec> for schedule_proto::IntervalSpec {
301 fn from(s: ScheduleIntervalSpec) -> Self {
302 Self {
303 interval: Some(s.every.try_into().unwrap_or_default()),
304 phase: s.offset.and_then(|d| d.try_into().ok()),
305 }
306 }
307}
308
309#[derive(Debug, Clone, Default, PartialEq, bon::Builder)]
313#[builder(on(String, into))]
314#[non_exhaustive]
315pub struct ScheduleCalendarSpec {
316 #[builder(default)]
318 pub second: String,
319 #[builder(default)]
321 pub minute: String,
322 #[builder(default)]
324 pub hour: String,
325 #[builder(default)]
327 pub day_of_month: String,
328 #[builder(default)]
330 pub month: String,
331 #[builder(default)]
333 pub day_of_week: String,
334 #[builder(default)]
336 pub year: String,
337 #[builder(default)]
339 pub comment: String,
340}
341
342impl From<ScheduleCalendarSpec> for schedule_proto::CalendarSpec {
343 fn from(s: ScheduleCalendarSpec) -> Self {
344 Self {
345 second: s.second,
346 minute: s.minute,
347 hour: s.hour,
348 day_of_month: s.day_of_month,
349 month: s.month,
350 day_of_week: s.day_of_week,
351 year: s.year,
352 comment: s.comment,
353 }
354 }
355}
356
357#[derive(Debug, Clone, Default, bon::Builder)]
359#[non_exhaustive]
360pub struct ListSchedulesOptions {
361 #[builder(default)]
363 pub maximum_page_size: i32,
364 #[builder(default)]
366 pub query: String,
367 #[builder(default)]
369 pub rpc_options: RpcOptions,
370}
371
372#[derive(Debug, Clone, Default, bon::Builder)]
374#[non_exhaustive]
375pub struct DeleteScheduleOptions {
376 #[builder(default)]
378 pub rpc_options: RpcOptions,
379}
380
381#[derive(Debug, Clone, Default, bon::Builder)]
383#[non_exhaustive]
384pub struct PauseScheduleOptions {
385 #[builder(default)]
387 pub rpc_options: RpcOptions,
388}
389
390#[derive(Debug, Clone, Default, bon::Builder)]
392#[non_exhaustive]
393pub struct UnpauseScheduleOptions {
394 #[builder(default)]
396 pub rpc_options: RpcOptions,
397}
398
399#[derive(Debug, Clone, Default, bon::Builder)]
401#[non_exhaustive]
402pub struct TriggerScheduleOptions {
403 #[builder(default)]
405 pub rpc_options: RpcOptions,
406}
407
408#[derive(Debug, Clone, Default, bon::Builder)]
410#[non_exhaustive]
411pub struct BackfillScheduleOptions {
412 #[builder(default)]
414 pub rpc_options: RpcOptions,
415}
416
417pub struct ListSchedulesStream {
420 inner: Pin<Box<dyn futures_util::Stream<Item = Result<ScheduleSummary, ScheduleError>> + Send>>,
421}
422
423impl ListSchedulesStream {
424 pub(crate) fn new(
425 inner: Pin<
426 Box<dyn futures_util::Stream<Item = Result<ScheduleSummary, ScheduleError>> + Send>,
427 >,
428 ) -> Self {
429 Self { inner }
430 }
431}
432
433impl futures_util::Stream for ListSchedulesStream {
434 type Item = Result<ScheduleSummary, ScheduleError>;
435
436 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
437 self.inner.as_mut().poll_next(cx)
438 }
439}
440
441#[derive(Debug, Clone, PartialEq)]
443#[non_exhaustive]
444pub struct ScheduleRecentAction {
445 pub schedule_time: Option<SystemTime>,
447 pub actual_time: Option<SystemTime>,
449 pub workflow_id: String,
451 pub run_id: String,
453}
454
455#[derive(Debug, Clone, PartialEq)]
457#[non_exhaustive]
458pub struct ScheduleRunningAction {
459 pub workflow_id: String,
461 pub run_id: String,
463}
464
465#[derive(Debug, Clone)]
467#[non_exhaustive]
468pub enum ScheduleDescriptionAction {
469 StartWorkflow(ScheduleDescriptionStartWorkflowAction),
471}
472
473impl ScheduleDescriptionAction {
474 fn from_proto(
475 action: &schedule_proto::ScheduleAction,
476 data_converter: DataConverter,
477 ) -> Option<Self> {
478 match action.action.as_ref()? {
479 schedule_proto::schedule_action::Action::StartWorkflow(info) => {
480 Some(Self::StartWorkflow(
481 ScheduleDescriptionStartWorkflowAction::from_proto(info, data_converter),
482 ))
483 }
484 }
485 }
486}
487
488#[derive(Debug, Clone)]
490#[non_exhaustive]
491pub struct ScheduleDescriptionStartWorkflowAction {
492 workflow_type: String,
493 task_queue: String,
494 workflow_id: String,
495 input: Option<common_proto::Payloads>,
496 data_converter: DataConverter,
497}
498
499impl ScheduleDescriptionStartWorkflowAction {
500 fn from_proto(
501 info: &workflow_proto::NewWorkflowExecutionInfo,
502 data_converter: DataConverter,
503 ) -> Self {
504 Self {
505 workflow_type: info
506 .workflow_type
507 .as_ref()
508 .map(|t| t.name.clone())
509 .unwrap_or_default(),
510 task_queue: info
511 .task_queue
512 .as_ref()
513 .map(|t| t.name.clone())
514 .unwrap_or_default(),
515 workflow_id: info.workflow_id.clone(),
516 input: info.input.clone(),
517 data_converter,
518 }
519 }
520
521 pub fn workflow_type(&self) -> &str {
523 &self.workflow_type
524 }
525
526 pub fn task_queue(&self) -> &str {
528 &self.task_queue
529 }
530
531 pub fn workflow_id(&self) -> &str {
533 &self.workflow_id
534 }
535
536 pub async fn args<T: TemporalDeserializable + 'static>(
538 &self,
539 ) -> Result<Option<T>, PayloadConversionError> {
540 match &self.input {
541 Some(input) => self
542 .data_converter
543 .from_payloads(&SerializationContextData::Workflow, input.payloads.clone())
544 .await
545 .map(Some),
546 None => Ok(None),
547 }
548 }
549
550 pub fn raw_args(&self) -> Option<&[common_proto::Payload]> {
552 self.input.as_ref().map(|input| input.payloads.as_slice())
553 }
554}
555
556impl From<&schedule_proto::ScheduleActionResult> for ScheduleRecentAction {
557 fn from(a: &schedule_proto::ScheduleActionResult) -> Self {
558 let workflow_result = a
559 .start_workflow_result
560 .as_ref()
561 .expect("unsupported schedule action: start_workflow_result should be present");
562 ScheduleRecentAction {
563 schedule_time: a.schedule_time.as_ref().and_then(proto_ts_to_system_time),
564 actual_time: a.actual_time.as_ref().and_then(proto_ts_to_system_time),
565 workflow_id: workflow_result.workflow_id.clone(),
566 run_id: workflow_result.run_id.clone(),
567 }
568 }
569}
570
571#[derive(Debug, Clone)]
577pub struct ScheduleDescription {
578 raw: DescribeScheduleResponse,
579 data_converter: DataConverter,
580}
581
582impl ScheduleDescription {
583 pub(crate) fn new(
584 raw: DescribeScheduleResponse,
585 data_converter: DataConverter,
586 schedule_id: &str,
587 ) -> Result<Self, ScheduleError> {
588 let action = raw
589 .schedule
590 .as_ref()
591 .ok_or_else(|| Self::malformed_description_error(schedule_id, "missing schedule"))?
592 .action
593 .as_ref()
594 .ok_or_else(|| {
595 Self::malformed_description_error(schedule_id, "missing schedule action")
596 })?;
597 if action.action.is_none() {
598 return Err(Self::malformed_description_error(
599 schedule_id,
600 "missing schedule action variant",
601 ));
602 }
603 Ok(Self {
604 raw,
605 data_converter,
606 })
607 }
608
609 fn malformed_description_error(schedule_id: &str, reason: impl Into<String>) -> ScheduleError {
610 ScheduleError::MalformedDescription {
611 schedule_id: schedule_id.to_string(),
612 reason: reason.into(),
613 }
614 }
615
616 pub fn conflict_token(&self) -> &[u8] {
618 &self.raw.conflict_token
619 }
620
621 pub fn action(&self) -> ScheduleDescriptionAction {
623 let action = self
624 .raw
625 .schedule
626 .as_ref()
627 .expect("schedule description should contain schedule")
628 .action
629 .as_ref()
630 .expect("schedule description should contain action");
631 ScheduleDescriptionAction::from_proto(action, self.data_converter.clone())
632 .expect("schedule action should contain an action variant")
633 }
634
635 pub fn paused(&self) -> bool {
637 self.raw
638 .schedule
639 .as_ref()
640 .and_then(|s| s.state.as_ref())
641 .is_some_and(|st| st.paused)
642 }
643
644 pub fn note(&self) -> Option<&str> {
647 self.raw
648 .schedule
649 .as_ref()
650 .and_then(|s| s.state.as_ref())
651 .map(|st| st.notes.as_str())
652 .filter(|s| !s.is_empty())
653 }
654
655 pub fn action_count(&self) -> i64 {
657 self.info().map_or(0, |i| i.action_count)
658 }
659
660 pub fn missed_catchup_window(&self) -> i64 {
662 self.info().map_or(0, |i| i.missed_catchup_window)
663 }
664
665 pub fn overlap_skipped(&self) -> i64 {
667 self.info().map_or(0, |i| i.overlap_skipped)
668 }
669
670 pub fn recent_actions(&self) -> Vec<ScheduleRecentAction> {
672 self.info()
673 .map(|i| {
674 i.recent_actions
675 .iter()
676 .map(ScheduleRecentAction::from)
677 .collect()
678 })
679 .unwrap_or_default()
680 }
681
682 pub fn running_actions(&self) -> Vec<ScheduleRunningAction> {
684 self.info()
685 .map(|i| {
686 i.running_workflows
687 .iter()
688 .map(|w| ScheduleRunningAction {
689 workflow_id: w.workflow_id.clone(),
690 run_id: w.run_id.clone(),
691 })
692 .collect()
693 })
694 .unwrap_or_default()
695 }
696
697 pub fn future_action_times(&self) -> Vec<SystemTime> {
699 self.info()
700 .map(|i| {
701 i.future_action_times
702 .iter()
703 .filter_map(proto_ts_to_system_time)
704 .collect()
705 })
706 .unwrap_or_default()
707 }
708
709 pub fn create_time(&self) -> Option<SystemTime> {
711 self.info()
712 .and_then(|i| i.create_time.as_ref())
713 .and_then(proto_ts_to_system_time)
714 }
715
716 pub fn update_time(&self) -> Option<SystemTime> {
718 self.info()
719 .and_then(|i| i.update_time.as_ref())
720 .and_then(proto_ts_to_system_time)
721 }
722
723 pub fn memo(&self) -> crate::Memo {
725 crate::Memo::from_raw(
726 self.raw.memo.clone(),
727 self.data_converter.payload_converter().clone(),
728 SerializationContextData::Workflow,
729 )
730 }
731
732 pub fn search_attributes(&self) -> SearchAttributes {
734 self.raw
735 .search_attributes
736 .as_ref()
737 .map(SearchAttributes::from_proto)
738 .unwrap_or_default()
739 }
740
741 pub fn raw(&self) -> &DescribeScheduleResponse {
743 &self.raw
744 }
745
746 pub fn into_raw(self) -> DescribeScheduleResponse {
748 self.raw
749 }
750
751 fn info(&self) -> Option<&schedule_proto::ScheduleInfo> {
752 self.raw.info.as_ref()
753 }
754
755 pub fn into_update(self) -> ScheduleUpdate {
760 ScheduleUpdate {
761 schedule: self.raw.schedule.unwrap_or_default(),
762 pending_action: None,
763 }
764 }
765}
766
767#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
769pub enum ScheduleOverlapPolicy {
770 #[default]
772 Unspecified,
773 Skip,
775 BufferOne,
777 BufferAll,
779 CancelOther,
781 TerminateOther,
783 AllowAll,
785}
786
787impl ScheduleOverlapPolicy {
788 pub(crate) fn to_proto(self) -> i32 {
789 match self {
790 Self::Unspecified => 0,
791 Self::Skip => 1,
792 Self::BufferOne => 2,
793 Self::BufferAll => 3,
794 Self::CancelOther => 4,
795 Self::TerminateOther => 5,
796 Self::AllowAll => 6,
797 }
798 }
799}
800
801#[derive(Debug, Clone, PartialEq, bon::Builder)]
803#[non_exhaustive]
804#[builder(start_fn = new)]
805pub struct ScheduleBackfill {
806 #[builder(start_fn)]
808 pub start_time: SystemTime,
809 #[builder(start_fn)]
811 pub end_time: SystemTime,
812 #[builder(default)]
814 pub overlap_policy: ScheduleOverlapPolicy,
815}
816
817#[derive(Debug, Clone)]
822pub struct ScheduleUpdate {
823 schedule: schedule_proto::Schedule,
824 pending_action: Option<ScheduleAction>,
825}
826
827impl ScheduleUpdate {
828 pub fn set_spec(&mut self, spec: ScheduleSpec) -> &mut Self {
830 self.schedule.spec = Some(spec.into_proto());
831 self
832 }
833
834 pub fn set_action(&mut self, action: ScheduleAction) -> &mut Self {
836 self.pending_action = Some(action);
837 self
838 }
839
840 pub fn set_paused(&mut self, paused: bool) -> &mut Self {
842 self.state_mut().paused = paused;
843 self
844 }
845
846 pub fn set_note(&mut self, note: impl Into<String>) -> &mut Self {
848 self.state_mut().notes = note.into();
849 self
850 }
851
852 pub fn set_overlap_policy(&mut self, policy: ScheduleOverlapPolicy) -> &mut Self {
854 self.policies_mut().overlap_policy = policy.to_proto();
855 self
856 }
857
858 pub fn set_catchup_window(&mut self, window: Duration) -> &mut Self {
861 self.policies_mut().catchup_window = window.try_into().ok();
862 self
863 }
864
865 pub fn set_pause_on_failure(&mut self, pause_on_failure: bool) -> &mut Self {
867 self.policies_mut().pause_on_failure = pause_on_failure;
868 self
869 }
870
871 pub fn set_keep_original_workflow_id(&mut self, keep: bool) -> &mut Self {
874 self.policies_mut().keep_original_workflow_id = keep;
875 self
876 }
877
878 pub fn set_remaining_actions(&mut self, count: Option<i64>) -> &mut Self {
881 let state = self.state_mut();
882 match count {
883 Some(n) => {
884 state.limited_actions = true;
885 state.remaining_actions = n;
886 }
887 None => {
888 state.limited_actions = false;
889 state.remaining_actions = 0;
890 }
891 }
892 self
893 }
894
895 pub fn raw(&self) -> &schedule_proto::Schedule {
897 &self.schedule
898 }
899
900 pub fn into_raw(self) -> schedule_proto::Schedule {
902 self.schedule
903 }
904
905 fn state_mut(&mut self) -> &mut schedule_proto::ScheduleState {
906 self.schedule.state.get_or_insert_with(Default::default)
907 }
908
909 fn policies_mut(&mut self) -> &mut schedule_proto::SchedulePolicies {
910 self.schedule.policies.get_or_insert_with(Default::default)
911 }
912}
913
914#[derive(Debug, Clone)]
920pub struct ScheduleSummary {
921 raw: schedule_proto::ScheduleListEntry,
922 data_converter: DataConverter,
923}
924
925impl ScheduleSummary {
926 pub fn schedule_id(&self) -> &str {
928 &self.raw.schedule_id
929 }
930
931 pub fn workflow_type(&self) -> Option<&str> {
933 self.info()
934 .and_then(|i| i.workflow_type.as_ref())
935 .map(|wt| wt.name.as_str())
936 }
937
938 pub fn note(&self) -> Option<&str> {
941 self.info()
942 .map(|i| i.notes.as_str())
943 .filter(|s| !s.is_empty())
944 }
945
946 pub fn paused(&self) -> bool {
948 self.info().is_some_and(|i| i.paused)
949 }
950
951 pub fn recent_actions(&self) -> Vec<ScheduleRecentAction> {
953 self.info()
954 .map(|i| {
955 i.recent_actions
956 .iter()
957 .map(ScheduleRecentAction::from)
958 .collect()
959 })
960 .unwrap_or_default()
961 }
962
963 pub fn future_action_times(&self) -> Vec<SystemTime> {
965 self.info()
966 .map(|i| {
967 i.future_action_times
968 .iter()
969 .filter_map(proto_ts_to_system_time)
970 .collect()
971 })
972 .unwrap_or_default()
973 }
974
975 pub fn memo(&self) -> crate::Memo {
977 crate::Memo::from_raw(
978 self.raw.memo.clone(),
979 self.data_converter.payload_converter().clone(),
980 SerializationContextData::Workflow,
981 )
982 }
983
984 pub fn search_attributes(&self) -> SearchAttributes {
986 self.raw
987 .search_attributes
988 .as_ref()
989 .map(SearchAttributes::from_proto)
990 .unwrap_or_default()
991 }
992
993 pub fn raw(&self) -> &schedule_proto::ScheduleListEntry {
995 &self.raw
996 }
997
998 pub fn into_raw(self) -> schedule_proto::ScheduleListEntry {
1000 self.raw
1001 }
1002
1003 fn info(&self) -> Option<&schedule_proto::ScheduleListInfo> {
1004 self.raw.info.as_ref()
1005 }
1006}
1007
1008impl From<schedule_proto::ScheduleListEntry> for ScheduleSummary {
1009 fn from(raw: schedule_proto::ScheduleListEntry) -> Self {
1010 Self::new(raw, DataConverter::default())
1011 }
1012}
1013
1014impl ScheduleSummary {
1015 fn new(raw: schedule_proto::ScheduleListEntry, data_converter: DataConverter) -> Self {
1016 Self {
1017 raw,
1018 data_converter,
1019 }
1020 }
1021}
1022
1023#[derive(Clone, derive_more::Debug)]
1027pub struct ScheduleHandle<CT> {
1028 #[debug(skip)]
1029 client: CT,
1030 namespace: String,
1031 schedule_id: String,
1032}
1033
1034impl<CT> ScheduleHandle<CT>
1035where
1036 CT: WorkflowService + NamespacedClient + Clone + Send + Sync,
1037{
1038 pub(crate) fn new(client: CT, namespace: String, schedule_id: String) -> Self {
1039 Self {
1040 client,
1041 namespace,
1042 schedule_id,
1043 }
1044 }
1045
1046 pub fn namespace(&self) -> &str {
1048 &self.namespace
1049 }
1050
1051 pub fn schedule_id(&self) -> &str {
1053 &self.schedule_id
1054 }
1055
1056 pub async fn describe(
1058 &self,
1059 rpc_options: RpcOptions,
1060 ) -> Result<ScheduleDescription, ScheduleError> {
1061 let output = interceptors::call_describe_schedule(
1062 self.client.client_interceptors(),
1063 DescribeScheduleInput {
1064 schedule_id: self.schedule_id.clone(),
1065 rpc_options,
1066 },
1067 Next::new({
1068 let handle = self.clone();
1069 move |input: DescribeScheduleInput| -> BoxFuture<
1070 '_,
1071 Result<DescribeScheduleOutput, ScheduleError>,
1072 > {
1073 Box::pin(async move {
1074 let response = handle
1075 .describe_raw(input.schedule_id, input.rpc_options)
1076 .await?;
1077 Ok(DescribeScheduleOutput::new(response))
1078 })
1079 }
1080 }),
1081 )
1082 .await?;
1083
1084 let mut resp = output.response;
1085 if let Some(memo) = resp.memo.as_mut() {
1086 decode_payloads(
1087 memo,
1088 self.client.data_converter().codec(),
1089 &SerializationContextData::Workflow,
1090 )
1091 .await?;
1092 }
1093
1094 ScheduleDescription::new(
1095 resp,
1096 self.client.data_converter().clone(),
1097 &self.schedule_id,
1098 )
1099 }
1100
1101 pub async fn update(
1124 &self,
1125 updater: impl FnOnce(&mut ScheduleUpdate) + Send + 'static,
1126 rpc_options: RpcOptions,
1127 ) -> Result<(), ScheduleError> {
1128 interceptors::call_update_schedule(
1129 self.client.client_interceptors(),
1130 UpdateScheduleInput {
1131 schedule_id: self.schedule_id.clone(),
1132 rpc_options,
1133 },
1134 Next::new({
1135 let handle = self.clone();
1136 move |input: UpdateScheduleInput| -> BoxFuture<'_, Result<(), ScheduleError>> {
1137 Box::pin(async move {
1138 let mut response = handle
1139 .describe_raw(input.schedule_id.clone(), input.rpc_options.clone())
1140 .await?;
1141 decode_payloads(
1142 &mut response,
1143 handle.client.data_converter().codec(),
1144 &SerializationContextData::Workflow,
1145 )
1146 .await?;
1147 let description = ScheduleDescription::new(
1148 response,
1149 handle.client.data_converter().clone(),
1150 &input.schedule_id,
1151 )?;
1152 let mut update = description.into_update();
1153 updater(&mut update);
1154 handle
1155 .send_update_raw(input.schedule_id, update, input.rpc_options)
1156 .await
1157 })
1158 }
1159 }),
1160 )
1161 .await
1162 }
1163
1164 pub async fn send_update(
1170 &self,
1171 update: ScheduleUpdate,
1172 rpc_options: RpcOptions,
1173 ) -> Result<(), ScheduleError> {
1174 interceptors::call_send_schedule_update(
1175 self.client.client_interceptors(),
1176 SendScheduleUpdateInput {
1177 schedule_id: self.schedule_id.clone(),
1178 update,
1179 rpc_options,
1180 },
1181 Next::new({
1182 let handle = self.clone();
1183 move |input: SendScheduleUpdateInput| -> BoxFuture<'_, Result<(), ScheduleError>> {
1184 Box::pin(async move {
1185 handle
1186 .send_update_raw(input.schedule_id, input.update, input.rpc_options)
1187 .await
1188 })
1189 }
1190 }),
1191 )
1192 .await
1193 }
1194
1195 async fn describe_raw(
1196 &self,
1197 schedule_id: String,
1198 rpc_options: RpcOptions,
1199 ) -> Result<DescribeScheduleResponse, ScheduleError> {
1200 let mut request = DescribeScheduleRequest {
1201 namespace: self.namespace.clone(),
1202 schedule_id,
1203 }
1204 .into_request();
1205 rpc_options.apply_to(&mut request);
1206 Ok(
1207 WorkflowService::describe_schedule(&mut self.client.clone(), request)
1208 .await?
1209 .into_inner(),
1210 )
1211 }
1212
1213 async fn send_update_raw(
1214 &self,
1215 schedule_id: String,
1216 mut update: ScheduleUpdate,
1217 rpc_options: RpcOptions,
1218 ) -> Result<(), ScheduleError> {
1219 if let Some(action) = update.pending_action.take() {
1220 update.schedule.action = Some(action.into_proto(self.client.data_converter()).await?);
1221 }
1222 let mut request = UpdateScheduleRequest {
1223 namespace: self.namespace.clone(),
1224 schedule_id,
1225 schedule: Some(update.schedule),
1226 identity: self.client.identity(),
1227 request_id: Uuid::new_v4().to_string(),
1228 ..Default::default()
1229 }
1230 .into_request();
1231 rpc_options.apply_to(&mut request);
1232 WorkflowService::update_schedule(&mut self.client.clone(), request).await?;
1233 Ok(())
1234 }
1235
1236 pub async fn delete(&self, options: DeleteScheduleOptions) -> Result<(), ScheduleError> {
1238 let rpc_options = options.rpc_options;
1239 interceptors::call_delete_schedule(
1240 self.client.client_interceptors(),
1241 DeleteScheduleInput {
1242 schedule_id: self.schedule_id.clone(),
1243 rpc_options,
1244 },
1245 Next::new({
1246 let mut client = self.client.clone();
1247 let namespace = self.namespace.clone();
1248 move |input: DeleteScheduleInput| -> BoxFuture<'_, Result<(), ScheduleError>> {
1249 Box::pin(async move {
1250 let mut request = DeleteScheduleRequest {
1251 namespace,
1252 schedule_id: input.schedule_id,
1253 identity: client.identity(),
1254 }
1255 .into_request();
1256 input.rpc_options.apply_to(&mut request);
1257 WorkflowService::delete_schedule(&mut client, request).await?;
1258 Ok(())
1259 })
1260 }
1261 }),
1262 )
1263 .await
1264 }
1265
1266 pub async fn pause(
1270 &self,
1271 note: Option<impl Into<String>>,
1272 options: PauseScheduleOptions,
1273 ) -> Result<(), ScheduleError> {
1274 let note = note.map_or_else(|| "Paused via Rust SDK".to_string(), |s| s.into());
1275 let rpc_options = options.rpc_options;
1276 interceptors::call_pause_schedule(
1277 self.client.client_interceptors(),
1278 PauseScheduleInput {
1279 schedule_id: self.schedule_id.clone(),
1280 note,
1281 rpc_options,
1282 },
1283 Next::new({
1284 let mut client = self.client.clone();
1285 let namespace = self.namespace.clone();
1286 move |input: PauseScheduleInput| -> BoxFuture<'_, Result<(), ScheduleError>> {
1287 Box::pin(async move {
1288 let mut request = PatchScheduleRequest {
1289 namespace,
1290 schedule_id: input.schedule_id,
1291 patch: Some(schedule_proto::SchedulePatch {
1292 pause: input.note,
1293 ..Default::default()
1294 }),
1295 identity: client.identity(),
1296 request_id: Uuid::new_v4().to_string(),
1297 }
1298 .into_request();
1299 input.rpc_options.apply_to(&mut request);
1300 WorkflowService::patch_schedule(&mut client, request).await?;
1301 Ok(())
1302 })
1303 }
1304 }),
1305 )
1306 .await
1307 }
1308
1309 pub async fn unpause(
1313 &self,
1314 note: Option<impl Into<String>>,
1315 options: UnpauseScheduleOptions,
1316 ) -> Result<(), ScheduleError> {
1317 let note = note.map_or_else(|| "Unpaused via Rust SDK".to_string(), |s| s.into());
1318 let rpc_options = options.rpc_options;
1319 interceptors::call_unpause_schedule(
1320 self.client.client_interceptors(),
1321 UnpauseScheduleInput {
1322 schedule_id: self.schedule_id.clone(),
1323 note,
1324 rpc_options,
1325 },
1326 Next::new({
1327 let mut client = self.client.clone();
1328 let namespace = self.namespace.clone();
1329 move |input: UnpauseScheduleInput| -> BoxFuture<'_, Result<(), ScheduleError>> {
1330 Box::pin(async move {
1331 let mut request = PatchScheduleRequest {
1332 namespace,
1333 schedule_id: input.schedule_id,
1334 patch: Some(schedule_proto::SchedulePatch {
1335 unpause: input.note,
1336 ..Default::default()
1337 }),
1338 identity: client.identity(),
1339 request_id: Uuid::new_v4().to_string(),
1340 }
1341 .into_request();
1342 input.rpc_options.apply_to(&mut request);
1343 WorkflowService::patch_schedule(&mut client, request).await?;
1344 Ok(())
1345 })
1346 }
1347 }),
1348 )
1349 .await
1350 }
1351
1352 pub async fn trigger(
1354 &self,
1355 overlap_policy: ScheduleOverlapPolicy,
1356 options: TriggerScheduleOptions,
1357 ) -> Result<(), ScheduleError> {
1358 let rpc_options = options.rpc_options;
1359 interceptors::call_trigger_schedule(
1360 self.client.client_interceptors(),
1361 TriggerScheduleInput {
1362 schedule_id: self.schedule_id.clone(),
1363 overlap_policy,
1364 rpc_options,
1365 },
1366 Next::new({
1367 let mut client = self.client.clone();
1368 let namespace = self.namespace.clone();
1369 move |input: TriggerScheduleInput| -> BoxFuture<'_, Result<(), ScheduleError>> {
1370 Box::pin(async move {
1371 let mut request = PatchScheduleRequest {
1372 namespace,
1373 schedule_id: input.schedule_id,
1374 patch: Some(schedule_proto::SchedulePatch {
1375 trigger_immediately: Some(
1376 schedule_proto::TriggerImmediatelyRequest {
1377 overlap_policy: input.overlap_policy.to_proto(),
1378 scheduled_time: None,
1379 },
1380 ),
1381 ..Default::default()
1382 }),
1383 identity: client.identity(),
1384 request_id: Uuid::new_v4().to_string(),
1385 }
1386 .into_request();
1387 input.rpc_options.apply_to(&mut request);
1388 WorkflowService::patch_schedule(&mut client, request).await?;
1389 Ok(())
1390 })
1391 }
1392 }),
1393 )
1394 .await
1395 }
1396
1397 pub async fn backfill(
1399 &self,
1400 backfills: impl IntoIterator<Item = ScheduleBackfill>,
1401 options: BackfillScheduleOptions,
1402 ) -> Result<(), ScheduleError> {
1403 let rpc_options = options.rpc_options;
1404 interceptors::call_backfill_schedule(
1405 self.client.client_interceptors(),
1406 BackfillScheduleInput {
1407 schedule_id: self.schedule_id.clone(),
1408 backfills: backfills.into_iter().collect(),
1409 rpc_options,
1410 },
1411 Next::new({
1412 let mut client = self.client.clone();
1413 let namespace = self.namespace.clone();
1414 move |input: BackfillScheduleInput| -> BoxFuture<'_, Result<(), ScheduleError>> {
1415 Box::pin(async move {
1416 let backfill_requests = input
1417 .backfills
1418 .into_iter()
1419 .map(|backfill| schedule_proto::BackfillRequest {
1420 start_time: Some(backfill.start_time.into()),
1421 end_time: Some(backfill.end_time.into()),
1422 overlap_policy: backfill.overlap_policy.to_proto(),
1423 })
1424 .collect();
1425 let mut request = PatchScheduleRequest {
1426 namespace,
1427 schedule_id: input.schedule_id,
1428 patch: Some(schedule_proto::SchedulePatch {
1429 backfill_request: backfill_requests,
1430 ..Default::default()
1431 }),
1432 identity: client.identity(),
1433 request_id: Uuid::new_v4().to_string(),
1434 }
1435 .into_request();
1436 input.rpc_options.apply_to(&mut request);
1437 WorkflowService::patch_schedule(&mut client, request).await?;
1438 Ok(())
1439 })
1440 }
1441 }),
1442 )
1443 .await
1444 }
1445}
1446
1447impl Client {
1449 pub async fn create_schedule(
1451 &self,
1452 schedule_id: impl Into<String>,
1453 opts: CreateScheduleOptions,
1454 ) -> Result<ScheduleHandle<Self>, ScheduleError> {
1455 let schedule_id = schedule_id.into();
1456 let namespace = self.namespace();
1457 let output = interceptors::call_create_schedule(
1458 self.client_interceptors(),
1459 CreateScheduleInput {
1460 schedule_id,
1461 options: opts,
1462 },
1463 Next::new({
1464 let mut client = self.clone();
1465 move |input: CreateScheduleInput| -> BoxFuture<
1466 '_,
1467 Result<CreateScheduleOutput, ScheduleError>,
1468 > {
1469 Box::pin(async move {
1470 let options = input.options;
1471 let initial_patch = options.trigger_immediately.then(|| {
1472 schedule_proto::SchedulePatch {
1473 trigger_immediately: Some(
1474 schedule_proto::TriggerImmediatelyRequest {
1475 overlap_policy: ScheduleOverlapPolicy::AllowAll.to_proto(),
1476 scheduled_time: None,
1477 },
1478 ),
1479 ..Default::default()
1480 }
1481 });
1482 let policies = (options.overlap_policy
1483 != ScheduleOverlapPolicy::Unspecified)
1484 .then(|| schedule_proto::SchedulePolicies {
1485 overlap_policy: options.overlap_policy.to_proto(),
1486 ..Default::default()
1487 });
1488 let schedule = schedule_proto::Schedule {
1489 spec: Some(options.spec.into_proto()),
1490 action: Some(
1491 options.action.into_proto(client.data_converter()).await?,
1492 ),
1493 policies,
1494 state: Some(schedule_proto::ScheduleState {
1495 paused: options.paused,
1496 notes: options.note,
1497 ..Default::default()
1498 }),
1499 };
1500 let mut request = CreateScheduleRequest {
1501 namespace: client.namespace(),
1502 schedule_id: input.schedule_id.clone(),
1503 schedule: Some(schedule),
1504 initial_patch,
1505 identity: client.identity(),
1506 request_id: Uuid::new_v4().to_string(),
1507 ..Default::default()
1508 }
1509 .into_request();
1510 options.rpc_options.apply_to(&mut request);
1511 WorkflowService::create_schedule(&mut client, request).await?;
1512 Ok(CreateScheduleOutput::new(input.schedule_id))
1513 })
1514 }
1515 }),
1516 )
1517 .await?;
1518 Ok(ScheduleHandle::new(
1519 self.clone(),
1520 namespace,
1521 output.schedule_id,
1522 ))
1523 }
1524
1525 pub fn get_schedule_handle(&self, schedule_id: impl Into<String>) -> ScheduleHandle<Self> {
1527 ScheduleHandle::new(self.clone(), self.namespace(), schedule_id.into())
1528 }
1529
1530 pub fn list_schedules(&self, opts: ListSchedulesOptions) -> ListSchedulesStream {
1533 list_schedules_stream(self.clone(), opts)
1534 }
1535}
1536
1537fn list_schedules_stream<CT>(client: CT, opts: ListSchedulesOptions) -> ListSchedulesStream
1538where
1539 CT: WorkflowService + NamespacedClient + Clone + Send + Sync + 'static,
1540{
1541 let namespace = client.namespace();
1542 let query = opts.query;
1543 let page_size = opts.maximum_page_size;
1544 let rpc_options = opts.rpc_options;
1545
1546 let stream = stream::unfold(
1547 (Vec::new(), VecDeque::new(), false),
1548 move |(next_page_token, mut buffer, exhausted)| {
1549 let client = client.clone();
1550 let namespace = namespace.clone();
1551 let query = query.clone();
1552 let rpc_options = rpc_options.clone();
1553
1554 async move {
1555 if let Some(item) = buffer.pop_front() {
1556 return Some((Ok(item), (next_page_token, buffer, exhausted)));
1557 } else if exhausted {
1558 return None;
1559 }
1560
1561 let response = interceptors::call_list_schedules_page(
1562 client.client_interceptors(),
1563 ListSchedulesPageInput {
1564 maximum_page_size: page_size,
1565 query,
1566 next_page_token: next_page_token.clone(),
1567 rpc_options,
1568 },
1569 Next::new({
1570 let mut rpc_client = client.clone();
1571 move |input: ListSchedulesPageInput| -> BoxFuture<
1572 '_,
1573 Result<ListSchedulesPageOutput, ScheduleError>,
1574 > {
1575 Box::pin(async move {
1576 let mut request = ListSchedulesRequest {
1577 namespace,
1578 maximum_page_size: input.maximum_page_size,
1579 next_page_token: input.next_page_token,
1580 query: input.query,
1581 }
1582 .into_request();
1583 input.rpc_options.apply_to(&mut request);
1584 let response =
1585 WorkflowService::list_schedules(&mut rpc_client, request)
1586 .await?
1587 .into_inner();
1588 Ok(ListSchedulesPageOutput::new(
1589 response.schedules,
1590 response.next_page_token,
1591 ))
1592 })
1593 }
1594 }),
1595 )
1596 .await;
1597
1598 match response {
1599 Ok(mut output) => {
1600 let new_exhausted = output.next_page_token.is_empty();
1601 let new_token = output.next_page_token;
1602
1603 let data_converter = client.data_converter().clone();
1604 for schedule in &mut output.schedules {
1605 if let Some(memo) = schedule.memo.as_mut()
1606 && let Err(err) = decode_payloads(
1607 memo,
1608 data_converter.codec(),
1609 &SerializationContextData::Workflow,
1610 )
1611 .await
1612 {
1613 return Some((
1614 Err(ScheduleError::from(err)),
1615 (new_token, buffer, true),
1616 ));
1617 }
1618 }
1619 buffer = output
1620 .schedules
1621 .into_iter()
1622 .map(|raw| ScheduleSummary::new(raw, data_converter.clone()))
1623 .collect();
1624
1625 buffer
1626 .pop_front()
1627 .map(|item| (Ok(item), (new_token, buffer, new_exhausted)))
1628 }
1629 Err(e) => Some((Err(e), (next_page_token, buffer, true))),
1630 }
1631 }
1632 },
1633 );
1634
1635 ListSchedulesStream::new(Box::pin(stream))
1636}
1637
1638#[cfg(test)]
1639mod tests {
1640 use super::*;
1641 use crate::{
1642 ClientInterceptor, DescribeScheduleInput, DescribeScheduleOutput, NamespacedClient, Next,
1643 SendScheduleUpdateInput, UpdateScheduleInput,
1644 grpc::WorkflowService,
1645 test_helpers::{FailingCodec, XorCodec},
1646 };
1647 use futures_util::{FutureExt, StreamExt};
1648 use std::{
1649 collections::HashMap,
1650 sync::{
1651 Arc,
1652 atomic::{AtomicUsize, Ordering},
1653 },
1654 time::SystemTime,
1655 };
1656 use temporalio_common::{
1657 UntypedWorkflow,
1658 data_converters::{
1659 DataConverter, DefaultFailureConverter, MultiArgs2, PayloadConverter, RawValue,
1660 },
1661 protos::temporal::api::{
1662 common::v1::{
1663 Memo, Payload, Payloads, SearchAttributes,
1664 WorkflowExecution as ProtoWorkflowExecution, WorkflowType,
1665 },
1666 schedule::v1::{
1667 Schedule, ScheduleActionResult, ScheduleInfo, ScheduleListEntry, ScheduleListInfo,
1668 ScheduleSpec, ScheduleState,
1669 },
1670 taskqueue::v1::TaskQueue,
1671 workflow::v1::NewWorkflowExecutionInfo,
1672 workflowservice::v1::{
1673 DeleteScheduleResponse, DescribeScheduleResponse, ListSchedulesResponse,
1674 PatchScheduleResponse, UpdateScheduleResponse,
1675 },
1676 },
1677 };
1678 use tonic::{Request, Response};
1679
1680 fn data_converter_with_codec() -> DataConverter {
1681 DataConverter::new(
1682 PayloadConverter::default(),
1683 DefaultFailureConverter,
1684 XorCodec,
1685 )
1686 }
1687
1688 #[derive(Default)]
1689 struct CapturedRequests {
1690 describe: AtomicUsize,
1691 update: AtomicUsize,
1692 delete: AtomicUsize,
1693 patch: AtomicUsize,
1694 list: AtomicUsize,
1695 }
1696
1697 #[derive(Clone)]
1698 struct MockScheduleClient {
1699 captured: Arc<CapturedRequests>,
1700 describe_response: DescribeScheduleResponse,
1701 list_response: ListSchedulesResponse,
1702 data_converter: DataConverter,
1703 should_error: bool,
1704 interceptors: Vec<Arc<dyn ClientInterceptor>>,
1705 }
1706
1707 impl Default for MockScheduleClient {
1708 fn default() -> Self {
1709 Self {
1710 captured: Arc::new(CapturedRequests::default()),
1711 describe_response: describe_response_with_start_workflow(None),
1712 list_response: ListSchedulesResponse::default(),
1713 data_converter: DataConverter::default(),
1714 should_error: false,
1715 interceptors: Vec::new(),
1716 }
1717 }
1718 }
1719
1720 impl NamespacedClient for MockScheduleClient {
1721 fn namespace(&self) -> String {
1722 "test-namespace".to_string()
1723 }
1724 fn identity(&self) -> String {
1725 "test-identity".to_string()
1726 }
1727 fn data_converter(&self) -> &DataConverter {
1728 &self.data_converter
1729 }
1730 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
1731 &self.interceptors
1732 }
1733 }
1734
1735 #[derive(Default)]
1736 struct ScheduleHookCounts {
1737 describe: AtomicUsize,
1738 update: AtomicUsize,
1739 send_update: AtomicUsize,
1740 }
1741
1742 struct RecordingScheduleInterceptor {
1743 counts: Arc<ScheduleHookCounts>,
1744 }
1745
1746 impl ClientInterceptor for RecordingScheduleInterceptor {
1747 fn describe_schedule<'a>(
1748 &'a self,
1749 input: DescribeScheduleInput,
1750 next: Next<
1751 'a,
1752 DescribeScheduleInput,
1753 BoxFuture<'a, Result<DescribeScheduleOutput, ScheduleError>>,
1754 >,
1755 ) -> BoxFuture<'a, Result<DescribeScheduleOutput, ScheduleError>> {
1756 self.counts.describe.fetch_add(1, Ordering::SeqCst);
1757 next.run(input)
1758 }
1759
1760 fn update_schedule<'a>(
1761 &'a self,
1762 input: UpdateScheduleInput,
1763 next: Next<'a, UpdateScheduleInput, BoxFuture<'a, Result<(), ScheduleError>>>,
1764 ) -> BoxFuture<'a, Result<(), ScheduleError>> {
1765 self.counts.update.fetch_add(1, Ordering::SeqCst);
1766 next.run(input)
1767 }
1768
1769 fn send_schedule_update<'a>(
1770 &'a self,
1771 input: SendScheduleUpdateInput,
1772 next: Next<'a, SendScheduleUpdateInput, BoxFuture<'a, Result<(), ScheduleError>>>,
1773 ) -> BoxFuture<'a, Result<(), ScheduleError>> {
1774 self.counts.send_update.fetch_add(1, Ordering::SeqCst);
1775 next.run(input)
1776 }
1777 }
1778
1779 impl WorkflowService for MockScheduleClient {
1780 fn describe_schedule(
1781 &mut self,
1782 _request: Request<DescribeScheduleRequest>,
1783 ) -> futures_util::future::BoxFuture<
1784 '_,
1785 Result<Response<DescribeScheduleResponse>, tonic::Status>,
1786 > {
1787 self.captured.describe.fetch_add(1, Ordering::SeqCst);
1788 let resp = self.describe_response.clone();
1789 let should_error = self.should_error;
1790 async move {
1791 if should_error {
1792 Err(tonic::Status::not_found("schedule not found"))
1793 } else {
1794 Ok(Response::new(resp))
1795 }
1796 }
1797 .boxed()
1798 }
1799
1800 fn update_schedule(
1801 &mut self,
1802 _request: Request<UpdateScheduleRequest>,
1803 ) -> futures_util::future::BoxFuture<
1804 '_,
1805 Result<Response<UpdateScheduleResponse>, tonic::Status>,
1806 > {
1807 self.captured.update.fetch_add(1, Ordering::SeqCst);
1808 let should_error = self.should_error;
1809 async move {
1810 if should_error {
1811 Err(tonic::Status::internal("update failed"))
1812 } else {
1813 Ok(Response::new(UpdateScheduleResponse::default()))
1814 }
1815 }
1816 .boxed()
1817 }
1818
1819 fn delete_schedule(
1820 &mut self,
1821 _request: Request<DeleteScheduleRequest>,
1822 ) -> futures_util::future::BoxFuture<
1823 '_,
1824 Result<Response<DeleteScheduleResponse>, tonic::Status>,
1825 > {
1826 self.captured.delete.fetch_add(1, Ordering::SeqCst);
1827 let should_error = self.should_error;
1828 async move {
1829 if should_error {
1830 Err(tonic::Status::internal("delete failed"))
1831 } else {
1832 Ok(Response::new(DeleteScheduleResponse::default()))
1833 }
1834 }
1835 .boxed()
1836 }
1837
1838 fn patch_schedule(
1839 &mut self,
1840 _request: Request<PatchScheduleRequest>,
1841 ) -> futures_util::future::BoxFuture<
1842 '_,
1843 Result<Response<PatchScheduleResponse>, tonic::Status>,
1844 > {
1845 self.captured.patch.fetch_add(1, Ordering::SeqCst);
1846 let should_error = self.should_error;
1847 async move {
1848 if should_error {
1849 Err(tonic::Status::internal("patch failed"))
1850 } else {
1851 Ok(Response::new(PatchScheduleResponse::default()))
1852 }
1853 }
1854 .boxed()
1855 }
1856
1857 fn list_schedules(
1858 &mut self,
1859 _request: Request<ListSchedulesRequest>,
1860 ) -> futures_util::future::BoxFuture<
1861 '_,
1862 Result<Response<ListSchedulesResponse>, tonic::Status>,
1863 > {
1864 self.captured.list.fetch_add(1, Ordering::SeqCst);
1865 let response = self.list_response.clone();
1866 async move { Ok(Response::new(response)) }.boxed()
1867 }
1868 }
1869
1870 fn make_schedule_handle(client: MockScheduleClient) -> ScheduleHandle<MockScheduleClient> {
1871 ScheduleHandle::new(
1872 client,
1873 "test-namespace".to_string(),
1874 "test-schedule-id".to_string(),
1875 )
1876 }
1877
1878 fn describe_response_with_start_workflow(input: Option<Payloads>) -> DescribeScheduleResponse {
1879 DescribeScheduleResponse {
1880 schedule: Some(Schedule {
1881 action: Some(schedule_proto::ScheduleAction {
1882 action: Some(schedule_proto::schedule_action::Action::StartWorkflow(
1883 NewWorkflowExecutionInfo {
1884 workflow_id: "wf-id".to_string(),
1885 workflow_type: Some(WorkflowType {
1886 name: "MyWorkflow".to_string(),
1887 }),
1888 task_queue: Some(TaskQueue {
1889 name: "task-queue".to_string(),
1890 ..Default::default()
1891 }),
1892 input,
1893 ..Default::default()
1894 },
1895 )),
1896 }),
1897 ..Default::default()
1898 }),
1899 ..Default::default()
1900 }
1901 }
1902
1903 fn schedule_description_from_response(raw: DescribeScheduleResponse) -> ScheduleDescription {
1904 ScheduleDescription::new(raw, DataConverter::default(), "test-schedule-id").unwrap()
1905 }
1906
1907 fn describe_response_without_schedule() -> DescribeScheduleResponse {
1908 DescribeScheduleResponse::default()
1909 }
1910
1911 fn describe_response_without_action() -> DescribeScheduleResponse {
1912 DescribeScheduleResponse {
1913 schedule: Some(Schedule::default()),
1914 ..Default::default()
1915 }
1916 }
1917
1918 fn describe_response_without_action_variant() -> DescribeScheduleResponse {
1919 DescribeScheduleResponse {
1920 schedule: Some(Schedule {
1921 action: Some(schedule_proto::ScheduleAction::default()),
1922 ..Default::default()
1923 }),
1924 ..Default::default()
1925 }
1926 }
1927
1928 #[test]
1929 fn schedule_handle_exposes_namespace_and_id() {
1930 let handle = make_schedule_handle(MockScheduleClient::default());
1931 assert_eq!(handle.namespace(), "test-namespace");
1932 assert_eq!(handle.schedule_id(), "test-schedule-id");
1933 }
1934
1935 #[tokio::test]
1936 async fn schedule_describe_returns_response_fields() {
1937 let conflict_token = b"token-123".to_vec();
1938 let mut describe_response = describe_response_with_start_workflow(None);
1939 describe_response.info = Some(ScheduleInfo::default());
1940 describe_response.memo = Some(Memo {
1941 fields: Default::default(),
1942 });
1943 describe_response.search_attributes = Some(SearchAttributes {
1944 indexed_fields: Default::default(),
1945 });
1946 describe_response.conflict_token = conflict_token.clone();
1947
1948 let client = MockScheduleClient {
1949 describe_response,
1950 ..Default::default()
1951 };
1952
1953 let handle = make_schedule_handle(client.clone());
1954 let desc = handle.describe(RpcOptions::default()).await.unwrap();
1955
1956 assert_eq!(client.captured.describe.load(Ordering::SeqCst), 1);
1957 assert!(desc.raw().schedule.is_some());
1958 assert!(desc.raw().info.is_some());
1959 assert!(desc.raw().memo.is_some());
1960 assert!(desc.raw().search_attributes.is_some());
1961 assert!(desc.search_attributes().is_empty());
1962 assert_eq!(desc.conflict_token(), conflict_token);
1963 }
1964
1965 #[tokio::test]
1966 async fn schedule_description_exposes_typed_memo() {
1967 let data_converter = data_converter_with_codec();
1968 let memo_payload = data_converter
1969 .to_payload(
1970 &SerializationContextData::Workflow,
1971 &"memo-value".to_owned(),
1972 )
1973 .await
1974 .unwrap();
1975 let mut describe_response = describe_response_with_start_workflow(None);
1976 describe_response.memo = Some(Memo {
1977 fields: HashMap::from([("memo-key".to_owned(), memo_payload)]),
1978 });
1979 let client = MockScheduleClient {
1980 describe_response,
1981 data_converter,
1982 ..Default::default()
1983 };
1984
1985 let description = make_schedule_handle(client)
1986 .describe(RpcOptions::default())
1987 .await
1988 .unwrap();
1989
1990 assert_eq!(
1991 description.memo().get::<String>("memo-key").unwrap(),
1992 Some("memo-value".to_owned())
1993 );
1994 }
1995
1996 #[tokio::test]
1997 async fn schedule_summary_exposes_typed_memo() {
1998 let data_converter = DataConverter::default();
1999 let memo_payload = data_converter
2000 .to_payload(
2001 &SerializationContextData::Workflow,
2002 &"memo-value".to_owned(),
2003 )
2004 .await
2005 .unwrap();
2006 let summary = ScheduleSummary::new(
2007 ScheduleListEntry {
2008 schedule_id: "schedule-id".to_owned(),
2009 memo: Some(Memo {
2010 fields: HashMap::from([("memo-key".to_owned(), memo_payload)]),
2011 }),
2012 ..Default::default()
2013 },
2014 data_converter,
2015 );
2016
2017 assert_eq!(
2018 summary.memo().get::<String>("memo-key").unwrap(),
2019 Some("memo-value".to_owned())
2020 );
2021 }
2022
2023 #[tokio::test]
2024 async fn list_schedules_yields_codec_error_then_ends() {
2025 let client = MockScheduleClient {
2026 list_response: ListSchedulesResponse {
2027 schedules: vec![ScheduleListEntry {
2028 memo: Some(Memo {
2029 fields: HashMap::from([("memo-key".to_owned(), Payload::default())]),
2030 }),
2031 ..Default::default()
2032 }],
2033 next_page_token: b"next-page".to_vec(),
2034 },
2035 data_converter: DataConverter::new(
2036 PayloadConverter::default(),
2037 DefaultFailureConverter,
2038 FailingCodec,
2039 ),
2040 ..Default::default()
2041 };
2042 let mut stream = list_schedules_stream(client.clone(), ListSchedulesOptions::default());
2043
2044 let err = stream.next().await.unwrap().unwrap_err();
2045
2046 assert!(matches!(err, ScheduleError::PayloadConversion(_)));
2047 assert!(stream.next().await.is_none());
2048 assert_eq!(client.captured.list.load(Ordering::SeqCst), 1);
2049 }
2050
2051 #[tokio::test]
2052 async fn schedule_update_describes_then_sends() {
2053 let client = MockScheduleClient::default();
2054 let handle = make_schedule_handle(client.clone());
2055
2056 handle
2057 .update(
2058 |u| {
2059 u.set_note("hi");
2060 },
2061 RpcOptions::default(),
2062 )
2063 .await
2064 .unwrap();
2065
2066 assert_eq!(client.captured.describe.load(Ordering::SeqCst), 1);
2067 assert_eq!(client.captured.update.load(Ordering::SeqCst), 1);
2068 }
2069
2070 #[tokio::test]
2071 async fn schedule_update_does_not_nest_describe_or_send_update_hooks() {
2072 let counts = Arc::new(ScheduleHookCounts::default());
2073 let client = MockScheduleClient {
2074 interceptors: vec![Arc::new(RecordingScheduleInterceptor {
2075 counts: counts.clone(),
2076 })],
2077 ..Default::default()
2078 };
2079 let handle = make_schedule_handle(client.clone());
2080
2081 handle
2082 .update(
2083 |update| {
2084 update.set_note("updated");
2085 },
2086 RpcOptions::default(),
2087 )
2088 .await
2089 .unwrap();
2090
2091 assert_eq!(counts.update.load(Ordering::SeqCst), 1);
2092 assert_eq!(counts.describe.load(Ordering::SeqCst), 0);
2093 assert_eq!(counts.send_update.load(Ordering::SeqCst), 0);
2094 assert_eq!(client.captured.describe.load(Ordering::SeqCst), 1);
2095 assert_eq!(client.captured.update.load(Ordering::SeqCst), 1);
2096 }
2097
2098 #[tokio::test]
2099 async fn schedule_multiple_updates_each_call_service() {
2100 let client = MockScheduleClient::default();
2101 let handle = make_schedule_handle(client.clone());
2102
2103 handle.update(|_| {}, RpcOptions::default()).await.unwrap();
2104 handle.update(|_| {}, RpcOptions::default()).await.unwrap();
2105
2106 assert_eq!(client.captured.update.load(Ordering::SeqCst), 2);
2107 }
2108
2109 #[tokio::test]
2110 async fn schedule_delete_calls_service() {
2111 let client = MockScheduleClient::default();
2112 let handle = make_schedule_handle(client.clone());
2113
2114 handle
2115 .delete(DeleteScheduleOptions::default())
2116 .await
2117 .unwrap();
2118
2119 assert_eq!(client.captured.delete.load(Ordering::SeqCst), 1);
2120 }
2121
2122 #[tokio::test]
2123 async fn schedule_pause_calls_patch() {
2124 let client = MockScheduleClient::default();
2125 let handle = make_schedule_handle(client.clone());
2126
2127 handle
2128 .pause(Some("taking a break"), PauseScheduleOptions::default())
2129 .await
2130 .unwrap();
2131
2132 assert_eq!(client.captured.patch.load(Ordering::SeqCst), 1);
2133 }
2134
2135 #[tokio::test]
2136 async fn schedule_pause_with_none_uses_default() {
2137 let client = MockScheduleClient::default();
2138 let handle = make_schedule_handle(client.clone());
2139
2140 handle
2141 .pause(None::<&str>, PauseScheduleOptions::default())
2142 .await
2143 .unwrap();
2144
2145 assert_eq!(client.captured.patch.load(Ordering::SeqCst), 1);
2146 }
2147
2148 #[tokio::test]
2149 async fn schedule_unpause_calls_patch() {
2150 let client = MockScheduleClient::default();
2151 let handle = make_schedule_handle(client.clone());
2152
2153 handle
2154 .unpause(Some("resuming work"), UnpauseScheduleOptions::default())
2155 .await
2156 .unwrap();
2157
2158 assert_eq!(client.captured.patch.load(Ordering::SeqCst), 1);
2159 }
2160
2161 #[tokio::test]
2162 async fn schedule_trigger_calls_patch() {
2163 let client = MockScheduleClient::default();
2164 let handle = make_schedule_handle(client.clone());
2165
2166 handle
2167 .trigger(
2168 ScheduleOverlapPolicy::Unspecified,
2169 TriggerScheduleOptions::default(),
2170 )
2171 .await
2172 .unwrap();
2173
2174 assert_eq!(client.captured.patch.load(Ordering::SeqCst), 1);
2175 }
2176
2177 #[tokio::test]
2178 async fn schedule_backfill_calls_patch() {
2179 let client = MockScheduleClient::default();
2180 let handle = make_schedule_handle(client.clone());
2181
2182 let now = SystemTime::now();
2183 handle
2184 .backfill(
2185 vec![
2186 ScheduleBackfill::new(now, now)
2187 .overlap_policy(ScheduleOverlapPolicy::Skip)
2188 .build(),
2189 ScheduleBackfill::new(now, now)
2190 .overlap_policy(ScheduleOverlapPolicy::BufferOne)
2191 .build(),
2192 ],
2193 BackfillScheduleOptions::default(),
2194 )
2195 .await
2196 .unwrap();
2197
2198 assert_eq!(client.captured.patch.load(Ordering::SeqCst), 1);
2199 }
2200
2201 #[tokio::test]
2202 async fn schedule_describe_propagates_rpc_errors() {
2203 let client = MockScheduleClient {
2204 should_error: true,
2205 ..Default::default()
2206 };
2207 let handle = make_schedule_handle(client);
2208
2209 let err = handle.describe(RpcOptions::default()).await.unwrap_err();
2210 assert!(
2211 matches!(err, ScheduleError::Rpc(_)),
2212 "expected Rpc variant, got: {err:?}"
2213 );
2214 assert!(err.to_string().contains("schedule not found"));
2215 }
2216
2217 #[tokio::test]
2218 async fn schedule_update_propagates_rpc_errors() {
2219 let client = MockScheduleClient {
2220 should_error: true,
2221 ..Default::default()
2222 };
2223 let handle = make_schedule_handle(client);
2224
2225 let err = handle
2226 .update(|_| {}, RpcOptions::default())
2227 .await
2228 .unwrap_err();
2229 assert!(matches!(err, ScheduleError::Rpc(_)));
2230 }
2231
2232 #[tokio::test]
2233 async fn schedule_delete_propagates_rpc_errors() {
2234 let client = MockScheduleClient {
2235 should_error: true,
2236 ..Default::default()
2237 };
2238 let handle = make_schedule_handle(client);
2239
2240 let err = handle
2241 .delete(DeleteScheduleOptions::default())
2242 .await
2243 .unwrap_err();
2244 assert!(matches!(err, ScheduleError::Rpc(_)));
2245 }
2246
2247 #[tokio::test]
2248 async fn schedule_patch_operations_propagate_rpc_errors() {
2249 let client = MockScheduleClient {
2250 should_error: true,
2251 ..Default::default()
2252 };
2253 let handle = make_schedule_handle(client);
2254
2255 assert!(
2256 handle
2257 .pause(Some(""), PauseScheduleOptions::default())
2258 .await
2259 .is_err()
2260 );
2261 assert!(
2262 handle
2263 .unpause(Some(""), UnpauseScheduleOptions::default())
2264 .await
2265 .is_err()
2266 );
2267 assert!(
2268 handle
2269 .trigger(Default::default(), TriggerScheduleOptions::default())
2270 .await
2271 .is_err()
2272 );
2273 assert!(
2274 handle
2275 .backfill(vec![], BackfillScheduleOptions::default())
2276 .await
2277 .is_err()
2278 );
2279 }
2280
2281 #[tokio::test]
2282 async fn schedule_all_patch_operations_call_service() {
2283 let client = MockScheduleClient::default();
2284 let handle = make_schedule_handle(client.clone());
2285
2286 handle
2287 .pause(Some("p"), PauseScheduleOptions::default())
2288 .await
2289 .unwrap();
2290 handle
2291 .unpause(Some("u"), UnpauseScheduleOptions::default())
2292 .await
2293 .unwrap();
2294 handle
2295 .trigger(Default::default(), TriggerScheduleOptions::default())
2296 .await
2297 .unwrap();
2298 handle
2299 .backfill(vec![], BackfillScheduleOptions::default())
2300 .await
2301 .unwrap();
2302
2303 assert_eq!(client.captured.patch.load(Ordering::SeqCst), 4);
2304 }
2305
2306 #[tokio::test]
2307 async fn schedule_describe_accessors_with_populated_fields() {
2308 let mut describe_response = describe_response_with_start_workflow(None);
2309 let schedule = describe_response.schedule.as_mut().unwrap();
2310 schedule.spec = Some(ScheduleSpec {
2311 timezone_name: "US/Eastern".to_string(),
2312 ..Default::default()
2313 });
2314 schedule.state = Some(ScheduleState {
2315 paused: true,
2316 notes: "maintenance window".to_string(),
2317 ..Default::default()
2318 });
2319 describe_response.info = Some(ScheduleInfo {
2320 action_count: 42,
2321 missed_catchup_window: 3,
2322 overlap_skipped: 5,
2323 recent_actions: vec![ScheduleActionResult {
2324 start_workflow_result: Some(ProtoWorkflowExecution {
2325 workflow_id: "ra-wf".to_string(),
2326 run_id: "ra-run".to_string(),
2327 }),
2328 ..Default::default()
2329 }],
2330 running_workflows: vec![ProtoWorkflowExecution {
2331 workflow_id: "wf-1".to_string(),
2332 run_id: "run-1".to_string(),
2333 }],
2334 create_time: Some(prost_types::Timestamp {
2335 seconds: 1_700_000_000,
2336 nanos: 0,
2337 }),
2338 update_time: Some(prost_types::Timestamp {
2339 seconds: 1_700_001_000,
2340 nanos: 0,
2341 }),
2342 future_action_times: vec![prost_types::Timestamp {
2343 seconds: 1_700_002_000,
2344 nanos: 0,
2345 }],
2346 ..Default::default()
2347 });
2348 describe_response.conflict_token = b"tok".to_vec();
2349
2350 let client = MockScheduleClient {
2351 describe_response,
2352 ..Default::default()
2353 };
2354
2355 let handle = make_schedule_handle(client);
2356 let desc = handle.describe(RpcOptions::default()).await.unwrap();
2357
2358 assert!(desc.paused());
2359 assert_eq!(desc.note(), Some("maintenance window"));
2360 assert_eq!(desc.action_count(), 42);
2361 assert_eq!(desc.missed_catchup_window(), 3);
2362 assert_eq!(desc.overlap_skipped(), 5);
2363
2364 assert_eq!(desc.recent_actions().len(), 1);
2365 assert_eq!(
2366 desc.running_actions(),
2367 vec![ScheduleRunningAction {
2368 workflow_id: "wf-1".to_string(),
2369 run_id: "run-1".to_string(),
2370 }]
2371 );
2372
2373 assert_eq!(desc.future_action_times().len(), 1);
2374 assert!(desc.create_time().is_some());
2375 assert!(desc.update_time().is_some());
2376 }
2377
2378 #[tokio::test]
2379 async fn schedule_describe_defaults_when_nested_fields_are_none() {
2380 let client = MockScheduleClient::default();
2381
2382 let handle = make_schedule_handle(client);
2383 let desc = handle.describe(RpcOptions::default()).await.unwrap();
2384
2385 assert!(!desc.paused());
2386 assert_eq!(desc.note(), None);
2387 assert_eq!(desc.action_count(), 0);
2388 assert_eq!(desc.missed_catchup_window(), 0);
2389 assert_eq!(desc.overlap_skipped(), 0);
2390 assert!(desc.recent_actions().is_empty());
2391 assert!(desc.running_actions().is_empty());
2392 assert!(desc.future_action_times().is_empty());
2393 assert!(desc.create_time().is_none());
2394 assert!(desc.update_time().is_none());
2395 assert!(desc.conflict_token().is_empty());
2396 }
2397
2398 #[tokio::test]
2399 async fn schedule_note_returns_none_for_empty_string() {
2400 let mut describe_response = describe_response_with_start_workflow(None);
2401 describe_response.schedule.as_mut().unwrap().state = Some(ScheduleState {
2402 notes: String::new(),
2403 ..Default::default()
2404 });
2405
2406 let client = MockScheduleClient {
2407 describe_response,
2408 ..Default::default()
2409 };
2410
2411 let handle = make_schedule_handle(client);
2412 let desc = handle.describe(RpcOptions::default()).await.unwrap();
2413 assert_eq!(desc.note(), None);
2414 }
2415
2416 #[test]
2417 fn schedule_summary_note_returns_none_for_empty_string() {
2418 let entry = ScheduleListEntry {
2419 schedule_id: "s".to_string(),
2420 info: Some(ScheduleListInfo {
2421 notes: String::new(),
2422 ..Default::default()
2423 }),
2424 ..Default::default()
2425 };
2426 let summary = ScheduleSummary::from(entry);
2427 assert_eq!(summary.note(), None);
2428 }
2429
2430 #[test]
2431 fn schedule_summary_accessors() {
2432 let entry = ScheduleListEntry {
2433 schedule_id: "sched-1".to_string(),
2434 memo: Some(Memo {
2435 fields: Default::default(),
2436 }),
2437 search_attributes: Some(SearchAttributes {
2438 indexed_fields: Default::default(),
2439 }),
2440 info: Some(ScheduleListInfo {
2441 spec: Some(ScheduleSpec::default()),
2442 workflow_type: Some(WorkflowType {
2443 name: "MyWorkflow".to_string(),
2444 }),
2445 notes: "some note".to_string(),
2446 paused: true,
2447 recent_actions: vec![ScheduleActionResult {
2448 start_workflow_result: Some(ProtoWorkflowExecution {
2449 workflow_id: "ra-wf".to_string(),
2450 run_id: "ra-run".to_string(),
2451 }),
2452 ..Default::default()
2453 }],
2454 future_action_times: vec![prost_types::Timestamp {
2455 seconds: 1_700_000_000,
2456 nanos: 0,
2457 }],
2458 state_size_bytes: 0,
2459 }),
2460 };
2461
2462 let summary = ScheduleSummary::from(entry);
2463 assert_eq!(summary.schedule_id(), "sched-1");
2464 assert!(summary.raw().memo.is_some());
2465 assert!(summary.raw().search_attributes.is_some());
2466 assert!(summary.search_attributes().is_empty());
2467 assert_eq!(summary.workflow_type(), Some("MyWorkflow"));
2468 assert_eq!(summary.note(), Some("some note"));
2469 assert!(summary.paused());
2470 assert_eq!(summary.recent_actions().len(), 1);
2471 assert_eq!(summary.future_action_times().len(), 1);
2472 }
2473
2474 #[test]
2475 fn schedule_summary_defaults_when_info_is_none() {
2476 let entry = ScheduleListEntry {
2477 schedule_id: "sched-2".to_string(),
2478 ..Default::default()
2479 };
2480
2481 let summary = ScheduleSummary::from(entry);
2482 assert_eq!(summary.schedule_id(), "sched-2");
2483 assert!(summary.raw().memo.is_none());
2484 assert!(summary.raw().search_attributes.is_none());
2485 assert!(summary.search_attributes().is_empty());
2486 assert_eq!(summary.workflow_type(), None);
2487 assert_eq!(summary.note(), None);
2488 assert!(!summary.paused());
2489 assert!(summary.recent_actions().is_empty());
2490 assert!(summary.future_action_times().is_empty());
2491 }
2492
2493 #[test]
2494 fn schedule_description_raw_round_trip() {
2495 let mut resp = describe_response_with_start_workflow(None);
2496 resp.conflict_token = b"ct".to_vec();
2497
2498 let desc = schedule_description_from_response(resp.clone());
2499 assert_eq!(desc.raw().conflict_token, b"ct");
2500 let recovered = desc.into_raw();
2501 assert_eq!(recovered.conflict_token, resp.conflict_token);
2502 assert!(recovered.schedule.is_some());
2503 }
2504
2505 #[tokio::test]
2506 async fn schedule_description_action_decodes_start_workflow_args() {
2507 let data_converter = DataConverter::default();
2508 let expected = MultiArgs2("hello".to_string(), 42i32);
2509 let payloads = data_converter
2510 .to_payloads(&SerializationContextData::Workflow, &expected)
2511 .await
2512 .unwrap();
2513 let desc = ScheduleDescription::new(
2514 describe_response_with_start_workflow(Some(Payloads { payloads })),
2515 data_converter,
2516 "test-schedule-id",
2517 )
2518 .unwrap();
2519
2520 let ScheduleDescriptionAction::StartWorkflow(action) = desc.action();
2521
2522 assert_eq!(action.workflow_type(), "MyWorkflow");
2523 assert_eq!(action.task_queue(), "task-queue");
2524 assert_eq!(action.workflow_id(), "wf-id");
2525 let decoded: MultiArgs2<String, i32> = action.args().await.unwrap().unwrap();
2526 assert_eq!(decoded, expected);
2527 }
2528
2529 #[tokio::test]
2530 async fn schedule_description_start_workflow_args_returns_none_without_input() {
2531 let desc = schedule_description_from_response(describe_response_with_start_workflow(None));
2532 let ScheduleDescriptionAction::StartWorkflow(action) = desc.action();
2533
2534 let decoded: Option<String> = action.args().await.unwrap();
2535 assert_eq!(decoded, None);
2536 }
2537
2538 #[tokio::test]
2539 async fn schedule_description_start_workflow_args_propagates_decode_errors() {
2540 let data_converter = DataConverter::default();
2541 let expected: String = "not-an-int".to_string();
2542 let payloads = data_converter
2543 .to_payloads(&SerializationContextData::Workflow, &expected)
2544 .await
2545 .unwrap();
2546 let desc = schedule_description_from_response(describe_response_with_start_workflow(Some(
2547 Payloads { payloads },
2548 )));
2549 let ScheduleDescriptionAction::StartWorkflow(action) = desc.action();
2550
2551 let err = action.args::<i32>().await.unwrap_err();
2552 assert!(matches!(err, PayloadConversionError::EncodingError(_)));
2553 }
2554
2555 #[rstest::rstest]
2556 #[case::schedule(describe_response_without_schedule(), "missing schedule")]
2557 #[case::action(describe_response_without_action(), "missing schedule action")]
2558 #[case::action_variant(
2559 describe_response_without_action_variant(),
2560 "missing schedule action variant"
2561 )]
2562 #[tokio::test]
2563 async fn schedule_describe_errors_when_required_field_is_missing(
2564 #[case] describe_response: DescribeScheduleResponse,
2565 #[case] reason: &str,
2566 ) {
2567 let client = MockScheduleClient {
2568 describe_response,
2569 ..Default::default()
2570 };
2571 let handle = make_schedule_handle(client);
2572
2573 let err = handle.describe(RpcOptions::default()).await.unwrap_err();
2574 assert_eq!(
2575 err.to_string(),
2576 format!("Malformed schedule description for schedule ID 'test-schedule-id': {reason}")
2577 );
2578 }
2579
2580 #[test]
2581 fn schedule_summary_raw_round_trip() {
2582 let entry = ScheduleListEntry {
2583 schedule_id: "rt-1".to_string(),
2584 ..Default::default()
2585 };
2586 let summary = ScheduleSummary::from(entry.clone());
2587 assert_eq!(summary.raw().schedule_id, "rt-1");
2588 let recovered = summary.into_raw();
2589 assert_eq!(recovered.schedule_id, entry.schedule_id);
2590 }
2591
2592 #[test]
2593 fn schedule_into_update_preserves_schedule() {
2594 let mut resp = describe_response_with_start_workflow(None);
2595 resp.schedule.as_mut().unwrap().state = Some(ScheduleState {
2596 notes: "my notes".to_string(),
2597 ..Default::default()
2598 });
2599 let desc = schedule_description_from_response(resp);
2600 let update = desc.into_update();
2601
2602 assert_eq!(update.raw().state.as_ref().unwrap().notes, "my notes");
2603 }
2604
2605 #[test]
2606 fn schedule_update_setters_are_chainable() {
2607 let desc = schedule_description_from_response(describe_response_with_start_workflow(None));
2608 let mut update = desc.into_update();
2609 update.set_note("chained").set_paused(true);
2610 assert_eq!(update.raw().state.as_ref().unwrap().notes, "chained");
2611 assert!(update.raw().state.as_ref().unwrap().paused);
2612 }
2613
2614 #[test]
2615 fn schedule_recent_action_from_proto_with_timestamps() {
2616 let ts = prost_types::Timestamp {
2617 seconds: 1_700_000_000,
2618 nanos: 0,
2619 };
2620 let proto = ScheduleActionResult {
2621 schedule_time: Some(ts),
2622 actual_time: Some(ts),
2623 start_workflow_result: Some(ProtoWorkflowExecution {
2624 workflow_id: "wf-abc".to_string(),
2625 run_id: "run-xyz".to_string(),
2626 }),
2627 ..Default::default()
2628 };
2629
2630 let action = ScheduleRecentAction::from(&proto);
2631
2632 assert!(action.schedule_time.is_some());
2633 assert!(action.actual_time.is_some());
2634 assert_eq!(action.workflow_id, "wf-abc");
2635 assert_eq!(action.run_id, "run-xyz");
2636 }
2637
2638 #[test]
2639 #[should_panic(expected = "unsupported schedule action")]
2640 fn schedule_recent_action_panics_without_workflow_result() {
2641 let _ = ScheduleRecentAction::from(&ScheduleActionResult::default());
2642 }
2643
2644 #[test]
2645 fn schedule_overlap_policy_default_is_unspecified() {
2646 assert_eq!(
2647 ScheduleOverlapPolicy::default(),
2648 ScheduleOverlapPolicy::Unspecified
2649 );
2650 }
2651
2652 #[tokio::test]
2653 async fn schedule_action_start_workflow_with_input_into_proto() {
2654 let payload = Payload {
2655 metadata: [("encoding".to_string(), b"json/plain".to_vec())]
2656 .into_iter()
2657 .collect(),
2658 data: b"42".to_vec(),
2659 ..Default::default()
2660 };
2661 let action = ScheduleAction::start_workflow(
2662 UntypedWorkflow::new("MyWorkflow"),
2663 RawValue::new(vec![payload.clone()]),
2664 "my-queue",
2665 "my-wf-id",
2666 );
2667 let proto = action.into_proto(&DataConverter::default()).await.unwrap();
2668 #[allow(irrefutable_let_patterns)]
2669 let schedule_proto::schedule_action::Action::StartWorkflow(wf_info) = proto.action.unwrap()
2670 else {
2671 panic!("expected StartWorkflow action")
2672 };
2673 assert_eq!(wf_info.input.unwrap().payloads, vec![payload]);
2674 }
2675}