reqwest_cross/
data_state_retry.rs1use tracing::warn;
2
3use crate::{data_state::CanMakeProgress, Awaiting, DataState, ErrorBounds};
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 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 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 #[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 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 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 pub fn reset_attempts(&mut self) {
172 self.attempts_left = self.max_attempts;
173 self.next_allowed_attempt = millis_since_epoch();
174 }
175
176 pub fn clear(&mut self) {
178 self.inner = DataState::default();
179 self.reset_attempts();
180 }
181
182 #[must_use]
184 pub fn is_present(&self) -> bool {
185 self.inner.is_present()
186 }
187
188 #[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
228fn 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