reqwest_cross/
data_state_retry.rsuse tracing::warn;
use crate::{data_state::CanMakeProgress, Awaiting, DataState, ErrorBounds};
use std::fmt::Debug;
use std::ops::Range;
#[derive(Debug)]
pub struct DataStateRetry<T, E: ErrorBounds = anyhow::Error> {
pub max_attempts: u8,
pub retry_delay_millis: Range<u16>,
attempts_left: u8,
inner: DataState<T, E>, next_allowed_attempt: u128,
}
impl<T, E: ErrorBounds> DataStateRetry<T, E> {
pub fn new(max_attempts: u8, retry_delay_millis: Range<u16>) -> Self {
Self {
max_attempts,
retry_delay_millis,
..Default::default()
}
}
pub fn attempts_left(&self) -> u8 {
self.attempts_left
}
pub fn next_allowed_attempt(&self) -> u128 {
self.next_allowed_attempt
}
pub fn inner(&self) -> &DataState<T, E> {
&self.inner
}
pub fn into_inner(self) -> DataState<T, E> {
self.inner
}
pub fn present(&self) -> Option<&T> {
if let DataState::Present(data) = self.inner.as_ref() {
Some(data)
} else {
None
}
}
pub fn present_mut(&mut self) -> Option<&mut T> {
if let DataState::Present(data) = self.inner.as_mut() {
Some(data)
} else {
None
}
}
#[cfg(feature = "egui")]
#[must_use]
pub fn egui_start_or_poll<F, R>(
&mut self,
ui: &mut egui::Ui,
retry_msg: Option<&str>,
fetch_fn: F,
) -> CanMakeProgress
where
F: FnOnce() -> R,
R: Into<Awaiting<T, E>>,
{
match self.inner.as_ref() {
DataState::None | DataState::AwaitingResponse(_) => {
self.ui_spinner_with_attempt_count(ui);
self.start_or_poll(fetch_fn)
}
DataState::Present(_data) => {
CanMakeProgress::UnableToMakeProgress
}
DataState::Failed(e) => {
if self.attempts_left == 0 {
ui.colored_label(
ui.visuals().error_fg_color,
format!("No attempts left from {}. {e}", self.max_attempts),
);
if ui.button(retry_msg.unwrap_or("Restart Requests")).clicked() {
self.reset_attempts();
self.inner = DataState::default();
}
} else {
let wait_left = wait_before_next_attempt(self.next_allowed_attempt);
ui.colored_label(
ui.visuals().error_fg_color,
format!(
"{} attempt(s) left. {} seconds before retry. {e}",
self.attempts_left,
wait_left / 1000
),
);
let can_make_progress = self.start_or_poll(fetch_fn);
debug_assert!(
can_make_progress.is_able_to_make_progress(),
"This should be able to make progress"
);
if ui.button("Stop Trying").clicked() {
self.attempts_left = 0;
}
}
CanMakeProgress::AbleToMakeProgress
}
}
}
#[must_use]
pub fn start_or_poll<F, R>(&mut self, fetch_fn: F) -> CanMakeProgress
where
F: FnOnce() -> R,
R: Into<Awaiting<T, E>>,
{
match self.inner.as_mut() {
DataState::None => {
use rand::Rng as _;
let wait_time_in_millis = rand::rng().random_range(self.retry_delay_millis.clone());
self.next_allowed_attempt = millis_since_epoch() + wait_time_in_millis as u128;
self.inner.start_request(fetch_fn)
}
DataState::AwaitingResponse(_) => {
if self.inner.poll().is_present() {
self.reset_attempts();
}
CanMakeProgress::AbleToMakeProgress
}
DataState::Present(_) => CanMakeProgress::UnableToMakeProgress,
DataState::Failed(err_msg) => {
if self.attempts_left == 0 {
CanMakeProgress::UnableToMakeProgress
} else {
let wait_left = wait_before_next_attempt(self.next_allowed_attempt);
if wait_left == 0 {
warn!(?err_msg, ?self.attempts_left, "retrying request");
self.attempts_left -= 1;
self.inner = DataState::None;
}
CanMakeProgress::AbleToMakeProgress
}
}
}
}
pub fn reset_attempts(&mut self) {
self.attempts_left = self.max_attempts;
self.next_allowed_attempt = millis_since_epoch();
}
pub fn clear(&mut self) {
self.inner = DataState::default();
self.reset_attempts();
}
#[must_use]
pub fn is_present(&self) -> bool {
self.inner.is_present()
}
#[must_use]
pub fn is_none(&self) -> bool {
self.inner.is_none()
}
#[cfg(feature = "egui")]
fn ui_spinner_with_attempt_count(&self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.spinner();
ui.separator();
ui.label(format!("{} attempts left", self.attempts_left))
});
}
}
impl<T, E: ErrorBounds> Default for DataStateRetry<T, E> {
fn default() -> Self {
Self {
inner: Default::default(),
max_attempts: 3,
retry_delay_millis: 1000..5000,
attempts_left: 3,
next_allowed_attempt: millis_since_epoch(),
}
}
}
impl<T, E: ErrorBounds> AsRef<DataStateRetry<T, E>> for DataStateRetry<T, E> {
fn as_ref(&self) -> &DataStateRetry<T, E> {
self
}
}
impl<T, E: ErrorBounds> AsMut<DataStateRetry<T, E>> for DataStateRetry<T, E> {
fn as_mut(&mut self) -> &mut DataStateRetry<T, E> {
self
}
}
fn wait_before_next_attempt(next_allowed_attempt: u128) -> u128 {
next_allowed_attempt.saturating_sub(millis_since_epoch())
}
fn millis_since_epoch() -> u128 {
web_time::SystemTime::UNIX_EPOCH
.elapsed()
.expect("expected date on system to be after the epoch")
.as_millis()
}