1 # Retry policy: a shared attempt budget, an operation deadline, bounded backoff, and a circuit breaker. 2 # All times are ms on one monotonic clock. The caller reads the clock; these defs are pure. 3 import Base 4 5 # left: attempts the whole operation may still start, the first one included. Nested layers share one Budget. 6 # deadline: no attempt starts at or after this time. None: no deadline. 7 type Budget is Data: 8 Budget{left: Nat, deadline: Maybe<&2, Nat>} 9 10 type Deny is Data: 11 DenyLate{} 12 DenySpent{} 13 DenyOpen{} 14 15 type Gate is Data: 16 GateGo{} 17 GateNo{why: Deny} 18 19 # threshold: failures in a row, each inside window ms of the newest, that open the circuit. cooldown: ms it stays open. 20 # probes: attempts allowed at once while half-open. 21 type Circuit is Data: 22 Circuit{threshold: Nat, window: Nat, cooldown: Nat, probes: Nat} 23 24 # Closed: the failures since the last success that are still inside the window, newest first. 25 # Open: no attempt before until. Half: probes in flight. 26 type State is Data: 27 CircuitClosed{fails: List<&2, Nat>} 28 CircuitOpen{until: Nat} 29 CircuitHalf{busy: Nat} 30 31 type Breaker is Data: 32 BreakerOff{} 33 BreakerOn{circuit: Circuit, state: State} 34 35 # Settled: a success, or a failure that another try would repeat. Unsafe: a transient failure of a 36 # request that must not repeat. Transient: another try can help; after is the server's wait in ms. 37 type Outcome is Data: 38 Settled{} 39 Unsafe{} 40 Transient{after: Maybe<&2, Nat>} 41 42 # attempts: tries in one layer, the first one included. base: the first backoff in ms. cap: the longest wait in ms. 43 # deadline: ms from the start of the operation to its deadline. None: no deadline. 44 type Policy is Data: 45 Policy{attempts: Nat, base: Nat, cap: U32, deadline: Maybe<&2, Nat>} 46 47 type Plan is Data: 48 PlanStop{} 49 PlanWait{ms: Nat} 50 51 # ---- budget 52 53 def Budget.new(+attempts: Nat, +now: Nat, ms: Maybe<&2, Nat>) -> Budget: 54 match ms: 55 case None{}: 56 Budget{attempts, None{}} 57 case Some{m}: 58 Budget{attempts, Some{Nat.add(now, m)}} 59 60 def Budget.late(+now: Nat, d: Maybe<&2, Nat>) -> Bool: 61 match d: 62 case None{}: 63 False{} 64 case Some{t}: 65 Nat.is_le(t, now) 66 67 # ---- circuit 68 69 # The failure times still inside the window: now < t + window. Newest first, so the first old time ends the list. 70 def prune(+now: Nat, +window: Nat, fs: List<&2, Nat>) -> List<&2, Nat>: 71 match fs: 72 case Nil{}: 73 Nil{} 74 case Con{+t, rest}: 75 Bool.pick(List<&2, Nat>, Nat.is_lt(now, Nat.add(t, window)), Con{t, prune(now, window, rest)}, Nil{}) 76 77 def Circuit.probes(c: Circuit) -> Nat: 78 Circuit{t, w, cd, p} = c 79 p 80 81 def Circuit.cooldown(c: Circuit) -> Nat: 82 Circuit{t, w, cd, p} = c 83 cd 84 85 def half.admit(go: Bool, +k: Nat) -> State & Bool: 86 match go: 87 case True{}: 88 (CircuitHalf{1n+k}, True{}) 89 case False{}: 90 (CircuitHalf{k}, False{}) 91 92 def open.admit(early: Bool, +p: Nat, +u: Nat) -> State & Bool: 93 match early: 94 case True{}: 95 (CircuitOpen{u}, False{}) 96 case False{}: 97 half.admit(Nat.is_lt(0n, p), 0n) 98 99 # Closed admits. Open admits nothing before until; at or after until it turns half-open and admits a probe. 100 # Half-open admits while fewer than probes are in flight. 101 def State.admit(+c: Circuit, s: State, +now: Nat) -> State & Bool: 102 match s: 103 case CircuitClosed{fs}: 104 (CircuitClosed{fs}, True{}) 105 case CircuitOpen{+u}: 106 open.admit(Nat.is_lt(now, u), Circuit.probes(c), u) 107 case CircuitHalf{+k}: 108 half.admit(Nat.is_lt(k, Circuit.probes(c)), k) 109 110 def closed.trip(under: Bool, fs: List<&2, Nat>, +until: Nat) -> State: 111 match under: 112 case True{}: 113 CircuitClosed{fs} 114 case False{}: 115 CircuitOpen{until} 116 117 def closed.fail(+c: Circuit, fs: List<&2, Nat>, +now: Nat) -> State: 118 Circuit{+t, +w, +cd, p} = c 119 +kept = {Con{now, prune(now, w, fs)} : List<&2, Nat>} 120 closed.trip(Nat.is_lt(List.length(&2, Nat, kept), t), kept, Nat.add(now, cd)) 121 122 # A closed success clears the failures. A closed failure opens the circuit when the window then holds threshold 123 # failures. A half-open success closes it; a half-open failure opens it again for cooldown. An attempt that ends 124 # while open changes nothing. 125 def State.done(+c: Circuit, s: State, +now: Nat, failed: Bool) -> State: 126 match s: 127 case CircuitClosed{fs}: 128 match failed: 129 case False{}: 130 CircuitClosed{Nil{}} 131 case True{}: 132 closed.fail(c, fs, now) 133 case CircuitOpen{u}: 134 CircuitOpen{u} 135 case CircuitHalf{k}: 136 match failed: 137 case False{}: 138 CircuitClosed{Nil{}} 139 case True{}: 140 CircuitOpen{Nat.add(now, Circuit.cooldown(c))} 141 142 def Breaker.new(c: Circuit) -> Breaker: 143 BreakerOn{c, CircuitClosed{Nil{}}} 144 145 def Breaker.on(+c: Circuit, x: State & Bool) -> Breaker & Bool: 146 (s, ok) = x 147 (BreakerOn{c, s}, ok) 148 149 def Breaker.admit(br: Breaker, +now: Nat) -> Breaker & Bool: 150 match br: 151 case BreakerOff{}: 152 (BreakerOff{}, True{}) 153 case BreakerOn{+c, s}: 154 Breaker.on(c, State.admit(c, s, now)) 155 156 def Breaker.done(br: Breaker, +now: Nat, failed: Bool) -> Breaker: 157 match br: 158 case BreakerOff{}: 159 BreakerOff{} 160 case BreakerOn{+c, s}: 161 BreakerOn{c, State.done(c, s, now, failed)} 162 163 # ---- admission 164 165 def admit.ok(ok: Bool, br: Breaker, +p: Nat, d: Maybe<&2, Nat>) -> Breaker & Budget & Gate: 166 match ok: 167 case True{}: 168 (br, Budget{p, d}, GateGo{}) 169 case False{}: 170 (br, Budget{1n+p, d}, GateNo{DenyOpen{}}) 171 172 def admit.breaker(x: Breaker & Bool, +p: Nat, d: Maybe<&2, Nat>) -> Breaker & Budget & Gate: 173 (br, ok) = x 174 admit.ok(ok, br, p, d) 175 176 def admit.late(late: Bool, br: Breaker, left: Nat, d: Maybe<&2, Nat>, +now: Nat) -> Breaker & Budget & Gate: 177 match late: 178 case True{}: 179 (br, Budget{left, d}, GateNo{DenyLate{}}) 180 case False{}: 181 match left: 182 case 0n: 183 (br, Budget{0n, d}, GateNo{DenySpent{}}) 184 case 1n+p: 185 admit.breaker(Breaker.admit(br, now), p, d) 186 187 # Ask to start one attempt at now. The deadline is checked first, then the budget, then the breaker. 188 # Only GateGo takes one attempt from the budget. 189 def admit(br: Breaker, b: Budget, +now: Nat) -> Breaker & Budget & Gate: 190 Budget{left, +d} = b 191 admit.late(Budget.late(now, d), br, left, d, now) 192 193 # ---- backoff 194 195 # min(cap, base * 2^n), doubled one step at a time, so no step passes cap. 196 def delay(n: Nat, +base: Nat, +cap: Nat) -> Nat: 197 match n: 198 case 0n: 199 Nat.min(base, cap) 200 case 1n+p: 201 +d = delay(p, base, cap) 202 Nat.min(Nat.add(d, d), cap) 203 204 # Equal jitter: d / 2, plus r mod (d / 2 + 1). The wait is in [d / 2, d] for every r. 205 def jitter(+d: Nat, +r: Nat) -> Nat: 206 +h = Nat.div(d, 2n) 207 Nat.add(h, Nat.mod(r, 1n+h)) 208 209 # The wait before retry n (0 first): the server's wait when it gave one, else the jittered backoff. At most cap. 210 def wait(after: Maybe<&2, Nat>, p: Policy, +n: Nat, +r: Nat) -> Nat: 211 match after: 212 case None{}: 213 Policy{a, +base, +cap, d} = p 214 jitter(delay(n, base, U32.to_nat(cap)), r) 215 case Some{+ms}: 216 Policy{a, base, +cap, d} = p 217 Nat.min(ms, U32.to_nat(cap)) 218 219 def plan.left(left: Nat, +w: Nat, d: Maybe<&2, Nat>, +now: Nat) -> Plan: 220 match left: 221 case 0n: 222 PlanStop{} 223 case 1n+k: 224 Bool.pick(Plan, Budget.late(Nat.add(now, w), d), PlanStop{}, PlanWait{w}) 225 226 def plan.at(+w: Nat, b: Budget, +now: Nat) -> Plan: 227 Budget{left, d} = b 228 plan.left(left, w, d, now) 229 230 # After an attempt ends at now: stop, or wait ms and then ask admit again. Settled and Unsafe always stop. 231 # A transient outcome stops when the budget is spent or the wait would reach the deadline. 232 def plan(o: Outcome, p: Policy, b: Budget, +n: Nat, +now: Nat, +r: Nat) -> Plan: 233 match o: 234 case Settled{}: 235 PlanStop{} 236 case Unsafe{}: 237 PlanStop{} 238 case Transient{after}: 239 plan.at(wait(after, p, n, r), b, now) 240 241 # Unsafe and Transient count as failures for the breaker. 242 def failed(o: Outcome) -> Bool: 243 match o: 244 case Settled{}: 245 False{} 246 case Unsafe{}: 247 True{} 248 case Transient{after}: 249 True{}