feat(fetch): shared fetch service, stage 1 (pooling, merging, host budgets, counters) (#702)

Core's own HTTP fetch paths (APIHelper, fetch_espn_scoreboard and its date chunks, BackgroundDataService, BaseOddsManager.get_odds) go through one service in src/common/fetch_service.py: shared connection pools per retry policy, merged identical in-flight GETs, per-host token-bucket budgets (fetch_service.rate_limits), and per-plugin request counters published to GET /api/v3/plugins/fetch-stats. Return values, exceptions, cache keys, TTLs and retry policies are unchanged. Core-internal in this release; plugins should not import it directly yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-01 15:38:11 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 9edeb6da14
commit 4be53d048b
19 changed files with 2407 additions and 31 deletions
+15
View File
@@ -132,6 +132,13 @@ class DisplayController:
# The web interface's /api/v3/errors/* read what this publishes.
from src.error_aggregator import start_error_snapshot_publisher
start_error_snapshot_publisher(self.cache_manager)
# Host budgets and the other fetch_service settings, before any plugin
# fetches; the web UI's fetch statistics read what the publisher
# writes (src/common/fetch_service.py).
from src.common.fetch_service import (
configure_fetch_service, start_fetch_stats_publisher)
configure_fetch_service(self.config.get('fetch_service'))
self._fetch_stats_publisher = start_fetch_stats_publisher(self.cache_manager)
logger.info("Config loaded in %.3f seconds (hot-reload: %s)", time.time() - start_time, enable_hot_reload)
# Validate startup configuration. Errors are logged, not fatal. The
@@ -3819,6 +3826,9 @@ class DisplayController:
read stale values after the user saves settings via the web UI.
"""
self.config = new_config
# A no-op unless the fetch_service section itself changed.
from src.common.fetch_service import configure_fetch_service
configure_fetch_service(new_config.get('fetch_service'))
self._normal_brightness = (
self.config.get('display', {}).get('hardware', {}).get('brightness', 90)
)
@@ -3886,6 +3896,11 @@ class DisplayController:
logger.warning("Error shutting down config service: %s", e)
if getattr(self, '_font_usage_publisher', None) is not None:
self._font_usage_publisher.stop()
if getattr(self, '_fetch_stats_publisher', None) is not None:
try:
self._fetch_stats_publisher.stop()
except Exception as e:
logger.warning("Error stopping the fetch statistics publisher: %s", e)
# Publishes "stopped", so the web UI stops reporting what was loaded.
if getattr(self, '_plugin_runtime_publisher', None) is not None:
try: