Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: compile to fail when using DefaultSleeper with no features enabled #136

Merged
merged 2 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion backon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ mod sleep;
pub use sleep::DefaultSleeper;
#[cfg(all(target_arch = "wasm32", feature = "gloo-timers-sleep"))]
pub use sleep::GlooTimersSleep;
pub(crate) use sleep::NoopSleeper;
pub use sleep::Sleeper;
#[cfg(all(not(target_arch = "wasm32"), feature = "tokio-sleep"))]
pub use sleep::TokioSleeper;
Expand Down
56 changes: 35 additions & 21 deletions backon/src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::task::Poll;
use std::time::Duration;

use crate::backoff::BackoffBuilder;
use crate::sleep::MayBeDefaultSleeper;
use crate::Backoff;
use crate::DefaultSleeper;
use crate::Sleeper;
Expand Down Expand Up @@ -67,7 +68,7 @@ pub struct Retry<
E,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
SF: Sleeper = DefaultSleeper,
SF: MayBeDefaultSleeper = DefaultSleeper,
RF = fn(&E) -> bool,
NF = fn(&E, Duration),
> {
Expand Down Expand Up @@ -104,7 +105,7 @@ where
B: Backoff,
Fut: Future<Output = Result<T, E>>,
FutureFn: FnMut() -> Fut,
SF: Sleeper,
SF: MayBeDefaultSleeper,
RF: FnMut(&E) -> bool,
NF: FnMut(&E, Duration),
{
Expand Down Expand Up @@ -263,11 +264,6 @@ where
type Output = Result<T, E>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
#[cfg(debug_assertions)]
if std::any::TypeId::of::<SF>() == std::any::TypeId::of::<crate::NoopSleeper>() {
panic!("BackON: No sleeper has been configured. Please enable the features or provide a custom implementation.")
}

// Safety: This is safe because we don't move the `Retry` struct itself,
// only its internal state.
//
Expand Down Expand Up @@ -324,8 +320,8 @@ where

#[cfg(test)]
#[cfg(any(feature = "tokio-sleep", feature = "gloo-timers-sleep"))]
mod tests {
use std::{future::ready, time::Duration};
mod default_sleeper_tests {
use std::time::Duration;
use tokio::sync::Mutex;

#[cfg(target_arch = "wasm32")]
Expand All @@ -352,18 +348,6 @@ mod tests {
Ok(())
}

#[test]
async fn test_retry_with_sleep() -> anyhow::Result<()> {
let result = always_error
.retry(ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)))
.sleep(|_| ready(()))
.await;

assert!(result.is_err());
assert_eq!("test_query meets error", result.unwrap_err().to_string());
Ok(())
}

#[test]
async fn test_retry_with_not_retryable_error() -> anyhow::Result<()> {
let error_times = Mutex::new(0);
Expand Down Expand Up @@ -442,3 +426,33 @@ mod tests {
Ok(())
}
}

#[cfg(test)]
mod custom_sleeper_tests {
use std::{future::ready, time::Duration};

#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;

#[cfg(not(target_arch = "wasm32"))]
use tokio::test;

use super::*;
use crate::ExponentialBuilder;

async fn always_error() -> anyhow::Result<()> {
Err(anyhow::anyhow!("test_query meets error"))
}

#[test]
async fn test_retry_with_sleep() -> anyhow::Result<()> {
let result = always_error
.retry(ExponentialBuilder::default().with_min_delay(Duration::from_millis(1)))
.sleep(|_| ready(()))
.await;

assert!(result.is_err());
assert_eq!("test_query meets error", result.unwrap_err().to_string());
Ok(())
}
}
9 changes: 3 additions & 6 deletions backon/src/retry_with_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::task::Poll;
use std::time::Duration;

use crate::backoff::BackoffBuilder;
use crate::sleep::MayBeDefaultSleeper;
use crate::Backoff;
use crate::DefaultSleeper;
use crate::Sleeper;
Expand Down Expand Up @@ -106,7 +107,7 @@ pub struct RetryWithContext<
Ctx,
Fut: Future<Output = (Ctx, Result<T, E>)>,
FutureFn: FnMut(Ctx) -> Fut,
SF: Sleeper = DefaultSleeper,
SF: MayBeDefaultSleeper = DefaultSleeper,
RF = fn(&E) -> bool,
NF = fn(&E, Duration),
> {
Expand Down Expand Up @@ -302,11 +303,6 @@ where
type Output = (Ctx, Result<T, E>);

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
#[cfg(debug_assertions)]
if std::any::TypeId::of::<SF>() == std::any::TypeId::of::<crate::NoopSleeper>() {
panic!("BackON: No sleeper has been configured. Please enable the features or provide a custom implementation.")
}

// Safety: This is safe because we don't move the `Retry` struct itself,
// only its internal state.
//
Expand Down Expand Up @@ -366,6 +362,7 @@ where
}

#[cfg(test)]
#[cfg(any(feature = "tokio-sleep", feature = "gloo-timers-sleep"))]
mod tests {
use std::time::Duration;

Expand Down
36 changes: 26 additions & 10 deletions backon/src/sleep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use std::{
time::Duration,
};

#[cfg(doc)]
use crate::Retry;

/// A sleeper is used to generate a future that completes after a specified duration.
pub trait Sleeper: 'static {
/// The future returned by the `sleep` method.
Expand All @@ -12,11 +15,18 @@ pub trait Sleeper: 'static {
fn sleep(&self, dur: Duration) -> Self::Sleep;
}

/// The default implementation of `Sleeper` is a no-op when no features are enabled.
///
/// It will panic on `debug` profile and do nothing on `release` profile.
/// A stub trait allowing non-[`Sleeper`] types to be used as a generic parameter in [`Retry`].
/// It does not provide actual functionality.
#[doc(hidden)]
pub trait MayBeDefaultSleeper: 'static {
type Sleep: Future<Output = ()>;
}

/// The default implementation of `Sleeper` when no features are enabled.
/// It will fail to compile if a containing [`Retry`] is `.await`ed without calling [`Retry::sleep`]
/// to provide a valid sleeper.
#[cfg(all(not(feature = "tokio-sleep"), not(feature = "gloo-timers-sleep")))]
pub type DefaultSleeper = NoopSleeper;
pub type DefaultSleeper = PleaseEnableAFeatureForSleeper;
/// The default implementation of `Sleeper` while feature `tokio-sleep` enabled.
///
/// it uses `tokio::time::sleep`.
Expand All @@ -28,16 +38,22 @@ pub type DefaultSleeper = TokioSleeper;
#[cfg(all(target_arch = "wasm32", feature = "gloo-timers-sleep"))]
pub type DefaultSleeper = GlooTimersSleep;

/// The no-op implementation of `Sleeper` that does nothing.
/// A stub type that does not implement [`Sleeper`] and hence will fail to compile if used as a
/// sleeper.
///
/// Users are expected to enable a feature of this crate that provides a valid implementation of
/// [`Sleeper`] when they see this type appearing in compilation errors. Otherwise, a custom [`Sleeper`]
/// implementation should be provided where needed, such as [`Retry::sleeper`].
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default)]
pub struct NoopSleeper;
pub struct PleaseEnableAFeatureForSleeper;

impl Sleeper for NoopSleeper {
impl MayBeDefaultSleeper for PleaseEnableAFeatureForSleeper {
type Sleep = Ready<()>;
}

fn sleep(&self, _: Duration) -> Self::Sleep {
std::future::ready(())
}
impl<T: Sleeper + ?Sized> MayBeDefaultSleeper for T {
type Sleep = <T as Sleeper>::Sleep;
}

impl<F: Fn(Duration) -> Fut + 'static, Fut: Future<Output = ()>> Sleeper for F {
Expand Down