nx_netscope_heal_loop.nx source
↩ module page · 122 lines · 5463 B
1// nx_netscope_heal_loop.nx -- NX-NETSCOPE rung 3, part 2: the live MAPE-K
2// loop's SAFETY BRAIN. Pure, deterministic, sovereign (raw NishiLang, no
3// deps) -> KAT-able offline. These are the "first do no harm" guards that
4// turn the decision core (nx_netscope_heal.nx) into a self-healing loop
5// that converges instead of thrashing. Concepts (re-implemented, no code
6// copied): AWS "Exponential Backoff and Jitter" (Full Jitter), the
7// circuit breaker (Nygard/Fowler), and a token-bucket retry budget
8// (metastable-failure avoidance). The remaining thin I/O wrapper (real
9// sys_clock_nanosleep sleep + set-resolver) is the deployment step.
10//
11// license_tier: ORIGINAL
12
13import "nx_netscope_heal.nx"
14const HLOOP_MAGIC_2654435761: i64 = 2654435761
15const HLOOP_MAGIC_1103515245: i64 = 1103515245
16const HLOOP_MAGIC_12345: i64 = 12345
17
18// ---- loop step outcomes (sealed) ----
19const HLOOP_STABLE: i64 = 0 // healthy -> no action, resume monitoring
20const HLOOP_ACT: i64 = 1 // execute the switch now
21const HLOOP_WAIT: i64 = 2 // back off (out_wait ms) then retry
22const HLOOP_GIVEUP: i64 = 3 // stop safely (no candidate / budget spent)
23
24// ---- circuit-breaker states ----
25const HCB_CLOSED: i64 = 0
26const HCB_OPEN: i64 = 1
27const HCB_HALFOPEN: i64 = 2
28
29struct HealCircuit { state: i64, fails: i64, opened_ms: i64 } // 24 B
30const HEAL_CIRCUIT_BYTES: i64 = 24
31struct HealBudget { tokens: i64, last_refill_ms: i64 } // 16 B
32const HEAL_BUDGET_BYTES: i64 = 16
33
34// ---- deterministic jitter (sovereign LCG mix; seedable for KATs) ----
35// Anti-synchronization only (not crypto): a fleet must not retry in
36// lockstep. Returns a value in [0, range).
37func heal_jitter(seed: i64, attempt: i64, range: i64) -> i64 {
38 if range <= 0 { return 0 }
39 var h: i64 = (seed + attempt * HLOOP_MAGIC_2654435761) * HLOOP_MAGIC_1103515245 + HLOOP_MAGIC_12345
40 h = h & 0x7fffffffffffffff // force non-negative
41 return h % range
42}
43
44// Full Jitter backoff: sleep in [0, min(cap_ms, base_ms * 2^attempt)].
45// The exponential CEILING grows then clamps to cap; the actual wait is a
46// uniform sample below it (AWS Full Jitter -- best anti-storm behavior).
47func heal_backoff_ms(base_ms: i64, cap_ms: i64, attempt: i64, seed: i64) -> i64 {
48 var ceil: i64 = base_ms
49 var a: i64 = 0
50 while a < attempt {
51 ceil = ceil * 2
52 if ceil >= cap_ms { ceil = cap_ms; a = attempt } else { a = a + 1 }
53 }
54 if ceil > cap_ms { ceil = cap_ms }
55 if ceil < 0 { ceil = cap_ms } // overflow guard
56 return heal_jitter(seed, attempt, ceil + 1)
57}
58
59// ---- circuit breaker (per candidate) ----
60func heal_cb_init(cb: *HealCircuit) -> i64 {
61 cb.state = HCB_CLOSED; cb.fails = 0; cb.opened_ms = 0
62 return 0
63}
64// allow a trial now? CLOSED=yes; OPEN=no until open_ms elapsed (then
65// HALF-OPEN allows ONE trial); HALF-OPEN=yes (the trial).
66func heal_cb_allow(cb: *HealCircuit, now_ms: i64, open_ms: i64) -> i64 {
67 if cb.state == HCB_CLOSED { return 1 }
68 if cb.state == HCB_OPEN {
69 if (now_ms - cb.opened_ms) >= open_ms { cb.state = HCB_HALFOPEN; return 1 }
70 return 0
71 }
72 return 1 // HALF-OPEN: permit the single probe
73}
74func heal_cb_on_fail(cb: *HealCircuit, now_ms: i64, threshold: i64) -> i64 {
75 if cb.state == HCB_HALFOPEN { cb.state = HCB_OPEN; cb.opened_ms = now_ms; return 0 }
76 cb.fails = cb.fails + 1
77 if cb.fails >= threshold { cb.state = HCB_OPEN; cb.opened_ms = now_ms }
78 return 0
79}
80func heal_cb_on_success(cb: *HealCircuit) -> i64 {
81 cb.state = HCB_CLOSED; cb.fails = 0
82 return 0
83}
84
85// ---- retry budget (token bucket: bounds total remediation churn) ----
86func heal_budget_init(b: *HealBudget, tokens: i64) -> i64 {
87 b.tokens = tokens; b.last_refill_ms = 0
88 return 0
89}
90// refill one token per refill_ms (cap max_tokens), then try to spend one.
91func heal_budget_try(b: *HealBudget, now_ms: i64, refill_ms: i64, max_tokens: i64) -> i64 {
92 if (now_ms - b.last_refill_ms) >= refill_ms {
93 if b.tokens < max_tokens { b.tokens = b.tokens + 1 }
94 b.last_refill_ms = now_ms
95 }
96 if b.tokens > 0 { b.tokens = b.tokens - 1; return 1 }
97 return 0
98}
99
100// ---- the composed MAPE-K step ----
101// Given the decision-core action + the candidate's circuit state + the
102// retry budget + the attempt count, decide the loop's next move and the
103// backoff wait. This is the PLAN/EXECUTE gate -- pure, so the whole loop
104// is KAT-able by scripting the inputs.
105func heal_loop_next(decision_action: i64, cb: *HealCircuit, budget: *HealBudget,
106 attempt: i64, now_ms: i64,
107 base_ms: i64, cap_ms: i64, cb_open_ms: i64,
108 refill_ms: i64, max_tokens: i64, seed: i64,
109 out_wait: *i64) -> i64 {
110 *out_wait = 0
111 if decision_action == HEAL_KEEP { return HLOOP_STABLE } // healthy
112 if decision_action == HEAL_GIVEUP { return HLOOP_GIVEUP } // nothing correct reachable
113 // decision == HEAL_SWITCH: we WANT to act.
114 if heal_cb_allow(cb, now_ms, cb_open_ms) == 0 {
115 *out_wait = heal_backoff_ms(base_ms, cap_ms, attempt, seed) // candidate cooling down
116 return HLOOP_WAIT
117 }
118 if heal_budget_try(budget, now_ms, refill_ms, max_tokens) == 0 {
119 return HLOOP_GIVEUP // remediation budget spent -> stop, don't thrash
120 }
121 return HLOOP_ACT // safe to switch now
122}