Session 22: Tracker-driven quota guard, configurable cache TTL (1hr default), opt-in odds prewarmer (1505 tests)
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Odds-cache prewarmer (Session 22).
|
||||
*
|
||||
* On the free 500-credit/month odds-api tier this script is a
|
||||
* negative-EV operation: a single fetchAllOdds() call costs
|
||||
* 1 (events lookup) + N (per-event odds) credits, and a 1h-TTL
|
||||
* warmer for one sport would spend ~24 credits/day, blowing the
|
||||
* monthly budget across multiple sports. It only makes sense
|
||||
* once the account is on a higher tier.
|
||||
*
|
||||
* Therefore: gated by `ODDS_PREWARM=1`. The script REFUSES to run
|
||||
* when the flag is unset — operators opt in explicitly per
|
||||
* deployment. The flag check is the first thing main() does so
|
||||
* accidental cron invocations no-op before any provider call.
|
||||
*
|
||||
* Usage (CLI):
|
||||
* node scripts/odds-prefetch.js --sports=nba,mlb,wnba
|
||||
* ODDS_PREWARM=1 node scripts/odds-prefetch.js --dry-run
|
||||
*
|
||||
* Returns a summary object with the per-sport outcome and the
|
||||
* total cost in odds-api credits (read from the tracker delta).
|
||||
* Designed to be wrapped by an internal HTTP endpoint or a cron;
|
||||
* not auto-mounted to either.
|
||||
*/
|
||||
|
||||
function parseArgs(argv) {
|
||||
const flags = { sports: ['nba', 'wnba', 'mlb'], dryRun: false };
|
||||
for (const a of argv.slice(2)) {
|
||||
if (a === '--dry-run') flags.dryRun = true;
|
||||
else if (a.startsWith('--sports=')) {
|
||||
const list = a.slice('--sports='.length).split(',').map((s) => s.trim()).filter(Boolean);
|
||||
if (list.length) flags.sports = list;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
async function main(argv = process.argv) {
|
||||
const enabled = process.env.ODDS_PREWARM === '1';
|
||||
const args = parseArgs(argv);
|
||||
const summary = {
|
||||
enabled,
|
||||
dryRun: args.dryRun,
|
||||
sports: {},
|
||||
creditsSpent: 0,
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (!enabled) {
|
||||
console.warn('[odds-prefetch] disabled — set ODDS_PREWARM=1 to enable');
|
||||
summary.skipped = 'not_enabled';
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Lazy-require so the script can be loaded for inspection (or
|
||||
// imported by tests) without dragging in axios + redis just to
|
||||
// hit the env-flag short-circuit above.
|
||||
const { getOdds } = require('../src/services/oddsService');
|
||||
const quotaTracker = require('../src/services/quotaTracker');
|
||||
|
||||
const before = await quotaTracker.getQuotaStatus('odds-api');
|
||||
console.log(`[odds-prefetch] starting — sports=${args.sports.join(',')} dry_run=${args.dryRun} quota_before=${before.used}/${before.limit}`);
|
||||
|
||||
for (const sport of args.sports) {
|
||||
if (args.dryRun) {
|
||||
summary.sports[sport] = { skipped: 'dry_run' };
|
||||
console.log(`[odds-prefetch] ${sport}: dry-run`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pre-flight: if quota is already blocked, bail out for the
|
||||
// remaining sports too — no point spending the next sport's
|
||||
// credits on a check that the tracker already failed.
|
||||
const status = await quotaTracker.getQuotaStatus('odds-api');
|
||||
if (!status.allowed) {
|
||||
summary.sports[sport] = { skipped: 'quota_blocked', pct: status.pct };
|
||||
console.warn(`[odds-prefetch] ${sport}: skipped — quota ${(status.pct * 100).toFixed(0)}%`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getOdds(sport);
|
||||
const propCount = Array.isArray(result.props) ? result.props.length : 0;
|
||||
summary.sports[sport] = {
|
||||
source: result.source || 'live',
|
||||
props: propCount,
|
||||
quota_remaining: result.quota_remaining,
|
||||
};
|
||||
console.log(`[odds-prefetch] ${sport}: ${result.source} (${propCount} props, quota=${result.quota_remaining})`);
|
||||
} catch (err) {
|
||||
summary.sports[sport] = { error: err && err.message ? err.message : String(err) };
|
||||
console.warn(`[odds-prefetch] ${sport}: failed — ${err && err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const after = await quotaTracker.getQuotaStatus('odds-api');
|
||||
summary.creditsSpent = Math.max(0, after.used - before.used);
|
||||
summary.quota = { before: before.used, after: after.used, limit: after.limit, pct: after.pct };
|
||||
console.log(`[odds-prefetch] done — credits_spent=${summary.creditsSpent} quota=${after.used}/${after.limit} (${(after.pct * 100).toFixed(0)}%)`);
|
||||
return summary;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().then((s) => {
|
||||
process.exit(s.skipped === 'not_enabled' ? 2 : 0);
|
||||
}).catch((err) => {
|
||||
console.error('[odds-prefetch] fatal:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
main,
|
||||
__internals: { parseArgs },
|
||||
};
|
||||
Reference in New Issue
Block a user