daemon/tick.bend
cswap-bend-cli@0.1.0.0 · 416 lines · 73 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/policy/algo.bend as A
3import cswap-bend-cli@0.1.0.0/json.bend as Json
4import cswap-bend-cli@0.1.0.0/act.bend as Act
5import cswap-bend-cli@0.1.0.0/clock.bend as Clock
6import cswap-bend-cli@0.1.0.0/store.bend as Store
7import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
8import cswap-bend-cli@0.1.0.0/swlive.bend as L
9import cswap-bend-cli@0.1.0.0/policy/types.bend as T
10import cswap-bend-cli@0.1.0.0/policy/usage.bend as PU
11import cswap-bend-cli@0.1.0.0/policy/rank.bend as PR
12import cswap-bend-cli@0.1.0.0/policy/decide.bend as D
13import cswap-bend-cli@0.1.0.0/daemon/events.bend as Ev
14import cswap-bend-cli@0.1.0.0/daemon/cadence.bend as Cad
15import cswap-bend-cli@0.1.0.0/daemon/config.bend as Cfg
16import cswap-bend-cli@0.1.0.0/daemon/gather.bend as G
17import cswap-bend-cli@0.1.0.0/daemon/view.bend as V
18import cswap-bend-cli@0.1.0.0/daemon/plan.bend as Plan
19import cswap-bend-cli@0.1.0.0/daemon/dec.bend as Dc
20import cswap-bend-cli@0.1.0.0/daemon/status.bend as Stat
21
22
23
24
25
26type Env is Data:
27 Env{ctx: SBase.Ctx, cfg: Cfg.Cfg, dry_run: Bool, opts: L.Opts, mem: T.Mem, checked: Bool, skip: List<&2, String>}
28
29
30
31type Out is Data:
32 Out{res: Dc.TickRes, mem: T.Mem, active: Maybe<&2, String>, entries: List<&2, Stat.Entry>, accts: Json.Json, checked: Bool}
33
34
35
36type Next is Data:
37 NDone{out: Out}
38 NAct{out: Out, ctx: Dc.Ctx, dec: Dc.Dec}
39
40
41
42def env_ctx(e: Env) -> SBase.Ctx:
43 match e:
44 case Env{c, cf, d, o, m, k, sk}:
45 c
46
47def env_cfg(e: Env) -> Cfg.Cfg:
48 match e:
49 case Env{c, cf, d, o, m, k, sk}:
50 cf
51
52def env_dry(e: Env) -> Bool:
53 match e:
54 case Env{c, cf, d, o, m, k, sk}:
55 d
56
57def env_opts(e: Env) -> L.Opts:
58 match e:
59 case Env{c, cf, d, o, m, k, sk}:
60 o
61
62def env_checked(e: Env) -> Bool:
63 match e:
64 case Env{c, cf, d, o, m, k, sk}:
65 k
66
67def env_skip(e: Env) -> List<&2, String>:
68 match e:
69 case Env{c, cf, d, o, m, k, sk}:
70 sk
71
72def with_skip(e: Env, +skip: List<&2, String>) -> Env:
73 match e:
74 case Env{c, cf, d, o, m, k, sk}:
75 Env{c, cf, d, o, m, k, skip}
76
77def env_mem(e: Env) -> T.Mem:
78 match e:
79 case Env{c, cf, d, o, m, k, sk}:
80 m
81
82def idle_detail(+live: Bool) -> String:
83 match live:
84 case True{}:
85 "run 'cswap --add-account' to include it in rotation"
86 case False{}:
87 "log in and run 'cswap --add-account' first"
88
89def idle_reason(+live: Bool) -> String:
90 match live:
91 case True{}:
92 "unmanaged-active-account"
93 case False{}:
94 "no-active-account"
95
96def idle_events(+ts: String, +thr: U32, +live: Bool) -> List<&2, Json.Json>:
97 [Ev.poll(ts, Json.JNull{}, [], thr), Ev.no_switch(ts, idle_reason(live), idle_detail(live))]
98
99def idle_out(+env: Env, +ts: String, +live: Bool) -> Out:
100 Out{Dc.TickRes{Cad.Info{Cad.NoAction{}, None{}, False{}, False{}, None{}}, idle_events(ts, Cfg.cfg_threshold(env_cfg(env)), live)}, env_mem(env), None{}, [], Json.arr([]), env_checked(env)}
101
102def has_live(+s: G.Snap) -> Bool:
103 match s:
104 case G.Snap{seq, +live, active, st, rows, ents}:
105 SBase.nonempty(L.lv_email(live)) || SBase.nonempty(L.lv_creds(live))
106
107
108
109def secs(+n: Nat) -> U32:
110 U32.from_nat(n)
111
112def opt_secs(+m: Maybe<&2, Nat>) -> Maybe<&2, U32>:
113 match m:
114 case Some{n}:
115 Some{secs(n)}
116 case None{}:
117 None{}
118
119def exhausted_dec(+e: Maybe<&2, Nat>) -> Dc.Dec:
120 Dc.DExhausted{opt_secs(e)}
121
122def left_headroom(+a: Maybe<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Maybe<&2, U32>:
123 match a:
124 case Some{u}:
125 V.head_u32(PU.headroom(u, now, ms))
126 case None{}:
127 None{}
128
129
130
131def dec_of(+d: T.Decision, +es: List<&2, G.Ent>, +active: Maybe<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Dc.Dec:
132 match d:
133 case T.NoSwitch{T.AllExhausted{e}, detail}:
134 exhausted_dec(e)
135 case T.NoSwitch{r, detail}:
136 Dc.DNone{T.reason.name(r), detail}
137 case T.Switch{+to, trig, why, h, mode, rec, bar}:
138 Dc.DGo{Nat.show(to), V.email_of(es, Nat.show(to)), T.trigger.name(trig), left_headroom(active, now, ms), opt_secs(PR.rec.row(active, now, ms))}
139
140def active_nat(+a: String) -> Nat:
141 V.slot_nat_of(a)
142
143
144
145def next_poll_of(+m: Maybe<&2, T.Usage>) -> Maybe<&2, U32>:
146 match m:
147 case Some{T.Usage{s, five, fr, seven, sr, ss, fa, bo, er, fl, np, pi, ad, hu, cu, se}}:
148 opt_secs(np)
149 case _:
150 None{}
151
152def iso_opt(+m: Maybe<&2, Nat>) -> Maybe<&2, String>:
153 match m:
154 case Some{n}:
155 Some{Clock.iso8601_utc(secs(n))}
156 case None{}:
157 None{}
158
159def sel_bo(+c: Bool, +x: Maybe<&2, String>, +y: Maybe<&2, String>) -> Maybe<&2, String>:
160 match c:
161 case True{}:
162 x
163 case False{}:
164 y
165
166def backoff_iso(+u: T.Usage, +now: Nat) -> Maybe<&2, String>:
167 match u:
168 case T.Usage{s, five, fr, seven, sr, ss, fa, +bo, er, fl, np, pi, ad, hu, cu, se}:
169 sel_bo(Nat.is_gt(bo, now), Some{Clock.iso8601_utc(secs(bo))}, None{})
170
171def opt_u(+m: Maybe<&2, Nat>) -> Maybe<&2, U32>:
172 V.head_u32(m)
173
174def entry_of(+u: T.Usage, +now: Nat, +ms: List<&2, String>, +rows: Json.Json, +e: G.Ent) -> Stat.Entry:
175 match u:
176 case T.Usage{+s, +five, fr, +seven, sr, ss, +fa, bo, er, fl, np, pi, ad, hu, cu, se}:
177 Stat.Entry{Nat.show(s), opt_u(five), opt_u(seven), iso_opt(fa), backoff_iso(u, now), V.err_text(V.row_of(rows, e))}
178
179def entry_push(+m: Maybe<&2, T.Usage>, +e: G.Ent, +rows: Json.Json, +now: Nat, +ms: List<&2, String>, +rest: List<&2, Stat.Entry>) -> List<&2, Stat.Entry>:
180 match m:
181 case Some{u}:
182 entry_of(u, now, ms, rows, e) <> rest
183 case None{}:
184 rest
185
186def entries_of(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +rows: Json.Json, +now: Nat, +ms: List<&2, String>) -> List<&2, Stat.Entry>:
187 match es:
188 case Nil{}:
189 []
190 case Con{e, t}:
191 entry_push(V.usage_for(us, V.slot_nat_of(V.ent_slot(e))), e, rows, now, ms, entries_of(t, us, rows, now, ms))
192
193def poll_event(+ts: String, +active: String, +s: G.Snap, +us: List<&2, T.Usage>, +rows: Json.Json, +now: U32, +cfg: Cfg.Cfg) -> Json.Json:
194 Ev.poll(ts, V.ref_json(active, G.snap_ents(s)), V.poll_rows(G.snap_ents(s), us, rows, Cfg.cfg_models(cfg), V.nat(now)), Cfg.cfg_threshold(cfg))
195
196def prefix_poll(+r: Dc.TickRes, +ts: String, +active: String, +s: G.Snap, +us: List<&2, T.Usage>, +rows: Json.Json, +now: U32, +cfg: Cfg.Cfg, +warn: List<&2, Json.Json>) -> Dc.TickRes:
197 match r:
198 case Dc.TickRes{info, evs}:
199 Dc.TickRes{info, poll_event(ts, active, s, us, rows, now, cfg) <> List.append(&2, Json.Json, warn, evs)}
200
201def attempted2(+m: Maybe<&2, Json.Json>, +now: U32) -> Bool:
202 match m:
203 case Some{Json.JNum{t}}:
204 String.eq(Maybe.default(&2, String, List.head(&2, String, String.split(t, '.')), ""), U32.show(now))
205 case _:
206 False{}
207
208def attempted_now(+row: Json.Json, +now: U32) -> Bool:
209 attempted2(Json.get("lastAttemptAt", row), now)
210
211def ent_kind(+e: G.Ent) -> T.AcctKind:
212 match e:
213 case G.Ent{s, em, o, c, T.Account{n, ae, k, d}}:
214 k
215
216def opt_pct(+m: Maybe<&2, Nat>) -> Json.Json:
217 match m:
218 case Some{p}:
219 Ev.fl(secs(p))
220 case None{}:
221 Json.JNull{}
222
223
224
225def source_of(+attempted_now: Bool, +k: T.AcctKind) -> String:
226 match attempted_now k:
227 case False{} _:
228 "cache"
229 case True{} T.SetupToken{}:
230 "header-probe"
231 case True{} _:
232 "usage-endpoint"
233
234def log_one(+e: G.Ent, +u: T.Usage, +rows: Json.Json, +now: U32, +ms: List<&2, String>) -> Json.Json:
235 match u:
236 case T.Usage{s, five, fr, seven, sr, ss, fa, bo, er, fl, np, pi, ad, hu, cu, se}:
237 Json.JObj{Json.JField{"slot", Json.JNum{Nat.show(s)}, Json.JField{"source", Json.JStr{source_of(attempted_now(V.row_of(rows, e), now), ent_kind(e))},
238 Json.JField{"pct_5h", opt_pct(five), Json.JField{"pct_7d", opt_pct(seven), Json.JField{"error", V.opt_json(V.err_text(V.row_of(rows, e))), Json.JNil{}}}}}}}
239
240def log_push(+m: Maybe<&2, T.Usage>, +e: G.Ent, +rows: Json.Json, +now: U32, +ms: List<&2, String>, +rest: List<&2, Json.Json>) -> List<&2, Json.Json>:
241 match m:
242 case Some{u}:
243 log_one(e, u, rows, now, ms) <> rest
244 case None{}:
245 rest
246
247def log_accts(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +rows: Json.Json, +now: U32, +ms: List<&2, String>) -> List<&2, Json.Json>:
248 match es:
249 case Nil{}:
250 []
251 case Con{e, t}:
252 log_push(V.usage_for(us, V.slot_nat_of(V.ent_slot(e))), e, rows, now, ms, log_accts(t, us, rows, now, ms))
253
254def readable2(+m: Maybe<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Bool:
255 match m:
256 case Some{u}:
257 PU.trusted(u, now, ms)
258 case None{}:
259 False{}
260
261def readable(+e: G.Ent, +us: List<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Bool:
262 readable2(V.usage_for(us, V.slot_nat_of(V.ent_slot(e))), now, ms)
263
264def is_api(+k: T.AcctKind) -> Bool:
265 match k:
266 case T.ApiKey{}:
267 True{}
268 case _:
269 False{}
270
271def watched(+e: G.Ent) -> Bool:
272 Plan.eligible(e) && Bool.not(is_api(ent_kind(e)))
273
274
275
276def all_readable(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Bool:
277 match es:
278 case Nil{}:
279 True{}
280 case Con{e, t}:
281 Bool.not(watched(e)) || (readable(e, us, now, ms) && all_readable(t, us, now, ms))
282
283def keep_name(+is_all: Bool, +h: String, +rest: List<&2, String>) -> List<&2, String>:
284 match is_all:
285 case True{}:
286 rest
287 case False{}:
288 h <> rest
289
290def lower_names(+ms: List<&2, String>) -> List<&2, String>:
291 match ms:
292 case Nil{}:
293 []
294 case Con{h, t}:
295 keep_name(String.eq(String.to_lower(h), "all"), h, lower_names(t))
296
297def checked_after(+env: Env, +es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +now: Nat) -> Bool:
298 env_checked(env) || List.is_empty(&2, String, lower_names(Cfg.cfg_models(env_cfg(env)))) || all_readable(es, us, now, Cfg.cfg_models(env_cfg(env)))
299
300def check_ready(+env: Env, +es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +now: Nat) -> Bool:
301 Bool.not(env_checked(env)) && Bool.not(List.is_empty(&2, String, lower_names(Cfg.cfg_models(env_cfg(env))))) && all_readable(es, us, now, Cfg.cfg_models(env_cfg(env)))
302
303def miss_pick(+found: Bool, +h: String, +rest: List<&2, String>) -> List<&2, String>:
304 match found:
305 case True{}:
306 rest
307 case False{}:
308 h <> rest
309
310def missing(+wanted: List<&2, String>, +seen: List<&2, String>) -> List<&2, String>:
311 match wanted:
312 case Nil{}:
313 []
314 case Con{h, t}:
315 miss_pick(PU.model.wanted(String.to_lower(h), seen), h, missing(t, seen))
316
317def scoped_names(+ss: List<&2, T.Scoped>) -> List<&2, String>:
318 match ss:
319 case Nil{}:
320 []
321 case Con{T.Scoped{n, p, r}, t}:
322 String.to_lower(n) <> scoped_names(t)
323
324def names_of(+m: Maybe<&2, T.Usage>) -> List<&2, String>:
325 match m:
326 case Some{T.Usage{s, five, fr, seven, sr, ss, fa, bo, er, fl, np, pi, ad, hu, cu, se}}:
327 scoped_names(ss)
328 case None{}:
329 []
330
331def seen_names(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>) -> List<&2, String>:
332 match es:
333 case Nil{}:
334 []
335 case Con{e, t}:
336 List.append(&2, String, names_of(V.usage_for(us, V.slot_nat_of(V.ent_slot(e)))), seen_names(t, us))
337
338def warn_text(+names: List<&2, String>) -> String:
339 "autoswitch.model: " ++ String.join(names, ", ") ++ " matches no account's usage windows — only the 5h/7d limits are being watched for it (typo?)"
340
341def warn_events(+ts: String, +miss: List<&2, String>) -> List<&2, Json.Json>:
342 match miss:
343 case Nil{}:
344 []
345 case Con{h, t}:
346 [Ev.config_warning(ts, warn_text(Con{h, t}))]
347
348def warn2(+ready: Bool, env: Env, +es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +ts: String) -> List<&2, Json.Json>:
349 match ready:
350 case True{}:
351 warn_events(ts, missing(lower_names(Cfg.cfg_models(env_cfg(env))), seen_names(es, us)))
352 case False{}:
353 []
354
355def model_warning(+env: Env, +es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +now: Nat, +ts: String) -> List<&2, Json.Json>:
356 warn2(check_ready(env, es, us, now), env, es, us, ts)
357
358def apply_ctx(+env: Env, +s: G.Snap, +active: String, +us: List<&2, T.Usage>, +now: U32, +ts: String) -> Dc.Ctx:
359 Dc.Ctx{env_ctx(env), env_dry(env), env_cfg(env), now, ts, env_opts(env), V.ref_json(active, G.snap_ents(s)), next_poll_of(V.usage_for(us, active_nat(active)))}
360
361def to_dec(env: Env, +s: G.Snap, +active: String, +us: List<&2, T.Usage>, +now: U32, +d: T.Decision) -> Dc.Dec:
362 dec_of(d, G.snap_ents(s), V.usage_for(us, active_nat(active)), V.nat(now), Cfg.cfg_models(env_cfg(env)))
363
364def mem_next(env: Env, +s: G.Snap, +active: String, +us: List<&2, T.Usage>, +now: U32, +pcfg: T.Config) -> T.Mem:
365 D.mem_after(active_nat(active), V.accounts_of(G.snap_ents(s)), us, V.nat(now), pcfg, env_mem(env))
366
367def entries_for(env: Env, +s: G.Snap, +us: List<&2, T.Usage>, +rows: Json.Json, +now: U32) -> List<&2, Stat.Entry>:
368 entries_of(G.snap_ents(s), us, rows, V.nat(now), Cfg.cfg_models(env_cfg(env)))
369
370def log_for(env: Env, +s: G.Snap, +us: List<&2, T.Usage>, +rows: Json.Json, +now: U32) -> Json.Json:
371 Json.arr(log_accts(G.snap_ents(s), us, rows, now, Cfg.cfg_models(env_cfg(env))))
372
373def lead_res(+env: Env, +s: G.Snap, +active: String, +rows: Json.Json, +now: U32, +ts: String, +us: List<&2, T.Usage>, r: Dc.TickRes) -> Dc.TickRes:
374 prefix_poll(r, ts, active, s, us, rows, now, env_cfg(env), model_warning(env, G.snap_ents(s), us, V.nat(now), ts))
375
376def made_out(+env: Env, +s: G.Snap, +active: String, +rows: Json.Json, +now: U32, +ts: String, +us: List<&2, T.Usage>, +pcfg: T.Config, r: Dc.TickRes) -> Out:
377 Out{lead_res(env, s, active, rows, now, ts, us, r), mem_next(env, s, active, us, now, pcfg), Some{active}, entries_for(env, s, us, rows, now), log_for(env, s, us, rows, now), checked_after(env, G.snap_ents(s), us, V.nat(now))}
378
379def lead_out(env: Env, +s: G.Snap, +active: String, +rows: Json.Json, +now: U32, +ts: String, +us: List<&2, T.Usage>, +pcfg: T.Config) -> Out:
380 made_out(env, s, active, rows, now, ts, us, pcfg, Dc.TickRes{Cad.Info{Cad.NoAction{}, None{}, False{}, False{}, None{}}, []})
381
382
383
384def decide_out3(+env: Env, +s: G.Snap, +active: String, +rows: Json.Json, +now: U32, +ts: String, +us: List<&2, T.Usage>, +pcfg: T.Config, +d: T.Decision) -> Next:
385 NAct{lead_out(env, s, active, rows, now, ts, us, pcfg), apply_ctx(env, s, active, us, now, ts), to_dec(env, s, active, us, now, d)}
386
387def decide_out2(+env: Env, +s: G.Snap, +active: String, +rows: Json.Json, +now: U32, +ts: String, +us: List<&2, T.Usage>, +pcfg: T.Config) -> Next:
388 decide_out3(env, s, active, rows, now, ts, us, pcfg, A.decide_cfg(Cfg.cfg_strategy(env_cfg(env)), active_nat(active), V.snap_accounts(s), us, V.nat(now), pcfg, V.snap_state(s), env_mem(env)))
389
390def decide_out(+env: Env, +s: G.Snap, +active: String, +rows: Json.Json, +now: U32, +ts: String) -> Next:
391 decide_out2(env, s, active, rows, now, ts, V.usages_of(rows, G.snap_ents(s), Some{active}, now), V.pol_cfg(env_cfg(env)))
392
393def active_out(env: Env, +s: G.Snap, +active: String, +now: U32, +ts: String) -> Next:
394 decide_out(env, s, active, G.snap_rows(s), now, ts)
395
396def snap_out2(env: Env, +now: U32, +ts: String, +s: G.Snap, +active: Maybe<&2, String>) -> Next:
397 match active:
398 case None{}:
399 NDone{idle_out(env, ts, has_live(s))}
400 case Some{a}:
401 active_out(env, s, a, now, ts)
402
403def snap_out(env: Env, +now: U32, +ts: String, +m: Maybe<&2, G.Snap>) -> Next:
404 match m:
405 case None{}:
406 NDone{idle_out(env, ts, False{})}
407 case Some{s}:
408 snap_out2(env, now, ts, s, G.snap_active(s))
409
410
411def tick(+env: Env, +now: U32, +m: Maybe<&2, G.Snap>) -> Next:
412 snap_out(env, now, Clock.iso8601_utc(now), G.skip_snap(m, env_skip(env)))
413
414
415
416