code wiki / _hdl_build / nx_restart_strategy.nx
nx_restart_strategy.nx source
↩ module page · 62 lines · 3220 B
1// nx_restart_strategy.nx -- S-CLASS supervision restart strategy (operator: "get those s class exceed stable ...
2// we arent s class stable compared to aws"). PURE decision logic (no syscalls -> unit-gateable offline), grounded
3// in the researched SOTA (knowledge/library/rel_erlang_otp.txt = OTP supervisor restart-INTENSITY; rel_backoff.txt
4// = exponential backoff + jitter; rel_circuit_breaker.txt = contain a failing component). THE GAP IT CLOSES: today
5// nx_hostctl restarts a crashing daemon FOREVER (the gallery/vroom crash-loops spin + spam logs + burn CPU). The
6// S-class rule (Erlang OTP): a child may restart at most MaxR times within MaxT seconds; exceed that and the
7// supervisor STOPS restarting it = QUARANTINE + escalate, so the failure is CONTAINED (bounded blast radius) and a
8// human/alert is engaged instead of an infinite loop. Restarts are spaced by EXPONENTIAL BACKOFF (capped + jittered)
9// so a flapping dependency gets time to recover and N daemons don't thunder-herd. license_tier: ORIGINAL
10
11const RS_RESTART: i64 = 0 // within intensity -> restart (after the backoff delay)
12const RS_QUARANTINE: i64 = 1 // crash-loop: restart intensity exceeded -> STOP restarting, escalate (contain it)
13
14// count how many of the recorded restart timestamps (unix sec, ascending or not) fall within `window_sec` of `now`.
15// Old restarts age out of the window, so a daemon that crashed long ago but is now stable is NOT quarantined.
16func rs_restarts_in_window(times: *i64, n: i64, now: i64, window_sec: i64) -> i64 {
17 var c: i64 = 0
18 var i: i64 = 0
19 while i < n {
20 let age: i64 = now - times[i]
21 if age >= 0 { if age <= window_sec { c = c + 1 } }
22 i = i + 1
23 }
24 return c
25}
26
27// the OTP restart-intensity decision: > max_restarts within the window -> QUARANTINE, else RESTART.
28func rs_decide(restarts_in_window: i64, max_restarts: i64) -> i64 {
29 if restarts_in_window > max_restarts { return RS_QUARANTINE }
30 return RS_RESTART
31}
32
33// exponential backoff, CAPPED: delay = min(base_ms * 2^attempt, cap_ms). Doubling each consecutive failure gives a
34// flapping dependency room to recover instead of being hammered every poll.
35func rs_backoff_ms(attempt: i64, base_ms: i64, cap_ms: i64) -> i64 {
36 if attempt < 0 { return base_ms }
37 var d: i64 = base_ms
38 var i: i64 = 0
39 while i < attempt {
40 d = d * 2
41 if d >= cap_ms { return cap_ms }
42 i = i + 1
43 }
44 if d > cap_ms { return cap_ms }
45 return d
46}
47
48// bounded "decorrelated" jitter (deterministic from a seed -> gateable): returns a value in [d/2, d], so N daemons
49// restarting at once don't thunder-herd the same instant (rel_backoff: jitter prevents synchronized retries).
50func rs_jitter_ms(d: i64, seed: i64) -> i64 {
51 if d <= 1 { return d }
52 let half: i64 = d / 2
53 var s: i64 = seed
54 if s < 0 { s = 0 - s }
55 let j: i64 = s % (half + 1)
56 return d - j
57}
58
59// the FULL per-restart decision: given the restart history + now, RESTART (within intensity) or QUARANTINE.
60func rs_should_restart(times: *i64, n: i64, now: i64, window_sec: i64, max_restarts: i64) -> i64 {
61 return rs_decide(rs_restarts_in_window(times, n, now, window_sec), max_restarts)
62}