Files
trex-core/trex_core/application/messaging.py
T

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."""
...