policy/usage.bend
cswap-bend-cli@0.1.0.0 · 378 lines · 56 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/policy/types.bend as T
3
4
5
6
7
8def pct.two(+t: Nat, +o: Nat) -> String:
9 match o:
10 case 0n:
11 Nat.show(t)
12 case 1n+p:
13 Nat.show(t) ++ Nat.show(o)
14
15def pct.frac.pair(qr: Nat & Nat) -> String:
16 (t, o) = qr
17 pct.two(t, o)
18
19def pct.frac(+q: Nat, +r: Nat) -> String:
20 match r:
21 case 0n:
22 Nat.show(q)
23 case 1n+p:
24 Nat.show(q) ++ "." ++ pct.frac.pair(Nat.divmod(r, 10n))
25
26def pct.label.pair(qr: Nat & Nat) -> String:
27 (q, r) = qr
28 pct.frac(q, r)
29
30
31def pct.label(+c: Nat) -> String:
32 pct.label.pair(Nat.divmod(c, 100n))
33
34
35
36def win.opt(+p: Maybe<&2, Nat>, +r: Maybe<&2, Nat>, +rest: List<&2, T.Win>) -> List<&2, T.Win>:
37 match p:
38 case None{}:
39 rest
40 case Some{x}:
41 Con{T.Win{x, r}, rest}
42
43def win.keep(+hit: Bool, +w: T.Win, +rest: List<&2, T.Win>) -> List<&2, T.Win>:
44 match hit:
45 case True{}:
46 Con{w, rest}
47 case False{}:
48 rest
49
50
51def model.wanted(+n: String, +ms: List<&2, String>) -> Bool:
52 match ms:
53 case Nil{}:
54 False{}
55 case Con{m, t}:
56 String.eq(n, String.to_lower(m)) || String.eq("all", String.to_lower(m)) || model.wanted(n, t)
57
58def win.scoped.one(+s: T.Scoped, +ms: List<&2, String>, +rest: List<&2, T.Win>) -> List<&2, T.Win>:
59 match s:
60 case T.Scoped{name, p, r}:
61 win.keep(model.wanted(String.to_lower(name), ms), T.Win{p, r}, rest)
62
63def win.scoped(+ss: List<&2, T.Scoped>, +ms: List<&2, String>) -> List<&2, T.Win>:
64 match ss:
65 case Nil{}:
66 Nil{}
67 case Con{s, t}:
68 win.scoped.one(s, ms, win.scoped(t, ms))
69
70
71
72def windows(+u: T.Usage, +ms: List<&2, String>) -> List<&2, T.Win>:
73 match u:
74 case T.Usage{slot, five, fr, seven, sr, ss, fa, bo, er, nfl, np, pi, ad, hu, cu, se}:
75 win.opt(five, fr, win.opt(seven, sr, win.scoped(ss, ms)))
76
77def win.max.step(+acc: Maybe<&2, Nat>, +p: Nat) -> Maybe<&2, Nat>:
78 match acc:
79 case None{}:
80 Some{p}
81 case Some{x}:
82 Some{Nat.max(x, p)}
83
84def win.max(+ws: List<&2, T.Win>, +acc: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
85 match ws:
86 case Nil{}:
87 acc
88 case Con{w, t}:
89 match w:
90 case T.Win{p, r}:
91 win.max(t, win.max.step(acc, p))
92
93def head.of(+m: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
94 match m:
95 case None{}:
96 None{}
97 case Some{p}:
98 Some{Nat.sub(T.full(), p)}
99
100
101def headroom.raw(+u: T.Usage, +ms: List<&2, String>) -> Maybe<&2, Nat>:
102 head.of(win.max(windows(u, ms), None{}))
103
104
105
106def err.is_none(+e: T.ErrKind) -> Bool:
107 match e:
108 case T.ErrNone{}:
109 True{}
110 case o:
111 False{}
112
113def before.opt(+now: Nat, +m: Maybe<&2, Nat>) -> Bool:
114 match m:
115 case None{}:
116 False{}
117 case Some{n}:
118 Nat.is_lt(now, n)
119
120
121
122def fresh.ext(+now: Nat, +nf: Nat, +np: Maybe<&2, Nat>, +hu: Nat, +cu: Nat) -> Bool:
123 Nat.is_gt(nf, 0n) || before.opt(now, np) || Nat.is_lt(now, hu) || Nat.is_lt(now, cu)
124
125def win.reset(+w: T.Win) -> Maybe<&2, Nat>:
126 match w:
127 case T.Win{p, r}:
128 r
129
130def soon.min(+a: Maybe<&2, Nat>, +b: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
131 match a b:
132 case None{} _:
133 b
134 case Some{x} None{}:
135 a
136 case Some{x} Some{y}:
137 Some{Nat.min(x, y)}
138
139
140
141def win.soonest(+ws: List<&2, T.Win>, +acc: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
142 match ws:
143 case Nil{}:
144 acc
145 case Con{w, t}:
146 win.soonest(t, soon.min(acc, win.reset(w)))
147
148
149def soon.ok(+now: Nat, +soonest: Maybe<&2, Nat>) -> Bool:
150 match soonest:
151 case None{}:
152 True{}
153 case Some{s}:
154 Nat.is_lt(now, s)
155
156
157
158
159
160def within.ceiling(+e: T.ErrKind, +age: Nat, +now: Nat, +soonest: Maybe<&2, Nat>) -> Bool:
161 match e:
162 case T.ErrHttp429{}:
163 Nat.is_lt(age, 7200n) && soon.ok(now, soonest)
164 case o:
165 Nat.is_le(age, 3600n)
166
167def fresh.age(+age: Nat, +ext: Bool, +within: Bool) -> Bool:
168 Nat.is_le(age, 300n) || (ext && within)
169
170
171
172
173def fresh.at(+fa: Maybe<&2, Nat>, +now: Nat, +e: T.ErrKind, +nf: Nat, +np: Maybe<&2, Nat>, +hu: Nat, +cu: Nat, +soonest: Maybe<&2, Nat>) -> Bool:
174 match fa:
175 case None{}:
176 False{}
177 case Some{f}:
178 fresh.age(Nat.sub(now, f), fresh.ext(now, nf, np, hu, cu), within.ceiling(e, Nat.sub(now, f), now, soonest))
179
180def fresh(+u: T.Usage, +now: Nat, +ms: List<&2, String>) -> Bool:
181 match u:
182 case T.Usage{slot, five, fr, seven, sr, ss, fa, bo, +er, nfl, np, pi, ad, hu, cu, +se}:
183 fresh.at(fa, now, er, nfl, np, hu, cu, win.soonest(windows(u, ms), None{}))
184
185def sent.none(+s: T.Sentinel) -> Bool:
186 match s:
187 case T.NoSentinel{}:
188 True{}
189 case o:
190 False{}
191
192
193def trusted(+u: T.Usage, +now: Nat, +ms: List<&2, String>) -> Bool:
194 match u:
195 case T.Usage{slot, five, fr, seven, sr, ss, fa, bo, +er, nfl, np, pi, ad, hu, cu, +se}:
196 sent.none(se) && fresh.at(fa, now, er, nfl, np, hu, cu, win.soonest(windows(u, ms), None{}))
197
198def head.if(+ok: Bool, +h: Maybe<&2, Nat>) -> Maybe<&2, Nat>:
199 match ok:
200 case True{}:
201 h
202 case False{}:
203 None{}
204
205
206
207def headroom(+u: T.Usage, +now: Nat, +ms: List<&2, String>) -> Maybe<&2, Nat>:
208 head.if(trusted(u, now, ms), headroom.raw(u, ms))
209
210def usage.slot(+u: T.Usage) -> Nat:
211 match u:
212 case T.Usage{slot, five, fr, seven, sr, ss, fa, bo, er, nfl, np, pi, ad, hu, cu, se}:
213 slot
214
215def usage.backoff_until(+u: T.Usage) -> Nat:
216 match u:
217 case T.Usage{slot, five, fr, seven, sr, ss, fa, bo, er, nfl, np, pi, ad, hu, cu, se}:
218 bo
219
220def in_backoff(+u: T.Usage, +now: Nat) -> Bool:
221 Nat.is_lt(now, usage.backoff_until(u))
222
223def usage.sentinel(+u: T.Usage) -> T.Sentinel:
224 match u:
225 case T.Usage{slot, five, fr, seven, sr, ss, fa, bo, er, nfl, np, pi, ad, hu, cu, se}:
226 se
227
228def usage.of.pick(+hit: Bool, +u: T.Usage, +rest: Maybe<&2, T.Usage>) -> Maybe<&2, T.Usage>:
229 match hit:
230 case True{}:
231 Some{u}
232 case False{}:
233 rest
234
235
236def usage.of(+slot: Nat, +us: List<&2, T.Usage>) -> Maybe<&2, T.Usage>:
237 match us:
238 case Nil{}:
239 None{}
240 case Con{u, t}:
241 usage.of.pick(Nat.is_eq(slot, usage.slot(u)), u, usage.of(slot, t))
242
243def account.slot(+a: T.Account) -> Nat:
244 match a:
245 case T.Account{slot, email, kind, disabled}:
246 slot
247
248def account.of.pick(+hit: Bool, +a: T.Account, +rest: Maybe<&2, T.Account>) -> Maybe<&2, T.Account>:
249 match hit:
250 case True{}:
251 Some{a}
252 case False{}:
253 rest
254
255def account.of(+slot: Nat, +as: List<&2, T.Account>) -> Maybe<&2, T.Account>:
256 match as:
257 case Nil{}:
258 None{}
259 case Con{a, t}:
260 account.of.pick(Nat.is_eq(slot, account.slot(a)), a, account.of(slot, t))
261
262
263def head.row(+m: Maybe<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Maybe<&2, Nat>:
264 match m:
265 case None{}:
266 None{}
267 case Some{u}:
268 headroom(u, now, ms)
269
270
271
272def poll.stale(+fa: Maybe<&2, Nat>, +now: Nat, +ttl: Nat) -> Bool:
273 match fa:
274 case None{}:
275 True{}
276 case Some{f}:
277 Nat.is_gt(Nat.sub(now, f), ttl)
278
279def poll.planned_due(+np: Maybe<&2, Nat>, +now: Nat) -> Bool:
280 match np:
281 case None{}:
282 False{}
283 case Some{n}:
284 Nat.is_ge(now, n)
285
286
287def poll.overslept(+np: Maybe<&2, Nat>, +now: Nat, +pi: Nat) -> Bool:
288 match np:
289 case None{}:
290 False{}
291 case Some{n}:
292 Nat.is_gt(Nat.mul(10n, Nat.sub(Nat.sub(n, now), 60n)), Nat.mul(11n, Nat.max(pi, 600n)))
293
294def poll.unplanned(+np: Maybe<&2, Nat>) -> Bool:
295 match np:
296 case None{}:
297 True{}
298 case Some{n}:
299 False{}
300
301def poll.ready(+np: Maybe<&2, Nat>, +now: Nat, +pi: Nat, +fa: Maybe<&2, Nat>, +ttl: Nat) -> Bool:
302 poll.planned_due(np, now)
303 || (poll.stale(fa, now, ttl) && (poll.unplanned(np) || poll.overslept(np, now, pi)))
304
305def poll.gate(+blocked: Bool, +ready: Bool) -> Bool:
306 match blocked:
307 case True{}:
308 False{}
309 case False{}:
310 ready
311
312
313
314def poll_due(+u: T.Usage, +now: Nat, +cfg: T.Config) -> Bool:
315 match u:
316 case T.Usage{slot, five, fr, seven, sr, ss, fa, +bo, er, nfl, np, pi, ad, hu, cu, se}:
317 match cfg:
318 case T.Config{thr, ms, st, iv, cd, hy, ttl, ut, ia}:
319 poll.gate(
320 Nat.is_lt(now, bo) || Nat.is_ge(ad, 1n) || Nat.is_lt(now, hu) || Nat.is_lt(now, cu),
321 poll.ready(np, now, pi, fa, ttl)
322 )
323
324
325
326def backoff.cap(+k: Nat) -> Nat:
327 Nat.min(600n, Nat.mul(30n, Nat.pow(2n, Nat.min(Nat.sub(k, 1n), 5n))))
328
329def backoff.zero(+rl: Bool, +computed: Nat) -> Nat:
330 match rl:
331 case True{}:
332 Nat.min(Nat.max(computed, 300n), 600n)
333 case False{}:
334 computed
335
336def backoff.bump(+big: Bool, +ra: Nat) -> Nat:
337 match big:
338 case True{}:
339 Nat.add(ra, 900n)
340 case False{}:
341 ra
342
343def backoff.asked(+rl: Bool, +ra: Nat) -> Nat:
344 match rl:
345 case True{}:
346 Nat.min(4500n, backoff.bump(Nat.is_gt(ra, 600n), ra))
347 case False{}:
348 Nat.min(3600n, ra)
349
350def backoff.some(+rl: Bool, +ra: Nat, +computed: Nat) -> Nat:
351 Nat.max(backoff.asked(rl, ra), computed)
352
353def backoff.retry.some(+r: Nat, +rl: Bool, +computed: Nat) -> Nat:
354 match r:
355 case 0n:
356 backoff.zero(rl, computed)
357 case 1n+p:
358 backoff.some(rl, r, computed)
359
360def backoff.rate_limited(+e: T.ErrKind) -> Bool:
361 match e:
362 case T.ErrHttp429{}:
363 True{}
364 case o:
365 False{}
366
367def backoff.retry(+rl: Bool, +ra: Maybe<&2, Nat>, +computed: Nat) -> Nat:
368 match ra:
369 case None{}:
370 computed
371 case Some{r}:
372 backoff.retry.some(r, rl, computed)
373
374
375
376
377def backoff_for(+n: Nat, +e: T.ErrKind, +ra: Maybe<&2, Nat>) -> Nat:
378 backoff.retry(backoff.rate_limited(e), ra, backoff.cap(n))