Files
trex-core/.proofline/lines/line-0012/dcy-0012.md
T

8.6 KiB

id, status
id status
dcy-0012 confirmed

Ledger outbox idle poll 로그 억제와 관측 보존

Problem

LedgerWriter.run_once()는 처리할 ledger_outbox row가 없는 정상 idle poll에도 매번 claim_result=empty INFO 로그를 남긴다. daemon 기본 주기 1초와 결합되어 실제 원장 work item이 거의 없는 운영 환경에서도 대량의 정상 로그가 생성되고, claim 상실·retry·conflict·delivery failure 같은 조치 가능한 신호가 묻힌다.

현재 claim:empty in-process counter는 증가하지만 writer 객체 밖의 운영 조회 표면에 연결되지 않는다. 따라서 empty INFO를 단순 삭제하면 로그 폭증은 해결되지만 empty poll 총량을 metric 또는 status에서 조회해야 한다는 Issue #97의 요구를 충족하지 못한다. 한편 polling cadence를 adaptive backoff나 event-driven wakeup으로 바꾸면 신규 item 처리 지연, process 간 알림, lease 만료·재claim에 대한 별도 계약이 필요하다.

Evidence

  • E-001 GitHub Issue #97 (https://github.com/fourblocks2020/trex-core/issues/97)은 2026-09-16 00:00 KST부터 2026-09-18 21:31 KST까지 두 instance에서 claim_result=empty INFO 494,029줄이 발생했고 실제 신규 work item은 8건이었다고 기록한다.
  • E-002 기준 revision 62e4f57c2b80ce28db87421c5e7b72a4316b8c5f의 trex_core/application/ledger_outbox.py에서 run_once()는 claim_pending() 결과에 따라 claim:empty 또는 claim:claimed counter를 증가시키지만, empty/non-empty 구분 없이 INFO claim 로그를 남긴다.
  • E-003 같은 파일의 LedgerWriter.counters는 process-local dict이며 이를 반환하는 status/RPC/exporter가 없다. 독립 read-only 조사도 현재 counter만으로는 운영 조회 가능성을 충족하지 못한다고 판정했다.
  • E-004 trex_core/runtime/config.py의 ledger_interval_sec 기본값은 1.0초이고 trex_core/runtime/daemon.py::_ledger_loop()는 매 interval마다 run_once()를 호출한다. writer 예외는 현재 로그 없이 삼켜진다.
  • E-005 trex_core/application/ledger_outbox.py의 batch claim INFO는 현재 instance_key, claim_result, count, limit만 남겨 correlation ID를 제공하지 않는다. targeted claim은 correlation ID를 남기며, claim lost 및 foreign/unowned item은 WARNING, retry/conflict/permanent/dead-letter outcome은 기존 counter와 로그 경로로 구분한다.
  • E-006 active AC-0029는 instance ownership, lease fencing, claim/outcome 관측과 secret 비노출을 소유하지만 idle poll 무로그와 운영 status 조회는 규정하지 않는다. 이번 capability는 AC-0029를 회귀 검증(satisfy)하면서 별도 delta AC를 생성(create)할 독립 운영 경계다.
  • E-007 focused baseline uv run pytest -q tests/application/test_ledger_outbox.py tests/application/test_ledger_outbox_backoff.py tests/adapters/persistence/test_postgresql_ledger_outbox.py 결과는 20 passed, 16 skipped였다. skip은 TREX_POSTGRESQL_TEST_DSN이 없는 PostgreSQL integration이다.
  • E-008 저장소에는 범용 metrics exporter나 daemon status RPC가 없다. 기존 instance-scoped trex.slots.req는 spy=true 운영 조회와 spy=false 전략 조회를 구분하므로, operator 응답에만 additive process-lifetime ledger outbox metrics를 싣는 것이 새 transport/DB schema 없이 가능한 최소 surface다.

Scope

  • S-001 create: 새 delta AC는 pending row가 없는 poll에서 claim:empty를 정확히 누적하되 기본 INFO 로그를 생성하지 않는 동작을 소유한다. 기본 ledger_interval_sec=1.0에서 24시간에 해당하는 N=86,400회의 run_once()를 실제 sleep 없이 두 writer(instance-a, instance-b)에 각각 호출한다. 각 writer의 empty_polls_total은 정확히 86,400이고 rendered INFO의 claim_result=empty는 0건이어야 한다.
  • S-002 create: instance-scoped trex.slots.req의 spy=true operator 응답에 top-level ledger_outbox_metrics snapshot을 additive로 노출한다. 정상 snapshot은 available=true, scope=process_lifetime, configured instance_key를 포함한다. claims object는 empty_polls_total, claimed_batches_total, unavailable_total, lost_total, foreign_instance_total을, outcomes object는 completed_total, duplicate_completed_total, retryable_failure_total, conflict_blocked_total, permanent_failure_total, dead_lettered_total을 누락 없이 0 이상의 정수로 반환한다. 값은 해당 daemon process의 configured instance writer만 집계하고 새 dict snapshot으로 반환한다. reader가 없거나 조회가 실패하면 available=false, scope=process_lifetime, configured instance_key, reason=ledger_outbox_metrics_unavailable을 반환하며 slots 조회 자체는 실패시키지 않는다. spy=false 전략 응답에는 ledger_outbox_metrics key 자체를 포함하지 않는다.
  • S-003 create: non-empty batch claim에서만 INFO를 남기고 non-zero count, limit, instance_key와 해당 batch의 non-empty correlation ID 목록을 보존한다. targeted claim은 기존 단일 correlation ID를 유지한다. empty poll은 correlation ID를 생성하거나 로그하지 않는다.
  • S-004 satisfy AC-0029: claim lost, foreign/unowned rejection, retryable, conflict, permanent failure, dead-letter와 성공/duplicate outcome의 severity, counter, instance ownership 및 lease fencing을 약화하지 않는다. 로그·metric·status에는 credential, account identifier, token, password, DSN, provider payload 또는 raw exception text를 포함하지 않는다.
  • S-005 TrexDaemon._ledger_loop()의 ledger_writer.run_once()가 예상 밖 예외를 던지면 ERROR 한 건에 reason=ledger_writer_run_once_failed, configured instance_key, exception class name인 exc_type만 남긴 뒤 다음 configured poll을 계속한다. 이 정책은 _ledger_loop에만 적용하며 raw exception message와 traceback/exc_info는 기록하지 않는다. sentinel 예외로 reason·type·loop survival과 secret 비노출을 함께 검증한다.
  • S-006 fixed ledger_interval_sec polling과 기존 retry next_retry_at, claim lease, ownership/filtering semantics는 변경하지 않는다. 신규 item은 daemon이 살아 있는 동안 enqueue 시점부터 다음 configured poll 기회에 처리 대상이 되어야 한다.
  • S-007 docs/runtime-flows.md와 관련 interface 문서를 idle logging, operator metrics, fixed cadence 결정에 맞게 동기화한다.

Out of Scope

  • OS-001 adaptive idle backoff, PostgreSQL LISTEN/NOTIFY, event-driven wakeup, 별도 metrics exporter 및 durable metric persistence는 이번 Line에서 구현하지 않는다. 이들은 cross-process signaling과 별도 latency/SLO 설계가 필요한 후속 capability다.
  • OS-002 ledger_outbox schema, correlation ID 형식, lease TTL, retry/backoff 공식, dead-letter/conflict 정책과 portfolio-hub API 계약은 변경하지 않는다.
  • OS-003 AC-0029 원문, 승인된 기존 REQ/Discovery bytes와 기존 lifecycle status는 변경하지 않는다.
  • OS-004 production daemon 재기동, production PostgreSQL row 조작, 실제 원장 delivery와 운영 로그 삭제는 수행하지 않는다.

Risks and Unknowns

Risks

  • R-001 empty와 non-empty 분기를 잘못 배치하면 실제 claim INFO까지 사라질 수 있다. empty/no-log와 non-empty/identity-log를 별도 회귀로 검증한다.
  • R-002 process-lifetime metric은 daemon 재시작 시 0으로 초기화된다. response에 scope를 명시하고 durable total로 오인하지 않도록 문서화한다.
  • R-003 operator metric을 strategy 응답에도 싣거나 mutable counters dict를 그대로 반환하면 불필요한 내부 정보 노출 또는 동시 변경 위험이 생긴다. spy=true에서만 정수 snapshot을 새 dict로 반환한다.
  • R-004 daemon exception 로그에 raw exception text나 traceback을 넣으면 DSN·credential이 노출될 수 있다. sentinel 예외로 sanitized logging과 loop survival을 함께 검증한다.
  • R-005 PostgreSQL fixture가 없으면 real-backend multi-instance/lease 회귀는 skip된다. 해당 gap을 명시하고 기존 gated integration selector를 유지한다.

Assumptions

  • A-001 Issue #97의 “metric 또는 status에서 조회”는 daemon process lifetime 동안 instance별 누적값을 operator의 spy=true slots RPC/CLI JSON 응답에서 조회하는 것으로 충족한다. durable historical aggregation은 요구하지 않는다.
  • A-002 허용 가능한 신규 item 지연은 기존 configured ledger_interval_sec의 다음 poll 기회까지다. cadence를 유지하므로 이번 변경이 추가 지연을 만들지 않는다.