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:
@@ -66,13 +66,25 @@ class PostgreSQLSlotRepository:
|
||||
conn.rollback()
|
||||
raise
|
||||
|
||||
def list_slots(self, instance_key: str) -> list[Slot]:
|
||||
def list_slots(
|
||||
self, instance_key: str, status_filter: Optional[set[str]] = None
|
||||
) -> list[Slot]:
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"SELECT {_SLOT_COLUMNS} FROM daily_slot WHERE instance_key = %s",
|
||||
(instance_key,),
|
||||
)
|
||||
if status_filter:
|
||||
# issue #25: filter by status at the DB instead of fetching
|
||||
# every row and filtering in Python. ANY(%s) binds the set
|
||||
# as a single PG array parameter.
|
||||
cur.execute(
|
||||
f"SELECT {_SLOT_COLUMNS} FROM daily_slot "
|
||||
"WHERE instance_key = %s AND status = ANY(%s)",
|
||||
(instance_key, list(status_filter)),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
f"SELECT {_SLOT_COLUMNS} FROM daily_slot WHERE instance_key = %s",
|
||||
(instance_key,),
|
||||
)
|
||||
return [self._row_to_slot(r) for r in cur.fetchall()]
|
||||
|
||||
def fetch_slot(self, slot_id: UUID) -> Optional[Slot]:
|
||||
|
||||
@@ -168,6 +168,17 @@ def _handle_update_thresholds(
|
||||
|
||||
# ---- slots RPC handler (REQ-073/074) ----------------------------------------
|
||||
|
||||
# Maps a `trex.slots.req` filter token to the set of slot status values it
|
||||
# selects (issue #25 — pushed to the repository via list_slots status_filter).
|
||||
# "all" and unrecognized tokens map to None (no filter — REQ-073 AC-2).
|
||||
_FILTER_STATUS: dict[str, set[str]] = {
|
||||
"inactive": {"inactive"},
|
||||
"pending": {"pending_buy", "pending_sell"},
|
||||
"holding": {"holding"},
|
||||
"error": {"error"},
|
||||
}
|
||||
|
||||
|
||||
def make_slots_rpc_handler(
|
||||
slot_repo: SlotRepository,
|
||||
*,
|
||||
@@ -196,21 +207,21 @@ def make_slots_rpc_handler(
|
||||
filter_val = str(body.get("filter") or "all").lower()
|
||||
spy = bool(body.get("spy", False))
|
||||
|
||||
# Retrieve and filter slots by status
|
||||
slots = slot_repo.list_slots(instance_key)
|
||||
if filter_val == "inactive":
|
||||
slots = [s for s in slots if s.status.value == "inactive"]
|
||||
elif filter_val == "pending":
|
||||
slots = [s for s in slots if s.status.value in ("pending_buy", "pending_sell")]
|
||||
elif filter_val == "holding":
|
||||
slots = [s for s in slots if s.status.value == "holding"]
|
||||
elif filter_val == "error":
|
||||
slots = [s for s in slots if s.status.value == "error"]
|
||||
# "all" or unrecognized → no filter (REQ-073 AC-2)
|
||||
# issue #25: filter by status at the DB (REQ-002/006 list_slots
|
||||
# status_filter) instead of fetching every row and filtering in Python.
|
||||
# "all"/unrecognized → None (no filter, REQ-073 AC-2).
|
||||
status_filter = _FILTER_STATUS.get(filter_val)
|
||||
slots = slot_repo.list_slots(instance_key, status_filter=status_filter)
|
||||
|
||||
# REQ-034 CHG-01: update watchdog for holding slots unless spy=True
|
||||
# REQ-034 CHG-01: update watchdog for holding slots unless spy=True.
|
||||
# When the fetched set already contains every holding slot (filter "all"
|
||||
# or "holding"), reuse it and skip the second connection (issue #25 (2)).
|
||||
if not spy and watchdog_store is not None:
|
||||
for s in slot_repo.list_holding_slots(instance_key):
|
||||
if status_filter is None or filter_val == "holding":
|
||||
holding_slots = [s for s in slots if s.status.value == "holding"]
|
||||
else:
|
||||
holding_slots = slot_repo.list_holding_slots(instance_key)
|
||||
for s in holding_slots:
|
||||
watchdog_store.update(str(s.slot_id))
|
||||
|
||||
# Build merged slot rows (REQ-074)
|
||||
|
||||
@@ -51,8 +51,14 @@ class InMemorySlotRepository:
|
||||
"""Test seeding helper — insert a slot row directly."""
|
||||
self._slots[slot.slot_id] = slot
|
||||
|
||||
def list_slots(self, instance_key: str) -> list[Slot]:
|
||||
return [s for s in self._slots.values() if s.instance_key == instance_key]
|
||||
def list_slots(
|
||||
self, instance_key: str, status_filter: Optional[set[str]] = None
|
||||
) -> list[Slot]:
|
||||
return [
|
||||
s for s in self._slots.values()
|
||||
if s.instance_key == instance_key
|
||||
and (status_filter is None or s.status.value in status_filter)
|
||||
]
|
||||
|
||||
def fetch_slot(self, slot_id: UUID) -> Optional[Slot]:
|
||||
return self._slots.get(slot_id)
|
||||
|
||||
@@ -37,8 +37,15 @@ class SlotRepository(Protocol):
|
||||
...
|
||||
|
||||
# --- daily_slot CRUD ---
|
||||
def list_slots(self, instance_key: str) -> list[Slot]:
|
||||
"""Return all slots owned by this instance."""
|
||||
def list_slots(
|
||||
self, instance_key: str, status_filter: Optional[set[str]] = None
|
||||
) -> list[Slot]:
|
||||
"""Return slots owned by this instance.
|
||||
|
||||
status_filter: when given, only slots whose status value is in the set
|
||||
are returned (filtered at the storage boundary — REQ-006 pushes this to
|
||||
SQL). None returns all slots (prior behaviour). issue #25.
|
||||
"""
|
||||
...
|
||||
|
||||
def fetch_slot(self, slot_id: UUID) -> Optional[Slot]:
|
||||
|
||||
Reference in New Issue
Block a user