saluki_io/net/util/retry/
mod.rs

1mod backoff;
2pub use self::backoff::ExponentialBackoff;
3
4mod classifier;
5pub use self::classifier::{HttpRetryPredicate, RetryClassifier, StandardHttpClassifier};
6
7mod lifecycle;
8pub use self::lifecycle::{RetryCauseTelemetry, StandardHttpRetryLifecycle};
9
10mod policy;
11pub use self::policy::{NoopRetryPolicy, RollingExponentialBackoffRetryPolicy};
12
13mod queue;
14pub use self::queue::{
15    DiskUsageRetriever, DiskUsageRetrieverImpl, EventContainer, PersistedQueueArgs, PushResult, RetryQueue, Retryable,
16};
17
18/// A batteries-included retry policy suitable for HTTP-based clients.
19pub type DefaultHttpRetryPolicy<B = ()> =
20    RollingExponentialBackoffRetryPolicy<StandardHttpClassifier<B>, StandardHttpRetryLifecycle>;
21
22impl<B: 'static> DefaultHttpRetryPolicy<B> {
23    /// Creates a new retry policy adapted to HTTP-based clients with the given exponential backoff strategy.
24    ///
25    /// This policy uses the standard HTTP classifier ([`StandardHttpClassifier`]) and retry lifecycle ([`StandardHttpRetryLifecycle`]).
26    pub fn with_backoff(backoff: ExponentialBackoff) -> Self {
27        Self::with_backoff_and_classifier(backoff, StandardHttpClassifier::new())
28    }
29
30    /// Creates a new retry policy adapted to HTTP-based clients with the given exponential backoff strategy and a
31    /// pre-built [`StandardHttpClassifier`].
32    ///
33    /// This is the same as [`DefaultHttpRetryPolicy::with_backoff`], but allows the caller to supply a classifier that
34    /// has been customized (for example, with additional [`HttpRetryPredicate`]s via
35    /// [`StandardHttpClassifier::with_predicate`]).
36    pub fn with_backoff_and_classifier(backoff: ExponentialBackoff, classifier: StandardHttpClassifier<B>) -> Self {
37        RollingExponentialBackoffRetryPolicy::new(classifier, backoff)
38            .with_retry_lifecycle(StandardHttpRetryLifecycle::new())
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use std::{sync::Arc, time::Duration};
45
46    use http::{Request, Response, StatusCode};
47    use tower::retry::Policy;
48
49    use super::*;
50
51    type BoxError = Box<dyn std::error::Error + Send + Sync>;
52    type TestRequest = Request<()>;
53
54    fn test_backoff() -> ExponentialBackoff {
55        ExponentialBackoff::with_jitter(Duration::from_millis(1), Duration::from_millis(10), 2.0)
56    }
57
58    fn test_request() -> TestRequest {
59        Request::builder()
60            .method("POST")
61            .uri("http://localhost/intake")
62            .body(())
63            .unwrap()
64    }
65
66    fn ok_response(status: StatusCode) -> Result<Response<()>, BoxError> {
67        Ok(Response::builder().status(status).body(()).unwrap())
68    }
69
70    fn would_retry(policy: &mut DefaultHttpRetryPolicy, status: StatusCode) -> bool {
71        let mut request = test_request();
72        let mut response = ok_response(status);
73        Policy::<TestRequest, Response<()>, BoxError>::retry(policy, &mut request, &mut response).is_some()
74    }
75
76    #[tokio::test]
77    async fn default_http_retry_policy_with_backoff_uses_default_classifier() {
78        let mut policy = DefaultHttpRetryPolicy::with_backoff(test_backoff());
79
80        assert!(!would_retry(&mut policy, StatusCode::OK));
81        assert!(!would_retry(&mut policy, StatusCode::FORBIDDEN));
82        assert!(!would_retry(&mut policy, StatusCode::BAD_REQUEST));
83        assert!(would_retry(&mut policy, StatusCode::INTERNAL_SERVER_ERROR));
84        assert!(would_retry(&mut policy, StatusCode::TOO_MANY_REQUESTS));
85    }
86
87    #[tokio::test]
88    async fn default_http_retry_policy_with_backoff_and_classifier_threads_predicate() {
89        // Build a classifier that flips 403 to retriable, then ensure the constructed policy honors it.
90        let predicate: HttpRetryPredicate = Arc::new(|response| response.status() == StatusCode::FORBIDDEN);
91        let classifier = StandardHttpClassifier::new().with_predicate(predicate);
92        let mut policy = DefaultHttpRetryPolicy::with_backoff_and_classifier(test_backoff(), classifier);
93
94        assert!(would_retry(&mut policy, StatusCode::FORBIDDEN));
95        // Other status codes still follow default classifier behavior.
96        assert!(!would_retry(&mut policy, StatusCode::OK));
97        assert!(!would_retry(&mut policy, StatusCode::UNAUTHORIZED));
98        assert!(would_retry(&mut policy, StatusCode::INTERNAL_SERVER_ERROR));
99    }
100}