fix(sports): share the ESPN rejected-range memo with the background service

BackgroundDataService always sent a season range first and, on a 400,
fell back to chunks without recording the rejection, so every background
season fetch spent a doomed request and live scoreboards learned nothing
from it (or it from them). The worker now consults and sets the same
6-hour memo fetch_espn_scoreboard() uses: a known rejection goes straight
to month/day chunks, and if every chunk fails the range is asked once for
a real error without re-spending the chunks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-16 18:27:40 -04:00
co-authored by Claude Opus 5
parent 7ae614aa35
commit 837dd1fde7
2 changed files with 86 additions and 10 deletions
+35 -9
View File
@@ -25,7 +25,14 @@ from enum import Enum
import queue
from concurrent.futures import ThreadPoolExecutor
from src.cache_manager import CacheManager
from src.common.espn_dates import clamp_espn_limit, fetch_espn_date_chunks
from src.common.espn_dates import (
RANGE_RETRY_SECONDS,
_note_range_rejected,
_ranges_known_rejected,
clamp_espn_limit,
fetch_espn_date_chunks,
parse_espn_date_range,
)
# Configure logging
logger = logging.getLogger(__name__)
@@ -350,19 +357,38 @@ class BackgroundDataService:
logger.info(f"Starting background fetch for {request.sport} {request.year}")
# Perform HTTP request with retry logic
response = self._make_request_with_retry(request)
# ESPN stopped accepting dates=YYYYMMDD-YYYYMMDD on 2026-09-15 and
# answers 400 for every sport. Re-ask in months and days rather
# than let a whole season fail. See src/common/espn_dates.py.
if response.status_code == 400:
# The "ranges are rejected" memo is shared with
# fetch_espn_scoreboard(): once either path has seen a range
# rejected, the other skips the doomed range request too.
is_range = parse_espn_date_range(request.params.get("dates")) is not None
data = None
chunks_tried = False
if is_range and _ranges_known_rejected():
data = self._fetch_in_date_chunks(request)
if data is None:
# Every chunk failed: ask for the range itself below so the
# failure carries a real HTTP error, without re-spending chunks.
chunks_tried = data is None
if data is None:
# Perform HTTP request with retry logic
response = self._make_request_with_retry(request)
if is_range and response.status_code == 400 and not chunks_tried:
_note_range_rejected()
logger.warning(
"ESPN rejected the date range %s (400); fetching it as "
"month/day chunks, and fetching ranges that way for the "
"next %d hours",
request.params.get("dates"), RANGE_RETRY_SECONDS // 3600,
)
data = self._fetch_in_date_chunks(request)
if data is None:
response.raise_for_status()
else:
response.raise_for_status()
else:
response.raise_for_status()
data = response.json()
data = response.json()
# Validate data structure
if not isinstance(data, dict):