feat(sports): persist daily rate-budget consumption across pod restarts#212
Open
doughknee wants to merge 1 commit into
Open
feat(sports): persist daily rate-budget consumption across pod restarts#212doughknee wants to merge 1 commit into
doughknee wants to merge 1 commit into
Conversation
The RateLimiter was purely in-memory, so every pod restart (deploy, OOM, node drain) reset all per-league reserved budgets and per-host shared pools to a fresh full 7,500/day quota — letting a restarted pod overshoot the upstream api-sports.io daily quota, which only resets at UTC midnight. Two complementary fixes: - Persistence: a new sports_rate_budget table stores consumed requests per host per UTC day. try_consume (and the standings/teams polls, which bypass it) feed a per-host flush buffer that a new supervised task drains to Postgres every 60s (additive upsert, final flush on graceful shutdown, deltas handed back on write failure). On startup the limiter is seeded from today's row via new_per_league_seeded, so budgets resume from what is actually left. The midnight reset task flushes before resetting and prunes rows older than a week. - Header clamp: RateLimiter::update now treats the x-ratelimit-requests-remaining header as authoritative — if it reports fewer requests than the local reserved+shared buckets sum to, every bucket is scaled down proportionally. Never grows budgets, no-op for the legacy constructor. Catches overshoot even when the last unflushed minute is lost to an unclean restart. Consumed counts track request attempts (consume happens before the HTTP call), so errors lean toward under-spending the quota. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The preview deployment for brandon-relentnet/myscrollr:main-eo04c4skwo0ckgck4oos4ock failed. 🔴 Open Build Logs | Open Application Logs Last updated at: 2026-06-11 21:26:53 CET |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
RateLimiterin the sports service is purely in-memory. Every pod restart (deploy, OOM, node drain) rebuilt it viaRateLimiter::new_per_leaguewith a fresh full 7,500/day quota per host — but the upstream api-sports.io counter only resets at UTC midnight, so a mid-day restart let the service overshoot the daily quota (root cause family of the June 11 soccer-ingestion outage, alongside #211).Fix
Persistence (primary):
sports_rate_budgettable (migration120000000008): one row per (host, UTC day) with a monotonically increasingconsumedcount.try_consumefeeds a per-host flush buffer; the standings/teams polls (which gate onhas_budgetinstead) callnote_consumeddirectly so nothing is missed.sports-budget-flushtask drains the buffer to Postgres every 60s (additive upsert, so flushes are restart-safe), does a final flush on graceful shutdown, and hands deltas back to the buffer if the write fails.new_per_league_seeded— per-league shares are computed fromdaily_total - consumed_today[host]. DB errors fail open to the old full-quota behavior.sports-budget-resettask flushes before resetting and prunes rows older than a week.Header clamp (backstop):
RateLimiter::updatenow treatsx-ratelimit-requests-remaining(already parsed on every response) as authoritative: if the header reports fewer requests than the host''s reserved+shared buckets sum to, every bucket is scaled down proportionally (integer floor, post-clamp sum ≤ header). Never grows budgets; no-op for the legacy constructor. This catches overshoot even when the last unflushed minute is lost to an unclean restart.Notes
Testing
types.rs: seeded budgets (subtraction, saturation, off-season donation from effective total, per-host isolation), flush buffer (drain/reset, shared-pool counting, add-back after failed flush, failed consume not counting), header clamp (proportional shrink, shared-pool scaling, never-grow, clamp-to-zero, per-host isolation, legacy no-op).🤖 Generated with Claude Code