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}