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}