Skip to main content

main_loop_async/
data_state_retry.rs

1use tracing::warn;
2
3use crate::{Awaiting, DataState, ErrorBounds, data_state::CanMakeProgress};
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        f: F,
82    ) -> CanMakeProgress
83    where
84        F: FnOnce() -> R,
85        R: Into<Awaiting<T, E>>,
86    {
87        // Register a request to repaint after to update the UI (10 FPS
88        // doesn't look so bad while minimizing uncessary work)
89        ui.request_repaint_after(std::time::Duration::from_millis(100));
90
91        match self.inner.as_ref() {
92            DataState::None | DataState::AwaitingResponse(_) => {
93                self.ui_spinner_with_attempt_count(ui);
94                self.start_or_poll(f)
95            }
96            DataState::Present(_data) => {
97                // Does nothing as data is already present
98                CanMakeProgress::UnableToMakeProgress
99            }
100            DataState::Failed(e) => {
101                if self.attempts_left == 0 {
102                    ui.colored_label(
103                        ui.visuals().error_fg_color,
104                        format!("No attempts left from {}. {e}", self.max_attempts),
105                    );
106                    if ui.button(retry_msg.unwrap_or("Restart Task")).clicked() {
107                        self.reset_attempts();
108                        self.inner = DataState::default();
109                    }
110                } else {
111                    let wait_left = wait_before_next_attempt(self.next_allowed_attempt);
112                    ui.colored_label(
113                        ui.visuals().error_fg_color,
114                        format!(
115                            "{} attempt(s) left. {} seconds before retry. {e}",
116                            self.attempts_left,
117                            wait_left / 1000
118                        ),
119                    );
120                    let can_make_progress = self.start_or_poll(f);
121                    debug_assert!(
122                        can_make_progress.is_able_to_make_progress(),
123                        "This should be able to make progress"
124                    );
125                    if ui.button("Stop Trying").clicked() {
126                        self.attempts_left = 0;
127                    }
128                }
129                CanMakeProgress::AbleToMakeProgress
130            }
131        }
132    }
133
134    /// Attempts to load the data and returns if it is able to make progress.
135    #[must_use]
136    pub fn start_or_poll<F, R>(&mut self, f: F) -> CanMakeProgress
137    where
138        F: FnOnce() -> R,
139        R: Into<Awaiting<T, E>>,
140    {
141        match self.inner.as_mut() {
142            DataState::None => {
143                // Going to make an attempt, set when the next attempt is allowed
144                use rand::RngExt as _;
145                let wait_time_in_millis = rand::rng().random_range(self.retry_delay_millis.clone());
146                self.next_allowed_attempt = millis_since_epoch() + wait_time_in_millis as u128;
147
148                self.inner.start_task(f)
149            }
150            DataState::AwaitingResponse(_) => {
151                if self.inner.poll().is_present() {
152                    // Data was successfully received because before it was Awaiting
153                    self.reset_attempts();
154                }
155                CanMakeProgress::AbleToMakeProgress
156            }
157            DataState::Present(_) => CanMakeProgress::UnableToMakeProgress,
158            DataState::Failed(err_msg) => {
159                if self.attempts_left == 0 {
160                    CanMakeProgress::UnableToMakeProgress
161                } else {
162                    let wait_left = wait_before_next_attempt(self.next_allowed_attempt);
163                    if wait_left == 0 {
164                        warn!(?err_msg, ?self.attempts_left, "retrying task");
165                        self.attempts_left -= 1;
166                        self.inner = DataState::None;
167                    }
168                    CanMakeProgress::AbleToMakeProgress
169                }
170            }
171        }
172    }
173
174    /// Resets the attempts taken
175    pub fn reset_attempts(&mut self) {
176        self.attempts_left = self.max_attempts;
177        self.next_allowed_attempt = millis_since_epoch();
178    }
179
180    /// Clear stored data
181    pub fn clear(&mut self) {
182        self.inner = DataState::default();
183        self.reset_attempts();
184    }
185
186    /// Returns `true` if the internal data state is [`DataState::Present`].
187    #[must_use]
188    pub fn is_present(&self) -> bool {
189        self.inner.is_present()
190    }
191
192    /// Returns `true` if the internal data state is
193    /// [`DataState::AwaitingResponse`].
194    #[must_use]
195    pub fn is_awaiting_response(&self) -> bool {
196        self.inner.is_awaiting_response()
197    }
198
199    /// Returns `true` if the internal data state is [`DataState::None`].
200    #[must_use]
201    pub fn is_none(&self) -> bool {
202        self.inner.is_none()
203    }
204
205    #[cfg(feature = "egui")]
206    fn ui_spinner_with_attempt_count(&self, ui: &mut egui::Ui) {
207        ui.horizontal(|ui| {
208            ui.spinner();
209            ui.separator();
210            ui.label(format!("{} attempts left", self.attempts_left))
211        });
212    }
213}
214
215impl<T, E: ErrorBounds> Default for DataStateRetry<T, E> {
216    fn default() -> Self {
217        Self {
218            inner: Default::default(),
219            max_attempts: 3,
220            retry_delay_millis: 1000..5000,
221            attempts_left: 3,
222            next_allowed_attempt: millis_since_epoch(),
223        }
224    }
225}
226
227impl<T, E: ErrorBounds> AsRef<Self> for DataStateRetry<T, E> {
228    fn as_ref(&self) -> &Self {
229        self
230    }
231}
232
233impl<T, E: ErrorBounds> AsMut<Self> for DataStateRetry<T, E> {
234    fn as_mut(&mut self) -> &mut Self {
235        self
236    }
237}
238
239/// The duration before the next attempt will be made
240fn wait_before_next_attempt(next_allowed_attempt: u128) -> u128 {
241    next_allowed_attempt.saturating_sub(millis_since_epoch())
242}
243
244fn millis_since_epoch() -> u128 {
245    web_time::SystemTime::UNIX_EPOCH
246        .elapsed()
247        .expect("expected date on system to be after the epoch")
248        .as_millis()
249}
250
251// TODO 4: Use mocking to add tests ensuring retires are executed