chore: improve exchange_ws startup safety

This commit is contained in:
Matthias
2026-04-25 08:37:09 +02:00
parent af1de46cd4
commit 3949efadfe
+19 -3
View File
@@ -2,7 +2,7 @@ import asyncio
import logging import logging
from copy import deepcopy from copy import deepcopy
from functools import partial from functools import partial
from threading import Thread from threading import Event, Thread
import ccxt import ccxt
@@ -23,6 +23,7 @@ class ExchangeWS:
self.config = config self.config = config
self._ccxt_object = ccxt_object self._ccxt_object = ccxt_object
self._background_tasks: set[asyncio.Task] = set() self._background_tasks: set[asyncio.Task] = set()
self._loop_ready = Event()
self._klines_watching: set[PairWithTimeframe] = set() self._klines_watching: set[PairWithTimeframe] = set()
self._klines_scheduled: set[PairWithTimeframe] = set() self._klines_scheduled: set[PairWithTimeframe] = set()
@@ -33,6 +34,7 @@ class ExchangeWS:
def _start_forever(self) -> None: def _start_forever(self) -> None:
self._loop = asyncio.new_event_loop() self._loop = asyncio.new_event_loop()
self._loop_ready.set()
try: try:
self._loop.run_forever() self._loop.run_forever()
finally: finally:
@@ -45,13 +47,24 @@ class ExchangeWS:
self._loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) self._loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
self._loop.run_until_complete(self._loop.shutdown_asyncgens()) self._loop.run_until_complete(self._loop.shutdown_asyncgens())
self._loop.close() self._loop.close()
self._loop_ready.clear()
def _wait_for_loop(self, timeout: float = 1.0) -> bool:
"""
Wait for the event loop to be ready
Returns True once the loop is ready.
Will probably only return false during startup/shutdown.
"""
if hasattr(self, "_loop"):
return True
return self._loop_ready.wait(timeout=timeout) and hasattr(self, "_loop")
def cleanup(self) -> None: def cleanup(self) -> None:
logger.debug("Cleanup called - stopping") logger.debug("Cleanup called - stopping")
self._klines_watching.clear() self._klines_watching.clear()
for task in self._background_tasks: for task in self._background_tasks:
task.cancel() task.cancel()
if hasattr(self, "_loop") and not self._loop.is_closed(): if self._wait_for_loop(timeout=0.2) and not self._loop.is_closed():
self.reset_connections(cleanup=True) self.reset_connections(cleanup=True)
self._loop.call_soon_threadsafe(self._loop.stop) self._loop.call_soon_threadsafe(self._loop.stop)
self._thread.join(timeout=5) self._thread.join(timeout=5)
@@ -63,7 +76,7 @@ class ExchangeWS:
""" """
Reset all connections - avoids "connection-reset" errors that happen after ~9 days Reset all connections - avoids "connection-reset" errors that happen after ~9 days
""" """
if hasattr(self, "_loop") and not self._loop.is_closed(): if self._wait_for_loop() and not self._loop.is_closed():
logger.info(f"{'Cleaning up' if cleanup else 'Resetting'} exchange WS connections.") logger.info(f"{'Cleaning up' if cleanup else 'Resetting'} exchange WS connections.")
try: try:
fut = asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop) fut = asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop)
@@ -202,6 +215,9 @@ class ExchangeWS:
""" """
Schedule a pair/timeframe combination to be watched Schedule a pair/timeframe combination to be watched
""" """
if not self._wait_for_loop():
logger.warning(f"Websocket loop not ready. Could not schedule {pair}, {timeframe}.")
return
self._klines_watching.add((pair, timeframe, candle_type)) self._klines_watching.add((pair, timeframe, candle_type))
self.klines_last_request[(pair, timeframe, candle_type)] = dt_ts() self.klines_last_request[(pair, timeframe, candle_type)] = dt_ts()
# asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop) # asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop)