Files
trex-core/docs/batches/bat-049_20260621_alarm-system-dcy-033/design.md
T
genichinandClaude Opus 4.8 d4c5fcbba1 feat(alarm): trex-core 알람 emitter + trex.alerts exchange (dcy-033 BAT-049, Implemented)
AlarmEmitter(application, best-effort 비차단)+trex.alerts topic exchange 가산·격리+8 CRITICAL code 배선. 608 passed, opus QC PASS, 팀장 e2e(텔레그램 HTTP200) 통과. REQ-098~101 Implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
2026-06-21 19:25:55 +09:00

7.5 KiB

BAT-049 Design

  • Status: confirmed
  • Batch: BAT-049

Architecture Summary

알람 시스템을 application 레이어의 AlarmEmitter(포트 경유) + adapters 레이어의 publish_alarm_event() 확장으로 구현한다. 기존 레이어드 아키텍처(domain/application/adapters/runtime)와 의존성 역전 규칙을 그대로 따른다.

발생 지점(execution.py 등)
    │ alarm_emitter.publish_alarm(code, title, fields)
    ▼
AlarmEmitter (application 레이어)
    │ MessagingTransport.publish_alarm_event(routing_key, payload)
    ▼
RabbitMQTransport.publish_alarm_event() (adapters 레이어)
    │ basic_publish(exchange=trex.alerts, routing_key=alarm.critical.trex-core, mandatory=False)
    ▼
trex.alerts exchange → (notifier가 alarm.# 패턴으로 바인딩)

AlarmEmitter는 MessagingTransport 포트만 주입받는다. pika 직접 import 없음. domain 레이어 접촉 없음.

Changed Areas

파일 변경 유형 내용
docs/interface-contract.md 추가(기존 불변) Interface: trex.alerts (알람 채널, dcy-033) 섹션 신규
trex_core/adapters/messaging/topology.py 추가 ALERTS_EXCHANGE = "trex.alerts" 상수 추가
trex_core/adapters/messaging/rabbitmq.py 확장 declare_topology()에 trex.alerts exchange 선언 + publish_alarm_event() 메서드 신규
trex_core/adapters/messaging/in_memory.py 확장 publish_alarm_event() stub 추가 (발행 기록 + unavailable 시 TransportUnavailableError)
trex_core/application/messaging.py 확장 MessagingTransport Protocol에 publish_alarm_event() 추가
trex_core/application/alarm_emitter.py 신규 AlarmEmitter 클래스 구현
trex_core/application/execution.py 배선 fill_price_unresolvable, sell_skipped_no_balance 알람 호출 추가
trex_core/application/reconciliation.py 배선 core_holding_broker_flat 알람 호출 추가
trex_core/application/ledger_outbox.py 배선 ledger_delivery_failed 알람 호출 추가
trex_core/application/killswitch.py 배선 kill_switch_fired 알람 호출 추가
trex_core/adapters/broker/rabbitmq.py(messaging/rabbitmq.py) 배선 publish_unavailable_after_reconnect / dlq_publish_failed — 콜백 패턴
trex_core/application/portfolio.py / snapshot_scheduler.py 배선 portfolio_hub_unavailable 알람 호출 추가
trex_core/runtime/reset.py 배선 schema_verification_failed / migration_failed 알람 호출 (transport None 안전)
trex_core/runtime/daemon.py 조립 AlarmEmitter 생성 + 각 서비스에 주입
tests/application/test_alarm_emitter.py 신규 AlarmEmitter 단위 테스트
tests/adapters/messaging/test_topology.py 확장 declare_topology trex.alerts assertion
docs/runtime-flows.md 추가(기존 불변) 알람 발신 흐름 섹션 신규

Key Decisions

KD-1: publish_unavailable_after_reconnect / dlq_publish_failed — 콜백 패턴 채택

문제: 두 alarm code는 trex_core/adapters/messaging/rabbitmq.py(어댑터) 내부에서 발생하지만, AlarmEmitter는 application 레이어에 있다. 어댑터가 application을 직접 import하면 의존성이 역전된다.

채택 결정: 콜백(callable) 주입

  • RabbitMQTransport에 alarm_callback: Optional[Callable[[str, str, dict], None]] 파라미터를 추가한다. 시그니처: (code: str, title: str, fields: dict) -> None.
  • daemon.py에서 AlarmEmitter.publish_alarm을 콜백으로 주입한다.
  • 어댑터는 application 타입을 import하지 않고, callable 타입 힌트(Optional[Callable])만 사용.
  • 콜백 미주입 시(기본값 None): 기존 동작 유지 (로그만).
  • 콜백이 raise하면 어댑터는 except로 삼킨다(best-effort 원칙 일관).

기각 대안:

  • 이벤트 버스/큐: 과도한 복잡성, 결정론적 단위 테스트 어려움.
  • AlarmEmitter를 adapters 레이어로 이동: domain/application 순수성 원칙 위반.
  • Transport 프로토콜에 alarm_callback getter 추가: Protocol 오염 + 테스트 어려움.

KD-2: reset.py transport None 안전 — AlarmEmitter None-guard 패턴

문제: reset.py는 CLI 진입점으로 MessagingTransport가 없다. AlarmEmitter에 None transport를 전달하면 호출 시 crash.

채택 결정: AlarmEmitter.__init__이 Optional[MessagingTransport]를 받고, publish_alarm() 내부에서 transport가 None이면 WARN 로그 후 skip.

  • reset.py는 AlarmEmitter(transport=None)로 생성하거나 publish_alarm() 호출 전 None 체크.
  • 실제로는 run_reset() 함수가 AlarmEmitter를 직접 받는 시그니처로 확장하거나, reset 전용 경량 emitter를 분리한다.
  • 가장 단순한 방법: AlarmEmitter(transport=None) 허용 + None guard → reset 코드에서 생성 부담 최소화.

KD-3: publish_alarm_event 시그니처 — transport 레이어 알람 전용 메서드

결정: MessagingTransport 프로토콜에 publish_alarm_event(routing_key: str, payload: dict) -> None 추가.

  • publish_event()와 구분하는 이유: alarm은 trex.alerts exchange, mandatory=False, delivery_mode=1(best-effort, 비영속) vs events는 trex.events, mandatory=True, delivery_mode=2.
  • InMemoryTransport: 발행된 alarm을 _alarm_events: list[tuple[str, dict]]에 기록 + unavailable 시 TransportUnavailableError 발생(호출자가 삼킴).
  • RabbitMQTransport: publish_alarm_event()는 basic_publish(exchange=trex.alerts, mandatory=False, delivery_mode=1). 연결 실패 시 TransportUnavailableError raise(AlarmEmitter가 삼킴). retry-on-failure 없음(best-effort 원칙).

KD-4: AlarmEmitter 인터페이스

class AlarmEmitter:
    def __init__(self, transport: Optional[MessagingTransport]) -> None: ...

    def publish_alarm(
        self,
        code: str,
        title: str,
        *,
        fields: dict | None = None,
        severity: str = "critical",
    ) -> None:
        """Best-effort 발행. 실패 시 ERROR 로그 후 예외 삼킴."""
        ...
  • source="trex-core", schema_version="1" 상수화.
  • occurred_at = 발행 시점 KST ISO-8601.
  • routing key = alarm.{severity}.trex-core.
  • 발행 실패(TransportUnavailableError, 기타 모든 예외): ERROR 로그 후 예외 삼킴(re-raise 금지).

Edge Cases

  • transport disconnect 중 알람 발행: publish_alarm_event() → TransportUnavailableError → AlarmEmitter가 ERROR 로그 후 삼킴. 본업 경로 비차단.
  • rabbitmq.py 콜백이 None인 경우: alarm_callback guard로 skip. 기존 코드 변경 없이 하위호환.
  • reset.py에서 AlarmEmitter(transport=None): None guard 분기 → INFO 로그("alarm skipped: no transport") 후 skip.
  • mandatory=False + notifier 미연결: silent drop. UnroutableError 없음.
  • 8개 code 중 일부 경로에서 AlarmEmitter 미주입: daemon.py에서 전부 주입. 미주입 시 AttributeError → execution_loop except로 삼킴(기존 보호 패턴).
  • AlarmEmitter.publish_alarm 내 예외 루프: 각 호출마다 독립 try/except. 한 코드 발행 실패가 동일 tick의 다른 호출에 영향 없음.

Architecture Impact

both — runtime-flows.md + interface-contract.md 양쪽 업데이트 필요.

Reference Doc Update Plan

  • docs/interface-contract.md: Interface: trex.alerts (알람 채널, dcy-033) 섹션 추가 + Change Log 갱신 + Last Updated 갱신.
  • docs/runtime-flows.md: Flow: 알람 발신 (trex.alerts, dcy-033) 섹션 추가. 기존 섹션 불변.