feat: add messaging transport rollout
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""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."""
|
||||
...
|
||||
Reference in New Issue
Block a user