nx_source_health.nx source
↩ module page · 186 lines · 9153 B
1// nx_source_health.nx -- SOURCE HEALTH, THE DECISION + THE PERSISTED VERDICT (/compare/mediaingest R1; contract
2// symbol sh_preflight). LIB: pure classification + a tiny per-host verdict table. The heavy I/O probe (fetch +
3// bot-wall classify) lives in the thin program nx_source_health_probe so THIS lib's import closure stays small
4// (nx_syscalls only) and the gate can prove every branch with planted numbers -- the same lib/program split that
5// keeps a gate honest and dodges the nx_cc module-limit that a fetch+antibot closure would hit here.
6//
7// WHY: Harvestr fast-fails a null-routed CDN (its April-2026 Coomer route-around) before queueing work against it;
8// ours had nx_site_liveness for OUR sites only. This is the general per-host pre-flight: probe once, PERSIST the
9// verdict, and let every capture read it so a dead host is skipped WITHOUT re-probing on every asset.
10//
11// FIVE STATES (a host that cannot be classified must not read as UP -- abstain, never flatter):
12// SH_UP the host answered (any 2xx/3xx, or a 4xx/5xx where the HOST is alive but the path is bad)
13// SH_SLOW it answered but slower than the slow bar -- usable, flagged
14// SH_DEAD connect failed / timed out -- null-routed or down; skip and say so
15// SH_WALLED it answered with a bot-wall (nx_antibot verdict) -- classified, never solved
16// SH_UNKNOWN never probed / stale -- the third state, so "no evidence" is not "healthy"
17// license_tier: ORIGINAL
18import "nx_syscalls.nx"
19
20const SH_UP: i64 = 0
21const SH_SLOW: i64 = 1
22const SH_DEAD: i64 = 2
23const SH_WALLED: i64 = 3
24const SH_UNKNOWN: i64 = 4
25
26// slow bar: a host taking longer than this to answer its root is SLOW. DATA (conf overrides); named for purpose.
27const SH_DEFAULT_SLOW_MS: i64 = 8000
28// recheck horizon: a verdict older than this is UNKNOWN again (a dead host may recover; a wall may lift). 30 min.
29const SH_DEFAULT_TTL_MS: i64 = 1800000
30const SH_SLOW_CONF: *u8 = "knowledge/status/source_health_slow_ms.conf"
31const SH_TABLE: *u8 = "knowledge/status/source_health.tbl"
32
33// nx_antibot verdict codes (mirrored so the caller can pass abt_classify's result straight in; CLEAN=0 is not a wall)
34const SH_ABT_CLEAN: i64 = 0
35
36// ---- PURE POLICY (deterministic, gate-tested; no I/O) ----
37// connect_ok: 1 the TCP+TLS connect succeeded, 0 it failed. status: the HTTP status (0 if no response). elapsed_ms:
38// wall time to first byte. wall_code: nx_antibot's abt_classify result (0=clean, >0 a wall class). slow_ms: the bar.
39func sh_decide(connect_ok: i64, status: i64, elapsed_ms: i64, wall_code: i64, slow_ms: i64) -> i64 {
40 if connect_ok == 0 { return SH_DEAD } // no connection at all -> null-routed / down
41 if wall_code != SH_ABT_CLEAN { return SH_WALLED } // the host answered but with a wall
42 if status <= 0 { return SH_DEAD } // connected, then no HTTP response -> treat as dead (fail-closed)
43 if elapsed_ms > slow_ms { return SH_SLOW }
44 return SH_UP
45}
46
47func sh_verdict_str(v: i64) -> *u8 {
48 if v == SH_UP { return "UP" as *u8 }
49 if v == SH_SLOW { return "SLOW" as *u8 }
50 if v == SH_DEAD { return "DEAD" as *u8 }
51 if v == SH_WALLED { return "WALLED" as *u8 }
52 return "UNKNOWN" as *u8
53}
54
55// should the capture path SKIP a host with this verdict? DEAD and WALLED are skip; UP/SLOW proceed; UNKNOWN proceeds
56// (probe it by trying -- an unprobed host is not assumed dead). A skip is the fast-fail Harvestr's pre-flight gives.
57func sh_should_skip(v: i64) -> i64 {
58 if v == SH_DEAD { return 1 }
59 if v == SH_WALLED { return 1 }
60 return 0
61}
62
63// ---- the configured slow bar (conf override, named default) ----
64func sh_atoi(s: *u8, n: i64) -> i64 {
65 var v: i64 = 0; var i: i64 = 0; var any: i64 = 0
66 while i < n { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v*10 + (c-48); any = 1 } } i = i + 1 }
67 if any == 0 { return 0 - 1 }
68 return v
69}
70func sh_slow_ms() -> i64 {
71 let szp: *i64 = sys_mmap(16) as *i64
72 szp[0] = 0
73 let buf: *u8 = sys_read_file(SH_SLOW_CONF, szp)
74 if (buf as i64) == 0 { return SH_DEFAULT_SLOW_MS }
75 if szp[0] <= 0 { return SH_DEFAULT_SLOW_MS }
76 var cap: i64 = szp[0]; if cap > 32 { cap = 32 }
77 let v: i64 = sh_atoi(buf, cap)
78 if v <= 0 { return SH_DEFAULT_SLOW_MS }
79 return v
80}
81
82// ---- the persisted per-host verdict table (flat, like the pace table; one 32-byte record per host) ----
83// record: host_hash(8) | verdict(8) | checked_ms(8) | reserved(8). Open-addressed, SH_SLOTS slots.
84const SH_SLOTS: i64 = 4096
85const SH_REC: i64 = 32
86const SH_TBL_BYTES: i64 = 131072 // SH_SLOTS * SH_REC
87const SH_OFF_HASH: i64 = 0
88const SH_OFF_VERDICT: i64 = 8
89const SH_OFF_CHECKED: i64 = 16
90const SH_MAGIC_5381: i64 = 5381
91
92func sh_hash(h: *u8, n: i64) -> i64 {
93 var x: i64 = SH_MAGIC_5381; var i: i64 = 0
94 while i < n { x = (((x << 5) + x) + (h[i] as i64)) & 0x7fffffffffffffff; i = i + 1 }
95 if x == 0 { x = 1 }
96 return x
97}
98func sh_ld(tbl: *u8, off: i64) -> i64 { var v: i64=0; var i: i64=0; while i<8 { v = v | ((tbl[off+i] as i64) << (i*8)); i=i+1 } return v }
99func sh_st(tbl: *u8, off: i64, val: i64) -> i64 { var i: i64=0; while i<8 { tbl[off+i] = ((val >> (i*8)) & 0xff) as u8; i=i+1 } return 0 }
100
101func sh_load_tbl_into(dst: *u8) -> i64 {
102 var z: i64 = 0
103 while z < SH_TBL_BYTES { dst[z] = 0 as u8; z = z + 1 }
104 let lp: *i64 = sys_mmap(16) as *i64
105 let src: *u8 = sys_read_file(SH_TABLE, lp)
106 if (src as i64) != 0 { if lp[0] >= SH_TBL_BYTES {
107 var j: i64 = 0; while j < SH_TBL_BYTES { dst[j] = src[j]; j = j + 1 }
108 } }
109 return 0
110}
111func sh_load_tbl() -> *u8 { let d: *u8 = sys_mmap(SH_TBL_BYTES); sh_load_tbl_into(d); return d }
112func sh_save_tbl(tbl: *u8) -> i64 {
113 let fd: i64 = sys_openat_wr(SH_TABLE, 0x1a4)
114 if fd < 0 { return 0 - 1 }
115 var w: i64 = 0
116 while w < SH_TBL_BYTES { let k: i64 = sys_write(fd, (tbl as i64 + w) as *u8, SH_TBL_BYTES - w); if k <= 0 { sys_close(fd); return 0 - 1 } w = w + k }
117 sys_close(fd); return 0
118}
119// find the slot for hh (existing or first empty), linear probe. On a full table evict the OLDEST checked_ms so a
120// write always lands (the same fail-open lesson R15 taught the pace table). -1 only if tbl is null.
121func sh_slot(tbl: *u8, hh: i64) -> i64 {
122 if (tbl as i64) == 0 { return 0 - 1 }
123 var s: i64 = hh % SH_SLOTS
124 var tries: i64 = 0
125 while tries < SH_SLOTS {
126 let stored: i64 = sh_ld(tbl, s * SH_REC)
127 if stored == 0 { return s }
128 if stored == hh { return s }
129 s = (s + 1) % SH_SLOTS
130 tries = tries + 1
131 }
132 // full -> evict oldest checked_ms
133 var victim: i64 = 0
134 var best: i64 = sh_ld(tbl, SH_OFF_CHECKED)
135 var i: i64 = 1
136 while i < SH_SLOTS { let ck: i64 = sh_ld(tbl, i*SH_REC + SH_OFF_CHECKED); if ck < best { best = ck; victim = i } i = i + 1 }
137 var f: i64 = 0; while f < SH_REC { tbl[victim*SH_REC + f] = 0 as u8; f = f + 1 }
138 return victim
139}
140
141// persist a verdict for a host (checked_ms = now). Loads, writes, saves. Returns 0 ok.
142func sh_record(host: *u8, host_len: i64, verdict: i64, now_ms: i64) -> i64 {
143 let tbl: *u8 = sh_load_tbl()
144 let hh: i64 = sh_hash(host, host_len)
145 let s: i64 = sh_slot(tbl, hh)
146 if s < 0 { return 0 - 1 }
147 let off: i64 = s * SH_REC
148 sh_st(tbl, off + SH_OFF_HASH, hh)
149 sh_st(tbl, off + SH_OFF_VERDICT, verdict)
150 sh_st(tbl, off + SH_OFF_CHECKED, now_ms)
151 sh_save_tbl(tbl)
152 return 0
153}
154
155// THE PRE-FLIGHT ENTRY POINT (contract symbol). A caller PROBES the host (the fetch I/O lives in the thin program
156// nx_source_health_probe, kept out of this lib so its closure stays small) and hands the raw results here; this
157// classifies (sh_decide) AND persists (sh_record) in one call and returns the verdict. So the decision and the
158// record can never disagree, and the whole thing is gate-testable with planted numbers -- no network in the lib.
159// connect_ok: 1/0 did TCP+TLS connect | status: HTTP status (0 if none) | elapsed_ms: time to first byte
160// wall_code: nx_antibot abt_classify result (0=clean) | now_ms: sys_now_realtime_ms()
161func sh_preflight(host: *u8, host_len: i64, connect_ok: i64, status: i64, elapsed_ms: i64, wall_code: i64, now_ms: i64) -> i64 {
162 let v: i64 = sh_decide(connect_ok, status, elapsed_ms, wall_code, sh_slow_ms())
163 sh_record(host, host_len, v, now_ms)
164 return v
165}
166
167// read the persisted verdict for a host; SH_UNKNOWN if absent or older than the TTL (stale -> re-probe).
168func sh_lookup(host: *u8, host_len: i64, now_ms: i64) -> i64 {
169 let tbl: *u8 = sh_load_tbl()
170 let hh: i64 = sh_hash(host, host_len)
171 var s: i64 = hh % SH_SLOTS
172 var tries: i64 = 0
173 while tries < SH_SLOTS {
174 let stored: i64 = sh_ld(tbl, s * SH_REC)
175 if stored == 0 { return SH_UNKNOWN }
176 if stored == hh {
177 let off: i64 = s * SH_REC
178 let checked: i64 = sh_ld(tbl, off + SH_OFF_CHECKED)
179 if now_ms - checked > SH_DEFAULT_TTL_MS { return SH_UNKNOWN } // stale -> re-probe
180 return sh_ld(tbl, off + SH_OFF_VERDICT)
181 }
182 s = (s + 1) % SH_SLOTS
183 tries = tries + 1
184 }
185 return SH_UNKNOWN
186}