proxima_primitives/pipe/
retry_rules.rs1#[cfg(proxima_alloc)]
7use alloc::collections::BTreeSet;
8
9use crate::pipe::capabilities::Retryable;
10#[cfg(not(proxima_alloc))]
11use crate::pipe::sized::RETRY_STATUS_CAP;
12
13#[derive(Debug, Clone)]
17pub struct RetryRules {
18 pub retry_on_status: StatusSet,
21 pub retry_on_error: bool,
22 pub idempotent_only: bool,
23}
24
25#[cfg(proxima_alloc)]
30pub type StatusSet = BTreeSet<u16>;
31
32#[cfg(not(proxima_alloc))]
37#[derive(Debug, Clone, Default)]
38pub struct StatusSet {
39 inner: heapless::Vec<u16, RETRY_STATUS_CAP>,
40}
41
42#[cfg(not(proxima_alloc))]
43impl StatusSet {
44 #[must_use]
46 pub fn new() -> Self {
47 Self {
48 inner: heapless::Vec::new(),
49 }
50 }
51
52 pub fn insert(&mut self, status: u16) -> bool {
55 match self.inner.binary_search(&status) {
56 Ok(_) => false,
57 Err(position) => self.inner.insert(position, status).is_ok(),
58 }
59 }
60
61 #[must_use]
63 pub fn contains(&self, status: &u16) -> bool {
64 self.inner.binary_search(status).is_ok()
65 }
66}
67
68#[cfg(not(proxima_alloc))]
71impl Extend<u16> for StatusSet {
72 fn extend<I: IntoIterator<Item = u16>>(&mut self, statuses: I) {
73 for status in statuses {
74 self.insert(status);
75 }
76 }
77}
78
79#[cfg(not(proxima_alloc))]
80impl IntoIterator for StatusSet {
81 type Item = u16;
82 type IntoIter = <heapless::Vec<u16, RETRY_STATUS_CAP> as IntoIterator>::IntoIter;
83
84 fn into_iter(self) -> Self::IntoIter {
85 self.inner.into_iter()
86 }
87}
88
89impl Default for RetryRules {
90 fn default() -> Self {
91 let mut retry_on_status = StatusSet::default();
92 for status in [502u16, 503, 504] {
93 retry_on_status.insert(status);
94 }
95 Self {
96 retry_on_status,
97 retry_on_error: true,
98 idempotent_only: false,
99 }
100 }
101}
102
103impl RetryRules {
104 pub fn should_retry<Out: Retryable, Err>(&self, outcome: &Result<Out, Err>) -> bool {
105 match outcome {
106 Ok(out) => out
107 .retry_status()
108 .is_some_and(|status| self.retry_on_status.contains(&status)),
109 Err(_) => self.retry_on_error,
110 }
111 }
112}
113
114#[cfg(test)]
115#[allow(clippy::unwrap_used, clippy::expect_used)]
116mod tests {
117 use super::*;
118
119 struct Outcome(Option<u16>);
121
122 impl Retryable for Outcome {
123 fn retry_status(&self) -> Option<u16> {
124 self.0
125 }
126 fn is_success(&self) -> bool {
127 self.0.is_none()
128 }
129 }
130
131 #[test]
132 fn default_retries_the_gateway_statuses() {
133 let rules = RetryRules::default();
134 for status in [502u16, 503, 504] {
135 assert!(
136 rules.retry_on_status.contains(&status),
137 "{status} should retry"
138 );
139 }
140 assert!(
141 !rules.retry_on_status.contains(&200),
142 "200 should not retry"
143 );
144 }
145
146 #[test]
147 fn should_retry_matches_status_membership() {
148 let rules = RetryRules::default();
149 let ok: Result<Outcome, ()> = Ok(Outcome(Some(503)));
150 let not_listed: Result<Outcome, ()> = Ok(Outcome(Some(418)));
151 let statusless: Result<Outcome, ()> = Ok(Outcome(None));
152 assert!(rules.should_retry(&ok), "503 is in the default set");
153 assert!(!rules.should_retry(¬_listed), "418 is not");
154 assert!(
155 !rules.should_retry(&statusless),
156 "no status => no status retry"
157 );
158 }
159
160 #[test]
161 fn should_retry_on_error_follows_the_flag() {
162 let err: Result<Outcome, ()> = Err(());
163 assert!(
164 RetryRules::default().should_retry(&err),
165 "default retries on error"
166 );
167 let no_error_retry = RetryRules {
168 retry_on_error: false,
169 ..RetryRules::default()
170 };
171 assert!(
172 !no_error_retry.should_retry(&err),
173 "flag off => no error retry"
174 );
175 }
176
177 #[test]
178 fn insert_is_idempotent_and_sorted() {
179 let mut set = StatusSet::default();
180 set.insert(503);
181 set.insert(502);
182 set.insert(503);
183 assert!(set.contains(&502) && set.contains(&503));
184 assert!(!set.contains(&504));
185 }
186}