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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Debug)]
10pub struct DataStateRetry<T, E: ErrorBounds = anyhow::Error> {
11 pub max_attempts: u8,
13
14 pub retry_delay_millis: Range<u16>,
17
18 attempts_left: u8,
19 inner: DataState<T, E>, next_allowed_attempt: u128,
21}
22
23impl<T, E: ErrorBounds> DataStateRetry<T, E> {
24 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 pub fn attempts_left(&self) -> u8 {
35 self.attempts_left
36 }
37
38 pub fn next_allowed_attempt(&self) -> u128 {
40 self.next_allowed_attempt
41 }
42
43 pub fn inner(&self) -> &DataState<T, E> {
45 &self.inner
46 }
47
48 pub fn into_inner(self) -> DataState<T, E> {
50 self.inner
51 }
52
53 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 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 #[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 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 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 #[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 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 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 pub fn reset_attempts(&mut self) {
177 self.attempts_left = self.max_attempts;
178 self.next_allowed_attempt = millis_since_epoch();
179 }
180
181 pub fn clear(&mut self) {
183 self.inner = DataState::default();
184 self.reset_attempts();
185 }
186
187 #[must_use]
189 pub fn is_present(&self) -> bool {
190 self.inner.is_present()
191 }
192
193 #[must_use]
196 pub fn is_awaiting_response(&self) -> bool {
197 self.inner.is_awaiting_response()
198 }
199
200 #[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
240fn 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