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
@@ -71,6 +71,19 @@ def test_buy_intent_idempotent_on_pg(pg_clean):
assert len(pending) == 1 # no double transition
def test_list_slots_status_filter_pg(pg_clean):
# REQ-006 CHG-20260612-01 (issue #25): DB-level status filter (status = ANY).
repo, svc, intents = _wire(pg_clean)
repo.reconcile_universe(INST, _SYMS) # 3 inactive
intents.submit_buy(BuyIntent(intent_id="pg-f1", qty=10, portfolio_id="PF-1")) # PF-1 -> pending_buy
assert len(repo.list_slots(INST)) == 3 # None -> all
assert len(repo.list_slots(INST, status_filter={"inactive"})) == 2
pending = repo.list_slots(INST, status_filter={"pending_buy", "pending_sell"})
assert len(pending) == 1 and pending[0].status is SlotState.PENDING_BUY
assert repo.list_slots(INST, status_filter={"error"}) == []
def test_buy_by_slot_id_contract_pg(pg_clean):
# dcy-020/REQ-061: slot_id is the canonical buy key against real PG.
repo, svc, intents = _wire(pg_clean)
@@ -0,0 +1,70 @@
"""list_slots status_filter (REQ-002/006 CHG-20260612-01, issue #25).
Pushes status filtering to the repository boundary so the slots RPC handler no
longer fetches every row and filters in Python. None preserves prior behaviour
(all slots), so existing unfiltered callers are unaffected.
"""
from __future__ import annotations
import pytest
from trex_core.domain.slot import SlotService, SlotState
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
INST = "inst-1"
@pytest.fixture
def repo():
return InMemorySlotRepository()
def _seed(repo):
"""3 inactive + 1 holding + 1 pending_buy for INST."""
svc = SlotService(repo, instance_key=INST)
repo.reconcile_universe(INST, {f"pf-{i}": "SYM" for i in range(5)})
# pf-0 -> holding
s0 = repo.find_slot_by_portfolio(INST, "pf-0")
svc.transit_slot(s0.slot_id, s0.status, SlotState.PENDING_BUY,
reason="r", actor="a", symbol="005930")
svc.transit_slot(s0.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING,
reason="r", actor="a", avg_price=70_000)
# pf-1 -> pending_buy
s1 = repo.find_slot_by_portfolio(INST, "pf-1")
svc.transit_slot(s1.slot_id, s1.status, SlotState.PENDING_BUY,
reason="r", actor="a", symbol="005930")
# pf-2,3,4 remain inactive
return svc
def test_none_returns_all(repo):
_seed(repo)
assert len(repo.list_slots(INST)) == 5
assert len(repo.list_slots(INST, status_filter=None)) == 5
def test_single_status_filter(repo):
_seed(repo)
holding = repo.list_slots(INST, status_filter={"holding"})
assert len(holding) == 1 and holding[0].status is SlotState.HOLDING
inactive = repo.list_slots(INST, status_filter={"inactive"})
assert len(inactive) == 3
assert all(s.status is SlotState.INACTIVE for s in inactive)
def test_pending_group_filter(repo):
_seed(repo)
pending = repo.list_slots(INST, status_filter={"pending_buy", "pending_sell"})
assert len(pending) == 1 and pending[0].status is SlotState.PENDING_BUY
def test_empty_when_no_match(repo):
_seed(repo)
assert repo.list_slots(INST, status_filter={"error"}) == []
def test_instance_scoped(repo):
_seed(repo)
# other instance has no slots
assert repo.list_slots("other", status_filter={"holding"}) == []