import logging from datetime import datetime from threading import Event, Lock, Thread import pytz from requests_sse import EventSource from core.constants import HTTP_HEADERS from providers.spot.spot_provider import SpotProvider class SSESpotProvider(SpotProvider): """Spot provider using Server-Sent Events.""" def __init__(self, name, provider_config, url): super().__init__(name, provider_config) self._url = url self._thread = None self._last_event_id = None self._stop_event = Event() self._event_source_lock = Lock() self._event_source = None def start(self): logging.info("Set up SSE connection to " + self.name + " spot API.") self._stop_event.clear() self._thread = Thread(target=self._run, name=f"SSESpotProvider-{self.name}") self._thread.daemon = True self._thread.start() def stop(self): self._stop_event.set() with self._event_source_lock: event_source = self._event_source if event_source: try: event_source.close() except Exception: logging.exception( "Exception closing SSE connection for " + self.name + " during stop()") if self._thread: self._thread.join(timeout=15) if self._thread.is_alive(): logging.warning(self.name + " SSE worker thread did not exit on time and will be killed.") def _on_open(self): self.status = "Waiting for Data" def _on_error(self): self.status = "Connecting" def _set_event_source(self, event_source): with self._event_source_lock: self._event_source = event_source def _run(self): while not self._stop_event.is_set(): try: logging.debug("Connecting to " + self.name + " spot API...") self.status = "Connecting" with EventSource(self._url, headers=HTTP_HEADERS, latest_event_id=self._last_event_id, timeout=10, on_open=self._on_open, on_error=self._on_error) as event_source: self._set_event_source(event_source) try: for event in event_source: if self._stop_event.is_set(): break if event.type == 'message': try: self._last_event_id = event.last_event_id new_spot = self._sse_message_to_spot(event.data) if new_spot: self._submit(new_spot) self.status = "OK" self.last_update_time = datetime.now(pytz.UTC) logging.debug("Received data from " + self.name + " spot API.") except Exception: logging.exception( "Exception processing message from SSE Spot Provider (" + self.name + ")") finally: self._set_event_source(None) except Exception: self.status = "Error" logging.exception("Exception in SSE Spot Provider (" + self.name + ")") else: self.status = "Disconnected" self._stop_event.wait(timeout=5) # Wait before trying to reconnect def _sse_message_to_spot(self, message_data): """Convert an SSE message received from the API into a spot. The whole message data is provided here so the subclass implementations can handle the message as JSON, XML, text, whatever the API actually provides.""" raise NotImplementedError("Subclasses must implement this method")