from __future__ import annotations import logging import threading import time from collections.abc import Sequence from typing import TYPE_CHECKING from core.config import config, create_provider_from_config if TYPE_CHECKING: # Can't find a way to resolve the circular dependency on types but apparently this is a way of managing that # while still having type safety in method definitions. from providers.activityrefdata.activity_ref_data_provider import ActivityRefDataProvider from providers.alert.alert_provider import AlertProvider from providers.callsigndata.callsign_data_provider import CallsignDataProvider from providers.solarconditions.solar_conditions_provider import SolarConditionsProvider from providers.spot.spot_provider import SpotProvider from providers.staticdata.static_data_provider import StaticDataProvider logger = logging.getLogger(__name__) class DataProviders: """Global object for storing data providers.""" def __init__(self) -> None: self.spot_providers: list[SpotProvider] = [] self.alert_providers: list[AlertProvider] = [] self.solar_condition_providers: list[SolarConditionsProvider] = [] self.static_data_providers: list[StaticDataProvider] = [] self.sig_ref_data_providers: list[ActivityRefDataProvider] = [] self.callsign_data_providers: list[CallsignDataProvider] = [] self._startup_timers: list[threading.Timer] = [] def setup(self) -> None: for entry in config["spot_providers"]: self.spot_providers.append(create_provider_from_config("providers.spot", entry)) for entry in config["alert_providers"]: self.alert_providers.append(create_provider_from_config("providers.alert", entry)) for entry in config.get("solar_condition_providers", []): self.solar_condition_providers.append(create_provider_from_config("providers.solarconditions", entry)) for entry in config.get("static_data_providers", []): self.static_data_providers.append(create_provider_from_config("providers.staticdata", entry)) for entry in config.get("sig_ref_data_providers", []): self.sig_ref_data_providers.append(create_provider_from_config("providers.activityrefdata", entry)) for entry in config.get("callsign_data_providers", []): self.callsign_data_providers.append(create_provider_from_config("providers.callsigndata", entry)) @staticmethod def start_providers( providers: Sequence[ SpotProvider | AlertProvider | SolarConditionsProvider | StaticDataProvider | ActivityRefDataProvider | CallsignDataProvider ], provider_type: str, ) -> None: """Helper method to activate enabled providers in the list.""" logger.info(f"Starting {provider_type} providers...") for p in providers: if p.enabled: p.start() def start(self) -> None: # Start data providers before spot/alert providers so the lookup data is there already for incoming spots. # Each category is fired off after a small delay to give the rest of Spothole chance to start up. self._startup_timers = [ threading.Timer(5.0, lambda: self.start_providers(self.static_data_providers, "static data")), threading.Timer(10.0, lambda: self.start_providers(self.callsign_data_providers, "callsign data")), threading.Timer(15.0, lambda: self.start_providers(self.spot_providers, "spot")), threading.Timer(20.0, lambda: self.start_providers(self.alert_providers, "alert")), threading.Timer( 25.0, lambda: self.start_providers(self.solar_condition_providers, "solar condition"), ), threading.Timer(30.0, lambda: self.start_providers(self.sig_ref_data_providers, "activity ref data")), ] for t in self._startup_timers: t.daemon = True t.start() def stop(self) -> None: # Cancel any startup timers that haven't fired yet for t in self._startup_timers: t.cancel() # Stop all providers all_providers = [ p for p in ( self.spot_providers + self.alert_providers + self.solar_condition_providers + self.sig_ref_data_providers + self.static_data_providers + self.callsign_data_providers ) if p.enabled ] if not all_providers: return def stop_provider( p: SpotProvider | AlertProvider | SolarConditionsProvider | StaticDataProvider | ActivityRefDataProvider | CallsignDataProvider, ) -> None: try: p.stop() except Exception: logger.exception("Exception stopping provider") threads = [threading.Thread(target=stop_provider, args=(p,), daemon=True) for p in all_providers] for thread in threads: thread.start() deadline = time.monotonic() + 15 for thread in threads: thread.join(timeout=max(0.0, deadline - time.monotonic())) still_running = [thread for thread in threads if thread.is_alive()] if still_running: logger.warning("Some threads did not stop in time!") # Global object DATA_PROVIDERS = DataProviders()