# 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 조회, 운영자 조치 절차.