///|
/// Retry delay strategy.
pub(all) enum RetryBackoff {
NoBackoff
FixedDelay
LinearBackoff
ExponentialBackoff
} derive(Eq, Debug)
///|
/// Declarative retry policy for failed WebHook processing.
pub(all) struct RetryPolicy {
max_attempts : Int
base_delay_ms : Int
max_delay_ms : Int
jitter_ms : Int
backoff : RetryBackoff
} derive(Eq, Debug)
///|
/// Create a retry policy.
pub fn RetryPolicy::RetryPolicy(
max_attempts? : Int = 3,
base_delay_ms? : Int = 1000,
max_delay_ms? : Int = 30000,
jitter_ms? : Int = 0,
backoff? : RetryBackoff = ExponentialBackoff,
) -> RetryPolicy {
{ max_attempts, base_delay_ms, max_delay_ms, jitter_ms, backoff }
}
///|
/// Disable retries.
pub fn no_retry_policy() -> RetryPolicy {
RetryPolicy(
max_attempts=1,
base_delay_ms=0,
max_delay_ms=0,
backoff=NoBackoff,
)
}
///|
/// Retry with a fixed delay.
pub fn fixed_retry_policy(
attempts? : Int = 3,
delay_ms? : Int = 1000,
) -> RetryPolicy {
RetryPolicy(
max_attempts=attempts,
base_delay_ms=delay_ms,
max_delay_ms=delay_ms,
backoff=FixedDelay,
)
}
///|
/// Retry with an exponential delay.
pub fn exponential_retry_policy(
attempts? : Int = 5,
base_delay_ms? : Int = 500,
max_delay_ms? : Int = 60000,
) -> RetryPolicy {
RetryPolicy(
max_attempts=attempts,
base_delay_ms~,
max_delay_ms~,
backoff=ExponentialBackoff,
)
}
///|
/// Decide whether a result should be attempted again.
pub fn RetryPolicy::can_retry(self : RetryPolicy, result : HookResult) -> Bool {
result.should_retry() && result.attempts < self.max_attempts
}
///|
/// Compute delay for a 1-based attempt number.
pub fn RetryPolicy::delay_for_attempt(self : RetryPolicy, attempt : Int) -> Int {
let raw = match self.backoff {
NoBackoff => 0
FixedDelay => self.base_delay_ms
LinearBackoff => self.base_delay_ms * attempt
ExponentialBackoff => self.base_delay_ms * pow2(attempt - 1)
}
clamp_delay(raw + self.jitter_ms, self.max_delay_ms)
}
///|
/// Convert a failed result into a scheduled retry when policy allows it.
pub fn RetryPolicy::schedule(
self : RetryPolicy,
result : HookResult,
) -> HookResult {
if self.can_retry(result) {
retried(
message="retry in " +
self.delay_for_attempt(result.attempts + 1).to_string() +
"ms",
attempts=result.attempts + 1,
)
} else {
result
}
}
///|
fn clamp_delay(value : Int, max_value : Int) -> Int {
if max_value <= 0 {
value
} else if value > max_value {
max_value
} else {
value
}
}
///|
fn pow2(exp : Int) -> Int {
let mut acc = 1
let mut i = 0
while i < exp {
acc = acc * 2
i = i + 1
}
acc
}