2025-06-09 17:42:06 +08:00
|
|
|
"""
|
|
|
|
|
Module for managing network connection and reconnection logic for data collectors.
|
|
|
|
|
|
|
|
|
|
This module encapsulates the complexities of connecting, disconnecting,
|
|
|
|
|
and handling reconnection attempts to a data source, promoting a clean
|
|
|
|
|
separation of concerns within the data collector architecture.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
from typing import List, Any
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
# from ..base_collector import DataType # Import from base_collector for now, will refactor later
|
|
|
|
|
from .collector_state_telemetry import CollectorStatus, CollectorStateAndTelemetry
|
|
|
|
|
from data.common.data_types import DataType
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConnectionManager:
|
|
|
|
|
"""
|
|
|
|
|
Manages the connection, disconnection, and reconnection logic for a data collector.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
|
exchange_name: str,
|
|
|
|
|
component_name: str,
|
|
|
|
|
max_reconnect_attempts: int = 5,
|
|
|
|
|
reconnect_delay: float = 5.0,
|
|
|
|
|
logger=None,
|
|
|
|
|
state_telemetry: CollectorStateAndTelemetry = None):
|
2025-06-11 18:36:34 +08:00
|
|
|
|
2025-06-09 17:42:06 +08:00
|
|
|
self.exchange_name = exchange_name
|
|
|
|
|
self.component_name = component_name
|
|
|
|
|
self._max_reconnect_attempts = max_reconnect_attempts
|
|
|
|
|
self._reconnect_delay = reconnect_delay
|
|
|
|
|
self.logger = logger
|
|
|
|
|
self._state_telemetry = state_telemetry
|
|
|
|
|
|
|
|
|
|
self._connection = None # Placeholder for the actual connection object
|
|
|
|
|
self._reconnect_attempts = 0
|
|
|
|
|
|
|
|
|
|
def _log_debug(self, message: str) -> None:
|
|
|
|
|
if self._state_telemetry:
|
|
|
|
|
self._state_telemetry._log_debug(f"{self.component_name}: {message}")
|
|
|
|
|
elif self.logger:
|
|
|
|
|
self.logger.debug(f"{self.component_name}: {message}")
|
|
|
|
|
|
|
|
|
|
def _log_info(self, message: str) -> None:
|
|
|
|
|
if self._state_telemetry:
|
|
|
|
|
self._state_telemetry._log_info(f"{self.component_name}: {message}")
|
|
|
|
|
elif self.logger:
|
|
|
|
|
self.logger.info(f"{self.component_name}: {message}")
|
|
|
|
|
|
|
|
|
|
def _log_warning(self, message: str) -> None:
|
|
|
|
|
if self._state_telemetry:
|
|
|
|
|
self._state_telemetry._log_warning(f"{self.component_name}: {message}")
|
|
|
|
|
elif self.logger:
|
|
|
|
|
self.logger.warning(f"{self.component_name}: {message}")
|
|
|
|
|
|
|
|
|
|
def _log_error(self, message: str, exc_info: bool = False) -> None:
|
|
|
|
|
if self._state_telemetry:
|
|
|
|
|
self._state_telemetry._log_error(f"{self.component_name}: {message}", exc_info=exc_info)
|
|
|
|
|
elif self.logger:
|
|
|
|
|
self.logger.error(f"{self.component_name}: {message}", exc_info=exc_info)
|
|
|
|
|
|
|
|
|
|
async def connect(self, connect_logic: callable) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Establish connection to the data source using provided logic.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
connect_logic: A callable (async function) that performs the actual connection.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if connection successful, False otherwise
|
|
|
|
|
"""
|
|
|
|
|
self._log_info(f"Connecting to {self.exchange_name} data source")
|
|
|
|
|
try:
|
|
|
|
|
success = await connect_logic()
|
|
|
|
|
if success:
|
|
|
|
|
self._connection = True # Indicate connection is established
|
|
|
|
|
self._state_telemetry.set_connection_uptime_start()
|
|
|
|
|
self._log_info(f"Successfully connected to {self.exchange_name}")
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
self._log_error(f"Failed to connect to {self.exchange_name}")
|
|
|
|
|
return False
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self._log_error(f"Error during connection to {self.exchange_name}: {e}", exc_info=True)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
async def disconnect(self, disconnect_logic: callable) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Disconnect from the data source using provided logic.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
disconnect_logic: A callable (async function) that performs the actual disconnection.
|
|
|
|
|
"""
|
|
|
|
|
self._log_info(f"Disconnecting from {self.exchange_name} data source")
|
|
|
|
|
try:
|
|
|
|
|
if self._connection:
|
|
|
|
|
await disconnect_logic()
|
|
|
|
|
self._connection = None
|
|
|
|
|
self._log_info(f"Disconnected from {self.exchange_name}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self._log_error(f"Error during disconnection from {self.exchange_name}: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
async def handle_connection_error(self, connect_logic: callable, subscribe_logic: callable, symbols: List[str], data_types: List[DataType]) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Handle connection errors and attempt reconnection.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
connect_logic: Callable for connecting.
|
|
|
|
|
subscribe_logic: Callable for subscribing.
|
|
|
|
|
symbols: List of symbols to re-subscribe to.
|
|
|
|
|
data_types: List of data types to re-subscribe to.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if reconnection successful, False if max attempts exceeded
|
|
|
|
|
"""
|
|
|
|
|
self._reconnect_attempts += 1
|
|
|
|
|
|
|
|
|
|
if self._reconnect_attempts > self._max_reconnect_attempts:
|
|
|
|
|
self._log_error(f"Max reconnection attempts ({self._max_reconnect_attempts}) exceeded for {self.exchange_name}")
|
|
|
|
|
if self._state_telemetry:
|
|
|
|
|
self._state_telemetry.update_status(CollectorStatus.ERROR)
|
|
|
|
|
self._state_telemetry.set_should_be_running(False)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if self._state_telemetry:
|
|
|
|
|
self._state_telemetry.update_status(CollectorStatus.RECONNECTING)
|
|
|
|
|
self._log_warning(f"Connection lost. Attempting reconnection {self._reconnect_attempts}/{self._max_reconnect_attempts} for {self.exchange_name}")
|
|
|
|
|
|
|
|
|
|
# Disconnect and wait before retrying
|
|
|
|
|
await self.disconnect(lambda: None) # Pass a no-op disconnect for internal use, actual disconnect handled by caller
|
|
|
|
|
await asyncio.sleep(self._reconnect_delay)
|
|
|
|
|
|
|
|
|
|
# Attempt to reconnect
|
|
|
|
|
try:
|
|
|
|
|
if await self.connect(connect_logic):
|
|
|
|
|
if await subscribe_logic(symbols, data_types):
|
|
|
|
|
self._log_info(f"Reconnection successful for {self.exchange_name}")
|
|
|
|
|
if self._state_telemetry:
|
|
|
|
|
self._state_telemetry.update_status(CollectorStatus.RUNNING)
|
|
|
|
|
self._reconnect_attempts = 0
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self._log_error(f"Reconnection attempt failed for {self.exchange_name}: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
return False
|