code wiki / (root) / nx_ingest_admit.nx

nx_ingest_admit.nx source

↩ module page · 81 lines · 3951 B

1// nx_ingest_admit.nx -- INGEST ADMISSION CONTROL (/compare/mediaingest R2; contract symbol ig_disk_floor). 2// 3// WHY: a capture path that writes an asset onto a filesystem below its free-space floor risks a TORN write -- 4// a half-written file that then classifies as a real item. Harvestr guards this with min_disk_gb plus a disk 5// manager; ours had the disk axis (sys_fs_avail_bytes, landed in nx_syscalls 2026-08-27) and NOTHING in the 6// capture path read it. This is the one admission check every capture verb calls before it opens a destination. 7// 8// THREE STATES, because "I could not measure the disk" is not "the disk is fine": 9// IG_ADMIT the volume holding <root> has at least <floor> bytes free to a non-root writer 10// IG_REFUSE_FULL measured, and below the floor -- the capture must not write 11// IG_UNMEASURABLE statfs failed (unmounted / permission / gone) -- FAIL-CLOSED: the caller must refuse, because 12// "unmeasurable" read as "empty" is the most flattering possible lie (nx_syscalls says so of 13// the permil twin). A transient statfs failure blocking one capture is safe; a torn write is not. 14// 15// The floor is DATA (rule 11): knowledge/status/ingest_floor.conf holds a single decimal byte count; absent or 16// unreadable falls back to IG_DEFAULT_FLOOR_BYTES, named for its purpose, never buried as a literal at a call site. 17// license_tier: ORIGINAL 18import "nx_syscalls.nx" 19 20const IG_ADMIT: i64 = 0 21const IG_REFUSE_FULL: i64 = 1 22const IG_UNMEASURABLE: i64 = 2 23 24// 5 GiB: the same order Harvestr's min_disk_gb defaults to -- enough headroom that a multi-GB recording or a 25// large album cannot torn-write the tail. A conf row overrides it; this is the fallback, not a tuning knob. 26const IG_DEFAULT_FLOOR_BYTES: i64 = 5368709120 27const IG_FLOOR_CONF: *u8 = "knowledge/status/ingest_floor.conf" 28const IG_CONF_CAP: i64 = 64 29 30func ig_atoi(s: *u8, n: i64) -> i64 { 31 var v: i64 = 0 32 var i: i64 = 0 33 var any: i64 = 0 34 while i < n { 35 let c: i64 = s[i] as i64 36 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1 } } 37 i = i + 1 38 } 39 if any == 0 { return 0 - 1 } 40 return v 41} 42 43// the configured floor in bytes: the conf value if present and positive, else the named default. 44func ig_floor_bytes() -> i64 { 45 let szp: *i64 = sys_mmap(16) as *i64 46 szp[0] = 0 47 let buf: *u8 = sys_read_file(IG_FLOOR_CONF, szp) 48 if (buf as i64) == 0 { return IG_DEFAULT_FLOOR_BYTES } 49 if szp[0] <= 0 { return IG_DEFAULT_FLOOR_BYTES } 50 var cap: i64 = szp[0] 51 if cap > IG_CONF_CAP { cap = IG_CONF_CAP } 52 let v: i64 = ig_atoi(buf, cap) 53 if v <= 0 { return IG_DEFAULT_FLOOR_BYTES } 54 return v 55} 56 57// PURE POLICY (gate-tested, no I/O): given the measured free bytes and a floor, decide. avail < 0 means the 58// measurement failed (STATFS_ERR) -> UNMEASURABLE. This is separate from the I/O so the gate can prove every 59// branch without a filesystem. 60func ig_decide(avail_bytes: i64, floor_bytes: i64) -> i64 { 61 if avail_bytes < 0 { return IG_UNMEASURABLE } 62 if avail_bytes < floor_bytes { return IG_REFUSE_FULL } 63 return IG_ADMIT 64} 65 66// THE admission check a capture verb calls before opening a destination on <root>. Reads the live free space and 67// the configured floor, returns the 3-state verdict. out_avail[0] and out_floor[0] receive the two numbers so the 68// caller can ANNOUNCE them (a refusal that does not say how much was free and how much was needed is not actionable). 69func ig_disk_floor(root: *u8, out_avail: *i64, out_floor: *i64) -> i64 { 70 let avail: i64 = sys_fs_avail_bytes(root) 71 let floor: i64 = ig_floor_bytes() 72 out_avail[0] = avail 73 out_floor[0] = floor 74 return ig_decide(avail, floor) 75} 76 77func ig_verdict_str(v: i64) -> *u8 { 78 if v == IG_ADMIT { return "ADMIT" as *u8 } 79 if v == IG_REFUSE_FULL { return "REFUSE-DISK-BELOW-FLOOR" as *u8 } 80 return "REFUSE-DISK-UNMEASURABLE" as *u8 81}