perf(slots): BAT-040 — slots RPC DB-level status 필터 (REQ-002/006/073, issue #25)

REQ-002/006 change-req(CHG-20260612-01)→구현→confirm-req-implemented,
REQ-073 change-req(CHG-20260612-02, 관찰 계약 불변 Implemented 유지) 전체 delivery.

- repository.py/in_memory_repository.py: SlotRepository.list_slots(instance_key,
  status_filter=None) — status 값 집합 필터, None=전체(기존 호출자 불변).
- postgresql.py: list_slots DB-level WHERE status = ANY(%s) (full scan + Python
  필터 제거). 스키마/마이그레이션 변경 없음.
- messaging_handlers.py: _FILTER_STATUS 매핑으로 filter→status 집합 위임,
  watchdog 갱신용 holding 조회를 all/holding fetch 결과에서 재사용(2nd 커넥션 생략).
- 관찰 가능한 RPC 계약(REQ-073 AC-1~6) 불변 — 동작 보존 최적화.
- tests: in-memory status_filter 5종 + PG 게이트 1종. 439 passed (+5), ruff clean.

REQ-002·006 Approved→Implemented, REQ-073 Implemented 유지, BAT-040 release-candidate.
issue #25 close.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 19:27:51 +09:00
co-authored by Claude Opus 4.8
parent 14da195b1e
commit d737467d70
16 changed files with 479 additions and 27 deletions
@@ -0,0 +1,40 @@
# BAT-040 Design
- Status: confirmed
- Batch: BAT-040
## Architecture Summary
- status 필터링을 핸들러(Python)에서 저장소 경계로 내린다. `SlotRepository.list_slots` 에 optional `status_filter: set[str]` 를 추가하고(REQ-002 포트), PostgreSQL 은 `status = ANY(%s)`(REQ-006), in-memory 는 set 멤버십으로 필터한다.
- 핸들러(REQ-073)는 요청 filter 토큰을 status 값 집합으로 매핑해 `list_slots` 에 위임하고, watchdog 갱신용 holding 슬롯을 이미 fetch 한 결과에서 재사용할 수 있으면(all/holding) 두 번째 쿼리를 생략한다.
- 관찰 가능한 응답 계약은 불변 — 동작 보존 최적화.
## Changed Areas
- Modules:
- `trex_core/domain/slot/repository.py` — `list_slots(self, instance_key, status_filter: Optional[set[str]] = None)`
- `trex_core/domain/slot/in_memory_repository.py` — `status_filter` 멤버십 필터
- `trex_core/adapters/persistence/postgresql.py` — `status_filter` → `WHERE instance_key = %s AND status = ANY(%s)`
- `trex_core/application/messaging_handlers.py` — filter→status 집합 매핑 + 위임 + watchdog 재사용
- Tests:
- `tests/domain/`(in-memory status_filter), `tests/application/test_slots_handler.py`(filter/spy/키 회귀), `tests/adapters/persistence/`(PG 필터 게이트)
- Docs: 없음(관찰 계약·흐름 불변)
## Key Decisions
1. **`status_filter: Optional[set[str]]` (status 값 집합)**: 단일 status 가 아니라 집합 — `pending` 토큰이 `{pending_buy, pending_sell}` 두 status 로 매핑되기 때문. `None` = 전체(기존). 핸들러가 토큰→집합 매핑을 소유(저장소는 status 값만 안다).
2. **filter 토큰 매핑(핸들러)**: `inactive→{"inactive"}`, `pending→{"pending_buy","pending_sell"}`, `holding→{"holding"}`, `error→{"error"}`, `all`/미인식→`None`. REQ-073 AC-2(미인식 filter → all 일관 처리) 보존.
3. **PG `status = ANY(%s)`**: psycopg 가 Python list 를 PG array 로 어댑팅. `IN` 대신 `ANY` 로 단일 파라미터 바인딩. None 이면 기존 `WHERE instance_key = %s` 분기 유지.
4. **watchdog 두 번째 쿼리 생략 조건**: `status_filter is None`(all) 또는 filter=="holding" 이면 이미 fetch 한 `slots` 에 holding 이 전부 포함되므로 거기서 추출. 그 외(inactive/pending/error)는 holding 이 응답에 없으므로 기존 `list_holding_slots` 호출. spy=True 면 watchdog 자체를 갱신하지 않아 어느 경우든 두 번째 쿼리 없음.
5. **동작 보존**: 핸들러 출력(slots/available_slots/degraded/reason/message, spy 시맨틱)은 불변. AC-1~6 회귀로 보증.
## Edge Cases
- `list_slots` 무인자 호출자(reconcile/safety-sell/kill-switch): default `None` → 기존 전체 fetch, 동작 불변.
- filter=="all": status_filter=None → 전체 fetch, watchdog 은 fetch 결과에서 holding 추출(두 번째 쿼리 생략).
- filter=="holding": status_filter={"holding"} → holding 만 fetch, watchdog 재사용(두 번째 쿼리 생략).
- filter=="inactive"/"pending"/"error": holding 미포함 → watchdog 위해 `list_holding_slots` 호출(spy=false 시).
- 빈 결과(해당 status 슬롯 0): 빈 리스트 반환(정상).
## Architecture Impact
- none (runtime-flows 흐름·관찰 계약 불변. 내부 쿼리 위치/호출 수 최적화만)
## Reference Doc Update Plan
- project-structure.md: none
- runtime-flows.md: none (체결/슬롯 흐름 불변, 핸들러 내부 쿼리 최적화)
@@ -0,0 +1,33 @@
# BAT-040 Implementation
- Status: completed
- Batch: BAT-040
## Plan Summary
- `list_slots(status_filter)` 포트(REQ-002) + in-memory + PG(REQ-006) 구현, 핸들러(REQ-073) filter→status 집합 위임 + watchdog 조회 재사용, 테스트.
## Changed Files
- `trex_core/domain/slot/repository.py` — `list_slots(instance_key, status_filter=None)` Protocol 시그니처 + docstring
- `trex_core/domain/slot/in_memory_repository.py` — `status_filter` 멤버십 필터
- `trex_core/adapters/persistence/postgresql.py` — `list_slots` `status = ANY(%s)` 분기(None=전체)
- `trex_core/application/messaging_handlers.py` — `_FILTER_STATUS` 매핑 + `list_slots(status_filter)` 위임 + watchdog 조회 재사용(all/holding 시 두 번째 커넥션 생략)
- `tests/domain/slot/test_list_slots_filter.py` (신규, 5)
- `tests/adapters/persistence/test_postgresql_repository.py` — `test_list_slots_status_filter_pg` (PG 게이트)
## Execution Log
1. `repository.py`: Protocol `list_slots` 에 `status_filter: Optional[set[str]] = None` 추가.
2. `in_memory_repository.py`: `status_filter is None or s.status.value in status_filter` 필터.
3. `postgresql.py`: `status_filter` truthy 면 `WHERE instance_key = %s AND status = ANY(%s)`(list 파라미터), 아니면 기존 전체 쿼리.
4. `messaging_handlers.py`: 모듈 상수 `_FILTER_STATUS`(inactive/pending/holding/error→status 집합, all/미인식→None). 핸들러가 `list_slots(instance_key, status_filter)` 위임. watchdog: `status_filter is None or filter=="holding"` 면 fetch 결과에서 holding 추출, 아니면 `list_holding_slots`(spy=true 면 둘 다 생략).
5. 테스트: in-memory 5종(None 전체/단일 status/pending 그룹/무매치 빈/instance 스코프), PG 게이트 1종.
## Validation
- `pytest tests/application/test_slots_handler.py` — 16 passed (filter/spy/응답 키/available_slots 회귀, 관찰 계약 불변 확인)
- `pytest tests/domain/slot/test_list_slots_filter.py` — 5 passed (신규)
- `pytest tests/` — **439 passed, 46 skipped** (BAT-039 434 → +5 in-memory; PG status_filter 테스트는 DSN 부재로 skip, 동일 게이트)
- `ruff check trex_core tests` — All checks passed
- 무인자 `list_slots` 호출자(reconcile/safety-sell/kill-switch 등) 전수 회귀 없음(default None 동작 보존).
## Remaining Risks
- 없음. R-1(무인자 호출자 회귀)=default None 보존+전체 회귀 green, R-2(ANY 어댑팅)=psycopg list→array(PG 게이트 테스트, DSN 가용 시 실행), R-3(watchdog 재사용)=spy/holding 터치 테스트 회귀 green.
- PG `status = ANY` 경로의 라이브 검증은 `TREX_POSTGRESQL_TEST_DSN` 설정 환경에서 게이트 테스트로 수행(CI/로컬 DSN). in-memory parity 가 동작 계약을 보증.
@@ -0,0 +1,34 @@
# BAT-040: slots RPC per-request full scan 개선 — list_slots DB-level status 필터 (CHG-20260612-01)
- Status: release-candidate
- Profile: standard
- Source Discovery: 없음 (issue #25, REQ-002/006/073 change-req)
- Included REQ: REQ-002 (CHG-20260612-01), REQ-006 (CHG-20260612-01), REQ-073 (CHG-20260612-02)
- Excluded REQ: 없음
## Summary
- `trex.slots.req` 핸들러가 매 요청마다 `list_slots`(전 행 fetch) 후 Python 에서 status 를 거르던 구조(issue #25)를 DB 레벨 필터로 바꾼다. `SlotRepository.list_slots` 에 `status_filter` 옵션을 추가(REQ-002 포트 + REQ-006 PG 구현)하고, 핸들러가 filter 토큰을 status 집합으로 매핑해 위임한다(REQ-073). watchdog 갱신용 holding 조회는 응답 필터가 이미 holding 을 포함한 경우(all/holding) fetch 결과를 재사용해 두 번째 커넥션을 생략한다.
## Scope
- In Scope:
- `trex_core/domain/slot/repository.py` — `SlotRepository.list_slots(instance_key, status_filter=None)` (REQ-002)
- `trex_core/domain/slot/in_memory_repository.py` — status_filter 구현 (REQ-002)
- `trex_core/adapters/persistence/postgresql.py` — `list_slots` DB-level `status = ANY(%s)` (REQ-006)
- `trex_core/application/messaging_handlers.py` — `make_slots_rpc_handler` filter→status 집합 매핑·위임 + watchdog 조회 재사용 (REQ-073)
- 테스트: in-memory status_filter 단위, PG 필터(게이트), 핸들러 filter/spy/응답 키 회귀
- Out of Scope:
- 관찰 가능한 RPC 계약 변경(REQ-073 AC-1~6 불변 — 동작 보존 최적화)
- 스키마/마이그레이션 변경(쿼리 한정)
- overlay(paused/warned) 서버측 필터(REQ-074 후속)
- 인덱스 추가/튜닝(별도 운영 판단)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- 동작 보존 내부 최적화 — REQ-073 관찰 계약(filter 결과/키 집합/spy/degraded/available_slots) 불변. REQ-073 은 Implemented 유지(재검증), REQ-002/006 은 신규 AC-6 으로 Approved→Implemented 복귀.
- 도메인 포트 시그니처 변경(인터페이스 영향) + 다중 REQ 횡단 → minor-change fast-path 불가, standard profile.
- `list_holding_slots` 는 이미 status-scoped(full scan 아님). 주 개선은 `list_slots` full scan 제거 + all/holding 필터 시 두 번째 커넥션 생략.
@@ -0,0 +1,37 @@
# BAT-040 Planning
- Status: confirmed
- Batch: BAT-040
- Profile: standard
## Included REQ
- REQ-002 (CHG-20260612-01): `SlotRepository.list_slots(instance_key, status_filter=None)` 포트 + in-memory 구현. AC-6 신규.
- REQ-006 (CHG-20260612-01): PostgreSQL `list_slots` DB-level `status = ANY(%s)`. AC-6 신규.
- REQ-073 (CHG-20260612-02): 핸들러 filter→status 집합 매핑·위임 + watchdog 조회 재사용. 관찰 계약 불변, Implemented 유지.
## Delivery Plan
1. `repository.py` — `list_slots` Protocol 시그니처에 `status_filter: Optional[set[str]] = None` 추가(docstring 갱신).
2. `in_memory_repository.py` — `status_filter` 가 주어지면 `s.status.value in status_filter` 필터, None 이면 전체.
3. `postgresql.py` — `status_filter` 주어지면 `WHERE instance_key = %s AND status = ANY(%s)`(list 파라미터), None 이면 기존 전체 쿼리.
4. `messaging_handlers.py` — filter 토큰→status 집합 매핑(`inactive→{inactive}`, `pending→{pending_buy,pending_sell}`, `holding→{holding}`, `error→{error}`, all/미인식→None) 후 `list_slots(instance_key, status_filter)`. watchdog: `status_filter is None or filter=="holding"` 면 fetch 결과에서 holding 추출(두 번째 쿼리 생략), 아니면 `list_holding_slots`.
5. 테스트 — in-memory status_filter 단위, 핸들러 filter/spy/응답 키 회귀(기존), PG 필터(게이트, DSN 가용 시).
## Dependencies
- Internal:
- `list_slots` 호출자 전수(reconcile/safety-sell/kill-switch 등)는 무인자 → default None 으로 동작 불변(회귀 확인 필요)
- `SlotState` 값 문자열(`inactive/pending_buy/holding/pending_sell/error`)
- External: PostgreSQL(통합 게이트). 단위는 in-memory.
## Design Gate
- Design Required Before Implementation: yes
- Reason: 도메인 포트 시그니처 변경(인터페이스) + status 집합 매핑 규칙 + watchdog 쿼리 재사용 조건 + in-memory/PG parity 를 design 에서 확정.
## Milestones
1. Design confirmed — 포트 시그니처·status 집합 매핑·watchdog 재사용 조건 확정
2. Implementation — repository/in_memory/postgresql/handler + 테스트
3. Verification — REQ-002/006 AC-6 + REQ-073 AC-1~6 회귀 + 기존 list_slots 호출자 회귀 + ruff
## Risks
- R-1 (Medium): `list_slots` 무인자 호출자(reconcile 등)가 status_filter default 로 동작이 바뀌면 회귀 — 완화: default None = 기존 전체, 전수 호출 회귀 테스트.
- R-2 (Low): PG `ANY(%s)` 파라미터 어댑팅(list→array) — 완화: psycopg list→array 어댑팅 사용, PG 게이트 통합 테스트로 확인.
- R-3 (Low): watchdog 조회 재사용 조건 오류 시 holding 워치독 미갱신 — 완화: filter별 spy/watchdog 테스트로 holding 터치 보존 확인(REQ-073 AC-4).
@@ -0,0 +1,53 @@
# BAT-040 Verification
- Status: confirmed
- Batch: BAT-040
## Acceptance Mapping
- REQ-002 AC-6 (`list_slots(instance_key, status_filter)` 포트 + in-memory, None 전체):
- Evidence (`tests/domain/slot/test_list_slots_filter.py`):
- `test_none_returns_all`: None/무인자 → 전체 5 PASSED.
- `test_single_status_filter`: `{"holding"}`→1, `{"inactive"}`→3 PASSED.
- `test_pending_group_filter`: `{"pending_buy","pending_sell"}`→pending 1 PASSED.
- `test_empty_when_no_match`: `{"error"}`→[] PASSED.
- `test_instance_scoped`: 타 instance 0 PASSED.
- REQ-006 AC-6 (PostgreSQL `list_slots` DB-level `status = ANY(%s)`, None 전체):
- Evidence: `tests/adapters/persistence/test_postgresql_repository.py::test_list_slots_status_filter_pg` — None→전체 3, `{"inactive"}`→2, pending 그룹→1, `{"error"}`→[]. `TREX_POSTGRESQL_TEST_DSN` 설정 시 실행(현재 미설정 → skip, 전 PG 테스트와 동일 게이트). in-memory parity(REQ-002 AC-6)가 동작 계약을 보증.
- REQ-073 AC-1 (토폴로지 — `trex.slots.req` 존재):
- 변경 범위 밖(핸들러 내부 위임만). Evidence: 전체 회귀 439 passed.
- REQ-073 AC-2 (filter 동작 — inactive/pending/holding/all):
- Evidence: `test_filter_*` 5개 PASSED (`test_slots_handler.py`) — DB 위임 후에도 filter 결과 동일. 미인식 filter→None(all) 일관 처리 보존.
- REQ-073 AC-3 (응답 키 집합 — slots/available_slots/degraded/reason/message):
- Evidence: `test_response_keys_only_slots_health` PASSED — 키 집합 불변.
- REQ-073 AC-4 (spy=true 워치독 미갱신 / spy=false 갱신):
- Evidence: `test_spy_true_no_watchdog_update`, `test_spy_false_updates_watchdog` PASSED — watchdog 조회 재사용 최적화 후에도 holding 터치 시맨틱 보존.
- REQ-073 AC-5 (degraded 신호 전파):
- Evidence: `test_degraded_signal_propagated` PASSED.
- REQ-073 AC-6 (available_slots 값 규칙):
- Evidence: `test_available_slots_*` 3개 PASSED — capacity hint 불변.
## Evidence
- Test:
- `pytest tests/domain/slot/test_list_slots_filter.py` — 5 passed (신규)
- `pytest tests/application/test_slots_handler.py` — 16 passed (REQ-073 AC-1~6 회귀, 관찰 계약 불변)
- `pytest tests/` — **439 passed, 46 skipped** (BAT-039 434 → +5; PG status_filter 테스트는 DSN 부재로 skip, 동일 게이트)
- `ruff check trex_core tests` — All checks passed
- Manual / Architecture Impact (none):
- design.md Architecture Impact = none — `docs/project-structure.md`, `docs/runtime-flows.md` 동기화 불필요(관찰 계약·흐름 불변, 핸들러 내부 쿼리 위치/호출 수 최적화만).
- 무인자 `list_slots` 호출자(reconcile/safety-sell/kill-switch) default None 동작 보존 — 전체 회귀로 확인.
- 핸들러 watchdog 조회 재사용 조건(all/holding fetch 재사용, 그 외 list_holding_slots) 코드 리뷰 + AC-4 테스트로 확인.
## Result
- Ready for Release: true
- Blocking Issues: 없음
- PG `status = ANY` 경로의 라이브 검증은 `TREX_POSTGRESQL_TEST_DSN` 가용 환경의 게이트 테스트로 수행(REQ-006 기존 PG 테스트 전부 동일 조건). in-memory parity 가 release-ready 를 보증 — 차단 아님.
+1
View File
@@ -47,3 +47,4 @@
| BAT-037 | standard | release-candidate | REQ-073 (CHG-20260612-01) | dcy-025 (REQ-073 change-req) | [bat-037](./bat-037_20260612_slots-rpc-available-slots/) |
| BAT-038 | standard | release-candidate | REQ-018 (CHG-20260612-01) | dcy-026 (issue #24) | [bat-038](./bat-038_20260612_sell-transaction-tax-fee/) |
| BAT-039 | standard | release-candidate | REQ-011 (CHG-20260612-01) | 없음 (issue #4) | [bat-039](./bat-039_20260612_paper-seq-redis-persistence/) |
| BAT-040 | standard | release-candidate | REQ-002 (CHG-20260612-01), REQ-006 (CHG-20260612-01), REQ-073 (CHG-20260612-02) | 없음 (issue #25) | [bat-040](./bat-040_20260612_slots-rpc-db-filter/) |