
Backoff.pudu
Pudu85 lines3.1 KB
1/** @Domain.Backoff.Module — delays between retries, with and without jitter */2module PuduLangResilience.Domain.Backoff34import Std.Math.Float as Float5import Std.Math as Math6import Std.Option as Option7import PuduLangResilience.Utils.Numeric as Numeric89/** @Domain.Backoff.Shape — how the delay grows with each retry */10export type Shape = Constant | Linear | Exponential1112/** @Domain.Backoff.Plan — the inputs every delay is computed from */13export type Plan = { shape: Shape, delay: Int, maxDelay: Option[Int], useJitter: Bool }141516const JITTER_SPREAD: Float = 0.5171819const CURVE_SMOOTHING: Float = 4.0202122const CURVE_SCALE: Float = 0.714285714285714323242526export fn next(plan: &Plan, attempt: Int, state: Float, draw: Float) -> (Int, Float) {27 if plan.delay <= 0 { return (0, state) }28 let raw = if plan.useJitter { jittered(plan, attempt, state, draw) } else { (steady(plan, attempt), state) }29 (capped(plan, raw[0]), raw[1])30}313233export fn steady(plan: &Plan, attempt: Int) -> Int {34 match plan.shape {35 case Constant => plan.delay36 case Linear => Numeric.multiply(Numeric.add(attempt, 1), plan.delay)37 case Exponential => doubled(plan.delay, attempt)38 }39}404142fn doubled(value: Int, times: Int) -> Int {43 var held = value44 var step = 045 while step < times {46 if held == Numeric.LARGEST { return held }47 held = Numeric.multiply(held, 2)48 step = step + 149 }50 held51}525354fn capped(plan: &Plan, delay: Int) -> Int {55 match plan.maxDelay {56 case Some(ceiling) => Math.min(delay, ceiling)57 case None => delay58 }59}606162fn jittered(plan: &Plan, attempt: Int, state: Float, draw: Float) -> (Int, Float) {63 match plan.shape {64 case Exponential => decorrelated(plan.delay, attempt, state, draw)65 case _ => (spread(steady(plan, attempt), draw), state)66 }67}686970fn spread(delay: Int, draw: Float) -> Int {71 let base = Numeric.toFloat(delay)72 Numeric.toInt(base + base * JITTER_SPREAD * draw - base * JITTER_SPREAD / 2.0)73}74757677fn decorrelated(delay: Int, attempt: Int, state: Float, draw: Float) -> (Int, Float) {78 let t = Numeric.toFloat(attempt) + draw79 let growth = Option.unwrapOr(Float.powf(2.0, t), 0.0 / 0.0)80 let root = Option.unwrapOr(Float.sqrt(CURVE_SMOOTHING * t), 0.0)81 let curve = growth * Float.tanh(root)82 if !Float.isFinite(curve) { return (Numeric.LARGEST, curve) }83 (Numeric.toInt((curve - state) * CURVE_SCALE * Numeric.toFloat(delay)), curve)84}85