Beta-tester safety: preview caps on the level override, and close a race in stop()
Two things needed before testers get level overrides. 1. The override decided what a position could SPEND as well as what it could SEE. A tester at Scintilla overridden to Corona would have had 150,000 real network impressions instead of 2,500, and a 200,000-impression grant pool to hand to real members — real inventory, spent for real. Ad allowances and split-test launches are now metered on trueLevel, and grant pools are capped to small preview amounts (5,000 impressions / 25 copy / 2 pages / 5 ad batches) when a position is overridden: enough to exercise the whole flow, trivial to lose. 2. suite-traffic.stop() read the ledger, made TWO network round trips to the ad network, then wrote back the object it had read seconds earlier — silently discarding any campaign another member launched in that window, with the impressions already spent on the network. It now re-reads after the awaits and mutates the fresh copy. Worth recording that this was the ONLY such race: Node's single thread makes a synchronous read-modify-write atomic, so the meter, grants and split ledgers were never at risk. The danger was only ever the await gap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+19
-4
@@ -189,9 +189,9 @@ async function stop(memberId, adId) {
|
||||
const all = readLedger();
|
||||
const mk = monthKey();
|
||||
const rows = ((all[mk] || {})[String(memberId)]) || [];
|
||||
const row = rows.find(function (r) { return Number(r.adId) === Number(adId); });
|
||||
if (!row) throw new Error('That banner is not one of yours from this month.');
|
||||
if (row.stopped) throw new Error('That banner is already stopped.');
|
||||
const pre = rows.find(function (r) { return Number(r.adId) === Number(adId); });
|
||||
if (!pre) throw new Error('That banner is not one of yours from this month.');
|
||||
if (pre.stopped) throw new Error('That banner is already stopped.');
|
||||
|
||||
let served = 0, unserved = 0;
|
||||
try {
|
||||
@@ -205,6 +205,21 @@ async function stop(memberId, adId) {
|
||||
|
||||
await callNas({ action: 'deactivate', ad_id: Number(adId) });
|
||||
|
||||
// RE-READ before writing. Everything above this point took two network round
|
||||
// trips to the ad network, and the ledger object read at the top of this
|
||||
// function is now seconds stale. Writing it back would silently discard any
|
||||
// campaign another member launched in the meantime — impressions spent on the
|
||||
// network with no record that they were. Node's single thread makes the block
|
||||
// below atomic; the danger was only ever the await gap.
|
||||
const fresh = readLedger();
|
||||
const freshRows = ((fresh[mk] || {})[String(memberId)]) || [];
|
||||
const row2 = freshRows.find(function (r) { return Number(r.adId) === Number(adId); });
|
||||
if (!row2) throw new Error('That banner is no longer in this month\'s ledger.');
|
||||
if (row2.stopped) return { adId: Number(adId), served: row2.served || 0, refunded: row2.refunded || 0 };
|
||||
|
||||
const all2 = fresh;
|
||||
const row = row2;
|
||||
|
||||
// Charge only what actually served; the rest returns to the allowance.
|
||||
// `bought` preserves the original order size so the member's history still
|
||||
// shows what they launched, not just what it ended up costing them.
|
||||
@@ -220,7 +235,7 @@ async function stop(memberId, adId) {
|
||||
row.served = served;
|
||||
row.refunded = unserved;
|
||||
row.impressions = served; // what this campaign counts against the month
|
||||
writeLedger(all);
|
||||
writeLedger(all2);
|
||||
return { adId: Number(adId), served: served, refunded: unserved };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user