use std::time::Duration;
use postgresql_archive::Error as ArchiveError;
use postgresql_embedded::Error as EmbeddedError;
use crate::error::{
BootstrapError,
BootstrapErrorKind,
BootstrapResult,
CachedBinariesUsed,
LifecycleTimeout,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct RetryPolicy {
attempts: u32,
first_delay: Duration,
}
impl RetryPolicy {
pub(super) const SHARED_CLUSTER: Self = Self::new(3, Duration::from_secs(1));
pub(super) const fn new(attempts: u32, first_delay: Duration) -> Self {
Self {
attempts,
first_delay,
}
}
const fn delay_after(self, made: u32) -> Duration {
self.first_delay
.saturating_mul(2_u32.saturating_pow(made.saturating_sub(1)))
}
}
pub(super) fn retry_transient<T, Attempt, Sleep>(
policy: RetryPolicy,
mut attempt: Attempt,
mut sleep: Sleep,
) -> BootstrapResult<T>
where
Attempt: FnMut() -> BootstrapResult<T>,
Sleep: FnMut(Duration),
{
let mut made = 1;
loop {
let err = match attempt() {
Ok(value) => return Ok(value),
Err(err) => err,
};
if !is_transient(&err) {
return Err(err);
}
if made >= policy.attempts {
return Err(exhausted(err, made));
}
log_retry(&err, made, policy.attempts);
sleep(policy.delay_after(made));
made += 1;
}
}
pub(crate) fn is_transient(err: &BootstrapError) -> bool {
let from_cache = err.report().downcast_ref::<CachedBinariesUsed>().is_some();
err.kind() == BootstrapErrorKind::ExtensionArchiveUnavailable
|| err
.report()
.chain()
.any(|cause| is_transient_cause(cause, from_cache))
}
fn is_transient_cause(cause: &(dyn std::error::Error + 'static), from_cache: bool) -> bool {
cause.is::<LifecycleTimeout>()
|| !from_cache
&& cause
.downcast_ref::<EmbeddedError>()
.is_some_and(is_transient_embedded)
}
const fn is_transient_embedded(err: &EmbeddedError) -> bool {
match err {
EmbeddedError::IoError(_) => true,
EmbeddedError::ArchiveError(inner) => {
matches!(
inner,
ArchiveError::IoError(_) | ArchiveError::RepositoryFailure(_)
)
}
_ => false,
}
}
fn exhausted(err: BootstrapError, made: u32) -> BootstrapError {
let kind = err.kind();
let report = err.into_report().wrap_err(format!(
"bootstrap failed after {made} attempts; each failure was transient"
));
BootstrapError::new(kind, report)
}
fn log_retry(err: &BootstrapError, made: u32, attempts: u32) {
tracing::warn!(
target: crate::observability::LOG_TARGET,
attempt = made,
attempts,
error = %err,
"transient bootstrap failure; retrying"
);
}
#[cfg(test)]
mod tests;