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