reqwest_cross/
data_state_retry.rsuse tracing::{error, warn};
use crate::{data_state::CanMakeProgress, Awaiting, DataState, ErrorBounds};
use std::fmt::Debug;
use std::ops::Range;
use std::time::{Duration, Instant};
#[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: Instant,
}
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) -> Instant {
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_get<F>(
&mut self,
ui: &mut egui::Ui,
retry_msg: Option<&str>,
fetch_fn: F,
) -> CanMakeProgress
where
F: FnOnce() -> Awaiting<T, E>,
{
match self.inner.as_ref() {
DataState::None | DataState::AwaitingResponse(_) => {
self.ui_spinner_with_attempt_count(ui);
self.get(fetch_fn)
}
DataState::Present(_data) => {
CanMakeProgress::UnableToMakeProgress
}
DataState::Failed(e) => {
if self.attempts_left == 0 {
ui.colored_label(
ui.visuals().error_fg_color,
format!("{} attempts exhausted. {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.as_secs()
),
);
let is_able_to_make_progress = self.get(fetch_fn).is_able_to_make_progress();
assert!(
is_able_to_make_progress,
"if this is not true something is very wrong"
);
}
CanMakeProgress::AbleToMakeProgress
}
}
}
#[must_use]
pub fn get<F>(&mut self, fetch_fn: F) -> CanMakeProgress
where
F: FnOnce() -> Awaiting<T, E>,
{
match self.inner.as_mut() {
DataState::None => {
use rand::Rng;
let wait_time_in_millis = rand::thread_rng()
.gen_range(self.retry_delay_millis.clone())
.into();
self.next_allowed_attempt = Instant::now()
.checked_add(Duration::from_millis(wait_time_in_millis))
.expect("failed to get random delay, value was out of range");
self.inner.get(fetch_fn)
}
DataState::AwaitingResponse(rx) => {
if let Some(new_state) = DataState::await_data(rx) {
self.inner = match new_state.as_ref() {
DataState::None => {
error!("Unexpected new state received of DataState::None");
unreachable!("Only expect Failed or Present variants to be returned but got None")
}
DataState::AwaitingResponse(_) => {
error!("Unexpected new state received of AwaitingResponse");
unreachable!("Only expect Failed or Present variants to be returned bug got AwaitingResponse")
}
DataState::Present(_) => {
self.reset_attempts();
new_state
}
DataState::Failed(_) => new_state,
};
}
CanMakeProgress::AbleToMakeProgress
}
DataState::Present(_) => self.inner.get(fetch_fn),
DataState::Failed(err_msg) => {
if self.attempts_left == 0 {
self.inner.get(fetch_fn)
} else {
let wait_left = wait_before_next_attempt(self.next_allowed_attempt);
if wait_left.is_zero() {
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 = Instant::now();
}
pub fn clear(&mut self) {
self.inner = DataState::default();
}
#[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: Instant::now(),
}
}
}
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: Instant) -> Duration {
next_allowed_attempt.saturating_duration_since(Instant::now())
}