Files
trex-core/docs/batches/bat-047_20260619_event-publisher-connection-reliability/design.md
T
genichinandClaude Opus 4.8 d182464b7a feat(messaging): OPS-005 이벤트 발행 연결 신뢰성 (BAT-047)
발행 전용 연결의 idle heartbeat 사망 후 첫 발행 유실 결함 제거.
- REQ-091 발행 재시도 + 강제 재연결(_force_reconnect, is_open 불신)
- REQ-092 idle keepalive(execution-loop 단일스레드 틱, process_data_events time_limit=0)
- REQ-093 DLQ 가시성 + emit 실패 ERROR 경보(구조화 로그, dcy-029 재사용)
- REQ-094 baseline 문서 정합(runtime-flows/operations/project-structure)

독립 opus QC PASS-WITH-NOTES → 시정 2건(ruff F401, AC-3 프로덕션 경로) 완료.
554 passed/47 skipped/회귀 0/ruff clean. NFR-1 wire 무회귀(interface-contract diff 0).
BAT-047 release-candidate, REQ-091~094 Implemented. dcy-031. M2 트랙B.

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

8.4 KiB

BAT-047 Design

  • Status: confirmed
  • Batch: BAT-047

Architecture Summary

발행 신뢰성 보강은 adapters/messaging/rabbitmq.py의 발행 경로(publish_event, _publish_dlq)와 runtime/daemon.py의 _make_event_emitter에 국소화한다. wire 계약(AMQP 토폴로지/exchange/routing key/payload 스키마)·consume/RPC 경로(consume_rpc_forever, consume_events_forever, call_rpc)·declare_topology·슬롯 도메인·broker 집행 로직은 불변이다.

변경 계층:

  • adapters/messaging/rabbitmq.py (transport 어댑터 레이어)
    • publish_event: 발행 실패 시 강제 재연결(close + _connect) 후 1회 재시도. 타임아웃 socket_timeout으로 상한 제어. is_open 맹신 제거.
    • keepalive_tick(): process_data_events(time_limit=0) 호출 — 발행 전용 연결 소유 스레드에서만 호출.
    • _publish_dlq: best-effort 실패 시 WARNING → ERROR 격상.
  • runtime/daemon.py (runtime 레이어)
    • _make_event_emitter: TransportUnavailableError 처리 WARNING → ERROR 격상, routing key + correlation 정보 포함.
    • _execution_loop: 각 틱 직전 event_publisher.keepalive_tick() 호출(동일 스레드 보장).

Changed Areas

  • Modules:
    • trex_core/adapters/messaging/rabbitmq.py — publish_event, keepalive_tick(신규), _publish_dlq
    • trex_core/runtime/daemon.py — _make_event_emitter, _execution_loop
  • Tests:
    • tests/adapters/messaging/test_publish_reliability.py (신규) — AC 커버
    • tests/application/test_messaging_event_publisher.py — 기존 회귀 유지
  • Docs:
    • docs/runtime-flows.md — 발행 재시도/keepalive 흐름, DLQ 관측 경로 추가(REQ-094)
    • docs/project-structure.md — keepalive_tick 컴포넌트 기술 추가(REQ-094)
    • docs/operations.md — DLQ 가시성/경보 운영 절차 신규(REQ-094)
  • Interfaces: AMQP wire 계약 변경 없음. interface-contract.md 불변.

Key Decisions

OQ-1: keepalive 실행 위치 — Answered

결정: execution-loop 틱에 event_publisher.keepalive_tick() 삽입(동일 스레드 모델).

근거:

  • pika BlockingConnection은 생성 스레드에서만 안전하게 I/O 호출 가능(공식 문서: "pika is not threadsafe").
  • event_publisher는 daemon 초기화 시 생성되어 _execution_loop 스레드(trex-execution)가 독점 소유.
  • 별도 keepalive 스레드를 띄우면 그 스레드가 발행 전용 연결에 process_data_events를 호출 → 스레드 경계를 넘어 연결에 접근하는 것 → pika 스레드 비안전성 위반.
  • execution-loop 틱은 self.stop.wait(interval) 주기로 이미 동기 루프 구조 → 각 틱 시작 시 keepalive_tick()을 먼저 호출하면 trex-execution 스레드 컨텍스트에서만 실행됨.
  • keepalive 틱은 process_data_events(time_limit=0) 호출 — pika 문서에서 heartbeat frame 처리를 위한 권장 패턴. time_limit=0은 blocking 없이 socket에서 pending frame만 처리.
  • execution_interval_sec(기본 1.0초) < heartbeat=30초의 절반(15초) → 30초 이내 I/O 보장, 서버 측 idle 종료 예방 성립.

별도 스레드 방식 기각 이유: 공유 연결에 두 스레드가 동시 접근 가능 → 발행과 keepalive가 겹치면 연결 손상 또는 예외 발생. 뮤텍스로 보호해도 pika 내부 소켓 상태가 스레드 경계를 넘어 교란될 수 있어 권고되지 않음.

OQ-3: DLQ 가시성 구현 방식 — Answered

결정: 구조화 로그 기반 (dcy-029 관측성 자산 재사용).

근거:

  • dcy-029(BAT-045/046)에서 구조화 로깅 인프라(logging_setup.py, _log.error(), correlation id, json 포맷, Loki 적재)가 이미 구축·검증됨.
  • 추가 인프라 의존성(Prometheus, StatsD, 소비자 데몬)은 본 주기 범위 밖(M3 후보).
  • 소비자 데몬 방식은 별도 상시 프로세스·새 큐·바인딩이 필요해 wire 계약 추가 가능성이 있어 교차 영향 사안(trex-orb-v1·trex-tsl-v1) 검토 필요 → 현 주기 제외.
  • 로그 기반: _publish_dlq 호출 시 _log.error("DLQ 유입: reason=%s rk=%s", reason, routing_key), DLQ 재발행 실패 시 _log.error("DLQ publish failed", exc_info=True), _make_event_emitter 재시도 최종 실패 시 _log.error("event publish failed (rk=%s)", rk, exc_info=True).
  • 운영자는 Loki 쿼리 {level="error"} |= "DLQ" 또는 |= "event publish failed" 로 즉시 식별 가능.
  • DLQ 깊이(queue depth)는 RabbitMQ 관리 UI(http://broker:15672) 또는 rabbitmqctl list_queues name messages로 조회 — 별도 구현 불요.

소비자 데몬 기각 이유: 별도 프로세스 관리 복잡성 + 새 큐/바인딩 = wire 계약 파급 가능성. M3 검토로 이월.

NFR-2: 재시도/keepalive 타임아웃 상한 — Answered

결정:

  • 단일 publish 경로 전체 블록 상한: ≤ 5초 (재연결 1회 + 재시도 1회 합산).
    • _connect() 내 pika.BlockingConnection(params): params.socket_timeout = 2.0 (기존 self._timeout = 5.0보다 강화 — 재연결용).
    • params.blocked_connection_timeout = 2.0.
    • basic_publish + confirm_delivery 대기: publisher confirms는 channel-level이므로 별도 타임아웃 없음(socket_timeout으로 상한됨).
    • 강제 재연결 close() + _connect(2.0s) + basic_publish ≤ 5초 내 완료 설계.
  • keepalive 틱: process_data_events(time_limit=0) — non-blocking, pending socket frame만 처리.
    • 실질 지연: 수 밀리초 이내, execution-loop 지연 없음.
  • 재시도 횟수: 1회 — at-least-once 의미를 최소화하고 중복 위험도 최소화.

근거: execution_interval_sec 기본 1.0초인데 발행 경로가 5초 이상 블록되면 매도 타이밍 손상 가능. 2초 socket_timeout + 재연결 overhead ≤ 5초가 실전 허용 범위. keepalive는 non-blocking으로 루프 지연 없음.

Edge Cases

  1. 재시도 중 중복 발행: at-least-once → orb/tsl 소비자 중복 수신 가능. 전이 이벤트는 sequence_at last-wins 적용 가능(멱등 가능 추정, OQ-2 delivery 검증 추적).
  2. 재시도에서도 연결 실패: TransportUnavailableError를 _make_event_emitter까지 전파 → ERROR 레벨 로그. 슬롯 전이 롤백 없음(NFR-3 — publish failure must not roll back).
  3. keepalive 호출 중 발행 겹침: execution-loop 단일 스레드이므로 실제 동시성 없음. _execution_loop에서 keepalive_tick() → run_once() 순서적 호출.
  4. MessagingTransport(Protocol) 인터페이스 변경: keepalive_tick() 메서드는 RabbitMQTransport 전용이므로 포트 인터페이스(MessagingTransport)에 추가하지 않는다. daemon.py에서 isinstance 체크 또는 hasattr로 호출하거나, InMemoryTransport에도 no-op 구현을 추가한다. 포트 인터페이스 확장은 wire 계약이 아니라 transport 구현 내부이므로 교차 영향 없음.
    • 결정: MessagingTransport 포트에 keepalive_tick() -> None (default no-op) 추가. 기존 구현(InMemoryTransport)에 no-op 추가. daemon에서 self.event_publisher.keepalive_tick() 직접 호출. 포트 인터페이스 추가는 transport 어댑터 내부이므로 wire 계약과 무관.
  5. _publish_dlq 내 keepalive: DLQ 발행은 같은 channel에서 _ensure_connected() 후 수행. keepalive와 별도 경로이므로 간섭 없음. DLQ 발행은 no publisher-confirms이므로 block 없음.
  6. close() 후 keepalive_tick() 호출: _conn is None 체크로 no-op. 데몬 shutdown 시 stop 이벤트 세팅 후 루프 종료 → keepalive 틱 호출 없음.

Architecture Impact

  • runtime-flows (발행 재시도/keepalive/DLQ 관측 경로 추가)
  • project-structure (keepalive_tick 메서드, MessagingTransport 포트 확장 기술)

Reference Doc Update Plan (REQ-094)

  • docs/runtime-flows.md:
    • 슬롯 이벤트 발행/구독 흐름: 재시도/강제 재연결 경로, idle keepalive 동작, DLQ 유입 관측 경로 추가.
    • 데몬 lifecycle: keepalive 틱이 execution-loop 틱에 삽입됨(OQ-1 결정 반영).
  • docs/project-structure.md:
    • messaging.py(MessagingTransport 포트) 설명에 keepalive_tick() 추가.
    • rabbitmq.py 설명에 발행 재시도·keepalive 기술 추가.
  • docs/operations.md:
    • 신규 섹션 "DLQ 가시성/경보": trex.events.dlq 유입 로그 확인 방법, RabbitMQ 관리 UI queue depth 조회, 운영자 조치 절차.