Session 22: Tracker-driven quota guard, configurable cache TTL (1hr default), opt-in odds prewarmer (1505 tests)

This commit is contained in:
Kev
2026-06-12 02:41:51 -04:00
parent ea848e327e
commit 6ab49d4c37
7 changed files with 587 additions and 9 deletions
+156 -1
View File
@@ -4,7 +4,162 @@
2026-06-12
## Current Phase
SHIP BUILD v21.0 — Every external HTTP call tracked + ntfy alerts (Session 21)
SHIP BUILD v22.0 — Tracker-driven quota guard, env-configurable cache TTL, opt-in odds prewarmer (Session 22)
## Session 22 (2026-06-12) — SHIPPED
Plumbed the cache + quota machinery so the platform can survive a
free-tier (500 credits/month) odds-api budget. Honest scope:
Chrome Claude's diagnosis ("pollers write to one key, API reads
from another") didn't hold up under trace — the keys it pointed
at were internal sentinels in `cascadeService` and
`lineMovementService`, not duplicate caches. No PM2 poller ever
fed the odds cache. The actual root cause is that the cache is
populated *on-demand* by `getOdds` itself, and when odds-api
fails the cache stays empty.
After confirming the trace with the user, the agreed scope was:
1. Replace the legacy stale quota guard with Session 20's tracker
2. Make the cache TTL env-configurable (default raised from 15min
to 1h)
3. Build an opt-in odds prewarmer script
### PHASE 1 — Trace (honest scope correction)
Grepped for `odds:players:*` and `odds:baseline_set:*` — both
are written by `cascadeService.detectScratches` and
`lineMovementService.processNewOdds` AFTER a successful
`getOdds()` call, as internal sentinels for scratch detection
and opening-line baseline capture respectively. Neither is a
duplicate cache feed.
Documented in BUILD-STATE so future operators don't re-chase
the same false lead.
### PHASE 3 — Tracker-driven quota guard
`src/services/oddsService.js#getOdds` previously checked
`getQuotaRemaining(redis)` — a Redis hash that only the file
itself updated, so it drifted (Chrome Claude observed 46 in the
hash while reality was 7). The check is now delegated to
Session 20's `quotaTracker.getQuotaStatus('odds-api')`, which:
- is synced from `x-requests-remaining` / `x-requests-used` on
every successful odds-api call (via gateway.fetch's
syncHeadersFrom hook)
- BLOCKs at ≥95% (matches the WARN/BLOCK constants the
dashboard surfaces)
- fails OPEN when Redis is degraded so a Redis hiccup doesn't
take down the platform
The 429 error now attaches `quotaStatus` to the thrown Error so
operators inspecting the response can see the actual `used /
limit / pct` that triggered the block.
Three new tests in `tests/unit/oddsService.test.js`:
- 80% (WARN, not BLOCK) → call proceeds
- 96% (BLOCK) → 429 thrown with `quotaStatus` attached
- 95% (BLOCK boundary) → axios.get never invoked
The legacy `getQuotaRemaining` / `updateQuota` machinery stays
exported for now — other call sites (the `/api/odds/*` route
layer pulls `quota_remaining` straight out of the response
envelope) still rely on the hash being populated. The hash is
a redundant signal; the tracker is the decision.
### Env-configurable cache TTL
`oddsService.CACHE_TTL` is now resolved from
`ODDS_CACHE_TTL_SECONDS` at module load, falling back to a new
default of **3600 seconds (1 hour)** — up from the legacy 900s.
Rationale: each cache miss fans out to (1 + N) upstream calls,
costing 510 credits per refresh. At 15-min TTL across 4 sports
that's ~3,840 credits/day — an order of magnitude over the free
tier's 500/month. At 1h TTL it's ~960/day — still over, but a
factor of 4 closer. Operators on the free tier with many sports
should bump to 7200 (2h) via Coolify.
Bounds-checked: rejects overrides <60 (would shred credits) and
>86400 (would hold stale forever); both fall back to 3600.
`getConfiguredCacheTTL` exported for direct test coverage. Five
new tests pin the parser.
### Opt-in odds prewarmer
`scripts/odds-prefetch.js` calls `getOdds(sport)` for each
configured sport to warm the cache out-of-band. **Gated by
`ODDS_PREWARM=1`** — the first thing main() does is check the
flag and bail out with exit code 2 if unset. This is a
hard safety: at the free tier the script would blow the
monthly budget if run accidentally.
CLI:
```
ODDS_PREWARM=1 node scripts/odds-prefetch.js --sports=nba,mlb
ODDS_PREWARM=1 node scripts/odds-prefetch.js --dry-run
```
Returns a structured summary including credits spent (computed
as the delta between pre-run and post-run tracker reads). The
script bails the moment the tracker reports `allowed:false`
mid-run, so subsequent sports don't add to the bleeding.
Module-exports `main` and `__internals.parseArgs` for testing.
11 unit tests cover gating, dry-run, happy path, credit-delta
calculation, mid-run block, and per-sport error isolation.
### PHASE 4 — Poller frequency review
Audit complete: the existing PM2 pollers (`poller.js` for
NBA/WNBA/MLB) hit ESPN scoreboards — free, no quota. The 60s
default is correct for ESPN. The soccer poller (`soccer.js`)
already received quota-aware tick-skipping in Session 20.
No changes — the spec's "60s → 900s" change would have applied
to a hypothetical odds-api poller that doesn't exist.
### Honest scope flags
- **The actual production 503 is NOT fully fixed by this
session.** This session changes the *cost ceiling* (4x lower
per-cache-miss) and the *quota check accuracy* (tracker, not
drifting hash). It does NOT change the fundamental constraint
that the free 500-credit/month tier cannot serve live props
across 3+ sports continuously. The real fix is a tier upgrade
or accepting longer cache (4h+).
- The prewarmer is **deliberately not wired to cron or PM2**.
When/if the account upgrades, the operator can schedule it
manually. Auto-mounting it would silently spend credits.
- The `getQuotaRemaining` legacy hash is **kept**, not removed.
Other call paths (the routes' response envelope) consume it
for the `quota_remaining` field. Removing requires migrating
those consumers — out of scope for a "make the guard
trustworthy" pass.
### Battery
- Express suite: **116 passed / 1505 tests** (+19 over
baseline 1476 → 1486 → 1505). 11 prewarmer + 5 TTL parser
+ 3 tracker guard.
- Web build: clean.
### Files changed (Session 22)
**Created:**
- `scripts/odds-prefetch.js`
- `tests/unit/oddsPrefetch.test.js`
**Modified:**
- `src/services/oddsService.js``getConfiguredCacheTTL`
+ tracker-driven preflight guard + new module exports
- `tests/unit/oddsService.test.js` — 3 tracker tests + 5 TTL
parser tests + 1 informational default-TTL test, removed
the legacy `hgetall.remaining:0` block test
---
## Session 21 (2026-06-12) — SHIPPED