code wiki / (root) / nx_resgov_core.nx

nx_resgov_core.nx source

↩ module page · 171 lines · 8871 B

1// nx_resgov_core.nx -- PURE DECISION CORE of the unified resource-governance regime (nx_resgov). 2// 3// WHY THIS SUPERSEDES THE VSZ WATCHDOG (operator 2026-08-04: "resource management should be proactive, 4// intelligent and SOTA, not watching the hardware get hammered"; debt 1785863639). The incumbent breaker 5// had three structural defects, each fixed here BY CONSTRUCTION: 6// 1. WRONG INSTRUMENT. It thresholded on VSZ. VSZ is reserved ADDRESS SPACE -- nx_wiki_gw sits at 47.4GB 7// VSZ on 355MB resident and is perfectly healthy; a naive VSZ rule kills it. The pressure that 8// actually degraded the estate on 08-04 was RESIDENT memory + SWAP. We meter VmRSS + VmSwap. 9// 2. COVERAGE BY ALLOW-LIST. Rows named 7 remembered processes; every grower nobody listed was 10// invisible (a transient 4GB nx_web_shard_co appeared and exited mid-session -- the population is 11// dynamic and unlistable by hand). Here the policy is INVERTED: a DEFAULT cap covers EVERY process 12// and exemptions are the explicit, auditable rows. ★A DETECTOR'S COVERAGE IS TWO SETS -- WHAT IT 13// MATCHES AND WHERE IT LOOKS; a default-deny breaker has no blind names. 14// 3. REACTIVE ONLY. It fired after a ceiling was crossed -- i.e. after the hardware was already being 15// hammered. Here the primary signal is PREDICTIVE: growth VELOCITY from persisted samples gives an 16// ETA to the ceiling, and the ladder engages while headroom still exists. 17// 18// ★ESCALATION, NEVER A BINARY KILL (rule 14, graceful degradation). Levels: OK -> WATCH (measured, logged) 19// -> WARN (predicted to breach inside the horizon) -> RENICE (deprioritise, let it finish) -> TERM (polite, 20// guard respawns fresh) -> KILL (last resort). A breaker whose only verb is SIGKILL cannot be trusted to 21// run every minute, so it gets disabled -- which is exactly how the incumbent came to be unwired. 22// 23// ★SYSTEM PRESSURE GATES ACTION. A 3GB process on an idle box is not a problem; the same process at 95% 24// memory is. Every destructive level requires BOTH a per-process finding AND real system scarcity, so a 25// healthy fleet is never disturbed. 26// 27// FAIL-SAFE BY CONSTRUCTION: unknown/unreadable measurement -> level OK (never act on ignorance); 28// exempt -> capped at OK; cap<1 -> inert row; cooldown suppresses storms; the CLI never touches pid<=300 29// or itself. Decisions live here (gate-locked, pure); the /proc walk, state and signals live in the CLI. 30// license_tier: ORIGINAL No hw writes (Rule 26). 31import "nx_syscalls.nx" 32const RG_MAGIC_1048576: i64 = 1048576 33 34// ---- levels ------------------------------------------------------------------------------------ 35const RG_OK: i64 = 0 36const RG_WATCH: i64 = 1 37const RG_WARN: i64 = 2 38const RG_RENICE: i64 = 3 39const RG_TERM: i64 = 4 40const RG_KILL: i64 = 5 41 42// ---- system pressure --------------------------------------------------------------------------- 43// headroom in permil of total (1000 = all free). -1 when unmeasurable -> callers treat as NO pressure. 44func rg_headroom_permil(avail_kb: i64, total_kb: i64) -> i64 { 45 if total_kb <= 0 { return 0 - 1 } 46 if avail_kb < 0 { return 0 - 1 } 47 return avail_kb * 1000 / total_kb 48} 49// swap consumed in permil. -1 unmeasurable / no swap configured. 50func rg_swap_permil(used_kb: i64, total_kb: i64) -> i64 { 51 if total_kb <= 0 { return 0 - 1 } 52 if used_kb < 0 { return 0 - 1 } 53 return used_kb * 1000 / total_kb 54} 55// Is the SYSTEM actually scarce? 1 only when memory headroom is below its floor OR swap is above its 56// ceiling. Unmeasurable inputs (-1) are NOT scarcity -- ignorance must never authorise a kill. 57func rg_system_pressured(headroom_permil: i64, swap_permil: i64, head_floor: i64, swap_ceil: i64) -> i64 { 58 if headroom_permil >= 0 { if headroom_permil < head_floor { return 1 } } 59 if swap_permil >= 0 { if swap_permil > swap_ceil { return 1 } } 60 return 0 61} 62 63// ---- growth velocity (the proactive signal) ---------------------------------------------------- 64// kB per second between two samples of the SAME process identity. 0 when no usable prior, when the 65// clock did not advance, or when the process SHRANK (a shrink is not negative growth to act on). 66// ⚠Identity is the caller's job: pid alone is reusable, so the CLI keys samples on pid+starttime. 67func rg_velocity_kb_s(prev_kb: i64, prev_t: i64, now_kb: i64, now_t: i64) -> i64 { 68 if prev_kb <= 0 { return 0 } 69 if prev_t <= 0 { return 0 } 70 if now_t <= prev_t { return 0 } 71 if now_kb <= prev_kb { return 0 } 72 return (now_kb - prev_kb) / (now_t - prev_t) 73} 74// seconds until `now_kb` reaches `ceil_kb` at `vel_kb_s`. -1 = not on course (not growing, or already 75// at/over the ceiling -- an already-breached process is the cap rule's business, not the forecast's). 76func rg_eta_s(now_kb: i64, ceil_kb: i64, vel_kb_s: i64) -> i64 { 77 if vel_kb_s <= 0 { return 0 - 1 } 78 if ceil_kb <= 0 { return 0 - 1 } 79 if now_kb >= ceil_kb { return 0 - 1 } 80 return (ceil_kb - now_kb) / vel_kb_s 81} 82 83// ---- the decision ------------------------------------------------------------------------------ 84// footprint = the resource that actually hurts: resident + swapped-out pages of ONE process. 85func rg_footprint_kb(rss_kb: i64, swap_kb: i64) -> i64 { 86 var t: i64 = 0 87 if rss_kb > 0 { t = t + rss_kb } 88 if swap_kb > 0 { t = t + swap_kb } 89 return t 90} 91 92// THE level decision. Pure; every input measured, every branch fail-safe. 93// exempt=1 -> OK always (an exemption is a promise, honoured unconditionally) 94// cap_gb < 1 -> OK (inert row; never a 0-threshold kill-everything) 95// footprint unmeasurable (<=0) -> OK (never act on ignorance) 96// over cap + system pressured -> TERM, or KILL if it ALSO already had a TERM this window 97// over cap, system calm -> WATCH (recorded, not acted on -- big is not the same as harmful) 98// ETA inside act horizon + pressured -> RENICE (proactive: slow it while headroom remains) 99// ETA inside warn horizon -> WARN (logged forecast; the operator-visible early signal) 100func rg_level(exempt: i64, cap_gb: i64, rss_kb: i64, swap_kb: i64, vel_kb_s: i64, 101 system_pressured: i64, already_termed: i64, 102 eta_warn_s: i64, eta_act_s: i64) -> i64 { 103 if exempt == 1 { return RG_OK } 104 if cap_gb < 1 { return RG_OK } 105 let foot: i64 = rg_footprint_kb(rss_kb, swap_kb) 106 if foot <= 0 { return RG_OK } 107 let ceil_kb: i64 = cap_gb * RG_MAGIC_1048576 108 if foot > ceil_kb { 109 if system_pressured == 1 { 110 if already_termed == 1 { return RG_KILL } 111 return RG_TERM 112 } 113 return RG_WATCH 114 } 115 let eta: i64 = rg_eta_s(foot, ceil_kb, vel_kb_s) 116 if eta >= 0 { 117 if eta <= eta_act_s { if system_pressured == 1 { return RG_RENICE } return RG_WARN } 118 if eta <= eta_warn_s { return RG_WARN } 119 } 120 return RG_OK 121} 122 123// cooldown: 1 = allowed to act again. A destructive level must never repeat inside its window. 124func rg_cooldown_ok(last_action_s: i64, now_s: i64, cooldown_s: i64) -> i64 { 125 if last_action_s <= 0 { return 1 } 126 if now_s - last_action_s >= cooldown_s { return 1 } 127 return 0 128} 129 130// level -> stable label (structured logging, rule 18: what happened and why) 131func rg_level_name(l: i64) -> *u8 { 132 if l == RG_OK { return "OK" as *u8 } 133 if l == RG_WATCH { return "WATCH" as *u8 } 134 if l == RG_WARN { return "WARN" as *u8 } 135 if l == RG_RENICE { return "RENICE" as *u8 } 136 if l == RG_TERM { return "TERM" as *u8 } 137 if l == RG_KILL { return "KILL" as *u8 } 138 return "?" as *u8 139} 140// is this level destructive (needs pressure + cooldown + a real signal)? 141func rg_is_destructive(l: i64) -> i64 { 142 if l == RG_TERM { return 1 } 143 if l == RG_KILL { return 1 } 144 return 0 145} 146 147// ---- /proc/meminfo parsing --------------------------------------------------------------------- 148// kB for a "<Label>:" row in a meminfo-shaped buffer. -1 absent (never 0 -- absent and zero differ). 149func rg_meminfo_kb(buf: *u8, n: i64, pat: *u8) -> i64 { 150 var pl: i64 = 0 151 while pat[pl] != (0 as u8) { pl = pl + 1 } 152 var i: i64 = 0 153 while i + pl <= n { 154 var k: i64 = 0 155 var ok: i64 = 1 156 while k < pl { if buf[i+k] != pat[k] { ok = 0; k = pl } k = k + 1 } 157 if ok == 1 { 158 var j: i64 = i + pl 159 var go: i64 = 1 160 while go == 1 { go = 0; if j < n { let c: i64 = buf[j] as i64; if c == 32 { j = j + 1; go = 1 } else { if c == 9 { j = j + 1; go = 1 } } } } 161 var v: i64 = 0 162 var any: i64 = 0 163 go = 1 164 while go == 1 { go = 0; if j < n { let c2: i64 = buf[j] as i64; if c2 >= 48 { if c2 <= 57 { v = v*10 + (c2-48); any = 1; j = j + 1; go = 1 } } } } 165 if any == 0 { return 0 - 1 } 166 return v 167 } 168 i = i + 1 169 } 170 return 0 - 1 171}