triangular_arbitrage_bot/executor/socket_server.py

100 lines
3.4 KiB
Python

"""
Unix-domain socket server that receives opportunity signals from oe_em.
Each JSON line on the socket is parsed and dispatched as an asyncio task
to avoid blocking the reader.
"""
import asyncio
import json
import time
from pathlib import Path
import structlog
logger = structlog.get_logger().bind(component="signal_socket_server")
class SignalSocketServer:
"""
Accepts JSON-serialized signals over a Unix domain socket.
Every valid "signal" message is wrapped in create_task so processing
is concurrent and a slow handler never blocks new connections.
"""
def __init__(self, socket_path: Path, on_signal) -> None:
self._socket_path = socket_path
self._on_signal = on_signal
self._log = logger
self._server: asyncio.Server | None = None
self._running = False
async def start(self) -> None:
"""Remove any stale socket file and start accepting connections."""
if self._socket_path.exists():
self._socket_path.unlink()
self._running = True
self._server = await asyncio.start_unix_server(
self._accept_client,
path=str(self._socket_path),
)
self._log.info("signal_socket_server_started", path=str(self._socket_path))
async with self._server:
await self._server.serve_forever()
async def stop(self) -> None:
"""Stop the server and remove the socket file."""
self._running = False
if self._server:
self._server.close()
await self._server.wait_closed()
if self._socket_path.exists():
self._socket_path.unlink()
self._log.info("signal_socket_server_stopped")
async def _accept_client(
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:
"""
Handle one client connection: read lines, parse JSON, dispatch signals.
The client is assumed to be oe_em's SignalSocketClient. Any line that
is not JSON or is not type="signal" is silently ignored.
"""
self._log.info("client_connected", addr=writer.get_extra_info("peername"))
try:
while self._running:
try:
line = await reader.readline()
except (ConnectionResetError, BrokenPipeError, asyncio.CancelledError):
break
except Exception:
break
if not line:
break
arrived_ms = int(time.time() * 1000)
try:
data = json.loads(line.decode())
except (json.JSONDecodeError, UnicodeDecodeError) as e:
self._log.warning("invalid_json", line=line[:50], error=str(e))
continue
if data.get("type") == "signal":
data["_receiver_ts_ms"] = arrived_ms
asyncio.create_task(self._on_signal(data))
else:
self._log.debug("ignored_non_signal", type=data.get("type"))
except asyncio.CancelledError:
pass
except Exception as e:
self._log.warning("client_error", error=str(e))
finally:
writer.close()
try:
await asyncio.wait_for(writer.wait_closed(), timeout=1.0)
except Exception:
pass
self._log.info("client_disconnected")