code wiki / _hdl_build / nx_adnet_campaign.nx
nx_adnet_campaign.nx source
↩ module page · 186 lines · 9273 B
1// nx_adnet_campaign.nx -- LIB: the CAMPAIGN, i.e. the decision "may this creative serve right now".
2// WHAT IT CLOSES: an advertiser record says WHO is buying and a rate card says AT WHAT PRICE, but nothing
3// in the plane says WHEN or HOW MUCH. Today an inventory row serves forever the moment it exists.
4//
5// THE GAP THAT MOTIVATED THIS, MEASURED: anb_capped clamps the BILL at cap_milli but nothing stops the
6// SERVING. So an advertiser past budget keeps receiving impressions that can never be invoiced -- we give
7// away inventory and call it a cap. A budget ceiling that stops billing but not delivery is not a ceiling,
8// it is a discount that grows without limit.
9//
10// ★ THE DECISION IS A PURE FUNCTION OF (row, now, spent). No clock read, no file read, no global state --
11// so the gate can drive every branch deterministically and the edge can call it per request without IO.
12// ★ FAIL-CLOSED ON MALFORMED: a row that cannot be EVALUATED is NOT servable. Never serve what you cannot
13// reason about; an unparseable campaign is exactly when you are least entitled to spend someone's money.
14// ★ SEALED VERDICTS, never a bare boolean. "did not serve" and "did not serve BECAUSE the flight ended"
15// are different facts to an advertiser asking why their spend stopped, and a support answer needs the
16// second one.
17//
18// Row contract (TAB-separated, same shape discipline as nx_adnet inventory):
19// <campaign_bk> <advertiser_bk> <creative_url> <start_epoch> <end_epoch> <budget_milli> <status>
20// status: live | paused end_epoch = -1 means OPEN-ENDED (an explicit fact, not a missing one)
21// budget_milli = -1 means NO BUDGET CEILING (again explicit; absent and unlimited must not look alike)
22// license_tier: ORIGINAL
23import "nx_syscalls.nx"
24import "_hdl_build/nx_adnet_slot.nx"
25
26// sealed verdicts
27const CAMP_SERVABLE: i64 = 1
28const CAMP_NOT_STARTED: i64 = 2
29const CAMP_ENDED: i64 = 3
30const CAMP_EXHAUSTED: i64 = 4
31const CAMP_PAUSED: i64 = 5
32const CAMP_MALFORMED: i64 = 6
33
34// bounded decimal parse allowing a single leading '-'. Returns the value; malformed -> CAMP_BAD_NUM.
35// A sentinel distinct from every legal value, so "unparseable" can never be mistaken for "-1 = unlimited".
36const CAMP_BAD_NUM: i64 = 0 - 999999999
37
38func camp_num(s: *u8) -> i64 {
39 var i: i64 = 0
40 var neg: i64 = 0
41 if s[0] == (45 as u8) { neg = 1; i = 1 }
42 var v: i64 = 0
43 var any: i64 = 0
44 while s[i] != (0 as u8) {
45 let c: u8 = s[i]
46 if c < (48 as u8) { return CAMP_BAD_NUM }
47 if c > (57 as u8) { return CAMP_BAD_NUM }
48 v = v * 10 + ((c as i64) - 48)
49 any = 1
50 i = i + 1
51 if i > 19 { return CAMP_BAD_NUM }
52 }
53 if any == 0 { return CAMP_BAD_NUM }
54 if neg == 1 { return 0 - v }
55 return v
56}
57
58// THE DECISION. row/rlen = one campaign row; now = epoch seconds; spent_milli = billed-to-date for this
59// campaign (the caller owns that number -- this lib does not read journals, so it stays pure and gateable).
60func camp_verdict(row: *u8, rlen: i64, now: i64, spent_milli: i64) -> i64 {
61 if rlen <= 0 { return CAMP_MALFORMED }
62 let f: *u8 = sys_mmap(1024)
63
64 aslot_field_b(row, rlen, 0, f, 1024)
65 if aslot_id_ok(f) == 0 { return CAMP_MALFORMED } // campaign_bk
66 aslot_field_b(row, rlen, 1, f, 1024)
67 if aslot_id_ok(f) == 0 { return CAMP_MALFORMED } // advertiser_bk
68 aslot_field_b(row, rlen, 2, f, 1024)
69 if aslot_img_ok(f) == 0 { return CAMP_MALFORMED } // creative must be first-party
70
71 aslot_field_b(row, rlen, 3, f, 1024)
72 let start: i64 = camp_num(f)
73 if start == CAMP_BAD_NUM { return CAMP_MALFORMED }
74 aslot_field_b(row, rlen, 4, f, 1024)
75 let end: i64 = camp_num(f)
76 if end == CAMP_BAD_NUM { return CAMP_MALFORMED }
77 aslot_field_b(row, rlen, 5, f, 1024)
78 let budget: i64 = camp_num(f)
79 if budget == CAMP_BAD_NUM { return CAMP_MALFORMED }
80
81 aslot_field_b(row, rlen, 6, f, 1024)
82 var st_live: i64 = 0
83 if ad_streq(f, "live" as *u8) == 1 { st_live = 1 }
84 var st_paused: i64 = 0
85 if ad_streq(f, "paused" as *u8) == 1 { st_paused = 1 }
86 // an UNRECOGNISED status is malformed, not a synonym for live. A typo must not spend money.
87 if st_live == 0 { if st_paused == 0 { return CAMP_MALFORMED } }
88 if st_paused == 1 { return CAMP_PAUSED }
89
90 if now < start { return CAMP_NOT_STARTED }
91 // end == -1 is OPEN-ENDED and declared as such; any other end in the past closes the flight.
92 if end >= 0 { if now > end { return CAMP_ENDED } }
93
94 // BUDGET STOPS DELIVERY, NOT JUST BILLING. budget == -1 is an explicit "no ceiling".
95 // spent < 0 means the caller could not measure spend -- refuse rather than assume zero, because
96 // assuming zero is precisely how an exhausted campaign keeps serving.
97 if budget >= 0 {
98 if spent_milli < 0 { return CAMP_MALFORMED }
99 if spent_milli >= budget { return CAMP_EXHAUSTED }
100 }
101 return CAMP_SERVABLE
102}
103
104// human/support-facing reason. Every non-serving verdict names something an operator can act on.
105func camp_reason(v: i64, out: *u8) -> i64 {
106 var o: i64 = 0
107 if v == CAMP_SERVABLE { o = ad_cat(out, o, "servable" as *u8) }
108 if v == CAMP_NOT_STARTED { o = ad_cat(out, o, "not-serving=flight-has-not-started" as *u8) }
109 if v == CAMP_ENDED { o = ad_cat(out, o, "not-serving=flight-ended" as *u8) }
110 if v == CAMP_EXHAUSTED { o = ad_cat(out, o, "not-serving=budget-exhausted" as *u8) }
111 if v == CAMP_PAUSED { o = ad_cat(out, o, "not-serving=paused-by-operator" as *u8) }
112 if v == CAMP_MALFORMED { o = ad_cat(out, o, "not-serving=campaign-row-unreadable" as *u8) }
113 out[o] = 0 as u8
114 return o
115}
116
117// ---- THE WIRING POINT -----------------------------------------------------------------------------
118// A campaign object nothing consults is decoration. This is the ONE call the serving path makes.
119//
120// Placement note: this lives in the CAMPAIGN lib (which already imports the slot lib) rather than inside
121// aslot_pick_b, because slot importing campaign would close an import cycle and the tree is a DAG by
122// measurement. The daemon picks with aslot_html as today, then asks this whether the picked ad may serve.
123//
124// SEMANTICS, chosen deliberately:
125// * An ad with NO campaign row is HOUSE inventory and serves. That preserves today's behaviour exactly
126// (do-no-harm) and keeps the house ads working with zero config.
127// * An ad WITH a campaign row is ENFORCED. Opting in is what binds you.
128// * An UNREADABLE campaign row does NOT serve. Never spend a client's budget off a row we cannot parse.
129// * On refusal the caller shows NO AD rather than falling back to another advertiser's creative --
130// silently substituting a different paying client into a slot they did not win is worse than a blank.
131//
132// camps/clen = the campaign conf; ad_id = what aslot_html just picked; now = epoch s;
133// spent_milli = billed-to-date for that campaign (caller owns it; -1 means unmeasurable -> refuse).
134// Returns 1 = may serve, 0 = must not.
135// Read spend from the row itself (field 7) so the SERVE PATH IS O(1) and never touches a journal.
136// A reconciler beat owns that field; serving only reads it. Returns the spend, or CAMP_BAD_NUM when the
137// field is absent/unparseable -- which only MATTERS when a budget ceiling exists (see camp_may_serve).
138func camp_row_spent(row: *u8, rlen: i64) -> i64 {
139 let f: *u8 = sys_mmap(64)
140 aslot_field_b(row, rlen, 7, f, 64)
141 if f[0] == (0 as u8) { return CAMP_BAD_NUM }
142 return camp_num(f)
143}
144
145// Read the declared budget (field 5) so a caller can tell whether spend is even load-bearing.
146func camp_row_budget(row: *u8, rlen: i64) -> i64 {
147 let f: *u8 = sys_mmap(64)
148 aslot_field_b(row, rlen, 5, f, 64)
149 return camp_num(f)
150}
151
152func camp_may_serve(camps: *u8, clen: i64, ad_id: *u8, now: i64) -> i64 {
153 if aslot_id_ok(ad_id) == 0 { return 0 }
154 if clen <= 0 { return 1 }
155 let f0: *u8 = sys_mmap(128)
156 var ls: i64 = 0
157 var found: i64 = 0
158 var verdict: i64 = CAMP_MALFORMED
159 while ls < clen {
160 let le: i64 = ad_eol(camps, ls, clen)
161 if le > ls {
162 let row: *u8 = ((camps as i64) + ls) as *u8
163 if row[0] != (35 as u8) {
164 aslot_field_b(row, le - ls, 0, f0, 128)
165 if ad_streq(f0, ad_id) == 1 {
166 found = 1
167 // Spend is load-bearing ONLY when a budget ceiling is declared. With budget = -1
168 // (explicitly unlimited) an absent spend field is harmless; with a real ceiling an
169 // unreadable spend must REFUSE, because assuming zero is exactly how an exhausted
170 // campaign keeps serving.
171 let bud: i64 = camp_row_budget(row, le - ls)
172 var sp: i64 = camp_row_spent(row, le - ls)
173 if sp == CAMP_BAD_NUM {
174 if bud >= 0 { verdict = CAMP_MALFORMED; break }
175 sp = 0
176 }
177 verdict = camp_verdict(row, le - ls, now, sp)
178 break
179 }
180 }
181 }
182 ls = le + 1
183 }
184 if found == 0 { return 1 }
185 if verdict == CAMP_SERVABLE { return 1 }
186 return 0
187}