daemon/plan.bend

cswap-bend-cli@0.1.0.0 · 146 lines · 23 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/store.bend as Store
3import cswap-bend-cli@0.1.0.0/policy/types.bend as T
4import cswap-bend-cli@0.1.0.0/policy/usage.bend as PU
5import cswap-bend-cli@0.1.0.0/daemon/gather.bend as G
6import cswap-bend-cli@0.1.0.0/daemon/view.bend as V
7
8# Which accounts a scheduled auto tick fetches (autoswitch.py
9# _collect_scheduled_usage, contract 4.2 step 3). No network for a fresh row.
10
11def sel_e(+c: Bool, +x: List<&2, G.Ent>, +y: List<&2, G.Ent>) -> List<&2, G.Ent>:
12  match c:
13    case True{}:
14      x
15    case False{}:
16      y
17
18def is_active(+e: G.Ent, +active: Maybe<&2, String>) -> Bool:
19  V.is_slot(active, V.ent_slot(e))
20
21def acct_disabled(+a: T.Account) -> Bool:
22  match a:
23    case T.Account{s, em, k, d}:
24      d
25
26# A candidate can be fetched: not disabled/quarantined/unswitchable.
27def eligible(+e: G.Ent) -> Bool:
28  match e:
29    case G.Ent{s, em, o, c, a}:
30      Bool.not(acct_disabled(a))
31
32def due2(+m: Maybe<&2, T.Usage>, +now: Nat, +cfg: T.Config) -> Bool:
33  match m:
34    case Some{u}:
35      PU.poll_due(u, now, cfg)
36    case None{}:
37      False{}
38
39def due(+e: G.Ent, +us: List<&2, T.Usage>, +now: Nat, +cfg: T.Config) -> Bool:
40  due2(V.usage_for(us, V.slot_nat_of(V.ent_slot(e))), now, cfg)
41
42def stale2(+m: Maybe<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Bool:
43  match m:
44    case Some{u}:
45      Bool.not(PU.fresh(u, now, ms))
46    case None{}:
47      False{}
48
49def stale(+e: G.Ent, +us: List<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Bool:
50  stale2(V.usage_for(us, V.slot_nat_of(V.ent_slot(e))), now, ms)
51
52def fa2(+m: Maybe<&2, T.Usage>) -> Nat:
53  match m:
54    case Some{T.Usage{s, five, fr, seven, sr, ss, fa, bo, er, fl, np, pi, ad, hu, cu, se}}:
55      Maybe.default(&2, Nat, fa, 0n)
56    case _:
57      0n
58
59def fetched_at(+e: G.Ent, +us: List<&2, T.Usage>) -> Nat:
60  fa2(V.usage_for(us, V.slot_nat_of(V.ent_slot(e))))
61
62# The active account, when it is due.
63def active_due(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +active: Maybe<&2, String>, +now: Nat, +cfg: T.Config) -> List<&2, G.Ent>:
64  match es:
65    case Nil{}:
66      []
67    case Con{e, t}:
68      sel_e(is_active(e, active) && eligible(e) && due(e, us, now, cfg), [e], active_due(t, us, active, now, cfg))
69
70# Due candidates, in roster order.
71def due_cands(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +active: Maybe<&2, String>, +now: Nat, +cfg: T.Config) -> List<&2, G.Ent>:
72  match es:
73    case Nil{}:
74      []
75    case Con{e, t}:
76      sel_e(Bool.not(is_active(e, active)) && eligible(e) && due(e, us, now, cfg), e <> due_cands(t, us, active, now, cfg), due_cands(t, us, active, now, cfg))
77
78def sel_m(+c: Bool, +x: Maybe<&2, G.Ent>, +y: Maybe<&2, G.Ent>) -> Maybe<&2, G.Ent>:
79  match c:
80    case True{}:
81      x
82    case False{}:
83      y
84
85def older(+e: G.Ent, +best: Maybe<&2, G.Ent>, +us: List<&2, T.Usage>) -> Maybe<&2, G.Ent>:
86  match best:
87    case None{}:
88      Some{e}
89    case Some{b}:
90      sel_m(Nat.is_lt(fetched_at(e, us), fetched_at(b, us)), Some{e}, best)
91
92# The due candidate with the oldest measurement (the first wins ties).
93def oldest(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +best: Maybe<&2, G.Ent>) -> Maybe<&2, G.Ent>:
94  match es:
95    case Nil{}:
96      best
97    case Con{e, t}:
98      oldest(t, us, older(e, best, us))
99
100def one_of(+m: Maybe<&2, G.Ent>) -> List<&2, G.Ent>:
101  match m:
102    case Some{e}:
103      [e]
104    case None{}:
105      []
106
107# Phase 1: the active account if due, plus at most one due candidate.
108def first_pass(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +active: Maybe<&2, String>, +now: Nat, +cfg: T.Config) -> List<&2, G.Ent>:
109  List.append(&2, G.Ent, active_due(es, us, active, now, cfg), one_of(oldest(due_cands(es, us, active, now, cfg), us, None{})))
110
111def same_slot(+a: G.Ent, +b: G.Ent) -> Bool:
112  String.eq(V.ent_slot(a), V.ent_slot(b))
113
114def in_list(+e: G.Ent, +xs: List<&2, G.Ent>) -> Bool:
115  match xs:
116    case Nil{}:
117      False{}
118    case Con{h, t}:
119      same_slot(e, h) || in_list(e, t)
120
121def exempt3(+u: T.Usage, +h: Maybe<&2, Nat>) -> Bool:
122  match u h:
123    case T.Usage{s, five, fr, seven, sr, ss, fa, bo, er, fl, Some{np}, pi, ad, hu, cu, se} Some{x}:
124      Nat.is_eq(x, 0n) && Nat.is_gt(pi, 600n)
125    case _ _:
126      False{}
127
128def exempt2(+m: Maybe<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Bool:
129  match m:
130    case Some{u}:
131      exempt3(u, PU.headroom(u, now, ms))
132    case None{}:
133      False{}
134
135# Rows planned past a long wait and exhausted are left alone.
136def exempt(+e: G.Ent, +us: List<&2, T.Usage>, +now: Nat, +ms: List<&2, String>) -> Bool:
137  exempt2(V.usage_for(us, V.slot_nat_of(V.ent_slot(e))), now, ms)
138
139# Phase 2 (escalation): every other candidate that is due or stale.
140def escalated(+es: List<&2, G.Ent>, +us: List<&2, T.Usage>, +active: Maybe<&2, String>, +skip: List<&2, G.Ent>, +now: Nat, +cfg: T.Config, +ms: List<&2, String>) -> List<&2, G.Ent>:
141  match es:
142    case Nil{}:
143      []
144    case Con{e, t}:
145      sel_e(Bool.not(is_active(e, active)) && eligible(e) && Bool.not(in_list(e, skip)) && (due(e, us, now, cfg) || stale(e, us, now, ms)) && Bool.not(exempt(e, us, now, ms)),
146        e <> escalated(t, us, active, skip, now, cfg, ms), escalated(t, us, active, skip, now, cfg, ms))