Skip to main content

cloud_sdk/retry/
permit.rs

1//! One-use retry execution permits.
2
3use core::fmt;
4
5use cloud_sdk_sanitization::sanitize_bytes;
6
7use super::policy::observe_monotonic;
8use super::{MonotonicDuration, MonotonicInstant};
9use crate::authentication::{AsyncAuthenticatedTransport, BlockingAuthenticatedTransport};
10use crate::operation::{CheckedResponseGuard, PreparedExecutionError, PreparedRequest};
11use crate::transport::BoundTransport;
12
13/// Why a one-use retry permit did not authorize execution.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum RetryPermitError {
16    /// The caller observed a time before the authorized delay completed.
17    TooEarly,
18    /// The caller observation moved backward relative to controller state.
19    MonotonicRollback,
20    /// The hard elapsed budget expired before execution.
21    ElapsedBudgetExhausted,
22}
23
24impl_static_error!(RetryPermitError,
25    Self::TooEarly => "retry permit used before its authorized delay",
26    Self::MonotonicRollback => "retry permit monotonic observation moved backward",
27    Self::ElapsedBudgetExhausted => "retry permit elapsed budget is exhausted",
28);
29
30/// Retry authorization or prepared transport execution failure.
31#[derive(Clone, Copy, Eq, PartialEq)]
32pub enum RetryExecutionError<E> {
33    /// The permit rejected its final monotonic observation.
34    Permit(RetryPermitError),
35    /// The exact prepared request failed during its single execution.
36    Execution(PreparedExecutionError<E>),
37}
38
39impl<E> fmt::Debug for RetryExecutionError<E> {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::Permit(error) => formatter.debug_tuple("Permit").field(error).finish(),
43            Self::Execution(error) => formatter.debug_tuple("Execution").field(error).finish(),
44        }
45    }
46}
47
48impl<E> fmt::Display for RetryExecutionError<E> {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter.write_str(match self {
51            Self::Permit(_) => "retry permit rejected execution",
52            Self::Execution(_) => "retry prepared execution failed",
53        })
54    }
55}
56
57impl<E: fmt::Debug> core::error::Error for RetryExecutionError<E> {}
58
59/// Non-cloneable authorization for one exact prepared replay.
60///
61/// The permit exclusively borrows controller monotonic state until it is
62/// consumed. Safe code therefore cannot retain two permits from one owner.
63/// Execution is performed by the permit and never returns a reusable request.
64///
65/// ```compile_fail
66/// use cloud_sdk::retry::RetryPermit;
67///
68/// fn duplicate(permit: RetryPermit<'_, '_, '_>) {
69///     let _second = permit.clone();
70/// }
71/// ```
72///
73/// ```compile_fail
74/// use cloud_sdk::retry::{
75///     MonotonicDuration, MonotonicInstant, RetryController, RetryEvent,
76///     RetrySubject,
77/// };
78/// use cloud_sdk::transport::DeliveryPhase;
79///
80/// fn fan_out<'initial, 'binding, 'replay, 'subject>(
81///     controller: &mut RetryController<'initial, 'binding>,
82///     replay: RetrySubject<'replay, 'subject>,
83/// ) {
84///     let first = controller.decide_retry(
85///         RetryEvent::Transport(DeliveryPhase::NotSent),
86///         replay,
87///         MonotonicDuration::new(0),
88///         MonotonicInstant::new(1),
89///     );
90///     let _second = controller.decide_retry(
91///         RetryEvent::Transport(DeliveryPhase::NotSent),
92///         replay,
93///         MonotonicDuration::new(0),
94///         MonotonicInstant::new(1),
95///     );
96///     drop(first);
97/// }
98/// ```
99#[must_use]
100pub struct RetryPermit<'controller, 'request, 'subject> {
101    last_observed: &'controller mut MonotonicInstant,
102    prepared: &'subject PreparedRequest<'request>,
103    attempt: u16,
104    delay: MonotonicDuration,
105    not_before: MonotonicInstant,
106    started: MonotonicInstant,
107    max_elapsed: MonotonicDuration,
108}
109
110impl<'controller, 'request, 'subject> RetryPermit<'controller, 'request, 'subject> {
111    pub(crate) fn new(
112        last_observed: &'controller mut MonotonicInstant,
113        prepared: &'subject PreparedRequest<'request>,
114        attempt: u16,
115        delay: MonotonicDuration,
116        not_before: MonotonicInstant,
117        started: MonotonicInstant,
118        max_elapsed: MonotonicDuration,
119    ) -> Self {
120        Self {
121            last_observed,
122            prepared,
123            attempt,
124            delay,
125            not_before,
126            started,
127            max_elapsed,
128        }
129    }
130
131    /// Returns the authorized attempt number, including the initial attempt.
132    #[must_use]
133    pub const fn attempt(&self) -> u16 {
134        self.attempt
135    }
136
137    /// Returns the exact caller-selected delay charged to retry budgets.
138    #[must_use]
139    pub const fn delay(&self) -> MonotonicDuration {
140        self.delay
141    }
142
143    /// Authorizes and executes the exact replay once on a blocking transport.
144    pub fn execute_blocking<'buffer, T>(
145        mut self,
146        now: MonotonicInstant,
147        transport: &T,
148        response_storage: &'buffer mut [u8],
149        response_header_storage: &'buffer mut [u8],
150    ) -> Result<CheckedResponseGuard<'buffer>, RetryExecutionError<T::Error>>
151    where
152        T: BlockingAuthenticatedTransport + BoundTransport,
153    {
154        sanitize_bytes(response_storage);
155        sanitize_bytes(response_header_storage);
156        self.authorize(now).map_err(RetryExecutionError::Permit)?;
157        self.prepared
158            .execute_blocking(transport, response_storage, response_header_storage)
159            .map_err(RetryExecutionError::Execution)
160    }
161
162    /// Authorizes and executes the exact replay once without owning an executor.
163    pub async fn execute_async<'transport, 'buffer, T>(
164        mut self,
165        now: MonotonicInstant,
166        transport: &'transport T,
167        response_storage: &'buffer mut [u8],
168        response_header_storage: &'buffer mut [u8],
169    ) -> Result<CheckedResponseGuard<'buffer>, RetryExecutionError<T::Error>>
170    where
171        T: AsyncAuthenticatedTransport + BoundTransport,
172        'request: 'transport,
173        'subject: 'transport,
174    {
175        sanitize_bytes(response_storage);
176        sanitize_bytes(response_header_storage);
177        self.authorize(now).map_err(RetryExecutionError::Permit)?;
178        self.prepared
179            .execute_async(transport, response_storage, response_header_storage)
180            .await
181            .map_err(RetryExecutionError::Execution)
182    }
183
184    fn authorize(&mut self, now: MonotonicInstant) -> Result<(), RetryPermitError> {
185        let elapsed = observe_monotonic(self.last_observed, self.started, now)
186            .map_err(|_| RetryPermitError::MonotonicRollback)?;
187        if now < self.not_before {
188            return Err(RetryPermitError::TooEarly);
189        }
190        if elapsed > self.max_elapsed {
191            return Err(RetryPermitError::ElapsedBudgetExhausted);
192        }
193        Ok(())
194    }
195}
196
197impl fmt::Debug for RetryPermit<'_, '_, '_> {
198    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
199        formatter
200            .debug_struct("RetryPermit")
201            .field("prepared", &"[bound request]")
202            .field("attempt", &self.attempt)
203            .field("delay", &self.delay)
204            .field("not_before", &self.not_before)
205            .field("started", &self.started)
206            .field("max_elapsed", &self.max_elapsed)
207            .finish()
208    }
209}