main_loop_async/
data_state_retry.rs1use tracing::warn;
2
3use crate::{Awaiting, DataState, ErrorBounds, data_state::CanMakeProgress};
4use std::fmt::Debug;
5use std::ops::Range;
6
7#[derive(Debug)]
9pub struct DataStateRetry<T, E: ErrorBounds = anyhow::Error> {
10 pub max_attempts: u8,
12
13 pub retry_delay_millis: Range<u16>,
16
17 attempts_left: u8,
18 inner: DataState<T, E>, next_allowed_attempt: u128,
20}
21
22impl<T, E: ErrorBounds> DataStateRetry<T, E> {
23 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 pub fn attempts_left(&self) -> u8 {
34 self.attempts_left
35 }
36
37 pub fn next_allowed_attempt(&self) -> u128 {
39 self.next_allowed_attempt
40 }
41
42 pub fn inner(&self) -> &DataState<T, E> {
44 &self.inner
45 }
46
47 pub fn into_inner(self) -> DataState<T, E> {
49 self.inner
50 }
51
52 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 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 #[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 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 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 #[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 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 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 pub fn reset_attempts(&mut self) {
176 self.attempts_left = self.max_attempts;
177 self.next_allowed_attempt = millis_since_epoch();
178 }
179
180 pub fn clear(&mut self) {
182 self.inner = DataState::default();
183 self.reset_attempts();
184 }
185
186 #[must_use]
188 pub fn is_present(&self) -> bool {
189 self.inner.is_present()
190 }
191
192 #[must_use]
195 pub fn is_awaiting_response(&self) -> bool {
196 self.inner.is_awaiting_response()
197 }
198
199 #[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
239fn 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