Skip to main content

coven_storage/
cooldown.rs

1//! Waiting between automatic attempts (§6.7, §21.2).
2//!
3//! Ordinary delays double from one second to five minutes. A provider's
4//! `Retry-After` is a cooldown for every consumer of the store's connection:
5//! the connection refuses requests while it is live and records it, durably,
6//! before returning the response that asked for it.
7
8use crate::{StorageError, StorageFailure};
9use std::time::Duration;
10
11/// The longest ordinary delay between unsuccessful attempts.
12pub const LONGEST_RETRY_DELAY: Duration = Duration::from_secs(300);
13
14/// The delay after the unsuccessful attempt numbered `attempt`, counting from
15/// zero: 1, 2, 4, …, 256, then 300 seconds.
16pub fn retry_delay(attempt: u32) -> Duration {
17    let seconds = 1u64.checked_shl(attempt).unwrap_or(u64::MAX).min(LONGEST_RETRY_DELAY.as_secs());
18    Duration::from_secs(seconds)
19}
20
21/// The wait a provider requested with a response (§6.7).
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum RetryAfter {
24    /// Delay-seconds, or an HTTP-date measured against the response's own
25    /// `Date`, so a skewed device clock cannot shorten it.
26    Wait(Duration),
27    /// An HTTP-date the response gives no usable `Date` to measure against.
28    NoTimeBasis,
29}
30
31impl RetryAfter {
32    /// The cooldown to arm. Without a time basis coven cannot convert the
33    /// requested date, and waits the longest ordinary delay instead of
34    /// treating the missing basis as permission to retry at once.
35    pub fn wait(self) -> Duration {
36        match self {
37            Self::Wait(wait) => wait,
38            Self::NoTimeBasis => LONGEST_RETRY_DELAY,
39        }
40    }
41}
42
43/// How long to wait before resending a request refused as `RateLimited`,
44/// after `attempt` earlier resends: what the provider asked for, or what is
45/// left of the cooldown that refused it before sending, and never less than
46/// the ordinary delay, so an absent or zero request still backs off.
47pub(crate) fn throttled_wait(error: &StorageError, attempt: u32) -> Duration {
48    let requested = error.retry_after().map(RetryAfter::wait).or_else(|| match error {
49        StorageError::Caused { source, .. } => {
50            source.downcast_ref::<CoolingDown>().map(|cooling| cooling.remaining)
51        }
52        _ => None,
53    });
54    requested.unwrap_or_default().max(retry_delay(attempt))
55}
56
57/// The store's provider cooldown, shared by every consumer of its connection:
58/// sync passes, operations, file transfers and joining.
59#[async_trait::async_trait]
60pub trait Cooldown: Send + Sync {
61    /// The live cooldown's remaining wait, measured monotonically.
62    fn remaining(&self) -> Option<Duration>;
63    /// Arm and persist a wait the provider requested, before any consumer can
64    /// retry. A failure is returned; it never permits an immediate retry.
65    async fn record(&self, wait: Duration) -> Result<(), StorageError>;
66}
67
68/// Refuse a request while the store's shared provider cooldown is live.
69pub(crate) fn cooling(cooldown: &dyn Cooldown) -> Result<(), StorageError> {
70    match cooldown.remaining() {
71        Some(remaining) => Err(StorageFailure::RateLimited.with_source(CoolingDown { remaining })),
72        None => Ok(()),
73    }
74}
75
76/// Persist provider-requested waits before returning them to any consumer.
77pub(crate) async fn observed<T>(
78    cooldown: &dyn Cooldown,
79    result: Result<T, StorageError>,
80) -> Result<T, StorageError> {
81    let error = match result {
82        Ok(value) => return Ok(value),
83        Err(error) => error,
84    };
85    let Some(after) = error.retry_after() else {
86        return Err(error);
87    };
88    if after == RetryAfter::NoTimeBasis {
89        tracing::warn!(
90            ?error,
91            "Retry-After names a date without a response Date; waiting the longest ordinary delay"
92        );
93    }
94    let wait = after.wait();
95    match cooldown.record(wait).await {
96        Ok(()) => Err(error),
97        Err(cause) => Err(StorageFailure::File.with_source(CooldownNotRecorded {
98            wait,
99            response: error,
100            cause,
101        })),
102    }
103}
104
105/// No cooldown is kept: raw fixtures and probes outside a store's lifetime.
106pub struct NoCooldown;
107
108#[async_trait::async_trait]
109impl Cooldown for NoCooldown {
110    fn remaining(&self) -> Option<Duration> {
111        None
112    }
113    async fn record(&self, _: Duration) -> Result<(), StorageError> {
114        Ok(())
115    }
116}
117
118/// A request refused before it was sent, while a provider cooldown is live.
119#[derive(Debug, thiserror::Error)]
120#[error("provider cooldown has {remaining:?} left")]
121pub struct CoolingDown {
122    /// The cooldown's remaining wait when the request was refused.
123    pub remaining: Duration,
124}
125
126/// The provider asked for a cooldown that could not be persisted. The work
127/// that observed it fails; automatic retry waits for the app.
128#[derive(Debug, thiserror::Error)]
129#[error("provider cooldown of {wait:?} was not persisted: {cause}")]
130pub struct CooldownNotRecorded {
131    /// The requested wait, still armed for this session.
132    pub wait: Duration,
133    /// The provider's response asking for it.
134    #[source]
135    pub response: StorageError,
136    /// Why persisting it failed.
137    pub cause: StorageError,
138}
139
140#[cfg(test)]
141#[path = "cooldown_tests.rs"]
142pub(crate) mod tests;