From 42e14f99b0b6ea063ad917bd57bd2ab7b8100ea2 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:13:41 -0400 Subject: [PATCH] Fix duplicate caching (#62) * Fix duplicate/redundant caching issue - Add get_background_cached_data() and is_background_data_available() methods to CacheManager - Update sports managers to use background service cache instead of individual caching - Ensure consistent cache key generation between background service and managers - Eliminate redundant API calls by making Recent/Upcoming managers cache consumers - Fix cache miss issues where TTL < update interval This addresses GitHub issue #57 by implementing a cleaner caching architecture where the background service is the primary data source and managers are cache consumers. * Update remaining sports managers to use background service cache - Update NHL managers to use background service cache - Update NCAA Football managers to use background service cache - Update NCAA Hockey managers to use background service cache - Update MLB managers to use background service cache for Recent/Upcoming All sports managers now use the new caching architecture to eliminate duplicate caching and redundant API calls. * cache improvements * updated cache manager --- LEDMatrix.wiki | 2 +- src/background_cache_mixin.py | 134 ++++++++++++++++++++++++++++ src/background_data_service.py | 17 +++- src/cache_manager.py | 132 ++++++++++++++++++++++++++- src/generic_cache_mixin.py | 148 +++++++++++++++++++++++++++++++ src/mlb_manager.py | 15 +++- src/nba_managers.py | 18 ++-- src/ncaa_fb_managers.py | 25 +++++- src/ncaam_basketball_managers.py | 25 +++++- src/ncaam_hockey_managers.py | 25 +++++- src/nfl_managers.py | 20 +++-- src/nhl_managers.py | 27 ++++-- src/soccer_managers.py | 25 +++++- ubmodule status | 51 ----------- 14 files changed, 581 insertions(+), 83 deletions(-) create mode 100644 src/background_cache_mixin.py create mode 100644 src/generic_cache_mixin.py delete mode 100644 ubmodule status diff --git a/LEDMatrix.wiki b/LEDMatrix.wiki index ebde098b..a01c72e1 160000 --- a/LEDMatrix.wiki +++ b/LEDMatrix.wiki @@ -1 +1 @@ -Subproject commit ebde098b50bcbe101aa648928f6afbf27358e334 +Subproject commit a01c72e156b46c08a5ef1c67db79acd73300a6f7 diff --git a/src/background_cache_mixin.py b/src/background_cache_mixin.py new file mode 100644 index 00000000..21fe6af7 --- /dev/null +++ b/src/background_cache_mixin.py @@ -0,0 +1,134 @@ +""" +Background Cache Mixin for Sports Managers + +This mixin provides common caching functionality to eliminate code duplication +across all sports managers. It implements the background service cache pattern +where Recent/Upcoming managers consume data from the background service cache. +""" + +import time +import logging +from typing import Dict, Optional, Any, Callable +from datetime import datetime +import pytz + + +class BackgroundCacheMixin: + """ + Mixin class that provides background service cache functionality to sports managers. + + This mixin eliminates code duplication by providing a common implementation + for the background service cache pattern used across all sports managers. + """ + + def _fetch_data_with_background_cache(self, + sport_key: str, + api_fetch_method: Callable, + live_manager_class: type = None) -> Optional[Dict]: + """ + Common logic for fetching data with background service cache support. + + This method implements the background service cache pattern: + 1. Live managers always fetch fresh data + 2. Recent/Upcoming managers try background cache first + 3. Fallback to direct API call if background data unavailable + + Args: + sport_key: Sport identifier (e.g., 'nba', 'nfl', 'ncaa_fb') + api_fetch_method: Method to call for direct API fetch + live_manager_class: Class to check if this is a live manager + + Returns: + Cached or fresh data from API + """ + start_time = time.time() + cache_hit = False + cache_source = None + + try: + # For Live managers, always fetch fresh data + if live_manager_class and isinstance(self, live_manager_class): + self.logger.info(f"[{sport_key.upper()}] Live manager - fetching fresh data") + result = api_fetch_method(use_cache=False) + cache_source = "live_fresh" + else: + # For Recent/Upcoming managers, try background service cache first + cache_key = self.cache_manager.generate_sport_cache_key(sport_key) + + # Check if background service has fresh data + if self.cache_manager.is_background_data_available(cache_key, sport_key): + cached_data = self.cache_manager.get_background_cached_data(cache_key, sport_key) + if cached_data: + self.logger.info(f"[{sport_key.upper()}] Using background service cache for {cache_key}") + result = cached_data + cache_hit = True + cache_source = "background_cache" + else: + self.logger.warning(f"[{sport_key.upper()}] Background cache check passed but no data returned for {cache_key}") + result = None + cache_source = "background_miss" + else: + self.logger.info(f"[{sport_key.upper()}] Background data not available for {cache_key}") + result = None + cache_source = "background_unavailable" + + # Fallback to direct API call if background data not available + if result is None: + self.logger.info(f"[{sport_key.upper()}] Fetching directly from API for {cache_key}") + result = api_fetch_method(use_cache=True) + cache_source = "api_fallback" + + # Record performance metrics + duration = time.time() - start_time + self.cache_manager.record_fetch_time(duration) + + # Log performance metrics + self._log_fetch_performance(sport_key, duration, cache_hit, cache_source) + + return result + + except Exception as e: + duration = time.time() - start_time + self.logger.error(f"[{sport_key.upper()}] Error in background cache fetch after {duration:.2f}s: {e}") + self.cache_manager.record_fetch_time(duration) + raise + + def _log_fetch_performance(self, sport_key: str, duration: float, cache_hit: bool, cache_source: str): + """ + Log detailed performance metrics for fetch operations. + + Args: + sport_key: Sport identifier + duration: Fetch operation duration in seconds + cache_hit: Whether this was a cache hit + cache_source: Source of the data (background_cache, api_fallback, etc.) + """ + # Log basic performance info + self.logger.info(f"[{sport_key.upper()}] Fetch completed in {duration:.2f}s " + f"(cache_hit={cache_hit}, source={cache_source})") + + # Log detailed metrics every 10 operations + if hasattr(self, '_fetch_count'): + self._fetch_count += 1 + else: + self._fetch_count = 1 + + if self._fetch_count % 10 == 0: + metrics = self.cache_manager.get_cache_metrics() + self.logger.info(f"[{sport_key.upper()}] Cache Performance Summary - " + f"Hit Rate: {metrics['cache_hit_rate']:.2%}, " + f"Background Hit Rate: {metrics['background_hit_rate']:.2%}, " + f"API Calls Saved: {metrics['api_calls_saved']}") + + def get_cache_performance_summary(self) -> Dict[str, Any]: + """ + Get cache performance summary for this manager. + + Returns: + Dictionary containing cache performance metrics + """ + return self.cache_manager.get_cache_metrics() + + def log_cache_performance(self): + """Log current cache performance metrics.""" + self.cache_manager.log_cache_metrics() diff --git a/src/background_data_service.py b/src/background_data_service.py index 8c16d15b..89ef4d4e 100644 --- a/src/background_data_service.py +++ b/src/background_data_service.py @@ -128,11 +128,22 @@ class BackgroundDataService: logger.info(f"BackgroundDataService initialized with {max_workers} workers") + def get_sport_cache_key(self, sport: str, date_str: str = None) -> str: + """ + Generate consistent cache keys for sports data. + This ensures Recent/Upcoming managers and background service + use the same cache keys. + """ + # Use the centralized cache key generation from CacheManager + from src.cache_manager import CacheManager + cache_manager = CacheManager() + return cache_manager.generate_sport_cache_key(sport, date_str) + def submit_fetch_request(self, sport: str, year: int, url: str, - cache_key: str, + cache_key: str = None, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, timeout: Optional[int] = None, @@ -160,6 +171,10 @@ class BackgroundDataService: if self._shutdown: raise RuntimeError("BackgroundDataService is shutting down") + # Generate cache key if not provided + if cache_key is None: + cache_key = self.get_sport_cache_key(sport) + request_id = f"{sport}_{year}_{int(time.time() * 1000)}" # Check cache first diff --git a/src/cache_manager.py b/src/cache_manager.py index 58d054b5..93ac7741 100644 --- a/src/cache_manager.py +++ b/src/cache_manager.py @@ -44,6 +44,17 @@ class CacheManager: except ImportError: self.config_manager = None self.logger.warning("ConfigManager not available, using default cache intervals") + + # Initialize performance metrics + self._cache_metrics = { + 'hits': 0, + 'misses': 0, + 'api_calls_saved': 0, + 'background_hits': 0, + 'background_misses': 0, + 'total_fetch_time': 0.0, + 'fetch_count': 0 + } def _get_writable_cache_dir(self) -> Optional[str]: """Tries to find or create a writable cache directory, preferring a system path when available.""" @@ -735,4 +746,123 @@ class CacheManager: Now respects sport-specific live_update_interval configurations. """ data_type = self.get_data_type_from_key(key) - return self.get_cached_data_with_strategy(key, data_type) \ No newline at end of file + return self.get_cached_data_with_strategy(key, data_type) + + def get_background_cached_data(self, key: str, sport_key: str = None) -> Optional[Dict]: + """ + Get data from background service cache with appropriate strategy. + This method is specifically designed for Recent/Upcoming managers + to use data cached by the background service. + + Args: + key: Cache key to retrieve + sport_key: Sport key for determining appropriate cache strategy + + Returns: + Cached data if available and fresh, None otherwise + """ + # Determine the appropriate cache strategy + data_type = self.get_data_type_from_key(key) + strategy = self.get_cache_strategy(data_type, sport_key) + + # For Recent/Upcoming managers, we want to use the background service cache + # which should have longer TTLs than the individual manager caches + max_age = strategy['max_age'] + memory_ttl = strategy.get('memory_ttl', max_age) + + # Get the cached data + cached_data = self.get_cached_data(key, max_age, memory_ttl) + + if cached_data: + # Record cache hit for performance monitoring + self.record_cache_hit('background') + # Unwrap if stored in { 'data': ..., 'timestamp': ... } format + if isinstance(cached_data, dict) and 'data' in cached_data: + return cached_data['data'] + return cached_data + + # Record cache miss for performance monitoring + self.record_cache_miss('background') + return None + + def is_background_data_available(self, key: str, sport_key: str = None) -> bool: + """ + Check if background service has fresh data available. + This helps Recent/Upcoming managers determine if they should + wait for background data or fetch immediately. + """ + data_type = self.get_data_type_from_key(key) + strategy = self.get_cache_strategy(data_type, sport_key) + + # Check if we have data that's still fresh according to background service TTL + cached_data = self.get_cached_data(key, strategy['max_age']) + return cached_data is not None + + def generate_sport_cache_key(self, sport: str, date_str: str = None) -> str: + """ + Centralized cache key generation for sports data. + This ensures consistent cache keys across background service and managers. + + Args: + sport: Sport identifier (e.g., 'nba', 'nfl', 'ncaa_fb') + date_str: Date string in YYYYMMDD format. If None, uses current UTC date. + + Returns: + Cache key in format: {sport}_{date} + """ + if date_str is None: + date_str = datetime.now(pytz.utc).strftime('%Y%m%d') + return f"{sport}_{date_str}" + + def record_cache_hit(self, cache_type: str = 'regular'): + """Record a cache hit for performance monitoring.""" + with self._cache_lock: + if cache_type == 'background': + self._cache_metrics['background_hits'] += 1 + else: + self._cache_metrics['hits'] += 1 + + def record_cache_miss(self, cache_type: str = 'regular'): + """Record a cache miss for performance monitoring.""" + with self._cache_lock: + if cache_type == 'background': + self._cache_metrics['background_misses'] += 1 + else: + self._cache_metrics['misses'] += 1 + self._cache_metrics['api_calls_saved'] += 1 + + def record_fetch_time(self, duration: float): + """Record fetch operation duration for performance monitoring.""" + with self._cache_lock: + self._cache_metrics['total_fetch_time'] += duration + self._cache_metrics['fetch_count'] += 1 + + def get_cache_metrics(self) -> Dict[str, Any]: + """Get current cache performance metrics.""" + with self._cache_lock: + total_hits = self._cache_metrics['hits'] + self._cache_metrics['background_hits'] + total_misses = self._cache_metrics['misses'] + self._cache_metrics['background_misses'] + total_requests = total_hits + total_misses + + avg_fetch_time = (self._cache_metrics['total_fetch_time'] / + self._cache_metrics['fetch_count']) if self._cache_metrics['fetch_count'] > 0 else 0.0 + + return { + 'total_requests': total_requests, + 'cache_hit_rate': total_hits / total_requests if total_requests > 0 else 0.0, + 'background_hit_rate': (self._cache_metrics['background_hits'] / + (self._cache_metrics['background_hits'] + self._cache_metrics['background_misses']) + if (self._cache_metrics['background_hits'] + self._cache_metrics['background_misses']) > 0 else 0.0), + 'api_calls_saved': self._cache_metrics['api_calls_saved'], + 'average_fetch_time': avg_fetch_time, + 'total_fetch_time': self._cache_metrics['total_fetch_time'], + 'fetch_count': self._cache_metrics['fetch_count'] + } + + def log_cache_metrics(self): + """Log current cache performance metrics.""" + metrics = self.get_cache_metrics() + self.logger.info(f"Cache Performance - Hit Rate: {metrics['cache_hit_rate']:.2%}, " + f"Background Hit Rate: {metrics['background_hit_rate']:.2%}, " + f"API Calls Saved: {metrics['api_calls_saved']}, " + f"Avg Fetch Time: {metrics['average_fetch_time']:.2f}s") \ No newline at end of file diff --git a/src/generic_cache_mixin.py b/src/generic_cache_mixin.py new file mode 100644 index 00000000..2a910caf --- /dev/null +++ b/src/generic_cache_mixin.py @@ -0,0 +1,148 @@ +""" +Generic Cache Mixin for Any Manager + +This mixin provides caching functionality that can be used by any manager +that needs to cache data, not just sports managers. It's a more general +version of BackgroundCacheMixin that works for weather, stocks, news, etc. +""" + +import time +import logging +from typing import Dict, Optional, Any, Callable + + +class GenericCacheMixin: + """ + Generic mixin class that provides caching functionality to any manager. + + This mixin can be used by weather, stock, news, or any other manager + that needs to cache data with performance monitoring. + """ + + def _fetch_data_with_cache(self, + cache_key: str, + api_fetch_method: Callable, + cache_ttl: int = 300, + force_refresh: bool = False) -> Optional[Dict]: + """ + Generic caching pattern for any manager. + + Args: + cache_key: Unique cache key for this data + api_fetch_method: Method to call for fresh data + cache_ttl: Time-to-live in seconds (default: 5 minutes) + force_refresh: Skip cache and fetch fresh data + + Returns: + Cached or fresh data from API + """ + start_time = time.time() + cache_hit = False + cache_source = None + + try: + # Check cache first (unless forcing refresh) + if not force_refresh: + cached_data = self.cache_manager.get_cached_data(cache_key, cache_ttl) + if cached_data: + self.logger.info(f"Using cached data for {cache_key}") + cache_hit = True + cache_source = "cache" + self.cache_manager.record_cache_hit('regular') + + # Record performance metrics + duration = time.time() - start_time + self.cache_manager.record_fetch_time(duration) + self._log_fetch_performance(cache_key, duration, cache_hit, cache_source) + + return cached_data + + # Fetch fresh data + self.logger.info(f"Fetching fresh data for {cache_key}") + result = api_fetch_method() + cache_source = "api_fresh" + + # Store in cache if we got data + if result: + self.cache_manager.save_cache(cache_key, result) + self.cache_manager.record_cache_miss('regular') + else: + self.logger.warning(f"No data returned for {cache_key}") + + # Record performance metrics + duration = time.time() - start_time + self.cache_manager.record_fetch_time(duration) + + # Log performance + self._log_fetch_performance(cache_key, duration, cache_hit, cache_source) + + return result + + except Exception as e: + duration = time.time() - start_time + self.logger.error(f"Error fetching data for {cache_key} after {duration:.2f}s: {e}") + self.cache_manager.record_fetch_time(duration) + raise + + def _log_fetch_performance(self, cache_key: str, duration: float, cache_hit: bool, cache_source: str): + """ + Log detailed performance metrics for fetch operations. + + Args: + cache_key: Cache key that was accessed + duration: Fetch operation duration in seconds + cache_hit: Whether this was a cache hit + cache_source: Source of the data (cache, api_fresh, etc.) + """ + # Log basic performance info + self.logger.info(f"Fetch completed for {cache_key} in {duration:.2f}s " + f"(cache_hit={cache_hit}, source={cache_source})") + + # Log detailed metrics every 10 operations + if hasattr(self, '_fetch_count'): + self._fetch_count += 1 + else: + self._fetch_count = 1 + + if self._fetch_count % 10 == 0: + metrics = self.cache_manager.get_cache_metrics() + self.logger.info(f"Cache Performance Summary - " + f"Hit Rate: {metrics['cache_hit_rate']:.2%}, " + f"API Calls Saved: {metrics['api_calls_saved']}, " + f"Avg Fetch Time: {metrics['average_fetch_time']:.2f}s") + + def get_cache_performance_summary(self) -> Dict[str, Any]: + """ + Get cache performance summary for this manager. + + Returns: + Dictionary containing cache performance metrics + """ + return self.cache_manager.get_cache_metrics() + + def log_cache_performance(self): + """Log current cache performance metrics.""" + self.cache_manager.log_cache_metrics() + + def clear_cache_for_key(self, cache_key: str): + """Clear cache for a specific key.""" + self.cache_manager.clear_cache(cache_key) + self.logger.info(f"Cleared cache for {cache_key}") + + def get_cache_info(self, cache_key: str) -> Dict[str, Any]: + """ + Get information about a cached item. + + Args: + cache_key: Cache key to check + + Returns: + Dictionary with cache information + """ + # This would need to be implemented in CacheManager + # For now, just return basic info + return { + 'key': cache_key, + 'exists': self.cache_manager.get_cached_data(cache_key, 0) is not None, + 'ttl': 'unknown' # Would need to be implemented + } diff --git a/src/mlb_manager.py b/src/mlb_manager.py index 9dfb2210..762154b4 100644 --- a/src/mlb_manager.py +++ b/src/mlb_manager.py @@ -410,7 +410,10 @@ class BaseMLBManager: return "TBD" def _fetch_mlb_api_data(self, use_cache: bool = True) -> Dict[str, Any]: - """Fetch MLB game data from the ESPN API.""" + """ + Fetch MLB game data from the ESPN API. + Updated to use background service cache for Recent/Upcoming managers. + """ # Define cache key based on dates now = datetime.now(timezone.utc) yesterday = now - timedelta(days=1) @@ -420,6 +423,16 @@ class BaseMLBManager: # If using cache, try to load from cache first if use_cache: + # For Recent/Upcoming managers, try background service cache first + if hasattr(self, '__class__') and any(x in self.__class__.__name__ for x in ['Recent', 'Upcoming']): + if self.cache_manager.is_background_data_available(cache_key, 'mlb'): + cached_data = self.cache_manager.get_background_cached_data(cache_key, 'mlb') + if cached_data: + self.logger.info(f"[MLB] Using background service cache for {cache_key}") + return cached_data + self.logger.info(f"[MLB] Background data not available, fetching directly for {cache_key}") + + # Fallback to regular cache strategy cached_data = self.cache_manager.get_with_auto_strategy(cache_key) if cached_data: self.logger.info("Using cached MLB API data.") diff --git a/src/nba_managers.py b/src/nba_managers.py index 38ece702..7b554834 100644 --- a/src/nba_managers.py +++ b/src/nba_managers.py @@ -12,6 +12,7 @@ from src.cache_manager import CacheManager from src.config_manager import ConfigManager from src.odds_manager import OddsManager from src.background_data_service import get_background_service +from src.background_cache_mixin import BackgroundCacheMixin import pytz # Import the API counter function from web interface @@ -32,7 +33,7 @@ logging.basicConfig( datefmt='%Y-%m-%d %H:%M:%S' ) -class BaseNBAManager: +class BaseNBAManager(BackgroundCacheMixin): """Base class for NBA managers with common functionality.""" # Class variables for warning tracking _no_data_warning_logged = False @@ -317,11 +318,16 @@ class BaseNBAManager: return None def _fetch_data(self, date_str: str = None) -> Optional[Dict]: - """Fetch data using shared data mechanism.""" - if isinstance(self, NBALiveManager): - return self._fetch_nba_api_data(use_cache=False) - else: - return self._fetch_nba_api_data(use_cache=True) + """ + Fetch data using background service cache first, fallback to direct API call. + This eliminates redundant caching and ensures Recent/Upcoming managers + use the same data source as the background service. + """ + return self._fetch_data_with_background_cache( + sport_key='nba', + api_fetch_method=self._fetch_nba_api_data, + live_manager_class=NBALiveManager + ) def _fetch_odds(self, game: Dict) -> None: """Fetch odds for a specific game if conditions are met.""" diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index 2ddefffa..ccd2d10a 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -377,11 +377,30 @@ class BaseNCAAFBManager: # Renamed class return [] def _fetch_data(self, date_str: str = None) -> Optional[Dict]: - """Fetch data using shared data mechanism or direct fetch for live.""" + """ + Fetch data using background service cache first, fallback to direct API call. + This eliminates redundant caching and ensures Recent/Upcoming managers + use the same data source as the background service. + """ + # For Live managers, always fetch fresh data if isinstance(self, NCAAFBLiveManager): return self._fetch_ncaa_fb_api_data(use_cache=False) - else: - return self._fetch_ncaa_fb_api_data(use_cache=True) + + # For Recent/Upcoming managers, try to use background service cache first + from datetime import datetime + import pytz + cache_key = f"ncaa_fb_{datetime.now(pytz.utc).strftime('%Y%m%d')}" + + # Check if background service has fresh data + if self.cache_manager.is_background_data_available(cache_key, 'ncaa_fb'): + cached_data = self.cache_manager.get_background_cached_data(cache_key, 'ncaa_fb') + if cached_data: + self.logger.info(f"[NCAAFB] Using background service cache for {cache_key}") + return cached_data + + # Fallback to direct API call if background data not available + self.logger.info(f"[NCAAFB] Background data not available, fetching directly for {cache_key}") + return self._fetch_ncaa_fb_api_data(use_cache=True) def _load_fonts(self): diff --git a/src/ncaam_basketball_managers.py b/src/ncaam_basketball_managers.py index c3165cef..58c6b401 100644 --- a/src/ncaam_basketball_managers.py +++ b/src/ncaam_basketball_managers.py @@ -347,11 +347,30 @@ class BaseNCAAMBasketballManager: return None def _fetch_data(self, date_str: str = None) -> Optional[Dict]: - """Fetch data using shared data mechanism.""" + """ + Fetch data using background service cache first, fallback to direct API call. + This eliminates redundant caching and ensures Recent/Upcoming managers + use the same data source as the background service. + """ + # For Live managers, always fetch fresh data if isinstance(self, NCAAMBasketballLiveManager): return self._fetch_ncaam_basketball_api_data(use_cache=False) - else: - return self._fetch_ncaam_basketball_api_data(use_cache=True) + + # For Recent/Upcoming managers, try to use background service cache first + from datetime import datetime + import pytz + cache_key = f"ncaam_basketball_{datetime.now(pytz.utc).strftime('%Y%m%d')}" + + # Check if background service has fresh data + if self.cache_manager.is_background_data_available(cache_key, 'ncaam_basketball'): + cached_data = self.cache_manager.get_background_cached_data(cache_key, 'ncaam_basketball') + if cached_data: + self.logger.info(f"[NCAAMBasketball] Using background service cache for {cache_key}") + return cached_data + + # Fallback to direct API call if background data not available + self.logger.info(f"[NCAAMBasketball] Background data not available, fetching directly for {cache_key}") + return self._fetch_ncaam_basketball_api_data(use_cache=True) def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: """Extract relevant game details from ESPN API response.""" diff --git a/src/ncaam_hockey_managers.py b/src/ncaam_hockey_managers.py index f4ae7e86..eb1ad365 100644 --- a/src/ncaam_hockey_managers.py +++ b/src/ncaam_hockey_managers.py @@ -244,11 +244,30 @@ class BaseNCAAMHockeyManager: # Renamed class return {'events': all_events} def _fetch_data(self, date_str: str = None) -> Optional[Dict]: - """Fetch data using shared data mechanism or direct fetch for live.""" + """ + Fetch data using background service cache first, fallback to direct API call. + This eliminates redundant caching and ensures Recent/Upcoming managers + use the same data source as the background service. + """ + # For Live managers, always fetch fresh data if isinstance(self, NCAAMHockeyLiveManager): return self._fetch_ncaa_fb_api_data(use_cache=False) - else: - return self._fetch_ncaa_fb_api_data(use_cache=True) + + # For Recent/Upcoming managers, try to use background service cache first + from datetime import datetime + import pytz + cache_key = f"ncaam_hockey_{datetime.now(pytz.utc).strftime('%Y%m%d')}" + + # Check if background service has fresh data + if self.cache_manager.is_background_data_available(cache_key, 'ncaam_hockey'): + cached_data = self.cache_manager.get_background_cached_data(cache_key, 'ncaam_hockey') + if cached_data: + self.logger.info(f"[NCAAMHockey] Using background service cache for {cache_key}") + return cached_data + + # Fallback to direct API call if background data not available + self.logger.info(f"[NCAAMHockey] Background data not available, fetching directly for {cache_key}") + return self._fetch_ncaa_fb_api_data(use_cache=True) def _load_fonts(self): """Load fonts used by the scoreboard.""" diff --git a/src/nfl_managers.py b/src/nfl_managers.py index ce6a6de6..e41872bc 100644 --- a/src/nfl_managers.py +++ b/src/nfl_managers.py @@ -12,6 +12,7 @@ from src.cache_manager import CacheManager from src.config_manager import ConfigManager from src.odds_manager import OddsManager from src.background_data_service import get_background_service +from src.background_cache_mixin import BackgroundCacheMixin import pytz # Constants @@ -27,7 +28,7 @@ logging.basicConfig( -class BaseNFLManager: # Renamed class +class BaseNFLManager(BackgroundCacheMixin): # Renamed class """Base class for NFL managers with common functionality.""" # Class variables for warning tracking _no_data_warning_logged = False @@ -330,13 +331,22 @@ class BaseNFLManager: # Renamed class return None def _fetch_data(self, date_str: str = None) -> Optional[Dict]: - """Fetch data using shared data mechanism or direct fetch for live.""" + """ + Fetch data using background service cache first, fallback to direct API call. + This eliminates redundant caching and ensures Recent/Upcoming managers + use the same data source as the background service. + """ + # For Live managers, always fetch fresh data if isinstance(self, NFLLiveManager): # Live games should fetch only current games, not entire season return self._fetch_current_nfl_games() - else: - # Recent and Upcoming managers should use cached season data - return self._fetch_nfl_api_data(use_cache=True) + + # For Recent/Upcoming managers, use the centralized background cache method + return self._fetch_data_with_background_cache( + sport_key='nfl', + api_fetch_method=self._fetch_nfl_api_data, + live_manager_class=NFLLiveManager + ) def _load_fonts(self): """Load fonts used by the scoreboard.""" diff --git a/src/nhl_managers.py b/src/nhl_managers.py index d820e8d0..4480cce5 100644 --- a/src/nhl_managers.py +++ b/src/nhl_managers.py @@ -163,13 +163,30 @@ class BaseNHLManager: return None def _fetch_data(self, date_str: str = None) -> Optional[Dict]: - """Fetch data using the new centralized method.""" - # For live games, bypass the shared cache to ensure fresh data + """ + Fetch data using background service cache first, fallback to direct API call. + This eliminates redundant caching and ensures Recent/Upcoming managers + use the same data source as the background service. + """ + # For Live managers, always fetch fresh data if isinstance(self, NHLLiveManager): return self._fetch_nhl_api_data(use_cache=False) - else: - # For non-live games, use the shared cache - return self._fetch_nhl_api_data(use_cache=True) + + # For Recent/Upcoming managers, try to use background service cache first + from datetime import datetime + import pytz + cache_key = f"nhl_{datetime.now(pytz.utc).strftime('%Y%m%d')}" + + # Check if background service has fresh data + if self.cache_manager.is_background_data_available(cache_key, 'nhl'): + cached_data = self.cache_manager.get_background_cached_data(cache_key, 'nhl') + if cached_data: + self.logger.info(f"[NHL] Using background service cache for {cache_key}") + return cached_data + + # Fallback to direct API call if background data not available + self.logger.info(f"[NHL] Background data not available, fetching directly for {cache_key}") + return self._fetch_nhl_api_data(use_cache=True) def _load_fonts(self): """Load fonts used by the scoreboard.""" diff --git a/src/soccer_managers.py b/src/soccer_managers.py index f9a0d717..2ee09609 100644 --- a/src/soccer_managers.py +++ b/src/soccer_managers.py @@ -332,11 +332,30 @@ class BaseSoccerManager: return set(self.target_leagues_config) def _fetch_data(self, date_str: str = None) -> Optional[Dict]: - """Fetch data using shared data mechanism or live fetching per league.""" + """ + Fetch data using background service cache first, fallback to direct API call. + This eliminates redundant caching and ensures Recent/Upcoming managers + use the same data source as the background service. + """ + # For Live managers, always fetch fresh data if isinstance(self, SoccerLiveManager) and not self.test_mode: return self._fetch_soccer_api_data(use_cache=False) - else: - return self._fetch_soccer_api_data(use_cache=True) + + # For Recent/Upcoming managers, try to use background service cache first + from datetime import datetime + import pytz + cache_key = f"soccer_{datetime.now(pytz.utc).strftime('%Y%m%d')}" + + # Check if background service has fresh data + if self.cache_manager.is_background_data_available(cache_key, 'soccer'): + cached_data = self.cache_manager.get_background_cached_data(cache_key, 'soccer') + if cached_data: + self.logger.info(f"[Soccer] Using background service cache for {cache_key}") + return cached_data + + # Fallback to direct API call if background data not available + self.logger.info(f"[Soccer] Background data not available, fetching directly for {cache_key}") + return self._fetch_soccer_api_data(use_cache=True) def _load_fonts(self): """Load fonts used by the scoreboard.""" diff --git a/ubmodule status b/ubmodule status deleted file mode 100644 index 46efff8f..00000000 --- a/ubmodule status +++ /dev/null @@ -1,51 +0,0 @@ -8c03e651 (HEAD -> development, origin/development) wiki page about team abbreviations -764d80e8 added more missing soccer logos -854c236a added portuguese soccer league to documentation for soccer manager and added auto-download missing logos for soccer teams -4b1b343a shift album font down 2 pixels -65f04bff adjust music manager album text location -80558561 shift of the day description and subtitle down in more situations -17a79976 shift of the day description down one more pixel for total of 4 -38062d0b shift album and artist font down -2ce25205 shift of the day description down one more pixel -c7ee9468 shift music album and artist font down the height of the text font -3afcbb75 add freetype error handling -bc182027 shift of the day underline up one pixel -a0973a2a shift whole display down 8 pixels -c18ab3f9 dialing of the day text spacing - revert back to too much spacing -97185950 dialing of the day text spacing -5f803f34 dialing of the day text spacing -03208307 of the day manager text refactor -9dd74425 of the day manager text positioning placement fix -67b6a6fd adjust spacing on of the day manager text -ca62fd71 Make sure sports displays are properly processing number of recent games to show -49346f9a change of the day file path detection -9200c9ca update logic on all sports displays that upcoming and recent games to show are based on each team, not just the first X # of games found -dbdb730b fix upcoming game logic for NFL display too -91211d5c fix upcoming game logic -d78c592d NCAA FB season now downloads in full but it slows down the display significantly, data fetch has been moved to the background and deferred during scrolling displays -4771ec8b add black buffer behind odds ticker to finish scroll -60f68ff2 add permission handling of the day to first time install script -c7634cbf NCAA FB odds fix -96cd3834 NCAA FB logging to figure out why recent and upcoming games aren't loading well -e39dd1e0 NCAA FB logging -7b133963 of the day try block fix -0579b3b8 more of the day debug logging and fix datetime duplicate in NCAA FB -e7e76eea more robust NCAA FB manager upcoming game check -3f431a54 path resolution for of the day manager -d0f87859 troubleshooting of the day manager -7618eafa troubleshooting of the day manager -f8f45390 hopefully fix of the day settings -0ab978d5 web ui config setting accuracy changes -c4a51d0f espn api update for NCAAFB -b20c3880 make sure web ui is pulling existing config options -652461a8 ensure leaderboard is in webui -691d3967 web ui bug fixes -9bc0cd56 moving away from dict errors -625a501d further dict wrapper update -28c2dcd2 fix dict class for web ui -c55511c0 webui changes to launch after config file changes -b96f1e39 make sure web ui save buttons work -fcbc6746 persistent config file via config.template.json and migrate_config.sh -4b36937a Update sports league logos -8ead8ad8 Fix NCAA Football recent games to show truly recent games (last 14 days) instead of entire season -fbff65fb fix NCAA FB Quarter logic. Fix -1th and 10 status text when negative yards are received