fix(youtube-stats): stop retrying a failing API once per frame - #323
Conversation
display() re-entered update() whenever channel_stats was empty:
if not self.channel_stats:
self.update()
and update() assigns the fetch result unconditionally, so any failure --
missing key, HTTP error, exhausted quota, network down -- left it empty and
the next frame tried again. That is one requests.get(timeout=10) per rendered
frame, on the render thread.
Two consequences, both self-sustaining. The render thread can block for the
full 10s timeout per frame. And the YouTube Data API's default quota is 10,000
units/day, so a broken key burns it down in minutes -- at which point quota
exhaustion becomes the error that keeps the result empty and the loop fed.
The cache in _get_channel_stats never absorbs this: a failed fetch caches
nothing.
display() now gates on whether a fetch has been ATTEMPTED rather than on
whether it produced data, and update() is throttled to update_interval. The
core already calls update() at about that cadence, so the throttle is a no-op
for it; it exists to bound the display-driven path.
monotonic(), not time(): these Pis have no RTC, so a wall-clock jump at NTP
sync must not make the next attempt look due or centuries away.
Tests: new test_no_per_frame_api_storm.py -- 200 frames against a permanently
failing API cost one call, the retry lands once the interval elapses, a
working API is unaffected. 6 of its 9 checks fail against the pre-fix manager.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 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 |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 5 |
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.
|
@coderabbitai review |
|
Codacy flagged line 59 as "No value for argument 'cls' in classmethod call". It is a false positive -- YouTubeStatsPlugin.__new__(YouTubeStatsPlugin) is valid -- but only this test triggers it, because its stub makes BasePlugin `object`, so __new__ resolves to object.__new__ and the explicit class argument reads as a bound call missing its cls. object.__new__(YouTubeStatsPlugin) says the same thing unambiguously. The sibling tests in hockey and soccer keep Cls.__new__(Cls); their stubs do not create the ambiguity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
Found by sweeping for network calls reachable from
display()— the render thread — after the thread-safety work on #322. Most hits were guarded or false positives; this one is real.The bug
update()assigns the result unconditionally, so a missing key, an HTTP error, an exhausted quota or a dropped network all leavechannel_statsempty — anddisplay()gates on that emptiness. The next frame tries again.That is one
requests.get(..., timeout=10)per rendered frame, on the render thread.Two consequences, both self-sustaining:
channels.listcosts 1. A broken key exhausts it in minutes — and quota exhaustion is itself an error, which keeps the result empty, which keeps the loop fed.The cache inside
_get_channel_statsdoesn't help: a failed fetch caches nothing, so there's never an entry to serve.Note the no-key path also
logger.error(...)s on every frame before returning.The fix
display()gates on whether a fetch has been attempted (_has_fetched), not on whether it produced data. This is the patternincoming-packagesandjellyfin-now-playingalready use for their first-paint call, and it's why neither has this bug.update()is throttled toupdate_interval. The core already calls it at roughly that cadence so this is a no-op for the core; it exists to bound the display-driven path so a failing API costs one request per interval rather than one per frame.time.monotonic(), nottime.time()— these Pis have no RTC, so a wall-clock jump at NTP sync must not make the next attempt look due (or centuries away).Verification
New
test_no_per_frame_api_storm.py(this plugin had no tests at all):display()no longer gates on the result;update()uses monotonic6 of its 9 checks fail against the pre-fix manager, so it pins the behaviour rather than describing it.
Not verified on hardware — both rigs have been unreachable all session.
plugins.jsondeliberately omitted;update-registry.ymlregenerates it on merge and carrying it conflicts with the other open PRs.🤖 Generated with Claude Code
https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW