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}