1 import Base 2 import ../json.bend as Json 3 import ../act.bend as Act 4 import ../fs.bend as Fs 5 import ../clock.bend as Clock 6 import ../store.bend as Store 7 import ../store/base.bend as SBase 8 import ../store/seq.bend as SSeq 9 import ../swlive.bend as L 10 import ../switch.bend as Sw 11 import ../swbase.bend as SwB 12 import ./dec.bend as Dc 13 import ./events.bend as Ev 14 import ./cadence.bend as Cad 15 import ./state.bend as St 16 import ./config.bend as Cfg 17 18 # Acting on a decision: print the event, and for a switch run it under the 19 # autoswitch state lock (cooldown re-checked there, as `_perform` does), 20 # record it, and say what the sleeper needs to know. A switch that fails 21 # becomes an `error` event, never a crash. 22 23 # What the policy decided, in the units the daemon acts on. Headroom is 24 # centi-percent, times unix seconds. 25 26 def ActM(-A: Type) -> Type: 27 Act.of(A) 28 29 def ActM.pure(-A: Type, x: A) -> Act.of(A): 30 Act.pure(A, x) 31 32 def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B): 33 Act.bind(A, B, m, f) 34 35 def warnings_of(+w: Json.Json) -> List<&2, String>: 36 SwB.texts_of(w) 37 38 def switched_res(+c: Dc.Ctx, +trigger: String, +r: Json.Json) -> Dc.TickRes: 39 Dc.one(Dc.info(Cad.Switched{}, None{}, False{}, False{}, Dc.ctx_np(c)), Ev.switch(Dc.ctx_ts(c), trigger, SSeq.get_or("from", r, Json.JNull{}), SSeq.get_or("to", r, Json.JNull{}), warnings_of(SSeq.get_or("warnings", r, Json.JNull{})), False{})) 40 41 def num_of(+m: Maybe<&2, Json.Json>) -> Maybe<&2, U32>: 42 match m: 43 case Some{j}: 44 Json.as_u32(j) 45 case None{}: 46 None{} 47 48 def from_number(+r: Json.Json) -> Maybe<&2, U32>: 49 num_of(Json.get("number", SSeq.get_or("from", r, Json.JNull{}))) 50 51 def record_sw(+c: Dc.Ctx, +root: String, +to: String, +trigger: String, +lh: Maybe<&2, U32>, +lr: Maybe<&2, U32>, +r: Json.Json) -> Act.of(Dc.TickRes): 52 do ActM: 53 st : Json.Json <- Act.lift(Json.Json, St.read_state(root)) 54 St.write_state(root, St.record_switch(st, Dc.ctx_now(c), to, from_number(r), lh, lr, trigger)) 55 return switched_res(c, trigger, r) 56 57 def after_switch2(+switched: Bool, +c: Dc.Ctx, +root: String, +to: String, +trigger: String, +lh: Maybe<&2, U32>, +lr: Maybe<&2, U32>, +r: Json.Json) -> Act.of(Dc.TickRes): 58 match switched: 59 case False{}: 60 Act.pure(Dc.TickRes, Dc.none_res(c, "already-active", SSeq.field_str(r, "reason"))) 61 case True{}: 62 record_sw(c, root, to, trigger, lh, lr, r) 63 64 def bool_of(+m: Maybe<&2, Json.Json>) -> Bool: 65 match m: 66 case Some{Json.JBool{b}}: 67 b 68 case _: 69 False{} 70 71 def is_switched(+r: Json.Json) -> Bool: 72 bool_of(Json.get("switched", r)) 73 74 def after_switch(+c: Dc.Ctx, +root: String, go: Dc.Dec, +r: Json.Json) -> Act.of(Dc.TickRes): 75 match go: 76 case Dc.DGo{to, email, trigger, lh, lr}: 77 after_switch2(is_switched(r), c, root, to, trigger, lh, lr, r) 78 case _: 79 Act.pure(Dc.TickRes, Dc.error_res(c, "internal: not a switch")) 80 81 def cooldown_applies(+trigger: String) -> Bool: 82 String.eq(trigger, "proactive") || String.eq(trigger, "consume-first") 83 84 def cfg_cooldown(+cf: Cfg.Cfg) -> U32: 85 match cf: 86 case Cfg.Cfg{th, iv, cd, hy, st, ia, ut, ms}: 87 cd 88 89 def run_switch(+c: Dc.Ctx, +root: String, go: Dc.Dec, +to: String, +trigger: String) -> Act.of(Dc.TickRes): 90 do ActM: 91 r : Json.Json <- Sw.switch_to(Dc.ctx_store(c), to, False{}, Dc.ctx_opts(c)) 92 after_switch(c, root, go, r) 93 94 def locked_switch2(+cool: Bool, +c: Dc.Ctx, +root: String, go: Dc.Dec, +to: String, +trigger: String) -> Act.of(Dc.TickRes): 95 Act.cond(Dc.TickRes, cool, Act.pure(Dc.TickRes, Dc.none_res(c, "cooldown", "")), run_switch(c, root, go, to, trigger)) 96 97 def in_cool(+c: Dc.Ctx, +st: Json.Json, +trigger: String) -> Bool: 98 cooldown_applies(trigger) && St.in_cooldown(st, Dc.ctx_now(c), cfg_cooldown(Dc.ctx_cfg(c))) 99 100 def locked_switch(+c: Dc.Ctx, +root: String, go: Dc.Dec, +to: String, +trigger: String) -> Act.of(Dc.TickRes): 101 do ActM: 102 st : Json.Json <- Act.lift(Json.Json, St.read_state(root)) 103 locked_switch2(in_cool(c, st, trigger), c, root, go, to, trigger) 104 105 def dry_res(+c: Dc.Ctx, +trigger: String, +to: String, +email: String) -> Dc.TickRes: 106 Dc.one(Dc.info(Cad.Switched{}, None{}, False{}, False{}, Dc.ctx_np(c)), Ev.switch(Dc.ctx_ts(c), trigger, Dc.ctx_active(c), Ev.ref(to, email), [], True{})) 107 108 def go_res(+c: Dc.Ctx, go: Dc.Dec) -> Act.of(Dc.TickRes): 109 match go: 110 case Dc.DGo{+to, +email, +trigger, lh, lr}: 111 Act.cond(Dc.TickRes, Dc.ctx_dry(c), Act.pure(Dc.TickRes, dry_res(c, trigger, to, email)), 112 St.with_state_lock(Dc.TickRes, SBase.root_of(Dc.ctx_store(c)), locked_switch(c, SBase.root_of(Dc.ctx_store(c)), go, to, trigger))) 113 case _: 114 Act.pure(Dc.TickRes, Dc.error_res(c, "internal: not a switch")) 115 116 # A failed switch is an error event, not a crashed tick. 117 def recover(+c: Dc.Ctx, r: Fs.Res(Dc.TickRes)) -> Act.of(Dc.TickRes): 118 match r: 119 case Done{t}: 120 Act.pure(Dc.TickRes, t) 121 case Fail{(code, msg)}: 122 Act.pure(Dc.TickRes, Dc.error_res(c, msg)) 123 124 def go_safe(+c: Dc.Ctx, go: Dc.Dec) -> Act.of(Dc.TickRes): 125 IO.bind(Fs.Res(Dc.TickRes), Fs.Res(Dc.TickRes), go_res(c, go), r => recover(c, r)) 126 127 # Act on `d`. 128 def apply(+c: Dc.Ctx, d: Dc.Dec) -> Act.of(Dc.TickRes): 129 match d: 130 case Dc.DNone{reason, detail}: 131 Act.pure(Dc.TickRes, Dc.none_res(c, reason, detail)) 132 case Dc.DExhausted{earliest}: 133 Act.pure(Dc.TickRes, Dc.exhausted_res(c, earliest)) 134 case Dc.DGo{to, email, trigger, lh, lr}: 135 go_safe(c, d)