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