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
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# 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 조회, 운영자 조치 절차.
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
# BAT-047 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-047
|
||||
|
||||
## Plan Summary
|
||||
|
||||
REQ-091~094 구현. 변경은 발행 경로에 국소화하고 wire 계약·consume/RPC 경로 불변 유지.
|
||||
|
||||
1. `rabbitmq.py` — `_connect` 타임아웃 파라미터화, `_force_reconnect` 신규, `keepalive_tick` 신규, `publish_event` 재시도 로직, `_publish_dlq` ERROR 격상.
|
||||
2. `application/messaging.py` — `MessagingTransport` 포트에 `keepalive_tick()` 추가 (no-op default).
|
||||
3. `adapters/messaging/in_memory.py` — `keepalive_tick()` no-op 구현.
|
||||
4. `runtime/daemon.py` — `_make_event_emitter` WARNING→ERROR 격상, `_execution_loop` keepalive 틱 삽입.
|
||||
5. `tests/adapters/messaging/test_publish_reliability.py` — 신규 테스트 17건.
|
||||
6. `docs/` — baseline 문서 정합 (REQ-094).
|
||||
|
||||
## Changed Files
|
||||
|
||||
### 코드
|
||||
- `trex_core/adapters/messaging/rabbitmq.py`
|
||||
- `_connect(connect_timeout=None)`: 파라미터화, 기본값 `self._timeout` 유지
|
||||
- `_force_reconnect()`: 신규 — `close()` + `_connect(connect_timeout=2.0)` (NFR-2 5초 블록 예산)
|
||||
- `keepalive_tick()`: 신규 — `process_data_events(time_limit=0)`, 비소유 스레드 호출 시 잠재적 오남용 방어(소유 스레드에서만 호출 규칙은 daemon.py에서 보장)
|
||||
- `publish_event()`: 1차 실패 시 `_force_reconnect` + 재시도, `UnroutableError`는 재시도 없이 DLQ
|
||||
- `_do_basic_publish()`: 신규 내부 헬퍼 — basic_publish 로직 추출(DRY)
|
||||
- `_publish_dlq()`: WARNING → ERROR 2건(DLQ 유입 + DLQ 재발행 실패)
|
||||
- `trex_core/application/messaging.py`
|
||||
- `MessagingTransport` 프로토콜에 `keepalive_tick() -> None` 추가
|
||||
- `trex_core/adapters/messaging/in_memory.py`
|
||||
- `InMemoryTransport.keepalive_tick()` no-op 구현
|
||||
- `trex_core/runtime/daemon.py`
|
||||
- `_make_event_emitter`: WARNING → ERROR, routing key + slot_id + strategy_id 포함
|
||||
- `_execution_loop`: `self.event_publisher.keepalive_tick()` 각 틱 시작 시 호출
|
||||
|
||||
### 테스트
|
||||
- `tests/adapters/messaging/test_publish_reliability.py` (신규, 17 테스트)
|
||||
|
||||
### 문서 (REQ-094)
|
||||
- `docs/runtime-flows.md` — `슬롯 이벤트 발행/구독`, `데몬 lifecycle` 갱신
|
||||
- `docs/project-structure.md` — messaging.py, rabbitmq.py 기술 갱신
|
||||
- `docs/operations.md` — DLQ 가시성/경보 섹션 신규
|
||||
|
||||
## Execution Log
|
||||
|
||||
1. `rabbitmq.py` — `_connect`, `_force_reconnect`, `keepalive_tick`, `publish_event`, `_do_basic_publish`, `_publish_dlq` 수정/추가.
|
||||
2. `application/messaging.py` — `keepalive_tick` 포트 메서드 추가.
|
||||
3. `in_memory.py` — no-op 추가.
|
||||
4. `daemon.py` — `_make_event_emitter` + `_execution_loop` 수정.
|
||||
5. 신규 테스트 17건 작성.
|
||||
6. `python3 -m pytest tests/adapters/messaging/test_publish_reliability.py -v` → **17 passed**.
|
||||
7. `python3 -m pytest tests/ --ignore=...rabbitmq_transport... --ignore=...rabbitmq_intent_e2e... -q` → **554 passed, 40 skipped** (기존 537 + 신규 17, 회귀 0).
|
||||
8. baseline 문서 정합 (runtime-flows, project-structure, operations).
|
||||
|
||||
## Validation (중간)
|
||||
|
||||
- 신규 17건 전부 통과.
|
||||
- 기존 537 passed → 554 passed (신규 17 추가). 회귀 0.
|
||||
- `TREX_RABBITMQ_TEST_URL` 필요 통합 테스트 2건(test_rabbitmq_transport.py, test_rabbitmq_intent_e2e.py): 환경 없어 skip — 기존과 동일.
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- R-3(Medium): at-least-once 재시도 — 중복 이벤트 발행 가능. OQ-2(orb/tsl 소비자 멱등성)는 delivery 검증으로 추적. trex-core 범위 밖.
|
||||
- R-live: 실제 pika 연결 환경(TREX_RABBITMQ_TEST_URL)에서의 keepalive/재연결 동작은 통합 테스트로만 검증 가능 — CI 환경 없어 미검증. unit mock 기반으로 logic을 검증했으나 실제 broker half-open 재현은 운영 환경 배포 후 확인 필요.
|
||||
@@ -0,0 +1,36 @@
|
||||
# BAT-047: 이벤트 발행 연결 신뢰성 — idle keepalive + 재시도/재연결 + DLQ 가시성 + baseline 정합 (OPS-005)
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-031
|
||||
- Included REQ: REQ-091, REQ-092, REQ-093, REQ-094
|
||||
- Excluded REQ: 없음
|
||||
|
||||
## Summary
|
||||
- OPS-005 운영 리포트 기반 발행 신뢰성 4건을 단일 batch로 묶는다. 발행 재시도+강제 재연결(REQ-091), idle keepalive(REQ-092), DLQ 가시성+emit ERROR 경보(REQ-093)는 같은 발행 경로(`publish_event`/`_publish_dlq`/`_make_event_emitter`)를 공유하고, 동일 messaging 검증 스위트에서 검증된다. baseline 문서 정합(REQ-094)은 구현 완료 후 같은 batch에서 처리한다.
|
||||
- M2(trex-tsl-v1 라이브, 6/28 목표) 선행 조건. idle 60초 후 첫 슬롯 전이 이벤트 유실 결함 제거.
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- `trex_core/adapters/messaging/rabbitmq.py` — `publish_event` 재시도+강제 재연결(REQ-091), keepalive `process_data_events` 틱(REQ-092), `_publish_dlq` ERROR 로그(REQ-093)
|
||||
- `trex_core/runtime/daemon.py` — `_make_event_emitter` ERROR 경보 강화(REQ-093), keepalive 틱 연동(REQ-092)
|
||||
- `tests/adapters/messaging/` — half-open 시뮬레이션, 재시도 소진, idle keepalive, DLQ 경보 신규 테스트
|
||||
- `tests/application/test_messaging_event_publisher.py` — 기존 회귀 유지
|
||||
- `docs/runtime-flows.md`, `docs/project-structure.md`, `docs/operations.md` — baseline 정합(REQ-094)
|
||||
- Out of Scope:
|
||||
- 이벤트 영속 outbox(옵션 C) — M3 후보
|
||||
- consumer(orb/tsl) idempotency 구현
|
||||
- RPC client(`call_rpc`) 연결 정책
|
||||
- `docs/interface-contract.md` 변경 — AMQP wire 계약 불변
|
||||
|
||||
## Documents
|
||||
- Planning: [planning.md](./planning.md)
|
||||
- Design: [design.md](./design.md)
|
||||
- Implementation: [implementation.md](./implementation.md)
|
||||
- Verification: [verification.md](./verification.md)
|
||||
|
||||
## Notes
|
||||
- R-1(High): M1 라이브 trex-core — 변경은 발행 경로에 국소화, wire 계약·consume/RPC 경로 불변. opus qc-reviewer 강제(dcy-031 §10 CONFIRM①).
|
||||
- R-2(Medium): pika 스레드 비안전성 — keepalive I/O는 발행 연결 소유 스레드에서만 실행(OQ-1 설계 확정).
|
||||
- R-3(Medium): 재시도 at-least-once 중복 가능 — OQ-2는 delivery 검증으로 추적(소비자 멱등성).
|
||||
- NFR-2 타임아웃 수치는 design 단계에서 확정.
|
||||
@@ -0,0 +1,67 @@
|
||||
# BAT-047 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-047
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-091: 이벤트 발행 재시도 + 강제 재연결 (idle 후 첫 발행 무유실)
|
||||
- REQ-092: 발행 전용 연결 idle keepalive (서버 측 heartbeat 사망 예방)
|
||||
- REQ-093: DLQ 가시성/경보 + emit 실패 경보 (trex.events.dlq 관측성)
|
||||
- REQ-094: 이벤트 발행 신뢰성 baseline 문서 정합 (runtime-flows / project-structure / operations 갱신)
|
||||
|
||||
## Delivery Plan
|
||||
|
||||
구현 순서는 의존성과 위험도 기준으로 아래와 같다.
|
||||
|
||||
1. **REQ-092 keepalive 설계 확정(OQ-1)**: draft-batch-design에서 pika 스레드 소유 모델 기준으로 keepalive 실행 위치(별도 스레드 vs. execution-loop 틱) 결정. 이 결정이 REQ-092 구현 전제.
|
||||
|
||||
2. **REQ-093 DLQ 가시성 방식 결정(OQ-3)**: draft-batch-design에서 dcy-029 로깅 자산 재사용 가능성 검토 후 메트릭/로그/소비자 중 택1. 이 결정이 REQ-093 DLQ 유입 관측 구현 전제.
|
||||
|
||||
3. **REQ-093 NFR-2 타임아웃 수치 확정**: design에서 단일 publish 경로 전체 블록 상한 수치 결정.
|
||||
|
||||
4. **구현(run-batch-implementation)**: 설계 결정 순서대로
|
||||
- Phase A: `rabbitmq.py` `publish_event` 재시도+강제 재연결(REQ-091) + `_publish_dlq` ERROR 로그(REQ-093)
|
||||
- Phase B: keepalive 구현(REQ-092) — OQ-1 결정 결과 적용
|
||||
- Phase C: `daemon.py` `_make_event_emitter` ERROR 경보 강화(REQ-093)
|
||||
- Phase D: 신규 테스트 작성 (half-open/재시도/keepalive/DLQ/emit ERROR AC 커버)
|
||||
|
||||
5. **REQ-094 baseline 문서 정합**: 구현 완료 후 `runtime-flows.md`, `project-structure.md`, `operations.md` 갱신.
|
||||
|
||||
선행 조건: REQ-091~093은 코드 범위가 분리되어 있어 병렬 구현 가능하나, keepalive(REQ-092)와 emit 경보(REQ-093)는 OQ-1/OQ-3 설계 결정이 전제여서 설계 완료 후 시작.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- `trex_core/adapters/messaging/rabbitmq.py` — 변경 대상 핵심 파일 (BAT-007에서 도입됨)
|
||||
- `trex_core/runtime/daemon.py` — `_make_event_emitter` 변경 대상
|
||||
- `tests/adapters/messaging/` — 기존 messaging 회귀 스위트(현재 537 passed) 전체 통과 유지 필수
|
||||
- `tests/application/test_messaging_event_publisher.py` — 기존 회귀 유지
|
||||
- dcy-029(BAT-045/046) — 구조화 로깅 인프라 자산(OQ-3 재사용 검토 대상)
|
||||
- External:
|
||||
- pika BlockingConnection API(`process_data_events`, `close`, `BlockingConnection`) — 버전 고정 (pyproject.toml 기준)
|
||||
- RabbitMQ broker — 통합 테스트는 `TREX_RABBITMQ_TEST_URL` skip(CI 환경 없으면 skip, unit은 mock 기반)
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason:
|
||||
- OQ-1(keepalive 실행 위치): pika 스레드 비안전성이 명시적 스레드 소유 모델 결정을 요구한다. 설계 없이 구현하면 다른 스레드에서 I/O 호출 시 연결 손상 리스크가 있다.
|
||||
- OQ-3(DLQ 가시성 방식): 메트릭/로그/소비자 데몬 중 선택이 구현 구조를 결정한다. 특히 소비자 데몬 방식이면 별도 스레드/모듈이 추가된다.
|
||||
- NFR-2(타임아웃 수치): execution-loop 블록 상한 수치가 코드에 상수로 박힌다. 설계에서 확정해야 AC-3 기준이 명확해진다.
|
||||
- architecture impact가 `runtime-flows`(keepalive 틱, 재시도 경로)이므로 standard profile design이 필요하다.
|
||||
|
||||
## Milestones
|
||||
|
||||
1. draft-batch-design 완료 — OQ-1(keepalive 위치), OQ-3(DLQ 방식), NFR-2(타임아웃 수치) 확정
|
||||
2. run-batch-implementation 완료 — 신규 테스트 포함, 기존 회귀 537 passed 유지
|
||||
3. draft-batch-verification 완료 — AC별 커버 확인, baseline 문서 정합 확인
|
||||
4. QC reviewer 검증 대기 (confirm-batch-verification은 팀장 게이트)
|
||||
|
||||
## Risks
|
||||
|
||||
- R-1(High): M1 라이브 trex-core — wire 계약·consume/RPC 경로 회귀 시 orb(라이브)·tsl(M2)에 즉시 영향. 완화: 변경을 `publish_event`/`_publish_dlq`/`_make_event_emitter` 경로에 국소화. `declare_topology`/`consume_rpc_forever`/`consume_events_forever`/`call_rpc` 경로 불변. 기존 회귀 스위트 전체 통과 강제.
|
||||
- R-2(Medium): pika 스레드 비안전성 — keepalive가 발행 전용 연결 소유 스레드 밖에서 실행되면 연결 손상. 완화: OQ-1을 design에서 확정, AC-2/AC-3(스레드 안전성) 테스트로 검증.
|
||||
- R-3(Medium): 재시도 at-least-once — 중복 이벤트 발행 가능. 완화: OQ-2는 delivery 검증(소비자 orb/tsl 멱등성 확인)으로 추적. 전이 이벤트는 멱등 적용 가능 추정.
|
||||
- R-4(Low): keepalive `process_data_events(time_limit=0)` 틱이 execution-loop와 같은 스레드에서 실행될 경우 루프 지연 유발 가능. 완화: NFR-2 타임아웃 수치를 design에서 확정, AC-4(블록 상한) 테스트로 검증.
|
||||
@@ -0,0 +1,131 @@
|
||||
# BAT-047 Verification
|
||||
|
||||
- Status: approved (confirm-batch-verification, 팀장 2026-06-19)
|
||||
- Batch: BAT-047
|
||||
- Verified By: trex-core-dev (draft-batch-verification → QC PASS-WITH-NOTES 시정 후 재검증)
|
||||
- Verified At (KST): 2026-06-19 (최초), 2026-06-19 (QC 시정 재검증)
|
||||
- Confirmed By: 팀장 (독립 opus qc-reviewer PASS-WITH-NOTES + 시정 2건 완료 + 팀장 직접 재확인)
|
||||
|
||||
## Human Approval Memo (팀장 confirm-batch-verification)
|
||||
- 독립 opus qc-reviewer 판정 = PASS-WITH-NOTES. 반환 2건 모두 시정 완료:
|
||||
- ① ruff F401 3건 제거 → 팀장 직접 재실행 `All checks passed!` 확인.
|
||||
- ② REQ-093 AC-3 테스트가 프로덕션 `Daemon._make_event_emitter()`를 실제 호출하도록 재작성 → 팀장 코드 확인(L398 `object.__new__(Daemon)` → L404 `_make_event_emitter()` → L422 `emit(...)`).
|
||||
- 팀장 직접 전체 재실행: **554 passed / 47 skipped / 0 실패** (회귀 0).
|
||||
- 수용한 잔여 한계(blocker 아님, release 노트에 명시할 것):
|
||||
- half-open / broker 재시작 / idle 60초 생존은 mock unit으로만 검증. 실 broker 통합(`TREX_RABBITMQ_TEST_URL`)은 환경 미설정으로 skip → M1 운영 또는 broker 포함 CI로 통합검증 위임.
|
||||
- NFR-2 "publish 경로 블록 ≤5초"는 재연결(2.0s)+재시도(2.0s) 구간에 엄밀 성립. 1차 실패 시도가 원 socket_timeout(5.0s)에 묶이면 최악 케이스 초과 가능 → 필요 시 후속 REQ로 초기 publish timeout 강화.
|
||||
- NFR-1(M1 라이브 무회귀): wire 계약·consume/RPC·토폴로지·`interface-contract.md`(diff 0) 불변 확인 — qc-reviewer 코드 추적 + 팀장 확인.
|
||||
|
||||
## Test Run Summary
|
||||
|
||||
> **정정 (QC PASS-WITH-NOTES 시정 후)**: 최초 verification.md에서 "ruff clean"으로 기재했으나, QC 독립 검증에서 ruff F401 3건(threading, unittest.mock.call, trex_core.runtime.daemon.Daemon)이 검출됐다. 또한 REQ-093 AC-3 테스트가 프로덕션 `_make_event_emitter` 경로를 호출하지 않고 emit 클로저를 테스트 본문에 직접 복제하는 구조였다. 이하는 시정 후 실제 재실행 결과다.
|
||||
|
||||
### 시정 후 재실행 결과
|
||||
|
||||
```
|
||||
명령: python3 -m ruff check .
|
||||
결과: All checks passed!
|
||||
```
|
||||
|
||||
```
|
||||
명령: python3 -m pytest tests/ --ignore=tests/adapters/messaging/test_rabbitmq_transport.py \
|
||||
--ignore=tests/adapters/messaging/test_rabbitmq_intent_e2e.py -q
|
||||
결과: 554 passed, 40 skipped in 1.51s
|
||||
기존 baseline: 537 passed (BAT-046 기준)
|
||||
신규 추가: 17 passed (tests/adapters/messaging/test_publish_reliability.py)
|
||||
회귀: 0건
|
||||
```
|
||||
|
||||
## Acceptance Criteria Mapping
|
||||
|
||||
### REQ-091: 이벤트 발행 재시도 + 강제 재연결
|
||||
|
||||
| AC | 내용 | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC-1 | half-open → 재연결+재시도 → 발행 성공 | `TestPublishRetryOnHalfOpen::test_half_open_first_attempt_fails_retry_succeeds` PASSED | PASS |
|
||||
| AC-2 | 재시도 소진 → `TransportUnavailableError` 전파 | `TestPublishRetryExhausted::test_retry_exhausted_raises_transport_unavailable` PASSED | PASS |
|
||||
| AC-3 | 타임아웃 상한 확인 (`connect_timeout=2.0`) | `TestForceReconnectTimeout::test_force_reconnect_uses_2s_timeout` PASSED | PASS |
|
||||
| AC-4 | 정상 발행(연결 살아 있음) — 재시도 경로 미호출 | `TestNormalPublishNoRetry::test_normal_publish_does_not_call_force_reconnect` PASSED | PASS |
|
||||
| AC-5 | consume/RPC 경로 무회귀 | `TestConsumePaths::test_rabbitmq_transport_has_consume_methods` PASSED + 전체 회귀 554 passed 0 회귀 | PASS |
|
||||
| AC-6 | broker 재시작 후 발행 복구 | `TestBrokerRestartScenario::test_broker_restart_publish_recovers` PASSED | PASS |
|
||||
|
||||
### REQ-092: 발행 전용 연결 idle keepalive
|
||||
|
||||
| AC | 내용 | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC-1 | idle 후 연결 생존 (keepalive가 `process_data_events` 호출) | `TestKeepaliveTick::test_keepalive_tick_calls_process_data_events_on_live_connection` PASSED | PASS |
|
||||
| AC-2 | 발행 소유 스레드에서만 I/O (design 결정: execution-loop 틱에 삽입) | OQ-1 design 문서에 Answered 기록. daemon.py `_execution_loop` 내 `keepalive_tick()` 호출 코드 diff 확인. pika 스레드 경계 불침범 | PASS |
|
||||
| AC-3 | 동시 발행+keepalive 충돌 없음 | `TestKeepaliveAndPublishSameThread::test_keepalive_then_publish_sequential_no_error` PASSED (단일 스레드 순차 호출) | PASS |
|
||||
| AC-4 | keepalive 비블록 (`time_limit=0`) | `TestKeepaliveTimeLimitZero::test_keepalive_uses_time_limit_zero` PASSED | PASS |
|
||||
| AC-5 | consume/RPC 경로 무회귀 | `TestKeepaliveDoesNotAffectConsumePaths::test_keepalive_tick_not_present_on_consume_rpc_forever_signature` PASSED | PASS |
|
||||
| AC-6 | OQ-1 종료 조건 (design 문서에 keepalive 위치 결정) | `design.md` "OQ-1: Answered — execution-loop 틱 삽입" 기록 완료 | PASS |
|
||||
|
||||
### REQ-093: DLQ 가시성/경보 + emit 실패 경보
|
||||
|
||||
| AC | 내용 | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC-1 | unroutable → DLQ 유입 + 관측 지표 노출 | `TestDlqVisibilityOnUnroutable::test_unroutable_calls_publish_dlq_and_logs_error` PASSED + `test_publish_dlq_logs_error_on_entry` PASSED | PASS |
|
||||
| AC-2 | DLQ 깊이 파악 가능 | 운영자 RabbitMQ 관리 UI + `rabbitmqctl list_queues` 조회 절차 `docs/operations.md` §4에 기재 | PASS |
|
||||
| AC-3 | emit 최종 실패 → ERROR 로그(rk+slot_id+strategy 포함) | `TestEmitErrorOnFinalPublishFailure::test_emit_logs_error_on_transport_unavailable` PASSED — **시정 후**: 복제 클로저 제거, `object.__new__(Daemon)`으로 stub 생성 후 `event_publisher`만 주입해 실제 `Daemon._make_event_emitter()`가 반환한 emit 클로저 직접 호출로 재작성. 프로덕션 경로 회귀 가드 확립. | PASS |
|
||||
| AC-4 | `_publish_dlq` 재발행 실패 → ERROR 로그 | `TestDlqPublishFailureLogsError::test_dlq_publish_failure_logs_error_not_warning` PASSED | PASS |
|
||||
| AC-5 | OQ-3 종료 조건 (design 문서에 DLQ 가시성 방식 결정) | `design.md` "OQ-3: Answered — 구조화 로그 기반 (dcy-029 자산 재사용)" 기록 완료 | PASS |
|
||||
| AC-6 | consume/RPC 경로 무회귀 | `TestConsumePaths` PASSED + 전체 회귀 554 passed 0 회귀 | PASS |
|
||||
|
||||
### REQ-094: baseline 문서 정합
|
||||
|
||||
| AC | 내용 | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC-1 | `runtime-flows.md` 발행/keepalive 경로 정합 | `docs/runtime-flows.md` — `슬롯 이벤트 발행/구독` 재시도/재연결/keepalive/DLQ 관측 경로 기술 추가 확인 | PASS |
|
||||
| AC-2 | OQ-1 결정(keepalive 위치) `runtime-flows.md` 반영 | `데몬 lifecycle` flow에 `trex-execution` 틱 시작 시 `keepalive_tick()` 호출 기술 추가 확인 | PASS |
|
||||
| AC-3 | OQ-3 결정(DLQ 가시성) `operations.md` DLQ 절차 신규 기재 | `docs/operations.md` §4 "DLQ 가시성 / 이벤트 발행 경보" 섹션 신규 생성 확인 | PASS |
|
||||
| AC-4 | `project-structure.md` 신규 컴포넌트 반영 | `messaging.py`(keepalive_tick), `rabbitmq.py`(재시도+keepalive), `daemon.py`(keepalive/ERROR 격상), 신규 테스트 파일 기술 추가 확인 | PASS |
|
||||
| AC-5 | `interface-contract.md` 불변 | diff 확인 — 본 batch에서 `docs/interface-contract.md` 변경 없음 (AMQP 토폴로지·큐·exchange·라우팅 키 불변) | PASS |
|
||||
|
||||
## Wire 계약 무회귀 검증 (NFR-1)
|
||||
|
||||
- `declare_topology` 경로: 변경 없음.
|
||||
- `consume_rpc_forever` / `consume_events_forever` / `call_rpc` 경로: 변경 없음.
|
||||
- `EVENTS_EXCHANGE`, `EVENTS_DLQ_EXCHANGE`, `EVENTS_DLQ_QUEUE` 토폴로지: 변경 없음.
|
||||
- 발행 routing key 형식(`{event_type}.{strategy_id}.{symbol}`): 변경 없음.
|
||||
- 페이로드 스키마: 변경 없음.
|
||||
- `docs/interface-contract.md`: 변경 없음.
|
||||
|
||||
## baseline 문서 동기화 (Architecture Impact: runtime-flows)
|
||||
|
||||
- `docs/runtime-flows.md` — 갱신 완료 (Last Updated: 2026-06-19 BAT-047)
|
||||
- `docs/project-structure.md` — 갱신 완료 (Last Updated: 2026-06-19 BAT-047)
|
||||
- `docs/operations.md` — DLQ 절차 신규 기재 완료
|
||||
|
||||
## 미검증 항목 (Blocking Issues 없음, 잔여 위험으로 분류)
|
||||
|
||||
- **실제 broker half-open 재현**: `TREX_RABBITMQ_TEST_URL` 환경이 없어 통합 테스트(`test_rabbitmq_transport.py`, `test_rabbitmq_intent_e2e.py`) skip됨 (기존 skip과 동일 조건). 신규 테스트는 모두 mock 기반 unit test로 로직을 검증함.
|
||||
- **OQ-2 소비자 멱등성**: orb/tsl 재시도 중복 이벤트 수신 시 멱등성 확인은 각 repo 별도 추적(본 batch 밖). trex-core 범위 완료.
|
||||
|
||||
## QC PASS-WITH-NOTES 시정 내역 (2026-06-19 재검증)
|
||||
|
||||
QC opus 검증에서 지적된 2건을 시정했다.
|
||||
|
||||
**시정 1 — ruff F401 3건 제거**
|
||||
- `tests/adapters/messaging/test_publish_reliability.py` L28: `import threading` 제거
|
||||
- `tests/adapters/messaging/test_publish_reliability.py` L29: `from unittest.mock import MagicMock, call, patch` → `call` 제거
|
||||
- `trex_core.runtime.daemon.Daemon` import는 시정 2에서 실제 사용하도록 재작성해 F401 해소
|
||||
|
||||
**시정 2 — REQ-093 AC-3 테스트를 프로덕션 경로로 재작성**
|
||||
- 구: 테스트 본문에 emit 클로저 직접 복제 (프로덕션 `_make_event_emitter` 미호출)
|
||||
- 신: `object.__new__(Daemon)`으로 stub 인스턴스 생성 → `event_publisher`만 mock 주입 → `daemon_stub._make_event_emitter()`가 반환한 emitter를 그대로 호출
|
||||
- 효과: `daemon.py` L247-253의 `_log.error(...)` 경로를 실제로 타므로, 해당 코드가 제거/변경되면 테스트가 실패해 회귀를 가드함
|
||||
|
||||
**재검증 실제 결과**
|
||||
- `python3 -m ruff check .` → `All checks passed!`
|
||||
- `python3 -m pytest tests/ --ignore=... -q` → `554 passed, 40 skipped in 1.51s`, 회귀 0
|
||||
|
||||
## Ready for confirm-batch-verification
|
||||
|
||||
- 신규 테스트: 17 passed
|
||||
- 전체 테스트: 554 passed, 40 skipped (기존 대비 +17, 회귀 0)
|
||||
- ruff: clean (F401 3건 시정 완료, 실제 실행 결과)
|
||||
- AC 커버: REQ-091 AC-1~6, REQ-092 AC-1~6, REQ-093 AC-1~6, REQ-094 AC-1~5 전부 PASS 또는 evidence 있음
|
||||
- wire 계약 무회귀: PASS
|
||||
- baseline 문서 정합: PASS
|
||||
- Blocking Issues: 없음
|
||||
- Ready for Release: false (멈춤 — confirm-batch-verification은 팀장 게이트)
|
||||
@@ -54,3 +54,4 @@
|
||||
| BAT-044 | standard | released | REQ-083, REQ-070 (CHG-20260616-01) | dcy-030 (issue #28) | [bat-044](./bat-044_20260616_slippage-tick-size-snapping/) |
|
||||
| BAT-045 | standard | released | REQ-084, REQ-085, REQ-087 | dcy-029 (issue #29, P0) | [bat-045](./bat-045_20260616_observability-logging-foundation/) |
|
||||
| BAT-046 | standard | released | REQ-086, REQ-088, REQ-089, REQ-090 | dcy-029 (issue #29, P1) | [bat-046](./bat-046_20260616_observability-classification-pnl-runbook/) |
|
||||
| BAT-047 | standard | release-candidate | REQ-091, REQ-092, REQ-093, REQ-094 | dcy-031 | [bat-047](./bat-047_20260619_event-publisher-connection-reliability/) |
|
||||
|
||||
Reference in New Issue
Block a user