Skip to main content

golem_rust/
retry.rs

1//! Semantic retry policies for host-managed operations and local user code.
2//!
3//! Named policies are installed and resolved by the Golem host. A resolved policy can also be
4//! compiled into a [`RetrySchedule`] to retry arbitrary user code in the component:
5//!
6//! ```no_run
7//! use golem_rust::retry::{
8//!     PredicateValue, Props, RetrySchedule, resolve_retry_policy,
9//! };
10//! use std::io;
11//!
12//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
13//! let context = vec![(
14//!     Props::STATUS_CODE.to_string(),
15//!     PredicateValue::Integer(503),
16//! )];
17//! let raw = resolve_retry_policy("send", "email://welcome", &context)
18//!     .map_err(io::Error::other)?
19//!     .ok_or_else(|| io::Error::other("no matching retry policy"))?;
20//! let schedule = RetrySchedule::try_from(&raw)?;
21//!
22//! schedule
23//!     .retry_with_properties(
24//!         async || Err::<(), _>(io::Error::other("temporarily unavailable")),
25//!         |_| context.clone(),
26//!     )
27//!     .await?;
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! Resolution remains host-driven, but `RetrySchedule` execution is a local guest loop. Local
33//! attempts do not become executor `RetryAttempt` entries or one host-managed retry sequence.
34
35use crate::bindings::golem::api::retry as retry_api;
36
37mod local;
38
39pub use builder::{NamedPolicy, Policy, Predicate, Props, RetryBuilderError, Value};
40pub use local::{RetryPolicyError, RetrySchedule};
41pub use retry_api::{
42    CountBoxConfig, NamedRetryPolicy, PolicyNode, PredicateNode, PredicateValue, RetryPolicy,
43    RetryPredicate,
44};
45
46/// Get all retry policies active for this agent.
47pub fn get_retry_policies() -> Vec<NamedRetryPolicy> {
48    retry_api::get_retry_policies()
49}
50
51/// Get a specific retry policy by name.
52pub fn get_retry_policy_by_name(name: &str) -> Option<NamedRetryPolicy> {
53    retry_api::get_retry_policy_by_name(name)
54}
55
56/// Resolve the matching retry policy for a given operation context.
57/// Evaluates named policies in descending priority order; returns the
58/// policy from the first rule whose predicate matches, or none.
59/// Invalid predicates return an error without trapping the invocation.
60pub fn resolve_retry_policy(
61    verb: &str,
62    noun_uri: &str,
63    properties: &[(String, PredicateValue)],
64) -> Result<Option<RetryPolicy>, String> {
65    let props: Vec<(String, PredicateValue)> = properties.to_vec();
66    retry_api::resolve_retry_policy(verb, noun_uri, &props)
67}
68
69/// Add or overwrite a named retry policy (persisted to oplog).
70/// If a policy with the same name exists, it is replaced.
71pub fn set_retry_policy(policy: &NamedRetryPolicy) {
72    retry_api::set_retry_policy(policy);
73}
74
75/// Add or overwrite a high-level named retry policy after validating and
76/// flattening it into the raw WIT representation.
77pub fn set_named_policy(policy: &NamedPolicy) -> Result<(), RetryBuilderError> {
78    let raw = policy.try_to_raw()?;
79    set_retry_policy(&raw);
80    Ok(())
81}
82
83/// Remove a named retry policy by name (persisted to oplog).
84pub fn remove_retry_policy(name: &str) {
85    retry_api::remove_retry_policy(name);
86}
87
88/// Guard that restores the previous state of a named retry policy on drop.
89/// If the policy existed before, it is restored; if it was newly added, it is removed.
90pub struct RetryPolicyGuard {
91    previous: Option<NamedRetryPolicy>,
92    name: String,
93}
94
95impl Drop for RetryPolicyGuard {
96    fn drop(&mut self) {
97        match self.previous.take() {
98            Some(original) => set_retry_policy(&original),
99            None => remove_retry_policy(&self.name),
100        }
101    }
102}
103
104/// Temporarily sets a named retry policy. When the returned guard is dropped,
105/// the previous policy with the same name is restored (or removed if it didn't exist).
106#[must_use]
107pub fn use_retry_policy(policy: NamedRetryPolicy) -> RetryPolicyGuard {
108    let previous = get_retry_policy_by_name(&policy.name);
109    let name = policy.name.clone();
110    set_retry_policy(&policy);
111    RetryPolicyGuard { previous, name }
112}
113
114/// Temporarily sets a high-level named retry policy after validating and
115/// flattening it into the raw WIT representation.
116pub fn use_named_policy(policy: &NamedPolicy) -> Result<RetryPolicyGuard, RetryBuilderError> {
117    let raw = policy.try_to_raw()?;
118    Ok(use_retry_policy(raw))
119}
120
121/// Executes the given function with a named retry policy temporarily set.
122pub fn with_retry_policy<R>(policy: NamedRetryPolicy, f: impl FnOnce() -> R) -> R {
123    let _guard = use_retry_policy(policy);
124    f()
125}
126
127/// Executes the given async function with a named retry policy temporarily set.
128pub async fn with_retry_policy_async<R, F: std::future::Future<Output = R>>(
129    policy: NamedRetryPolicy,
130    f: impl FnOnce() -> F,
131) -> R {
132    let _guard = use_retry_policy(policy);
133    f().await
134}
135
136/// Executes the given function with a high-level named retry policy temporarily set.
137pub fn with_named_policy<R>(
138    policy: &NamedPolicy,
139    f: impl FnOnce() -> R,
140) -> Result<R, RetryBuilderError> {
141    let _guard = use_named_policy(policy)?;
142    Ok(f())
143}
144
145/// Executes the given async function with a high-level named retry policy temporarily set.
146///
147/// This mutates the agent's active named retry policies for the lifetime of the future.
148/// If the future yields, other interleaved work in the same agent may observe the
149/// temporary policy until the guard is dropped.
150pub async fn with_named_policy_async<R, F: std::future::Future<Output = R>>(
151    policy: &NamedPolicy,
152    f: impl FnOnce() -> F,
153) -> Result<R, RetryBuilderError> {
154    let _guard = use_named_policy(policy)?;
155    Ok(f().await)
156}
157
158pub mod builder {
159    use super::retry_api;
160    use std::error::Error;
161    use std::fmt;
162    use std::time::Duration;
163
164    /// Standard retry-property keys populated by the platform retry context.
165    pub struct Props;
166
167    impl Props {
168        pub const VERB: &str = "verb";
169        pub const NOUN_URI: &str = "noun-uri";
170        pub const URI_SCHEME: &str = "uri-scheme";
171        pub const URI_HOST: &str = "uri-host";
172        pub const URI_PORT: &str = "uri-port";
173        pub const URI_PATH: &str = "uri-path";
174        pub const STATUS_CODE: &str = "status-code";
175        pub const ERROR_TYPE: &str = "error-type";
176        pub const FUNCTION: &str = "function";
177        pub const TARGET_COMPONENT_ID: &str = "target-component-id";
178        pub const TARGET_AGENT_TYPE: &str = "target-agent-type";
179        pub const DB_TYPE: &str = "db-type";
180        pub const TRAP_TYPE: &str = "trap-type";
181    }
182
183    /// High-level predicate value accepted by the retry builder layer.
184    #[derive(Clone, Debug, PartialEq, Eq)]
185    pub enum Value {
186        Text(String),
187        Integer(i128),
188        Boolean(bool),
189    }
190
191    impl From<String> for Value {
192        fn from(value: String) -> Self {
193            Self::Text(value)
194        }
195    }
196
197    impl From<&str> for Value {
198        fn from(value: &str) -> Self {
199            Self::Text(value.to_string())
200        }
201    }
202
203    impl From<bool> for Value {
204        fn from(value: bool) -> Self {
205            Self::Boolean(value)
206        }
207    }
208
209    impl From<i8> for Value {
210        fn from(value: i8) -> Self {
211            Self::Integer(value.into())
212        }
213    }
214
215    impl From<i16> for Value {
216        fn from(value: i16) -> Self {
217            Self::Integer(value.into())
218        }
219    }
220
221    impl From<i32> for Value {
222        fn from(value: i32) -> Self {
223            Self::Integer(value.into())
224        }
225    }
226
227    impl From<i64> for Value {
228        fn from(value: i64) -> Self {
229            Self::Integer(value.into())
230        }
231    }
232
233    impl From<isize> for Value {
234        fn from(value: isize) -> Self {
235            Self::Integer(value as i128)
236        }
237    }
238
239    impl From<u8> for Value {
240        fn from(value: u8) -> Self {
241            Self::Integer(value.into())
242        }
243    }
244
245    impl From<u16> for Value {
246        fn from(value: u16) -> Self {
247            Self::Integer(value.into())
248        }
249    }
250
251    impl From<u32> for Value {
252        fn from(value: u32) -> Self {
253            Self::Integer(value.into())
254        }
255    }
256
257    impl From<u64> for Value {
258        fn from(value: u64) -> Self {
259            Self::Integer(value.into())
260        }
261    }
262
263    impl From<usize> for Value {
264        fn from(value: usize) -> Self {
265            Self::Integer(value as i128)
266        }
267    }
268
269    /// High-level retry predicate that keeps tree structure until conversion.
270    #[derive(Clone, Debug, PartialEq)]
271    pub enum Predicate {
272        Eq {
273            property: String,
274            value: Value,
275        },
276        Neq {
277            property: String,
278            value: Value,
279        },
280        Gt {
281            property: String,
282            value: Value,
283        },
284        Gte {
285            property: String,
286            value: Value,
287        },
288        Lt {
289            property: String,
290            value: Value,
291        },
292        Lte {
293            property: String,
294            value: Value,
295        },
296        Exists(String),
297        OneOf {
298            property: String,
299            values: Vec<Value>,
300        },
301        MatchesGlob {
302            property: String,
303            pattern: String,
304        },
305        StartsWith {
306            property: String,
307            prefix: String,
308        },
309        Contains {
310            property: String,
311            substring: String,
312        },
313        And(Box<Predicate>, Box<Predicate>),
314        Or(Box<Predicate>, Box<Predicate>),
315        Not(Box<Predicate>),
316        Always,
317        Never,
318    }
319
320    impl Predicate {
321        pub fn always() -> Self {
322            Self::Always
323        }
324
325        pub fn never() -> Self {
326            Self::Never
327        }
328
329        pub fn eq(property: impl Into<String>, value: impl Into<Value>) -> Self {
330            Self::Eq {
331                property: property.into(),
332                value: value.into(),
333            }
334        }
335
336        pub fn neq(property: impl Into<String>, value: impl Into<Value>) -> Self {
337            Self::Neq {
338                property: property.into(),
339                value: value.into(),
340            }
341        }
342
343        pub fn gt(property: impl Into<String>, value: impl Into<Value>) -> Self {
344            Self::Gt {
345                property: property.into(),
346                value: value.into(),
347            }
348        }
349
350        pub fn gte(property: impl Into<String>, value: impl Into<Value>) -> Self {
351            Self::Gte {
352                property: property.into(),
353                value: value.into(),
354            }
355        }
356
357        pub fn lt(property: impl Into<String>, value: impl Into<Value>) -> Self {
358            Self::Lt {
359                property: property.into(),
360                value: value.into(),
361            }
362        }
363
364        pub fn lte(property: impl Into<String>, value: impl Into<Value>) -> Self {
365            Self::Lte {
366                property: property.into(),
367                value: value.into(),
368            }
369        }
370
371        pub fn exists(property: impl Into<String>) -> Self {
372            Self::Exists(property.into())
373        }
374
375        pub fn one_of<I, V>(property: impl Into<String>, values: I) -> Self
376        where
377            I: IntoIterator<Item = V>,
378            V: Into<Value>,
379        {
380            Self::OneOf {
381                property: property.into(),
382                values: values.into_iter().map(Into::into).collect(),
383            }
384        }
385
386        pub fn matches_glob(property: impl Into<String>, pattern: impl Into<String>) -> Self {
387            Self::MatchesGlob {
388                property: property.into(),
389                pattern: pattern.into(),
390            }
391        }
392
393        pub fn starts_with(property: impl Into<String>, prefix: impl Into<String>) -> Self {
394            Self::StartsWith {
395                property: property.into(),
396                prefix: prefix.into(),
397            }
398        }
399
400        pub fn contains(property: impl Into<String>, substring: impl Into<String>) -> Self {
401            Self::Contains {
402                property: property.into(),
403                substring: substring.into(),
404            }
405        }
406
407        pub fn and(left: Predicate, right: Predicate) -> Self {
408            Self::And(Box::new(left), Box::new(right))
409        }
410
411        pub fn or(left: Predicate, right: Predicate) -> Self {
412            Self::Or(Box::new(left), Box::new(right))
413        }
414
415        #[allow(clippy::should_implement_trait)]
416        pub fn not(inner: Predicate) -> Self {
417            Self::Not(Box::new(inner))
418        }
419
420        pub fn try_to_raw(&self) -> Result<retry_api::RetryPredicate, RetryBuilderError> {
421            self.clone().try_into()
422        }
423    }
424
425    /// High-level retry policy that keeps tree structure until conversion.
426    #[derive(Clone, Debug, PartialEq)]
427    pub enum Policy {
428        Periodic(Duration),
429        Exponential {
430            base_delay: Duration,
431            factor: f64,
432        },
433        Fibonacci {
434            first: Duration,
435            second: Duration,
436        },
437        Immediate,
438        Never,
439        CountBox {
440            max_retries: u32,
441            inner: Box<Policy>,
442        },
443        TimeBox {
444            limit: Duration,
445            inner: Box<Policy>,
446        },
447        Clamp {
448            min_delay: Duration,
449            max_delay: Duration,
450            inner: Box<Policy>,
451        },
452        AddDelay {
453            delay: Duration,
454            inner: Box<Policy>,
455        },
456        Jitter {
457            factor: f64,
458            inner: Box<Policy>,
459        },
460        OnlyWhen {
461            predicate: Predicate,
462            inner: Box<Policy>,
463        },
464        AndThen(Box<Policy>, Box<Policy>),
465        Union(Box<Policy>, Box<Policy>),
466        Intersect(Box<Policy>, Box<Policy>),
467    }
468
469    impl Policy {
470        pub fn immediate() -> Self {
471            Self::Immediate
472        }
473
474        pub fn never() -> Self {
475            Self::Never
476        }
477
478        pub fn periodic(delay: Duration) -> Self {
479            Self::Periodic(delay)
480        }
481
482        pub fn exponential(base_delay: Duration, factor: f64) -> Self {
483            Self::Exponential { base_delay, factor }
484        }
485
486        pub fn fibonacci(first: Duration, second: Duration) -> Self {
487            Self::Fibonacci { first, second }
488        }
489
490        pub fn max_retries(self, max_retries: u32) -> Self {
491            Self::CountBox {
492                max_retries,
493                inner: Box::new(self),
494            }
495        }
496
497        pub fn within(self, limit: Duration) -> Self {
498            Self::TimeBox {
499                limit,
500                inner: Box::new(self),
501            }
502        }
503
504        pub fn clamp(self, min_delay: Duration, max_delay: Duration) -> Self {
505            Self::Clamp {
506                min_delay,
507                max_delay,
508                inner: Box::new(self),
509            }
510        }
511
512        pub fn add_delay(self, delay: Duration) -> Self {
513            Self::AddDelay {
514                delay,
515                inner: Box::new(self),
516            }
517        }
518
519        pub fn with_jitter(self, factor: f64) -> Self {
520            Self::Jitter {
521                factor,
522                inner: Box::new(self),
523            }
524        }
525
526        pub fn only_when(self, predicate: Predicate) -> Self {
527            Self::OnlyWhen {
528                predicate,
529                inner: Box::new(self),
530            }
531        }
532
533        pub fn and_then(self, other: Policy) -> Self {
534            Self::AndThen(Box::new(self), Box::new(other))
535        }
536
537        pub fn union(self, other: Policy) -> Self {
538            Self::Union(Box::new(self), Box::new(other))
539        }
540
541        pub fn intersect(self, other: Policy) -> Self {
542            Self::Intersect(Box::new(self), Box::new(other))
543        }
544
545        pub fn try_to_raw(&self) -> Result<retry_api::RetryPolicy, RetryBuilderError> {
546            self.clone().try_into()
547        }
548    }
549
550    /// High-level named retry rule with an outer applicability predicate.
551    #[derive(Clone, Debug, PartialEq)]
552    pub struct NamedPolicy {
553        name: String,
554        priority: u32,
555        predicate: Predicate,
556        policy: Policy,
557    }
558
559    impl NamedPolicy {
560        pub fn named(name: impl Into<String>, policy: Policy) -> Self {
561            Self {
562                name: name.into(),
563                priority: 0,
564                predicate: Predicate::always(),
565                policy,
566            }
567        }
568
569        pub fn priority(mut self, priority: u32) -> Self {
570            self.priority = priority;
571            self
572        }
573
574        pub fn applies_when(mut self, predicate: Predicate) -> Self {
575            self.predicate = predicate;
576            self
577        }
578
579        pub fn try_to_raw(&self) -> Result<retry_api::NamedRetryPolicy, RetryBuilderError> {
580            self.clone().try_into()
581        }
582    }
583
584    /// Validation errors produced when flattening high-level retry builders into raw WIT types.
585    #[derive(Clone, Debug, PartialEq)]
586    pub enum RetryBuilderError {
587        IntegerOutOfRange {
588            value: i128,
589        },
590        DurationOutOfRange {
591            field: &'static str,
592            duration: Duration,
593        },
594        InvalidExponentialFactor {
595            factor: f64,
596        },
597        InvalidJitterFactor {
598            factor: f64,
599        },
600        InvalidClampRange {
601            min_delay: Duration,
602            max_delay: Duration,
603        },
604        PolicyTooComplex,
605    }
606
607    impl fmt::Display for RetryBuilderError {
608        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
609            match self {
610                Self::IntegerOutOfRange { value } => {
611                    write!(
612                        f,
613                        "predicate integer value {value} does not fit into WIT s64"
614                    )
615                }
616                Self::DurationOutOfRange { field, duration } => write!(
617                    f,
618                    "{field} duration {duration:?} does not fit into WIT duration nanoseconds"
619                ),
620                Self::InvalidExponentialFactor { factor } => write!(
621                    f,
622                    "exponential factor must be finite and greater than 0, got {factor}"
623                ),
624                Self::InvalidJitterFactor { factor } => write!(
625                    f,
626                    "jitter factor must be finite and greater than or equal to 0, got {factor}"
627                ),
628                Self::InvalidClampRange {
629                    min_delay,
630                    max_delay,
631                } => write!(
632                    f,
633                    "clamp min delay {min_delay:?} must be less than or equal to max delay {max_delay:?}"
634                ),
635                Self::PolicyTooComplex => {
636                    write!(f, "retry policy is too deeply nested or expansive")
637                }
638            }
639        }
640    }
641
642    impl Error for RetryBuilderError {}
643
644    impl TryFrom<Value> for retry_api::PredicateValue {
645        type Error = RetryBuilderError;
646
647        fn try_from(value: Value) -> Result<Self, Self::Error> {
648            match value {
649                Value::Text(value) => Ok(Self::Text(value)),
650                Value::Boolean(value) => Ok(Self::Boolean(value)),
651                Value::Integer(value) => i64::try_from(value)
652                    .map(Self::Integer)
653                    .map_err(|_| RetryBuilderError::IntegerOutOfRange { value }),
654            }
655        }
656    }
657
658    impl TryFrom<Predicate> for retry_api::RetryPredicate {
659        type Error = RetryBuilderError;
660
661        fn try_from(predicate: Predicate) -> Result<Self, Self::Error> {
662            let mut nodes = Vec::new();
663            push_predicate_node(predicate, &mut nodes)?;
664            Ok(Self { nodes })
665        }
666    }
667
668    impl TryFrom<Policy> for retry_api::RetryPolicy {
669        type Error = RetryBuilderError;
670
671        fn try_from(policy: Policy) -> Result<Self, Self::Error> {
672            let mut nodes = Vec::new();
673            push_policy_node(policy, &mut nodes)?;
674            Ok(Self { nodes })
675        }
676    }
677
678    impl TryFrom<NamedPolicy> for retry_api::NamedRetryPolicy {
679        type Error = RetryBuilderError;
680
681        fn try_from(policy: NamedPolicy) -> Result<Self, Self::Error> {
682            Ok(Self {
683                name: policy.name,
684                priority: policy.priority,
685                predicate: policy.predicate.try_to_raw()?,
686                policy: policy.policy.try_to_raw()?,
687            })
688        }
689    }
690
691    fn push_predicate_node(
692        predicate: Predicate,
693        nodes: &mut Vec<retry_api::PredicateNode>,
694    ) -> Result<i32, RetryBuilderError> {
695        let index = nodes.len() as i32;
696        nodes.push(retry_api::PredicateNode::PredFalse);
697
698        let node = match predicate {
699            Predicate::Eq { property, value } => {
700                retry_api::PredicateNode::PropEq(retry_api::PropertyComparison {
701                    property_name: property,
702                    value: value.try_into()?,
703                })
704            }
705            Predicate::Neq { property, value } => {
706                retry_api::PredicateNode::PropNeq(retry_api::PropertyComparison {
707                    property_name: property,
708                    value: value.try_into()?,
709                })
710            }
711            Predicate::Gt { property, value } => {
712                retry_api::PredicateNode::PropGt(retry_api::PropertyComparison {
713                    property_name: property,
714                    value: value.try_into()?,
715                })
716            }
717            Predicate::Gte { property, value } => {
718                retry_api::PredicateNode::PropGte(retry_api::PropertyComparison {
719                    property_name: property,
720                    value: value.try_into()?,
721                })
722            }
723            Predicate::Lt { property, value } => {
724                retry_api::PredicateNode::PropLt(retry_api::PropertyComparison {
725                    property_name: property,
726                    value: value.try_into()?,
727                })
728            }
729            Predicate::Lte { property, value } => {
730                retry_api::PredicateNode::PropLte(retry_api::PropertyComparison {
731                    property_name: property,
732                    value: value.try_into()?,
733                })
734            }
735            Predicate::Exists(property) => retry_api::PredicateNode::PropExists(property),
736            Predicate::OneOf { property, values } => {
737                retry_api::PredicateNode::PropIn(retry_api::PropertySetCheck {
738                    property_name: property,
739                    values: values
740                        .into_iter()
741                        .map(TryInto::try_into)
742                        .collect::<Result<Vec<_>, _>>()?,
743                })
744            }
745            Predicate::MatchesGlob { property, pattern } => {
746                retry_api::PredicateNode::PropMatches(retry_api::PropertyPattern {
747                    property_name: property,
748                    pattern,
749                })
750            }
751            Predicate::StartsWith { property, prefix } => {
752                retry_api::PredicateNode::PropStartsWith(retry_api::PropertyPattern {
753                    property_name: property,
754                    pattern: prefix,
755                })
756            }
757            Predicate::Contains {
758                property,
759                substring,
760            } => retry_api::PredicateNode::PropContains(retry_api::PropertyPattern {
761                property_name: property,
762                pattern: substring,
763            }),
764            Predicate::And(left, right) => {
765                let left_index = push_predicate_node(*left, nodes)?;
766                let right_index = push_predicate_node(*right, nodes)?;
767                retry_api::PredicateNode::PredAnd((left_index, right_index))
768            }
769            Predicate::Or(left, right) => {
770                let left_index = push_predicate_node(*left, nodes)?;
771                let right_index = push_predicate_node(*right, nodes)?;
772                retry_api::PredicateNode::PredOr((left_index, right_index))
773            }
774            Predicate::Not(inner) => {
775                let inner_index = push_predicate_node(*inner, nodes)?;
776                retry_api::PredicateNode::PredNot(inner_index)
777            }
778            Predicate::Always => retry_api::PredicateNode::PredTrue,
779            Predicate::Never => retry_api::PredicateNode::PredFalse,
780        };
781
782        nodes[index as usize] = node;
783        Ok(index)
784    }
785
786    fn push_policy_node(
787        policy: Policy,
788        nodes: &mut Vec<retry_api::PolicyNode>,
789    ) -> Result<i32, RetryBuilderError> {
790        let index = nodes.len() as i32;
791        nodes.push(retry_api::PolicyNode::Never);
792
793        let node = match policy {
794            Policy::Periodic(delay) => {
795                retry_api::PolicyNode::Periodic(duration_to_wit(delay, "periodic delay")?)
796            }
797            Policy::Exponential { base_delay, factor } => {
798                if !factor.is_finite() || factor <= 0.0 {
799                    return Err(RetryBuilderError::InvalidExponentialFactor { factor });
800                }
801
802                retry_api::PolicyNode::Exponential(retry_api::ExponentialConfig {
803                    base_delay: duration_to_wit(base_delay, "exponential base delay")?,
804                    factor,
805                })
806            }
807            Policy::Fibonacci { first, second } => {
808                retry_api::PolicyNode::Fibonacci(retry_api::FibonacciConfig {
809                    first: duration_to_wit(first, "fibonacci first delay")?,
810                    second: duration_to_wit(second, "fibonacci second delay")?,
811                })
812            }
813            Policy::Immediate => retry_api::PolicyNode::Immediate,
814            Policy::Never => retry_api::PolicyNode::Never,
815            Policy::CountBox { max_retries, inner } => {
816                let inner_index = push_policy_node(*inner, nodes)?;
817                retry_api::PolicyNode::CountBox(retry_api::CountBoxConfig {
818                    max_retries,
819                    inner: inner_index,
820                })
821            }
822            Policy::TimeBox { limit, inner } => {
823                let inner_index = push_policy_node(*inner, nodes)?;
824                retry_api::PolicyNode::TimeBox(retry_api::TimeBoxConfig {
825                    limit: duration_to_wit(limit, "time-box limit")?,
826                    inner: inner_index,
827                })
828            }
829            Policy::Clamp {
830                min_delay,
831                max_delay,
832                inner,
833            } => {
834                if min_delay > max_delay {
835                    return Err(RetryBuilderError::InvalidClampRange {
836                        min_delay,
837                        max_delay,
838                    });
839                }
840
841                let inner_index = push_policy_node(*inner, nodes)?;
842                retry_api::PolicyNode::ClampDelay(retry_api::ClampConfig {
843                    min_delay: duration_to_wit(min_delay, "clamp min delay")?,
844                    max_delay: duration_to_wit(max_delay, "clamp max delay")?,
845                    inner: inner_index,
846                })
847            }
848            Policy::AddDelay { delay, inner } => {
849                let inner_index = push_policy_node(*inner, nodes)?;
850                retry_api::PolicyNode::AddDelay(retry_api::AddDelayConfig {
851                    delay: duration_to_wit(delay, "added delay")?,
852                    inner: inner_index,
853                })
854            }
855            Policy::Jitter { factor, inner } => {
856                if !factor.is_finite() || factor < 0.0 {
857                    return Err(RetryBuilderError::InvalidJitterFactor { factor });
858                }
859
860                let inner_index = push_policy_node(*inner, nodes)?;
861                retry_api::PolicyNode::Jitter(retry_api::JitterConfig {
862                    factor,
863                    inner: inner_index,
864                })
865            }
866            Policy::OnlyWhen { predicate, inner } => {
867                let inner_index = push_policy_node(*inner, nodes)?;
868                retry_api::PolicyNode::FilteredOn(retry_api::FilteredConfig {
869                    predicate: predicate.try_to_raw()?,
870                    inner: inner_index,
871                })
872            }
873            Policy::AndThen(left, right) => {
874                let left_index = push_policy_node(*left, nodes)?;
875                let right_index = push_policy_node(*right, nodes)?;
876                retry_api::PolicyNode::AndThen((left_index, right_index))
877            }
878            Policy::Union(left, right) => {
879                let left_index = push_policy_node(*left, nodes)?;
880                let right_index = push_policy_node(*right, nodes)?;
881                retry_api::PolicyNode::PolicyUnion((left_index, right_index))
882            }
883            Policy::Intersect(left, right) => {
884                let left_index = push_policy_node(*left, nodes)?;
885                let right_index = push_policy_node(*right, nodes)?;
886                retry_api::PolicyNode::PolicyIntersect((left_index, right_index))
887            }
888        };
889
890        nodes[index as usize] = node;
891        Ok(index)
892    }
893
894    fn duration_to_wit(duration: Duration, field: &'static str) -> Result<u64, RetryBuilderError> {
895        let nanos = duration.as_nanos();
896        if nanos > u64::MAX as u128 {
897            return Err(RetryBuilderError::DurationOutOfRange { field, duration });
898        }
899
900        Ok(nanos as u64)
901    }
902}
903
904#[cfg(test)]
905mod tests {
906    use std::time::Duration;
907
908    use test_r::test;
909
910    use super::builder::{NamedPolicy, Policy, Predicate, Props, RetryBuilderError};
911    use super::retry_api;
912
913    #[test]
914    fn named_policy_builder_flattens_to_raw_wit_types() {
915        let named = NamedPolicy::named(
916            "transient-http",
917            Policy::exponential(Duration::from_millis(200), 2.0)
918                .clamp(Duration::from_millis(100), Duration::from_secs(5))
919                .only_when(Predicate::one_of(Props::STATUS_CODE, [502_u16, 503, 504]))
920                .max_retries(3),
921        )
922        .priority(10)
923        .applies_when(Predicate::and(
924            Predicate::eq(Props::VERB, "GET"),
925            Predicate::matches_glob(Props::URI_HOST, "*.example.com"),
926        ));
927
928        let raw = named
929            .try_to_raw()
930            .expect("builder should convert to raw WIT types");
931
932        assert_eq!(raw.name, "transient-http");
933        assert_eq!(raw.priority, 10);
934        assert_eq!(raw.predicate.nodes.len(), 3);
935        match &raw.predicate.nodes[0] {
936            retry_api::PredicateNode::PredAnd((left, right)) => {
937                assert_eq!((*left, *right), (1, 2));
938            }
939            node => panic!("unexpected root predicate node: {node:?}"),
940        }
941        match &raw.predicate.nodes[1] {
942            retry_api::PredicateNode::PropEq(retry_api::PropertyComparison {
943                property_name,
944                value: retry_api::PredicateValue::Text(value),
945            }) => {
946                assert_eq!(property_name, Props::VERB);
947                assert_eq!(value, "GET");
948            }
949            node => panic!("unexpected left predicate node: {node:?}"),
950        }
951        match &raw.predicate.nodes[2] {
952            retry_api::PredicateNode::PropMatches(retry_api::PropertyPattern {
953                property_name,
954                pattern,
955            }) => {
956                assert_eq!(property_name, Props::URI_HOST);
957                assert_eq!(pattern, "*.example.com");
958            }
959            node => panic!("unexpected right predicate node: {node:?}"),
960        }
961
962        assert_eq!(raw.policy.nodes.len(), 4);
963        match &raw.policy.nodes[0] {
964            retry_api::PolicyNode::CountBox(retry_api::CountBoxConfig { max_retries, inner }) => {
965                assert_eq!((*max_retries, *inner), (3, 1));
966            }
967            node => panic!("unexpected root policy node: {node:?}"),
968        }
969        match &raw.policy.nodes[1] {
970            retry_api::PolicyNode::FilteredOn(retry_api::FilteredConfig { predicate, inner }) => {
971                assert_eq!(*inner, 2);
972                assert_eq!(predicate.nodes.len(), 1);
973                match &predicate.nodes[0] {
974                    retry_api::PredicateNode::PropIn(retry_api::PropertySetCheck {
975                        property_name,
976                        values,
977                    }) => {
978                        assert_eq!(property_name, Props::STATUS_CODE);
979                        assert_eq!(values.len(), 3);
980                        match &values[..] {
981                            [
982                                retry_api::PredicateValue::Integer(502),
983                                retry_api::PredicateValue::Integer(503),
984                                retry_api::PredicateValue::Integer(504),
985                            ] => {}
986                            other => panic!("unexpected predicate values: {other:?}"),
987                        }
988                    }
989                    node => panic!("unexpected filtered predicate node: {node:?}"),
990                }
991            }
992            node => panic!("unexpected filtered policy node: {node:?}"),
993        }
994        match &raw.policy.nodes[2] {
995            retry_api::PolicyNode::ClampDelay(retry_api::ClampConfig {
996                min_delay,
997                max_delay,
998                inner,
999            }) => {
1000                assert_eq!(
1001                    (*min_delay, *max_delay, *inner),
1002                    (100_000_000, 5_000_000_000, 3)
1003                );
1004            }
1005            node => panic!("unexpected clamp policy node: {node:?}"),
1006        }
1007        match &raw.policy.nodes[3] {
1008            retry_api::PolicyNode::Exponential(retry_api::ExponentialConfig {
1009                base_delay,
1010                factor,
1011            }) => {
1012                assert_eq!(*base_delay, 200_000_000);
1013                assert_eq!(*factor, 2.0);
1014            }
1015            node => panic!("unexpected exponential policy node: {node:?}"),
1016        }
1017    }
1018
1019    #[test]
1020    fn predicate_builder_rejects_integer_values_that_do_not_fit_wit() {
1021        let predicate = Predicate::eq(Props::STATUS_CODE, u64::try_from(i64::MAX).unwrap() + 1);
1022
1023        assert_eq!(
1024            predicate.try_to_raw().unwrap_err(),
1025            RetryBuilderError::IntegerOutOfRange {
1026                value: i128::from(i64::MAX) + 1,
1027            }
1028        );
1029    }
1030
1031    #[test]
1032    fn policy_builder_rejects_invalid_exponential_factors() {
1033        for factor in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1034            assert!(matches!(
1035                Policy::exponential(Duration::from_millis(100), factor).try_to_raw(),
1036                Err(RetryBuilderError::InvalidExponentialFactor { .. })
1037            ));
1038        }
1039    }
1040
1041    #[test]
1042    fn policy_builder_rejects_invalid_jitter_factors() {
1043        for factor in [-0.1, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1044            assert!(matches!(
1045                Policy::periodic(Duration::from_millis(100))
1046                    .with_jitter(factor)
1047                    .try_to_raw(),
1048                Err(RetryBuilderError::InvalidJitterFactor { .. })
1049            ));
1050        }
1051
1052        assert!(
1053            Policy::periodic(Duration::from_millis(100))
1054                .with_jitter(0.0)
1055                .try_to_raw()
1056                .is_ok()
1057        );
1058    }
1059
1060    #[test]
1061    fn policy_builder_rejects_invalid_clamp_ranges() {
1062        let policy = Policy::periodic(Duration::from_millis(100))
1063            .clamp(Duration::from_secs(2), Duration::from_secs(1));
1064
1065        assert_eq!(
1066            policy.try_to_raw().unwrap_err(),
1067            RetryBuilderError::InvalidClampRange {
1068                min_delay: Duration::from_secs(2),
1069                max_delay: Duration::from_secs(1),
1070            }
1071        );
1072    }
1073
1074    #[test]
1075    fn policy_builder_rejects_durations_that_do_not_fit_wit() {
1076        let policy = Policy::periodic(Duration::MAX);
1077
1078        assert_eq!(
1079            policy.try_to_raw().unwrap_err(),
1080            RetryBuilderError::DurationOutOfRange {
1081                field: "periodic delay",
1082                duration: Duration::MAX,
1083            }
1084        );
1085    }
1086}