reqwest_cross/
data_state_retry.rs

1use tracing::warn;
2
3use crate::{data_state::CanMakeProgress, Awaiting, DataState, ErrorBounds};
4use std::fmt::Debug;
5use std::ops::Range;
6
7/// Automatically retries with a delay on failure until attempts are exhausted
8#[derive(Debug)]
9pub struct DataStateRetry<T, E: ErrorBounds = anyhow::Error> {
10    /// Number of attempts that the retries get reset to
11    pub max_attempts: u8,
12
13    /// The range of milliseconds to select a random value from to set the delay
14    /// to retry
15    pub retry_delay_millis: Range<u16>,
16
17    attempts_left: u8,
18    inner: DataState<T, E>, // Not public to ensure resets happen as they should
19    next_allowed_attempt: u128,
20}
21
22impl<T, E: ErrorBounds> DataStateRetry<T, E> {
23    /// Creates a new instance of [DataStateRetry]
24    pub fn new(max_attempts: u8, retry_delay_millis: Range<u16>) -> Self {
25        Self {
26            max_attempts,
27            retry_delay_millis,
28            ..Default::default()
29        }
30    }
31
32    /// The number times left to retry before stopping trying
33    pub fn attempts_left(&self) -> u8 {
34        self.attempts_left
35    }
36
37    /// The number of millis after the epoch that an attempt is allowed
38    pub fn next_allowed_attempt(&self) -> u128 {
39        self.next_allowed_attempt
40    }
41
42    /// Provides access to the inner [`DataState`]
43    pub fn inner(&self) -> &DataState<T, E> {
44        &self.inner
45    }
46
47    /// Consumes self and returns the unwrapped inner
48    pub fn into_inner(self) -> DataState<T, E> {
49        self.inner
50    }
51
52    /// Provides access to the stored data if available (returns Some if
53    /// self.inner is `Data::Present(_)`)
54    pub fn present(&self) -> Option<&T> {
55        if let DataState::Present(data) = self.inner.as_ref() {
56            Some(data)
57        } else {
58            None
59        }
60    }
61
62    /// Provides mutable access to the stored data if available (returns Some if
63    /// self.inner is `Data::Present(_)`)
64    pub fn present_mut(&mut self) -> Option<&mut T> {
65        if let DataState::Present(data) = self.inner.as_mut() {
66            Some(data)
67        } else {
68            None
69        }
70    }
71
72    #[cfg(feature = "egui")]
73    /// Attempts to load the data and displays appropriate UI if applicable.
74    ///
75    /// Note see [`DataState::egui_get`] for more info.
76    #[must_use]
77    pub fn egui_start_or_poll<F, R>(
78        &mut self,
79        ui: &mut egui::Ui,
80        retry_msg: Option<&str>,
81        fetch_fn: F,
82    ) -> CanMakeProgress
83    where
84        F: FnOnce() -> R,
85        R: Into<Awaiting<T, E>>,
86    {
87        match self.inner.as_ref() {
88            DataState::None | DataState::AwaitingResponse(_) => {
89                self.ui_spinner_with_attempt_count(ui);
90                self.start_or_poll(fetch_fn)
91            }
92            DataState::Present(_data) => {
93                // Does nothing as data is already present
94                CanMakeProgress::UnableToMakeProgress
95            }
96            DataState::Failed(e) => {
97                if self.attempts_left == 0 {
98                    ui.colored_label(
99                        ui.visuals().error_fg_color,
100                        format!("No attempts left from {}. {e}", self.max_attempts),
101                    );
102                    if ui.button(retry_msg.unwrap_or("Restart Requests")).clicked() {
103                        self.reset_attempts();
104                        self.inner = DataState::default();
105                    }
106                } else {
107                    let wait_left = wait_before_next_attempt(self.next_allowed_attempt);
108                    ui.colored_label(
109                        ui.visuals().error_fg_color,
110                        format!(
111                            "{} attempt(s) left. {} seconds before retry. {e}",
112                            self.attempts_left,
113                            wait_left / 1000
114                        ),
115                    );
116                    let can_make_progress = self.start_or_poll(fetch_fn);
117                    debug_assert!(
118                        can_make_progress.is_able_to_make_progress(),
119                        "This should be able to make progress"
120                    );
121                    if ui.button("Stop Trying").clicked() {
122                        self.attempts_left = 0;
123                    }
124                }
125                CanMakeProgress::AbleToMakeProgress
126            }
127        }
128    }
129
130    /// Attempts to load the data and returns if it is able to make progress.
131    #[must_use]
132    pub fn start_or_poll<F, R>(&mut self, fetch_fn: F) -> CanMakeProgress
133    where
134        F: FnOnce() -> R,
135        R: Into<Awaiting<T, E>>,
136    {
137        match self.inner.as_mut() {
138            DataState::None => {
139                // Going to make an attempt, set when the next attempt is allowed
140                use rand::Rng as _;
141                let wait_time_in_millis = rand::rng().random_range(self.retry_delay_millis.clone());
142                self.next_allowed_attempt = millis_since_epoch() + wait_time_in_millis as u128;
143
144                self.inner.start_request(fetch_fn)
145            }
146            DataState::AwaitingResponse(_) => {
147                if self.inner.poll().is_present() {
148                    // Data was successfully received because before it was Awaiting
149                    self.reset_attempts();
150                }
151                CanMakeProgress::AbleToMakeProgress
152            }
153            DataState::Present(_) => CanMakeProgress::UnableToMakeProgress,
154            DataState::Failed(err_msg) => {
155                if self.attempts_left == 0 {
156                    CanMakeProgress::UnableToMakeProgress
157                } else {
158                    let wait_left = wait_before_next_attempt(self.next_allowed_attempt);
159                    if wait_left == 0 {
160                        warn!(?err_msg, ?self.attempts_left, "retrying request");
161                        self.attempts_left -= 1;
162                        self.inner = DataState::None;
163                    }
164                    CanMakeProgress::AbleToMakeProgress
165                }
166            }
167        }
168    }
169
170    /// Resets the attempts taken
171    pub fn reset_attempts(&mut self) {
172        self.attempts_left = self.max_attempts;
173        self.next_allowed_attempt = millis_since_epoch();
174    }
175
176    /// Clear stored data
177    pub fn clear(&mut self) {
178        self.inner = DataState::default();
179        self.reset_attempts();
180    }
181
182    /// Returns `true` if the internal data state is [`DataState::Present`].
183    #[must_use]
184    pub fn is_present(&self) -> bool {
185        self.inner.is_present()
186    }
187
188    /// Returns `true` if the internal data state is [`DataState::None`].
189    #[must_use]
190    pub fn is_none(&self) -> bool {
191        self.inner.is_none()
192    }
193
194    #[cfg(feature = "egui")]
195    fn ui_spinner_with_attempt_count(&self, ui: &mut egui::Ui) {
196        ui.horizontal(|ui| {
197            ui.spinner();
198            ui.separator();
199            ui.label(format!("{} attempts left", self.attempts_left))
200        });
201    }
202}
203
204impl<T, E: ErrorBounds> Default for DataStateRetry<T, E> {
205    fn default() -> Self {
206        Self {
207            inner: Default::default(),
208            max_attempts: 3,
209            retry_delay_millis: 1000..5000,
210            attempts_left: 3,
211            next_allowed_attempt: millis_since_epoch(),
212        }
213    }
214}
215
216impl<T, E: ErrorBounds> AsRef<DataStateRetry<T, E>> for DataStateRetry<T, E> {
217    fn as_ref(&self) -> &DataStateRetry<T, E> {
218        self
219    }
220}
221
222impl<T, E: ErrorBounds> AsMut<DataStateRetry<T, E>> for DataStateRetry<T, E> {
223    fn as_mut(&mut self) -> &mut DataStateRetry<T, E> {
224        self
225    }
226}
227
228/// The duration before the next attempt will be made
229fn wait_before_next_attempt(next_allowed_attempt: u128) -> u128 {
230    next_allowed_attempt.saturating_sub(millis_since_epoch())
231}
232
233fn millis_since_epoch() -> u128 {
234    web_time::SystemTime::UNIX_EPOCH
235        .elapsed()
236        .expect("expected date on system to be after the epoch")
237        .as_millis()
238}
239
240// TODO 4: Use mocking to add tests ensuring retires are executed