122 lines
3.5 KiB
Python
122 lines
3.5 KiB
Python
"""Messaging transport port + DTOs (REQ-021, REQ-022, dcy-006).
|
|
|
|
Application-layer contract for trex-core ↔ 전략 모듈 통신. Domain stays
|
|
free of AMQP / pika dependencies (NFR-1); concrete adapters live in
|
|
trex_core.adapters.messaging.
|
|
|
|
Three patterns (dcy-006 OQ-2):
|
|
1. snapshot RPC (request-reply) — read-only, transient.
|
|
2. intent admission RPC (request-reply) — durable, correlation_id dedup.
|
|
3. event pub/sub — topic exchange, per-strategy queue, last-wins.
|
|
|
|
Request payloads carry view/strategy_id in the message body; events carry
|
|
strategy_id/symbol in the routing key (REQ-021 mapping principle).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any, Callable, Optional, Protocol
|
|
|
|
|
|
class TransportHealth(str, Enum):
|
|
"""Connection state exposed to application for degrade branches (REQ-024)."""
|
|
CONNECTED = "connected"
|
|
DISCONNECTED = "disconnected"
|
|
RECONNECTING = "reconnecting"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RpcRequest:
|
|
"""Inbound RPC message handed to a consumer handler."""
|
|
payload: dict
|
|
correlation_id: str
|
|
reply_to: Optional[str] = None
|
|
properties: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RpcResponse:
|
|
"""Outbound RPC response returned from a consumer handler."""
|
|
payload: dict
|
|
|
|
|
|
class TransportError(Exception):
|
|
"""Base class for messaging transport failures."""
|
|
|
|
|
|
class TransportUnavailableError(TransportError):
|
|
"""Raised when transport.health() is not CONNECTED and the caller
|
|
cannot proceed (REQ-024: no local buffering, conservative halt)."""
|
|
|
|
|
|
class RpcTimeoutError(TransportError):
|
|
"""RPC reply not received within the configured timeout."""
|
|
|
|
|
|
# Type aliases for handlers
|
|
RpcHandler = Callable[[RpcRequest], RpcResponse]
|
|
EventHandler = Callable[[str, dict], None] # (routing_key, payload)
|
|
|
|
|
|
class MessagingTransport(Protocol):
|
|
"""Transport boundary for messaging-based communication (REQ-022)."""
|
|
|
|
def declare_topology(self) -> None:
|
|
"""Declare exchanges, queues, bindings (REQ-021)."""
|
|
...
|
|
|
|
def publish_event(
|
|
self,
|
|
routing_key: str,
|
|
payload: dict,
|
|
*,
|
|
mandatory: bool = True,
|
|
) -> None:
|
|
"""Publish to the events topic exchange. Uses publisher confirms;
|
|
unroutable or NACKed messages are routed to the DLQ (REQ-023).
|
|
Raises TransportUnavailableError if not connected."""
|
|
...
|
|
|
|
def call_rpc(
|
|
self,
|
|
queue: str,
|
|
payload: dict,
|
|
*,
|
|
correlation_id: str,
|
|
timeout: float,
|
|
) -> dict:
|
|
"""Client-side RPC: publish to `queue`, wait for reply.
|
|
Raises RpcTimeoutError on timeout, TransportUnavailableError on outage."""
|
|
...
|
|
|
|
def consume_rpc_forever(
|
|
self,
|
|
queue: str,
|
|
handler: RpcHandler,
|
|
stop: threading.Event,
|
|
) -> None:
|
|
"""Server-side RPC consumption. Blocks until `stop` is set.
|
|
Auto-reconnects on connection drop (REQ-022 AC-4)."""
|
|
...
|
|
|
|
def consume_events_forever(
|
|
self,
|
|
queue: str,
|
|
binding_pattern: str,
|
|
handler: EventHandler,
|
|
stop: threading.Event,
|
|
) -> None:
|
|
"""Subscribe to events matching `binding_pattern` on the events
|
|
exchange, dispatch to handler. Blocks until `stop` is set."""
|
|
...
|
|
|
|
def health(self) -> TransportHealth:
|
|
"""Current connection state (REQ-024)."""
|
|
...
|
|
|
|
def close(self) -> None:
|
|
"""Release connection resources."""
|
|
...
|