Skip to main content

temporalio_common_wasm/
retry_policy.rs

1use std::time::Duration;
2
3use crate::protos::temporal::api::common::v1::RetryPolicy as ProtoRetryPolicy;
4
5const DEFAULT_INITIAL_INTERVAL: Duration = Duration::from_secs(1);
6const DEFAULT_BACKOFF_COEFFICIENT: f64 = 2.0;
7const MAX_PROTO_DURATION: prost_types::Duration = prost_types::Duration {
8    seconds: 315_576_000_000,
9    nanos: 999_999_999,
10};
11
12fn duration_to_proto(duration: Duration) -> prost_types::Duration {
13    duration.try_into().unwrap_or(MAX_PROTO_DURATION)
14}
15
16/// Options for retrying workflows and activities.
17///
18/// Durations longer than 10,000 years are clamped to the maximum valid protobuf duration.
19#[derive(Clone, Debug, PartialEq)]
20#[non_exhaustive]
21pub struct RetryPolicy {
22    raw: ProtoRetryPolicy,
23}
24
25impl Default for RetryPolicy {
26    fn default() -> Self {
27        Self::builder().build()
28    }
29}
30
31#[bon::bon]
32impl RetryPolicy {
33    /// Create a retry policy.
34    #[builder(state_mod(vis = "pub"))]
35    pub fn new(
36        #[builder(default = DEFAULT_INITIAL_INTERVAL)] initial_interval: Duration,
37        #[builder(default = DEFAULT_BACKOFF_COEFFICIENT)] backoff_coefficient: f64,
38        maximum_interval: Option<Duration>,
39        #[builder(default)] maximum_attempts: u32,
40        #[builder(
41            with = |values: impl IntoIterator<Item = impl Into<String>>| values
42                .into_iter()
43                .map(Into::into)
44                .collect(),
45            default
46        )]
47        non_retryable_error_types: Vec<String>,
48    ) -> Self {
49        let mut policy = Self {
50            raw: ProtoRetryPolicy::default(),
51        };
52        policy
53            .set_initial_interval(initial_interval)
54            .set_backoff_coefficient(backoff_coefficient)
55            .set_maximum_interval(maximum_interval)
56            .set_maximum_attempts(maximum_attempts)
57            .set_non_retryable_error_types(non_retryable_error_types);
58        policy
59    }
60
61    /// Backoff interval for the first retry.
62    pub fn initial_interval(&self) -> Duration {
63        self.raw
64            .initial_interval
65            .map(|duration| duration.try_into().ok().unwrap_or(Duration::ZERO))
66            .unwrap_or(DEFAULT_INITIAL_INTERVAL)
67    }
68
69    /// Set the backoff interval for the first retry.
70    pub fn set_initial_interval(&mut self, initial_interval: Duration) -> &mut Self {
71        self.raw.initial_interval = Some(duration_to_proto(initial_interval));
72        self
73    }
74
75    /// Coefficient used to calculate the next retry interval.
76    pub fn backoff_coefficient(&self) -> f64 {
77        if self.raw.backoff_coefficient == 0.0 {
78            DEFAULT_BACKOFF_COEFFICIENT
79        } else {
80            self.raw.backoff_coefficient
81        }
82    }
83
84    /// Set the coefficient used to calculate the next retry interval.
85    pub fn set_backoff_coefficient(&mut self, backoff_coefficient: f64) -> &mut Self {
86        self.raw.backoff_coefficient = backoff_coefficient;
87        self
88    }
89
90    /// Maximum backoff interval between retries.
91    pub fn maximum_interval(&self) -> Option<Duration> {
92        self.raw
93            .maximum_interval
94            .map(|duration| duration.try_into().ok().unwrap_or(Duration::ZERO))
95    }
96
97    /// Set the maximum backoff interval between retries.
98    pub fn set_maximum_interval(&mut self, maximum_interval: Option<Duration>) -> &mut Self {
99        self.raw.maximum_interval = maximum_interval.map(duration_to_proto);
100        self
101    }
102
103    /// Maximum number of attempts. Zero means unlimited attempts.
104    pub fn maximum_attempts(&self) -> u32 {
105        self.raw.maximum_attempts.try_into().unwrap_or_default()
106    }
107
108    /// Set the maximum number of attempts. Zero means unlimited attempts. Values greater than
109    /// [`i32::MAX`] are clamped to [`i32::MAX`].
110    pub fn set_maximum_attempts(&mut self, maximum_attempts: u32) -> &mut Self {
111        self.raw.maximum_attempts = maximum_attempts.try_into().unwrap_or(i32::MAX);
112        self
113    }
114
115    /// Error type names that should not be retried.
116    pub fn non_retryable_error_types(&self) -> &[String] {
117        &self.raw.non_retryable_error_types
118    }
119
120    /// Set the error type names that should not be retried.
121    pub fn set_non_retryable_error_types(
122        &mut self,
123        values: impl IntoIterator<Item = impl Into<String>>,
124    ) -> &mut Self {
125        self.raw.non_retryable_error_types = values.into_iter().map(Into::into).collect();
126        self
127    }
128
129    /// Access the underlying retry policy protobuf.
130    pub fn raw(&self) -> &ProtoRetryPolicy {
131        &self.raw
132    }
133
134    /// Consume this wrapper and return the underlying retry policy protobuf.
135    pub fn into_raw(self) -> ProtoRetryPolicy {
136        self.raw
137    }
138}
139
140impl From<ProtoRetryPolicy> for RetryPolicy {
141    fn from(value: ProtoRetryPolicy) -> Self {
142        Self { raw: value }
143    }
144}
145
146impl From<RetryPolicy> for ProtoRetryPolicy {
147    fn from(value: RetryPolicy) -> Self {
148        value.raw
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use rstest::rstest;
156
157    #[rstest]
158    #[case::default(RetryPolicy::default())]
159    #[case::builder(RetryPolicy::builder().build())]
160    #[case::proto(RetryPolicy::from(ProtoRetryPolicy::default()))]
161    fn defaults_match_temporal_retry_defaults(#[case] policy: RetryPolicy) {
162        assert_eq!(policy.initial_interval(), Duration::from_secs(1));
163        assert_eq!(policy.backoff_coefficient(), 2.0);
164        assert_eq!(policy.maximum_attempts(), 0);
165        assert_eq!(policy.maximum_interval(), None);
166    }
167
168    #[test]
169    fn retry_policy_round_trips() {
170        let policy = RetryPolicy::builder()
171            .initial_interval(Duration::from_millis(250))
172            .backoff_coefficient(1.5)
173            .maximum_interval(Duration::from_secs(10))
174            .maximum_attempts(5)
175            .non_retryable_error_types(["InvalidInput"])
176            .build();
177
178        assert_eq!(
179            RetryPolicy::from(ProtoRetryPolicy::from(policy.clone())),
180            policy
181        );
182    }
183
184    #[test]
185    fn setters_update_raw_proto() {
186        let mut policy = RetryPolicy::default();
187        policy
188            .set_initial_interval(Duration::from_millis(250))
189            .set_backoff_coefficient(1.5)
190            .set_maximum_interval(Some(Duration::from_secs(10)))
191            .set_maximum_attempts(5)
192            .set_non_retryable_error_types(["InvalidInput"]);
193
194        assert_eq!(policy.initial_interval(), Duration::from_millis(250));
195        assert_eq!(policy.backoff_coefficient(), 1.5);
196        assert_eq!(policy.maximum_interval(), Some(Duration::from_secs(10)));
197        assert_eq!(policy.maximum_attempts(), 5);
198        assert_eq!(policy.non_retryable_error_types(), ["InvalidInput"]);
199        assert_eq!(
200            policy.raw().initial_interval,
201            Duration::from_millis(250).try_into().ok()
202        );
203        assert_eq!(policy.raw().backoff_coefficient, 1.5);
204        assert_eq!(
205            policy.raw().maximum_interval,
206            Duration::from_secs(10).try_into().ok()
207        );
208        assert_eq!(policy.raw().maximum_attempts, 5);
209        assert_eq!(policy.raw().non_retryable_error_types, ["InvalidInput"]);
210    }
211
212    #[test]
213    fn invalid_raw_values_are_normalized_by_getters() {
214        let raw = ProtoRetryPolicy {
215            initial_interval: Some(prost_types::Duration {
216                seconds: i64::MIN,
217                nanos: 999_999_999,
218            }),
219            maximum_interval: Some(prost_types::Duration {
220                seconds: i64::MIN,
221                nanos: 999_999_999,
222            }),
223            maximum_attempts: -1,
224            ..Default::default()
225        };
226        let policy = RetryPolicy::from(raw.clone());
227
228        assert_eq!(policy.initial_interval(), Duration::ZERO);
229        assert_eq!(policy.maximum_interval(), Some(Duration::ZERO));
230        assert_eq!(policy.maximum_attempts(), 0);
231        assert_eq!(policy.raw(), &raw);
232    }
233}