feat(fetch): one ESPN scoreboard cache key and a max-age response cache (fetch service stage 2) - #728
Conversation
…he (fetch service stage 2) - espn_scoreboard_cache_key(sport, league, dates) names a scoreboard the same way for every consumer; get_espn_scoreboard / read_ / store_ are the shared cache-through read. A read checks the record's own timestamp, so nothing older than the reader's max_age comes back whatever ttl the writer stored. Old keys are read as legacy_keys for one release. - APIHelper.fetch_espn_scoreboard and SportsFetchMixin (_schedule_cache_key, _cached_schedule, _fetch_season_directly(cache_key=None)) use the key. - FetchService keeps 200s with Cache-Control max-age (minus Age) and answers identical GETs from them, never older than the caller's cache_max_age (default 30 s). Odds pass their interval, APIHelper its cache_ttl. - Counters: memo_hits, cache_hits, legacy_cache_hits; response_cache size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dicate A consumer whose limit depends on the payload (odds-ticker holds a scoreboard with a live game to its live interval) can turn an entry down in one read, and a turned-down entry is not counted as a hit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 7 minutes. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (11)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Security | 1 medium |
🟢 Metrics 142 complexity
Metric Results Complexity 142
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
The disk cache now reads a record's age from the newer of its embedded timestamp and the file's mtime (an unchanged re-save only touches the file). The #728 tests aged a record by rewriting its timestamp, which moved the mtime to now, so the record read as fresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 2 of the shared fetch service (#702): one cache key per ESPN scoreboard, a short response cache that honours ESPN's
Cache-Control: max-age, and counters for both. Every change to an existing signature is additive. No new core module, so the plugins'check_min_core_versiontable needs no follow-up for this PR.1. One cache key per ESPN scoreboard
Before this PR, three consumers cached the same endpoint under three different names:
scoreboard_data_{sport}_{league}_{date}APIHelper.fetch_espn_scoreboardespn_{sport}_{league}_{date}{sport_key}_schedule_{window}and{sport_key}_scoreboard_currentNew in
src/common/espn_dates.py:espn_scoreboard_cache_key(sport, league, dates=None)buildsespn_scoreboard_{sport}_{league}_{dates}.sportandleagueare ESPN's path segments (football/college-football), not a plugin'ssport_key, so every plugin names a league the same way.datesis thedates=value:YYYY,YYYYMM,YYYYMMDD,YYYYMMDD-YYYYMMDD, adate, or a(start, end)pair.Noneis the undatedcurrentscoreboard.ValueErrorrather than inventing a key.espn_scoreboard_cache_key_for_url(url, dates)andespn_scoreboard_url()go with it.get_espn_scoreboard(session, sport, league, dates, *, cache_manager, max_age, legacy_keys, ...)is the cache-through read.max_ageseconds old, whoever wrote it.fetch_espn_scoreboard(limit=500, ranges split) and caches the result under the canonical key.max_age=0always fetches, and still leaves its copy for other readers.read_espn_scoreboard_cache()andstore_espn_scoreboard_cache()are the two halves. The read takes anaccept(data, age)predicate, which odds-ticker uses to hold a payload with a live game to its live interval.CacheManagerlets a writer's storedttloverride the reader'smax_age, and its memory tier times an entry from when it was loaded, not when it was written. A key shared by readers with different TTLs can rely on neither.CacheManagershows the hazard: a plainget(max_age=30)returns a 60 s old entry stored withttl=3600, and the new read does not.legacy_keysand read after the canonical key for one release, so an upgrade serves the copies already on disk instead of refetching every league at once.The core helpers that cache a scoreboard now use the canonical key:
APIHelper.fetch_espn_scoreboarduses it by default. The old default key is a fallback; an explicitcache_keybehaves as before.SportsFetchMixin._schedule_cache_key(datestring)/_cached_schedule(key, legacy_keys)are for the scoreboards' schedule windows._cached_scheduleis the same read as before (same default max age, a stored ttl still wins), so only where a window is cached changes, not for how long._fetch_season_directly(cache_key=None)caches under the canonical key.2. Response cache (
Cache-Control: max-age)ESPN sends no validators, only max-age. Measured from this machine on 2026-10-02: most scoreboard answers say 1-9 s, some 56-75 s, and Premier League days 169-496 s. It looks like the CDN's remaining freshness.
FetchServicekeeps a finished200for its max-age minusAge. An identical GET inside that window is answered without a request.cache_max_age=<its TTL>tofetch_getorfetch_espn_scoreboard;0always asks the network.fetch_service.response_cache.default_max_age).BaseOddsManager.get_oddspasses its update interval,APIHelper.getitscache_ttl, andget_espn_scoreboarditsmax_age.no-store,no-cache,private,Vary: *,Set-Cookie, non-200 responses and streams.fetch_service.response_cache.enabled: false. Any failure in the cache falls back to a plain request.3. Counters
Three new counters in
GET /api/v3/plugins/fetch-stats, per plugin, per host and in total:memo_hits: answered by the response cache;cache_hits: scoreboard fetches answered by a shared cache entry;legacy_cache_hits: reads served from a pre-stage-2 key. It should fall to zero within a day of an upgrade, which says when the fallback can go.response_cachereports the cache's current size. A plugin'shostscounts now include both kinds of hit.Plugins: branch pushed, no PR
ChuckBuilds/ledmatrix-pluginsbranchclaude/espn-canonical-scoreboard-keys(2 commits on 82dbb0df). It waits for the core release that ships this PR. It floors on 3.9.0, assuming the release after 3.8.0 is 3.9.0; adjustversions[0]andcompatible_versionsif it ships under another number.read_/store_espn_scoreboard_cacheunder the canonical key, withscoreboard_data_*as the fallback.fetch_espn_scoreboard(limit=500, the identifying session, counted) instead of a barerequests.get._schedule_cache_key/_cached_schedule, with{sport_key}_schedule_{window}as the fallback.get_espn_scoreboard(max_age=0)).{sport_key}_scoreboard_current(whatever the dates) to the shared key for the dates actually asked.versions[]entries, CHANGELOGs andplugins.json.check_version_bump(with the 8 ids, and--all),check_manifest_version_fields(with the ids),test_espn_dates_copies,check_sports_helpers_parity,check_module_collisions,check_sports_drift,check_min_core_versionand its test,check_core_api_signatures,check_manifests_asciiandtest_odds_fetch_scope.test_afl_plugin(arequestsimport shadow), baseballtest_odds_placement, and football's interactivetest_football_plugin.currentscoreboard (stilllimit=1000through a rawsession.get) and afl/nrl's live 30 s cache. Odds-ticker doesn't overlap with either.No core-module follow-up is needed: this PR adds no
src/module, socheck_min_core_version'sMODULE_FIRST_VERSIONtable is unaffected.Live A/B against ESPN
ab_live.pyis the plugins' two call patterns, copied from plugins main (before) and the branch (after). Consumer A is a scoreboard's live poll: college football in the football style (no cache, thenmax_age=0) and the Premier League in the soccer style (30 s cache). Consumer B is odds-ticker's day read (300 s). The day is 2026-10-03, 54 college-football games.CacheManageron a temp dir.requests.Session.send.requests.getthe fetch service never saw (6 sent, 4 counted). After, it is served from the scoreboard's copy, and every request is counted.memo_hits0): ESPN saidmax-age=1for that college-football day. Its own effect is pinned by the unit tests.Tests
test/test_espn_scoreboard_cache.py(new, 50):get_espn_scoreboard,APIHelper, the mixin's_schedule_cache_keyand_fetch_season_directly, URL-derived); everydatesform; refusals.now=); never older than the reader's TTL whatever ttl the writer stored, also against a realCacheManager, on disk and after loading into memory; the canonical entry stores no ttl; theacceptpredicate.limit=500and caches; a hit sends nothing;max_age=0; ranges in chunks; a failure caches nothing; the response cache is bounded by the caller's TTL._cached_schedule's legacy read and the previous-day window retirement.test/test_fetch_service.py(+36):Age, the caller's TTL, the 30 s default, 0,cache_max_agenever reaching the session, nine kinds of response that must not be kept, streams, representation keys, cookie sessions, size bounds and LRU, expiry on insert, the off switch, merging plus caching.APIHelper.getbounded by itscache_ttl.avoided_request=False, every snapshot field.test/test_api_helper.py: the two tests that pinned the old default key now pin the canonical one. Added: an explicit key works as before, and the old key is read after an upgrade.Age,no-store/no-cache/private, storing on the merge path, the record-timestamp check, the legacy flag,get_espn_scoreboard's TTL passed to the cache, legacy keys, window retirement, the mixin's legacy keys, the odds interval andAPIHelper's TTL.origin/mainbaseline worktree (5aa7a63), on Windows: the sorted FAILED/ERROR test IDs are identical.test_sports_stage3_paritycases auto-generated for the newsports_fetchmembers. They skip because no plugin bundles a copy.scripts/check_types.py(mypy 1.20.2): clean on all 93 modules, includingfetch_service,espn_dates,sports_fetch,api_helperandbase_odds_manager.Soak needed (not run; no rig was touched)
On hdpi or ledpi, with the plugins branch installed on this core, alternating with the current main + plugins main, 45 min per phase:
/api/v3/plugins/fetch-statsevery 15 min:requestsper host,cache_hits,memo_hitsandlegacy_cache_hits.legacy_cache_hitsshould be non-zero only in the first phase after the upgrade.update() timed outat boot beyond what main shows, and no refetch storm on the first boot after the upgrade.espn_scoreboard_*_<window>file per league, not a growing set. Also the extra live-poll writes from baseball, football and hockey: one day's scoreboard per live poll, the same rate basketball and soccer already write.scripts/frame_soak.py --previewfor no regression from the cache writes.🤖 Generated with Claude Code