// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// How long to wait before trying a request again.
///
/// The specification requires an exponential backoff with jitter so that a fleet
/// of exporters does not return to a recovering collector in step. The numbers are
/// the ones the reference implementations settled on: five seconds at first,
/// growing by half each time, never more than thirty, and a fifth of the wait
/// spread either way by the caller's own randomness.
#derive(Eq, Debug)
pub(all) struct Backoff {
initial : Int64
multiplier : Double
maximum : Int64
jitter : Double
}
///|
pub extend Backoff with Eq::{equal, not_equal}
///|
pub extend Backoff with Debug::{to_repr}
///|
/// The default: 5s, times 1.5, capped at 30s, with 20% jitter.
pub let otlp_backoff : Backoff = {
initial: 5000L,
multiplier: 1.5,
maximum: 30000L,
jitter: 0.2,
}
///|
/// A backoff with any of the four values changed.
pub fn Backoff::new(
initial~ : Int64 = otlp_backoff.initial,
multiplier~ : Double = otlp_backoff.multiplier,
maximum~ : Int64 = otlp_backoff.maximum,
jitter~ : Double = otlp_backoff.jitter,
) -> Backoff {
{ initial, multiplier, maximum, jitter, }
}
///|
/// The wait before attempt number `attempt`, counting the first retry as one.
///
/// `rand` is the caller's randomness: four octets are read for the jitter, and a
/// generator that returns anything else leaves the wait unjittered rather than
/// making the exporter fail. A delay is a scheduling decision, not a reason to
/// drop telemetry.
pub fn Backoff::delay(
self : Backoff,
attempt : Int32,
rand : (Int32) -> Bytes,
) -> Int64 {
let ceiling = self.maximum.to_double()
let mut wait = self.initial.to_double()
let mut step = 1
while step < attempt && wait < ceiling {
wait = wait * self.multiplier
step = step + 1
}
if wait > ceiling {
wait = ceiling
}
let spread = wait * self.jitter
let bytes = rand(4)
if bytes.length() != 4 {
return wait.to_int64()
}
let mut bits : UInt64 = 0UL
for byte in bytes {
bits = (bits << 8) | byte.to_int().to_uint64()
}
// An interval of four octets over the whole of `UInt64`'s range, so the jitter
// is uniform over `wait ± spread` rather than always late or always early.
let fraction = bits.to_double() / 4294967296.0
let low = wait - spread
let high = wait + spread
let jittered = (low + (high - low) * fraction).to_int64()
// The maximum is a bound, not a starting point: a jittered wait that would
// overrun it is the bound.
if jittered > self.maximum {
self.maximum
} else {
jittered
}
}