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