code wiki / _hdl_build / nx_adnet_reconcile.nx
nx_adnet_reconcile.nx source
↩ module page · 93 lines · 4323 B
1// nx_adnet_reconcile.nx -- LIB: the BEAT that owns campaign field 7 (spent_milli).
2// WHY IT EXISTS: camp_may_serve enforces a budget ceiling by reading spend from the campaign ROW, so the
3// serve path stays O(1) and never scans a journal. Something has to WRITE that field, and it must not be
4// the serve path -- ainv_spent_milli walks three journals, which with 158k served rows is a per-request
5// table scan on the hot path. Putting that behind an ad impression is how an ad network takes its own
6// site down. So: a beat reconciles, serving reads.
7//
8// ★ PURE TRANSFORM, no IO. Takes the confs as buffers and emits the NEW conf into out. The caller owns
9// reading and the atomic write, which keeps this gateable with fixtures and keeps the dangerous part
10// (overwriting a live conf) in one reviewable place.
11// ★ PRESERVES EVERYTHING IT DOES NOT UNDERSTAND. Comment rows, blank lines and rows whose spend cannot be
12// computed pass through BYTE-IDENTICAL. A reconciler that drops what it cannot parse is a data-loss bug
13// wearing a maintenance-task costume.
14// ★ AN UNMEASURABLE SPEND IS LEFT ALONE, NEVER ZEROED. ainv_spent_milli returns -1 when a campaign is
15// unpriced, its id is malformed, or viewable > served (a forged beacon). Writing 0 there would silently
16// REVIVE an exhausted campaign -- the exact failure the ceiling exists to prevent. The old value stands
17// and camp_may_serve keeps refusing.
18// license_tier: ORIGINAL
19import "nx_syscalls.nx"
20import "_hdl_build/nx_adnet_campaign.nx"
21import "_hdl_build/nx_adnet_invoice.nx"
22
23func arec_catd(dst: *u8, off: i64, v: i64) -> i64 {
24 var o: i64 = off
25 var m: i64 = v
26 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m }
27 let t: *u8 = sys_mmap(24)
28 var k: i64 = 0
29 if m == 0 { t[0] = 48 as u8; k = 1 }
30 while m > 0 { t[k] = (48 + (m - (m / 10) * 10)) as u8; m = m / 10; k = k + 1 }
31 var i: i64 = k - 1
32 while i >= 0 { dst[o] = t[i]; o = o + 1; i = i - 1 }
33 return o
34}
35
36// copy fields 0..6 of a campaign row verbatim, then append the freshly computed spend as field 7.
37func arec_row_with_spend(row: *u8, rlen: i64, spent: i64, out: *u8, cap: i64) -> i64 {
38 if cap < rlen + 32 { return 0 }
39 var o: i64 = 0
40 var fs: i64 = 0
41 var f: i64 = 0
42 while f < 7 {
43 let fe: i64 = ad_tab(row, fs, rlen)
44 var k: i64 = fs
45 while k < fe { out[o] = row[k]; o = o + 1; k = k + 1 }
46 out[o] = 9 as u8; o = o + 1
47 fs = fe + 1
48 f = f + 1
49 }
50 o = arec_catd(out, o, spent)
51 out[o] = 0 as u8
52 return o
53}
54
55// Emit a refreshed campaign conf. Returns bytes written.
56// Rows are rewritten ONLY when a spend can actually be computed; everything else survives untouched.
57func arec_refresh(camps: *u8, clen: i64, rates: *u8, rlen: i64, slog: *u8, sl: i64, vlog: *u8, vl: i64, clog: *u8, cl: i64, out: *u8, cap: i64) -> i64 {
58 var o: i64 = 0
59 let id: *u8 = sys_mmap(128)
60 var ls: i64 = 0
61 while ls < clen {
62 let le: i64 = ad_eol(camps, ls, clen)
63 let rowlen: i64 = le - ls
64 let row: *u8 = ((camps as i64) + ls) as *u8
65 var wrote: i64 = 0
66 if rowlen > 0 {
67 if row[0] != (35 as u8) {
68 aslot_field_b(row, rowlen, 0, id, 128)
69 if aslot_id_ok(id) == 1 {
70 let sp: i64 = ainv_spent_milli(rates, rlen, slog, sl, vlog, vl, clog, cl, id)
71 // -1 = UNMEASURABLE. Leave the row exactly as it was: writing 0 would revive an
72 // exhausted campaign, which is the one thing a spend reconciler must never do.
73 if sp >= 0 {
74 if o + rowlen + 64 < cap {
75 let w: i64 = arec_row_with_spend(row, rowlen, sp, ((out as i64) + o) as *u8, cap - o)
76 if w > 0 { o = o + w; wrote = 1 }
77 }
78 }
79 }
80 }
81 }
82 if wrote == 0 {
83 // byte-identical passthrough for comments, blanks, and anything unmeasurable
84 if o + rowlen + 2 < cap {
85 var k: i64 = 0
86 while k < rowlen { out[o] = row[k]; o = o + 1; k = k + 1 }
87 }
88 }
89 out[o] = 10 as u8; o = o + 1
90 ls = le + 1
91 }
92 out[o] = 0 as u8
93 return o
94}