// Retry backoff scheduling and deadlines, built on the @async wall clock.
// Growth is a pure, testable function; jitter comes from wall-clock low
// bits, which is fine for retry pacing (it is not security-sensitive).
///|
/// Pure exponential growth: base_ms shifted by `attempt`, capped at max_ms
/// (and at a shift ceiling so large attempts cannot overflow).
pub fn backoff_ms(base_ms : Int, max_ms : Int, attempt : Int) -> Int {
let attempt = if attempt < 0 {
0
} else if attempt > 16 {
16
} else {
attempt
}
let raw = base_ms << attempt
if raw > max_ms || raw <= 0 {
max_ms
} else {
raw
}
}
///|
/// Stateful backoff schedule: next_ms() grows exponentially per call and
/// applies ±20% wall-clock jitter so retried clients do not synchronize.
pub struct Backoff {
base_ms : Int
max_ms : Int
mut attempt : Int
} derive(@debug.Debug)
///|
pub fn Backoff::new(
base_ms? : Int = 100,
max_ms? : Int = 1000,
) -> Backoff raise {
if base_ms <= 0 || max_ms < base_ms {
raise ProtocolError::ProtocolError(
"backoff needs base_ms > 0 and max_ms >= base_ms, got \{base_ms}/\{max_ms}",
)
}
{ base_ms, max_ms, attempt: 0, }
}
///|
/// Next delay in milliseconds, advancing the attempt counter.
pub fn Backoff::next_ms(self : Backoff) -> Int {
let ms = backoff_ms(self.base_ms, self.max_ms, self.attempt)
self.attempt = self.attempt + 1
let jitter = 80 + (@async.now() % 41L).to_int() // 80..120 percent
if ms > 16_000_000 {
ms // too large for the multiplication; skip jitter
} else {
let jittered = ms * jitter / 100
if jittered < 1 {
1
} else {
jittered
}
}
}
///|
pub fn Backoff::reset(self : Backoff) -> Unit {
self.attempt = 0
}
///|
/// A point in time measured against the @async wall clock.
pub struct Deadline {
at_ms : Int64
} derive(@debug.Debug)
///|
pub fn Deadline::after_ms(ms : Int64) -> Deadline {
{ at_ms: @async.now() + ms, }
}
///|
pub fn Deadline::expired(self : Deadline) -> Bool {
@async.now() >= self.at_ms
}
///|
/// Milliseconds until expiry; negative once past.
pub fn Deadline::remaining_ms(self : Deadline) -> Int64 {
self.at_ms - @async.now()
}