from .coordinator import Coordinator from backup.worker import Worker from injector import inject, singleton from backup.time import Time from backup.logger import getLogger from backup.config import Config, Setting from .model import BackupDestination from .precache import Precache from random import Random from datetime import datetime, timedelta from dataclasses import dataclass from typing import Any, Dict from logging import DEBUG logger = getLogger(__name__) @dataclass class CacheItem: """Class for keeping track of an item in inventory.""" valid_until: datetime data: Any @singleton class DestinationPrecache(Worker, Precache): @inject def __init__(self, coord: Coordinator, time: Time, dest: BackupDestination, config: Config): super().__init__("Traffic Smoothing Cache", self.checkForSmoothing, time, 60) self._config = config self._coord = coord self._dest = dest self._offset = Random().random() self._cache: Dict[str, CacheItem] = {} self._last_error: datetime = None async def checkForSmoothing(self): if self._config.get(Setting.CACHE_WARMUP_MAX_SECONDS) == 0: # disable cache warmup return try: self._coord.setPrecache(self) nextSync = self._coord.nextSyncAttempt() now = self._time.now() if nextSync <= now: # No reason to warm the cache if we should sync right now anyway return if self.cached(self._dest.name(), now): # A value is already cached, so don't do anything return if now >= self.getNextWarmDate(): # Warm the cache logger.debug("Preemptively retrieving and caching info from the backup destination to avoid peak demand") data = await self._dest.get() validity = nextSync + timedelta(minutes=1) self._cache[self._dest.name()] = CacheItem(validity, data) self._offset = Random().random() except Exception as e: # Any error should make us avoid precaching for a solid day. logger.debug("Unable to precache data from backup destination") logger.printException(e, level=DEBUG) self._offset = Random().random() if self._config.get(Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS) != 0: self._last_error = self._time.now() def getNextWarmDate(self): warm_date = self._coord.nextSyncAttempt() - timedelta(seconds=self._config.get(Setting.CACHE_WARMUP_MAX_SECONDS) * self._offset) if self._last_error: return max(warm_date, self._last_error + timedelta(self._config.get(Setting.CACHE_WARMUP_ERROR_TIMEOUT_SECONDS))) return warm_date def cached(self, source: str, date: datetime) -> Any: cached = self._cache.get(source) if cached and cached.valid_until >= date: return cached.data return None def clear(self): """Clears any precached data""" self._cache = {} self._offset = Random().random()