fix(odds): stop a stalled ESPN taking the whole plugin update with it - #449
Conversation
📝 WalkthroughWalkthrough
ChangesOdds cache resilience
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant BaseOddsManager
participant ESPN
participant Cache
BaseOddsManager->>ESPN: Request odds with 5-second timeout
ESPN-->>BaseOddsManager: Successful response
BaseOddsManager->>BaseOddsManager: Clear failure cooldown
BaseOddsManager->>Cache: Write result with jittered TTL
ESPN-->>BaseOddsManager: Network failure
BaseOddsManager->>BaseOddsManager: Start 60-second cooldown
BaseOddsManager->>ESPN: Skip request during cooldown
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/base_odds_manager.py`:
- Around line 156-163: Update the cache read path used by the odds manager so
each cached entry expires using its stored jittered ttl, rather than the default
max_age. Align the read-side logic in CacheManager with the ttl written by
BaseOddsManager’s _jittered_ttl flow, while preserving the existing serial ESPN
refetch behavior.
In `@test/test_odds_cache_stampede.py`:
- Around line 61-75: Update
test_a_slate_fetched_together_does_not_expire_together to remove the
probabilistic worst <= 6 assertion; either mock
src.base_odds_manager.random.uniform with evenly spaced deterministic values
before calling _jittered_ttl, or assert only a deterministic property that does
not impose a random maximum-concurrency guarantee.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 71fe319f-706f-4339-bb01-9f6ed63f763c
📒 Files selected for processing (2)
src/base_odds_manager.pytest/test_odds_cache_stampede.py
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
| Duplication | 0 |
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.
16e1cef to
3eb8602
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/base_odds_manager.py`:
- Around line 194-201: Update the interval selection before _jittered_ttl in the
odds caching flow to use self.update_interval only when update_interval_seconds
is None, preserving an explicit zero override so _jittered_ttl returns the
no-expiry TTL.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4f56b4ec-0d74-4961-b32a-c94d19d00d98
📒 Files selected for processing (3)
src/base_odds_manager.pytest/test_base_odds_manager.pytest/test_odds_cache_stampede.py
Odds are fetched per live game from inside SportsLive.update(), with
show_odds defaulting on, and the plugin executor kills an operation at
30s. The odds request timeout was also 30s, so a single stalled request
consumed the entire budget and the update carrying every game's score
was killed.
Out of season that is invisible: preseason week 1 returns one game. A
Sunday slate is around sixteen, so the odds of at least one slow request
rise sharply just as the cost of losing the update does.
Shorten the request timeout to 5s, and after a network failure skip the
network for 60s. The timeout alone is not enough -- sixteen consecutive
5s timeouts still blow through -- and when ESPN is unreachable it is
unreachable for the whole slate, so the first failure already answers
the question for the rest of the pass.
before: one stalled request = 30s = the entire budget
after : 5s, the rest of the slate skipped, retry after 60s
The stale-cache fallback is unchanged: the cache is consulted before any
of this, and the failing request still falls back to it.
An earlier version of this branch also jittered the cache TTL to stagger
expiry across a slate. That has been dropped: CacheManager.set() stores
ttl for compatibility but the read path expires entries by a per-type
max_age (1800s for odds), so the jitter was inert. Making the read path
honour a per-entry ttl is a real fix but changes a contract 48 plugin
call sites already rely on, which is not a change to make two weeks
before the season.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
354f05c to
ecf9195
Compare
|
Both remaining findings predate the rewrite of this branch and no longer apply, so I'm closing them out rather than acting on them. "Make the cache read path apply the per-entry TTL" — this was the important one, and it was right: The underlying defect is now fixed properly in #450, which makes both cache layers honour a stored "Remove the random maximum-concurrency guarantee" — the file it refers to, "Preserve an explicit zero cache interval" — no longer reachable from this branch. |
Odds are fetched per live game from inside
SportsLive.update(), withshow_oddsdefaulting on, and the plugin executor kills an operation at 30s. Two numbers were the same and shouldn't have been:So a single stalled ESPN request consumed the entire budget and the update carrying every game's score was killed. Observed on a live device:
Out of season this is invisible — preseason week 1 returns one game. A Sunday slate is ~16, so the chance of at least one slow request rises sharply just as the cost of losing the update does.
Fix
A 5s request timeout plus a 60s cooldown after any network failure. The timeout alone isn't sufficient: 16 consecutive 5s timeouts still blow through. And when ESPN is unreachable it's unreachable for the whole slate, so the first failure already answers the question for the rest of the pass.
The stale-cache fallback is unchanged — the cache is consulted before any of this, and the failing request still falls back to it.
What I removed, and why
An earlier version of this branch jittered the cache TTL to stagger expiry across a slate. I've dropped it: it was inert.
CacheManager.set()storesttlfor compatibility, but the read path expires entries by a per-data-typemax_age— 1800s for odds — and never consults the stored value. Its own docstring says so, and I confirmed it inget_cached_data(), which passesmax_ageto both cache layers.Credit to the review for catching that; I'd asserted a mechanism without checking the read side.
Making the read path honour a per-entry
ttlis a genuine fix, but 48 plugin call sites already passttl=believing it works, so changing that contract would alter their expiry behaviour all at once. That's not a change to make two weeks before the season, and it deserves its own PR.I've also stopped claiming the stampede caused the hourly timeouts. Neither cache is hourly (schedule 86400s, odds 1800s), and normal updates already take 6–10s ("consider optimizing"), so the cadence isn't explained yet. This PR fixes a real hazard on its own terms; the hourly timeout still wants a proper investigation.
Verification
8 tests in
test_odds_request_budget.py: the timeout leaves room in the budget and is the one actually used; a 16-game slate makes exactly one network call when ESPN stalls; the worst case stays inside the budget; the breaker reopens on schedule; a healthy fetch clears it; and the stale-cache fallback still returns cached odds for the failing request.Two tests on main pinned the 30s timeout and now assert the new value plus the property that matters. Full suite: 2703 passed, 1 pre-existing unrelated failure (
test_install_lowmem).🤖 Generated with Claude Code
https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5