code wiki / _hdl_build / nx_adnet_recbeat.nx

nx_adnet_recbeat.nx source

↩ module page · 168 lines · 8208 B

1// nx_adnet_recbeat.nx -- THE BEAT: refresh campaign spend (field 7) from the measured journals. 2// nx_adnet_reconcile is a PURE transform with no IO so it stays gateable; this is the thin runner that 3// owns the dangerous half -- reading the confs and replacing a live file. Keeping that in one small, 4// reviewable organ is the point of the split. 5// 6// ★ ATOMIC BY CONSTRUCTION: write a temp beside the target, then rename. A half-written campaign conf is 7// a serving outage -- every paid campaign would read as malformed and stop serving at once. rename(2) 8// on the same filesystem is the only step that touches the live path. 9// ★ NEVER-BRICK: the previous conf is banked as .prev before the swap, and EVERY refusal below leaves the 10// live file byte-untouched. An absent campaigns conf is NOT an error -- it means no paid campaigns yet, 11// and the beat exits 0 having done nothing. 12// ★ FAIL-LOUD ON A SHRINKING CONF: if the refreshed output is materially smaller than the input, something 13// was dropped and we REFUSE to install it. A reconciler that silently truncates the file it maintains is 14// the worst outcome here, so the size check is a hard gate rather than a warning. 15// 16// run: nx_adnet_recbeat (paths are compiled in; no argv, so a cron row cannot mis-target it) 17// expect_exit: 0 license_tier: ORIGINAL 18import "nx_syscalls.nx" 19import "_hdl_build/nx_adnet_reconcile.nx" 20 21const ARB_CAMPS: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/status/adnet_campaigns.conf" as *u8 22const ARB_TMP: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/status/adnet_campaigns.conf.new" as *u8 23const ARB_PREV: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/status/adnet_campaigns.conf.prev" as *u8 24const ARB_RATES: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/status/adnet_rates.conf" as *u8 25const ARB_SERVED:*u8 = "/volume1/homes/elderwesto/nishihost/adnet_impressions.log" as *u8 26const ARB_CLICKS:*u8 = "/volume1/homes/elderwesto/nishihost/adnet_clicks.log" as *u8 27const ARB_VIEW: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/status/adnet_viewable.log" as *u8 28const ARB_CAP: i64 = 1048576 29 30func arb_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 31func arb_pn(v: i64) -> i64 { 32 let b: *u8 = sys_mmap(32) 33 if v == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 } 34 var n: i64 = 0 35 var x: i64 = v 36 while x > 0 { b[n] = ((x - (x / 10) * 10) + 48) as u8; n = n + 1; x = x / 10 } 37 let r: *u8 = sys_mmap(32) 38 var i: i64 = 0 39 while i < n { r[i] = b[n - 1 - i]; i = i + 1 } 40 sys_write(1, r, n) 41 return 0 42} 43 44// DRYRUN: read fixture paths from argv, run the SAME transform, print the result, WRITE NOTHING. 45// This exists so the runner is testable at all. The production path still takes NO argv, so a cron row 46// cannot mis-target it; dryrun cannot write by construction, so testability costs no safety. 47// nx_adnet_recbeat dryrun <camps> <rates> <served> <view> <clicks> 48func arb_dryrun(argv: *i64) -> i64 { 49 let bx: *i64 = sys_mmap(16) as *i64 50 let camps: *u8 = sys_read_file(argv[2] as *u8, bx) 51 if (camps as i64) == 0 { arb_puts("DRYRUN: campaigns fixture unreadable 52" as *u8); return 1 } 53 let cn: i64 = bx[0] 54 let b2: *i64 = sys_mmap(16) as *i64 55 let rates: *u8 = sys_read_file(argv[3] as *u8, b2) 56 if (rates as i64) == 0 { arb_puts("DRYRUN: rates fixture unreadable 57" as *u8); return 1 } 58 let rn: i64 = b2[0] 59 let b3: *i64 = sys_mmap(16) as *i64 60 var slog: *u8 = sys_read_file(argv[4] as *u8, b3) 61 var sl: i64 = b3[0] 62 if (slog as i64) == 0 { slog = "" as *u8; sl = 0 } 63 let b4: *i64 = sys_mmap(16) as *i64 64 var vlog: *u8 = sys_read_file(argv[5] as *u8, b4) 65 var vl: i64 = b4[0] 66 if (vlog as i64) == 0 { vlog = "" as *u8; vl = 0 } 67 let b5: *i64 = sys_mmap(16) as *i64 68 var clog: *u8 = sys_read_file(argv[6] as *u8, b5) 69 var cl: i64 = b5[0] 70 if (clog as i64) == 0 { clog = "" as *u8; cl = 0 } 71 let out: *u8 = sys_mmap(ARB_CAP) 72 let n: i64 = arec_refresh(camps, cn, rates, rn, slog, sl, vlog, vl, clog, cl, out, ARB_CAP) 73 arb_puts("DRYRUN in=" as *u8); arb_pn(cn); arb_puts(" out=" as *u8); arb_pn(n); arb_puts(" 74" as *u8) 75 sys_write(1, out, n) 76 if n < cn - (cn / 8) { arb_puts("DRYRUN: WOULD REFUSE (shrinkage) 77" as *u8); return 1 } 78 arb_puts("=== ADNET-RECBEAT-DRYRUN verdict=GREEN === 79" as *u8) 80 return 0 81} 82 83func main(argc: i64, argv: *i64) -> i64 { 84 if argc > 6 { 85 let v: *u8 = argv[1] as *u8 86 var isdry: i64 = 1 87 let d: *u8 = "dryrun" as *u8 88 var i: i64 = 0 89 while i < 6 { if v[i] != d[i] { isdry = 0; break } i = i + 1 } 90 if isdry == 1 { return arb_dryrun(argv) } 91 } 92 let bx: *i64 = sys_mmap(16) as *i64 93 let camps: *u8 = sys_read_file(ARB_CAMPS, bx) 94 if (camps as i64) == 0 { 95 // NOT an error: no campaigns conf means no paid campaigns yet. House inventory is unaffected. 96 arb_puts("ADNET-RECBEAT: no campaigns conf -- nothing to reconcile (house inventory unaffected)\n" as *u8) 97 arb_puts("=== ADNET-RECBEAT verdict=GREEN ===\n" as *u8) 98 return 0 99 } 100 let cn: i64 = bx[0] 101 102 let bx2: *i64 = sys_mmap(16) as *i64 103 let rates: *u8 = sys_read_file(ARB_RATES, bx2) 104 if (rates as i64) == 0 { 105 // A rate card we cannot read means every spend is unmeasurable. arec_refresh would leave every row 106 // alone anyway, so rewriting the file could only lose data. Refuse and leave the live file alone. 107 arb_puts("ADNET-RECBEAT: REFUSED -- rate card unreadable; live conf left byte-untouched\n" as *u8) 108 arb_puts("=== ADNET-RECBEAT verdict=RED ===\n" as *u8) 109 return 1 110 } 111 let rn: i64 = bx2[0] 112 113 // An absent JOURNAL is legitimate (zero events) and degrades to an empty buffer, unlike an absent conf. 114 let b3: *i64 = sys_mmap(16) as *i64 115 var slog: *u8 = sys_read_file(ARB_SERVED, b3) 116 var sl: i64 = b3[0] 117 if (slog as i64) == 0 { slog = "" as *u8; sl = 0 } 118 let b4: *i64 = sys_mmap(16) as *i64 119 var vlog: *u8 = sys_read_file(ARB_VIEW, b4) 120 var vl: i64 = b4[0] 121 if (vlog as i64) == 0 { vlog = "" as *u8; vl = 0 } 122 let b5: *i64 = sys_mmap(16) as *i64 123 var clog: *u8 = sys_read_file(ARB_CLICKS, b5) 124 var cl: i64 = b5[0] 125 if (clog as i64) == 0 { clog = "" as *u8; cl = 0 } 126 127 let out: *u8 = sys_mmap(ARB_CAP) 128 let n: i64 = arec_refresh(camps, cn, rates, rn, slog, sl, vlog, vl, clog, cl, out, ARB_CAP) 129 130 // FAIL-LOUD ON SHRINKAGE. The refreshed conf rewrites field 7 in place, so it can only grow or stay 131 // about the same; materially smaller means rows were dropped and installing it would lose campaigns. 132 if n < cn - (cn / 8) { 133 arb_puts("ADNET-RECBEAT: REFUSED -- output shrank from " as *u8); arb_pn(cn) 134 arb_puts(" to " as *u8); arb_pn(n) 135 arb_puts(" bytes; rows were dropped. Live conf left byte-untouched.\n" as *u8) 136 arb_puts("=== ADNET-RECBEAT verdict=RED ===\n" as *u8) 137 return 1 138 } 139 140 let fd: i64 = sys_openat_wr(ARB_TMP, 420) 141 if fd < 0 { 142 arb_puts("ADNET-RECBEAT: REFUSED -- cannot stage temp; live conf untouched\n" as *u8) 143 arb_puts("=== ADNET-RECBEAT verdict=RED ===\n" as *u8) 144 return 1 145 } 146 let w: i64 = sys_write(fd, out, n) 147 sys_close(fd) 148 if w != n { 149 arb_puts("ADNET-RECBEAT: REFUSED -- short write to temp; live conf untouched\n" as *u8) 150 arb_puts("=== ADNET-RECBEAT verdict=RED ===\n" as *u8) 151 return 1 152 } 153 154 sys_renameat(ARB_CAMPS, ARB_PREV) // bank the old one for rollback 155 let rc: i64 = sys_renameat(ARB_TMP, ARB_CAMPS) 156 if rc != 0 { 157 // put it back rather than leaving no conf at all 158 sys_renameat(ARB_PREV, ARB_CAMPS) 159 arb_puts("ADNET-RECBEAT: REFUSED -- install rename failed; previous conf restored\n" as *u8) 160 arb_puts("=== ADNET-RECBEAT verdict=RED ===\n" as *u8) 161 return 1 162 } 163 164 arb_puts("ADNET-RECBEAT: reconciled " as *u8); arb_pn(cn) 165 arb_puts(" -> " as *u8); arb_pn(n) 166 arb_puts(" bytes (.prev banked)\n" as *u8) 167 arb_puts("=== ADNET-RECBEAT verdict=GREEN ===\n" as *u8) 168 return 0 169}