feat(ledger): quiet idle outbox polling

This commit is contained in:
Hermes Agent
2026-09-20 21:57:30 +09:00
parent 62e4f57c2b
commit a2be76b199
14 changed files with 437 additions and 15 deletions
+13 -2
View File
@@ -4,7 +4,7 @@
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
- Status: draft
- Owner: project maintainers
- Last Updated (KST): 2026-09-19 (portfolio-hub Issue #32 venue provenance transport mapping)
- Last Updated (KST): 2026-09-20 (trex-core Issue #97 ledger outbox idle observability)
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); HTTP API = dcy-002 / BAT-002; operator broker truth = dcy-037 / BAT-057; ledger venue transport = portfolio-hub ProofLine req-0009
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
@@ -63,7 +63,7 @@
| 패턴 | 표면 | 요청 속성 | 응답 |
|---|---|---|---|
| 슬롯 RPC | `trex.slots.req` | `correlation_id`, `reply_to=amq.rabbitmq.reply-to` | reply-to 큐로 `{slots[], degraded, reason, message}` (REQ-073/074, BAT-035) |
| 슬롯 RPC | `trex.slots.req` | `correlation_id`, `reply_to=amq.rabbitmq.reply-to` | reply-to 큐로 `{slots[], degraded, reason, message, ledger_outbox_metrics?}` (`ledger_outbox_metrics`는 `spy=true` 전용, REQ-073/074, AC-0040) |
| intent RPC | `trex.intent.req` | `correlation_id` = intent 멱등키, `reply_to=amq.rabbitmq.reply-to`, `delivery_mode=2` | reply-to 큐로 `{intent_id, status:admitted|rejected, slot_id?, reason?}`. retryability·broker attempt·KIS failure 정보는 strategy 응답에 포함하지 않는다. 체결/종료는 응답 아닌 이벤트로 |
| update_thresholds | `trex.intent.req` | `action="update_thresholds"`, `reply_to` | reply-to 큐로 `{status:updated|ignored, slot_id, reason?}` (REQ-040) |
| 이벤트 pub/sub | `trex.events` (topic) | routing key `{event_type}.{strategy_id}.{symbol}` | 전략 큐가 자기 `strategy_id` 패턴으로 바인딩 |
@@ -162,6 +162,17 @@
- `spy=false`(기본): holding 슬롯 응답 후 `WatchdogStore.update(slot_id)` 호출 — 전략 폴링 = 생존 신호.
- `spy=true`: watchdog 갱신 건너뜀 — 운영자 CLI(`universe`/`slots`) 가 항상 사용.
#### `spy=true` ledger outbox metrics (AC-0040)
- `ledger_outbox_metrics`는 `spy=true` 운영자 응답의 top-level additive field다. `spy=false` 전략 응답에는 key 자체를 포함하지 않는다.
- 정상 snapshot은 `available=true`, `scope="process_lifetime"`, configured `instance_key`와 다음 누적 정수를 제공한다.
- `claims`: `empty_polls_total`, `claimed_batches_total`, `unavailable_total`, `lost_total`, `foreign_instance_total`
- `outcomes`: `completed_total`, `duplicate_completed_total`, `retryable_failure_total`, `conflict_blocked_total`, `permanent_failure_total`, `dead_lettered_total`
- 각 값은 해당 daemon process의 configured instance writer에서만 누적하며 0 이상의 정수다. snapshot은 내부 mutable counter와 분리된 새 값이고 daemon restart 때 0으로 초기화된다.
- metrics reader가 없거나 조회에 실패하면 slots 조회는 계속 성공하고 `available=false`, `scope="process_lifetime"`, configured `instance_key`, `reason="ledger_outbox_metrics_unavailable"`만 반환한다. raw exception text는 응답에 포함하지 않는다.
- ledger writer는 fixed `ledger_interval_sec` cadence를 유지한다. empty claim은 `empty_polls_total`만 증가시키고 INFO를 남기지 않는다. non-empty batch INFO만 `instance_key`, non-zero `count`, `limit`, repository claim 순서의 `correlation_ids` JSON string array를 포함한다.
- `_ledger_loop`의 예상 밖 writer exception은 `reason=ledger_writer_run_once_failed`, configured instance, exception class name만 ERROR로 남긴다. raw exception message, traceback과 `exc_info`는 금지하며 loop는 다음 configured poll을 계속한다.
### 운영자 CLI ↔ RPC 매핑 (dcy-016 / REQ-032)
운영자 CLI 명령은 데몬 RabbitMQ RPC 로 통일된다(단일 writer admission 보존, DB 직접 쓰기 없음). 인스턴스 라우팅: `--config <toml>`(canonical, REQ-048) 또는 `--instance <key>`(단축·override), 미지정 시 `default`.
+3 -1
View File
@@ -50,7 +50,7 @@
1. 전략 모듈 또는 운영자가 RPC 요청을 `trex.slots.req` 큐로 publish — payload 에 `filter`(status 필터) + `spy`(워치독 억제 여부), properties 에 `correlation_id` + `reply_to`.
2. `trex-slots-rpc` 스레드가 메시지 수신 → `make_slots_rpc_handler(slot_repo, ...)` 로 슬롯 목록 산출.
3. `spy=false`(기본) 면 holding 슬롯 `WatchdogStore.update(slot_id)` 호출(전략 폴링 = 생존 신호, REQ-034).
4. 응답 `{slots[], degraded, reason, message}` 를 `reply_to` 큐로 publish → manual ack.
4. 응답 `{slots[], degraded, reason, message}` 를 `reply_to` 큐로 publish → manual ack. `spy=true` 운영자 응답에는 process-lifetime `ledger_outbox_metrics` snapshot을 top-level에 추가하고, `spy=false` 전략 응답에는 해당 key를 노출하지 않는다.
5. read-only 이므로 dedup 불요; 재요청 시 동일 응답을 다시 생성한다.
## Flow: `매수/매도 intent admission RPC` (RabbitMQ `trex.intent.req`, BAT-007)
@@ -314,6 +314,8 @@ timeout 미지정·취소 실패로 잔여 고착된 pending 슬롯을 처분:
4. 터미널 실패 가시화(dcy-015/REQ-018): `delivery_failed`(permanent/dead-letter)는 `WriterStats.delivery_failed` 카운터 + WARN 로그 + 구분 status 로 노출되어 정상 `pending` 과 구별된다(무성 영속 실패 금지). dead-letter 는 폐기가 아니라 운영자 재처리 가능 상태이며 `find_ledger_gap_slots`(REQ-055)·skip 가시화와 연계해 누락을 추적한다.
5. 백오프 파라미터는 env 설정: `TREX_LEDGER_RETRY_BASE_SEC`(기본 1.0)/`TREX_LEDGER_RETRY_CAP_SEC`(기본 300)/`TREX_LEDGER_MAX_ATTEMPTS`(기본 8).
6. portfolio-hub 장기 장애에도 outbox 가 적재되어 무유실. 재시작 시 PostgreSQL `ledger_outbox.status='pending'` 행을 그대로 이어 처리한다(터미널 `delivery_failed`/`conflict_blocked` 는 제외).
7. 데몬은 configured `ledger_interval_sec`(기본 1초)의 fixed cadence를 유지한다. pending row가 없는 정상 idle poll은 process-local `claim:empty` counter만 증가시키고 INFO 로그를 남기지 않는다. non-empty batch만 INFO를 남기며 configured `instance_key`, non-zero `count`, `limit`, repository claim 순서의 `correlation_ids` JSON string array를 포함한다.
8. process-lifetime counter는 daemon restart 때 0으로 초기화되며 DB나 별도 exporter에 영속화하지 않는다. `trex.slots.req`의 `spy=true` 응답에서 claim/outcome totals를 조회할 수 있다. writer 호출이 예상 밖 예외를 던지면 `_ledger_loop`는 `reason=ledger_writer_run_once_failed`, configured instance와 exception class name만 ERROR로 남기고 다음 poll을 계속한다. raw exception message, traceback과 `exc_info`는 기록하지 않는다.
## Flow: `구조화 로깅 / 관측성` (BAT-045·046 / dcy-029, REQ-084~088)