code wiki / (root) / nx_supervisor.nx

nx_supervisor.nx source

↩ module page · 275 lines · 10627 B

1// nx_supervisor.nx -- Erlang-OTP-style restart-policy state machine. 2// 3// Closes the SSS-class "subsystem restart gap" named in 4// NISHI_HONEST_TRADE_OFFS.md row 2 + NISHI_PREEMPTIVE_BUG_ABSORPTION.md. 5// Per cardinal user-owns-every-bit: this is a STATE MACHINE primitive. 6// The substrate doesn't fork or exec anything -- caller does the 7// actual process management. Supervisor returns "what action to 8// take now"; caller obeys. 9// 10// Substrate's structural prevention of three known supervisor anti- 11// patterns: 12// 13// 1. Restart storms: max_restarts counter -> ESCALATE once exceeded 14// (don't burn CPU on infinite crash-restart loop) 15// 2. Split-brain restarts: caller is single-supervisor-per-child; 16// sealed enum state means "is this child being restarted right 17// now or not" is unambiguous 18// 3. State-loss across restart: snapshot-before-mark via 19// nx_module_cas integration (queued v2; v1 is stateless) 20// 21// Exponential backoff (RFC 7234-class): 22// 1st crash: base_backoff_ms wait 23// 2nd: 2 * base wait 24// 3rd: 4 * base wait 25// Nth: min(2^(N-1) * base, max_backoff_ms) 26// 27// Healthy-window reset: 28// if (now - last_restart_ms) > healthy_threshold_ms, counter 29// resets to 0. Per Erlang OTP: a child that's been up long 30// enough has "recovered" and the next crash isn't part of the 31// crash storm. 32// 33// Sealed enum: NxSupState (4 states): 34// HEALTHY child running normally (or never-started) 35// BACKING_OFF child crashed; waiting for backoff_ms before restart 36// ESCALATING exceeded max_restarts; parent supervisor should take over 37// TERMINATED explicit stop; no further restart 38// 39// Sealed enum: NxSupAction (4 decisions): 40// ACTION_NONE child healthy, no action needed 41// ACTION_RESTART restart child immediately 42// ACTION_WAIT still backing off, check again at next_check_ms 43// ACTION_ESCALATE give up; let parent supervisor handle 44// 45// nx_capability_claims: 46// needs: [sealed_enum, sys_now_ms, exp_backoff_math] 47// provides: [supervisor_state_machine, restart_policy_decision, 48// max_restarts_escalate, healthy_window_reset] 49// safety: [no_unchecked_deref, no_floating_point, 50// no_process_management_in_substrate (caller-driven)] 51// verdict: [sealed_enum_4_state + sealed_enum_4_action] 52// license: ORIGINAL 53// kind: racing_crew_specialist 54// layer: L3 (algorithm: state machine over time + counters) 55// sss: [S6 sealed verdicts; row-2 SSS issue closer] 56 57import "nx_syscalls_x86_64.nx" 58 59// ---- Sealed enum: supervisor state ------------------------------- 60 61const NX_SUP_HEALTHY: i64 = 0 62const NX_SUP_BACKING_OFF: i64 = 1 63const NX_SUP_ESCALATING: i64 = 2 64const NX_SUP_TERMINATED: i64 = 3 65const NX_SUP_STATE_N: i64 = 4 66 67func nx_supervisor_state_is_valid(s: i64) -> i64 { 68 if s < 0 { return 0 } 69 if s >= NX_SUP_STATE_N { return 0 } 70 return 1 71} 72 73func nx_supervisor_state_name(s: i64) -> *u8 { 74 if s == NX_SUP_HEALTHY { return "HEALTHY" as *u8 } 75 if s == NX_SUP_BACKING_OFF { return "BACKING_OFF" as *u8 } 76 if s == NX_SUP_ESCALATING { return "ESCALATING" as *u8 } 77 if s == NX_SUP_TERMINATED { return "TERMINATED" as *u8 } 78 return "INVALID" as *u8 79} 80 81// ---- Sealed enum: supervisor action ------------------------------ 82 83const NX_SUP_ACTION_NONE: i64 = 0 84const NX_SUP_ACTION_RESTART: i64 = 1 85const NX_SUP_ACTION_WAIT: i64 = 2 86const NX_SUP_ACTION_ESCALATE: i64 = 3 87const NX_SUP_ACTION_N: i64 = 4 88 89func nx_supervisor_action_is_valid(a: i64) -> i64 { 90 if a < 0 { return 0 } 91 if a >= NX_SUP_ACTION_N { return 0 } 92 return 1 93} 94 95func nx_supervisor_action_name(a: i64) -> *u8 { 96 if a == NX_SUP_ACTION_NONE { return "NONE" as *u8 } 97 if a == NX_SUP_ACTION_RESTART { return "RESTART" as *u8 } 98 if a == NX_SUP_ACTION_WAIT { return "WAIT" as *u8 } 99 if a == NX_SUP_ACTION_ESCALATE { return "ESCALATE" as *u8 } 100 return "INVALID" as *u8 101} 102 103// ---- Supervisor state struct ------------------------------------- 104 105struct NxSupervisor { 106 // Configuration (set at init; never modified after) 107 max_restarts: i64, // hard ceiling; over -> ESCALATING 108 base_backoff_ms: i64, // first crash wait 109 max_backoff_ms: i64, // cap on exponential growth 110 healthy_threshold_ms: i64, // uptime -> counter reset 111 112 // Mutable state 113 state: i64, // sealed enum NX_SUP_* 114 n_restarts: i64, // accumulated this storm 115 n_escalations: i64, // lifetime escalation count 116 last_crash_ms: i64, // when child last crashed 117 last_restart_ms: i64, // when caller last restarted 118 next_check_ms: i64, // earliest time WAIT becomes RESTART 119} 120 121const NX_SUPERVISOR_BYTES: i64 = 80 // 10 i64 fields 122 123// ---- Init / config ----------------------------------------------- 124 125// Initialize a caller-allocated NxSupervisor. Returns 0 or -1 on 126// BAD_ARG. 127func nx_supervisor_init(sup: *NxSupervisor, 128 max_restarts: i64, 129 base_backoff_ms: i64, 130 max_backoff_ms: i64, 131 healthy_threshold_ms: i64) -> i64 { 132 if sup == (0 as *NxSupervisor) { return -1 } 133 if max_restarts < 0 { return -1 } 134 if base_backoff_ms < 0 { return -1 } 135 if max_backoff_ms < base_backoff_ms { return -1 } 136 if healthy_threshold_ms < 0 { return -1 } 137 sup.max_restarts = max_restarts 138 sup.base_backoff_ms = base_backoff_ms 139 sup.max_backoff_ms = max_backoff_ms 140 sup.healthy_threshold_ms = healthy_threshold_ms 141 sup.state = NX_SUP_HEALTHY 142 sup.n_restarts = 0 143 sup.n_escalations = 0 144 sup.last_crash_ms = 0 145 sup.last_restart_ms = 0 146 sup.next_check_ms = 0 147 return 0 148} 149 150// ---- Exp backoff helper ------------------------------------------ 151 152// Compute backoff_ms = min(base * 2^(n_restarts), max_backoff). 153// n_restarts is the count of THIS storm (resets after healthy window). 154// Returns the backoff value (>=0); never panics on overflow. 155func nx_supervisor_backoff_ms(sup: *NxSupervisor) -> i64 { 156 if sup == (0 as *NxSupervisor) { return 0 } 157 if sup.n_restarts <= 0 { return sup.base_backoff_ms } 158 // Bound the shift to avoid overflow. base * 2^30 is plenty. 159 var shift: i64 = sup.n_restarts - 1 160 if shift > 30 { shift = 30 } 161 let mult: i64 = 1 << shift 162 let candidate: i64 = sup.base_backoff_ms * mult 163 if candidate > sup.max_backoff_ms { return sup.max_backoff_ms } 164 if candidate < 0 { return sup.max_backoff_ms } // overflow guard 165 return candidate 166} 167 168// ---- Event recorders --------------------------------------------- 169// 170// Caller calls these AT THE EVENT (not on the decision tick). 171 172// Record that the child crashed AT now_ms. Transitions HEALTHY -> 173// BACKING_OFF (with backoff computed from current counter). If 174// already in ESCALATING / TERMINATED, no-op. 175func nx_supervisor_record_crash(sup: *NxSupervisor, now_ms: i64) -> i64 { 176 if sup == (0 as *NxSupervisor) { return -1 } 177 if sup.state == NX_SUP_ESCALATING { return 0 } 178 if sup.state == NX_SUP_TERMINATED { return 0 } 179 // Healthy-window reset: if it's been a long time since the last 180 // crash, this isn't part of the same storm. 181 if sup.last_crash_ms > 0 { 182 let since: i64 = now_ms - sup.last_crash_ms 183 if since > sup.healthy_threshold_ms { 184 sup.n_restarts = 0 185 } 186 } 187 sup.last_crash_ms = now_ms 188 sup.n_restarts = sup.n_restarts + 1 189 // Exceeded the max -> escalate. 190 if sup.n_restarts > sup.max_restarts { 191 sup.state = NX_SUP_ESCALATING 192 sup.n_escalations = sup.n_escalations + 1 193 return 0 194 } 195 let backoff: i64 = nx_supervisor_backoff_ms(sup) 196 sup.next_check_ms = now_ms + backoff 197 sup.state = NX_SUP_BACKING_OFF 198 return 0 199} 200 201// Record that the caller successfully restarted the child AT now_ms. 202// Transitions BACKING_OFF -> HEALTHY. 203func nx_supervisor_record_start(sup: *NxSupervisor, now_ms: i64) -> i64 { 204 if sup == (0 as *NxSupervisor) { return -1 } 205 if sup.state == NX_SUP_ESCALATING { return 0 } 206 if sup.state == NX_SUP_TERMINATED { return 0 } 207 sup.last_restart_ms = now_ms 208 sup.state = NX_SUP_HEALTHY 209 return 0 210} 211 212// Explicit stop. Transitions any -> TERMINATED. Caller should 213// invoke before deliberate shutdown so decide() returns ACTION_NONE 214// rather than ACTION_RESTART after a clean exit. 215func nx_supervisor_terminate(sup: *NxSupervisor) -> i64 { 216 if sup == (0 as *NxSupervisor) { return -1 } 217 sup.state = NX_SUP_TERMINATED 218 return 0 219} 220 221// ---- Decision tick ----------------------------------------------- 222// 223// The single load-bearing entry point. Caller calls this on every 224// tick of its supervision loop with (child_alive, now_ms). 225// Returns one of NX_SUP_ACTION_*. 226 227func nx_supervisor_decide(sup: *NxSupervisor, 228 child_alive: i64, 229 now_ms: i64) -> i64 { 230 if sup == (0 as *NxSupervisor) { return NX_SUP_ACTION_ESCALATE } 231 232 // TERMINATED: never act again. 233 if sup.state == NX_SUP_TERMINATED { return NX_SUP_ACTION_NONE } 234 235 // ESCALATING: tell parent supervisor. 236 if sup.state == NX_SUP_ESCALATING { return NX_SUP_ACTION_ESCALATE } 237 238 // HEALTHY + alive: nothing to do. 239 if sup.state == NX_SUP_HEALTHY { 240 if child_alive == 1 { return NX_SUP_ACTION_NONE } 241 // Healthy but child died -- caller hasn't called record_crash yet. 242 // Treat this as the start of a backoff. (Defensive: caller 243 // should have called record_crash, but we don't trust.) 244 nx_supervisor_record_crash(sup, now_ms) 245 // After record_crash, state is BACKING_OFF or ESCALATING; fall 246 // through to BACKING_OFF logic below. 247 } 248 249 if sup.state == NX_SUP_ESCALATING { return NX_SUP_ACTION_ESCALATE } 250 251 // BACKING_OFF: time to restart? 252 if sup.state == NX_SUP_BACKING_OFF { 253 if now_ms < sup.next_check_ms { return NX_SUP_ACTION_WAIT } 254 return NX_SUP_ACTION_RESTART 255 } 256 257 return NX_SUP_ACTION_NONE 258} 259 260// ---- Observers --------------------------------------------------- 261 262func nx_supervisor_state(sup: *NxSupervisor) -> i64 { 263 if sup == (0 as *NxSupervisor) { return -1 } 264 return sup.state 265} 266 267func nx_supervisor_n_restarts(sup: *NxSupervisor) -> i64 { 268 if sup == (0 as *NxSupervisor) { return -1 } 269 return sup.n_restarts 270} 271 272func nx_supervisor_n_escalations(sup: *NxSupervisor) -> i64 { 273 if sup == (0 as *NxSupervisor) { return -1 } 274 return sup.n_escalations 275}