feat(slots-rpc): available_slots capacity hint 추가 (BAT-037 / REQ-073 AC-6)
slots RPC 응답에 available_slots: int|null 포함. max_active_slots 미설정 시 null, 설정 시 max(0, M-active). 16 tests passed (AC-6 신규 3개 포함), 423 전체 통과. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# BAT-037 Implementation
|
# BAT-037 Implementation
|
||||||
|
|
||||||
- Status: draft
|
- Status: completed
|
||||||
- Batch: BAT-037
|
- Batch: BAT-037
|
||||||
|
|
||||||
## Plan Summary
|
## Plan Summary
|
||||||
- `make_slots_rpc_handler` 에 `max_active_slots` 파라미터 추가 및 응답에 `available_slots` 포함
|
- `make_slots_rpc_handler` 에 `max_active_slots: Optional[int] = None` 파라미터 추가, 응답에 `available_slots` 포함
|
||||||
- `daemon.py` wiring 에 `max_active_slots` 전달
|
- `daemon.py` 핸들러 wiring 에 `max_active_slots=self.config.max_active_slots` 전달
|
||||||
- `test_slots_handler.py` AC-3/AC-6 갱신
|
- `test_slots_handler.py` AC-3 반전 + AC-6 테스트 3개 추가
|
||||||
|
|
||||||
## Changed Files
|
## Changed Files
|
||||||
- `trex_core/application/messaging_handlers.py`
|
- `trex_core/application/messaging_handlers.py`
|
||||||
@@ -14,10 +14,15 @@
|
|||||||
- `tests/application/test_slots_handler.py`
|
- `tests/application/test_slots_handler.py`
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
1. 구현 예정
|
1. `make_slots_rpc_handler` 시그니처에 `max_active_slots: Optional[int] = None` 추가
|
||||||
|
2. 응답 구성 직전 `available_slots` 계산: None → null, 설정 시 `max(0, M − count_active)`
|
||||||
|
3. `daemon.py` `_slots_rpc_loop` 에 `max_active_slots=self.config.max_active_slots` wiring
|
||||||
|
4. `test_slots_handler.py` AC-3 키 집합 갱신(`available_slots` 포함), `_wire` 에 `max_active_slots` 파라미터 전달
|
||||||
|
5. AC-6 테스트 3개 추가: uncapped(null), remaining(M-N), full(0)
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
- 구현 후 기록
|
- `pytest tests/application/test_slots_handler.py` — 16 passed (신규 3개 포함)
|
||||||
|
- `pytest tests/` — 423 passed, 45 skipped, 회귀 없음
|
||||||
|
|
||||||
## Remaining Risks
|
## Remaining Risks
|
||||||
- 없음
|
- 없음
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# BAT-037: slots RPC 응답에 available_slots capacity hint 추가 (CHG-20260612-01)
|
# BAT-037: slots RPC 응답에 available_slots capacity hint 추가 (CHG-20260612-01)
|
||||||
|
|
||||||
- Status: draft
|
- Status: in-delivery
|
||||||
- Profile: standard
|
- Profile: standard
|
||||||
- Source Discovery: dcy-025 (REQ-073 change-req)
|
- Source Discovery: dcy-025 (REQ-073 change-req)
|
||||||
- Included REQ: REQ-073 (CHG-20260612-01)
|
- Included REQ: REQ-073 (CHG-20260612-01)
|
||||||
|
|||||||
@@ -44,4 +44,4 @@
|
|||||||
| BAT-034 | standard | release-candidate | REQ-002 (CHG-20260610-02), REQ-006 (CHG-20260610-02), REQ-007 (CHG-20260610-02), REQ-028 (CHG-20260610-01), REQ-032 (CHG-20260610-07) | dcy-023 (issue #22) | [bat-034](./bat-034_20260610_drop-allocated-capital/) |
|
| BAT-034 | standard | release-candidate | REQ-002 (CHG-20260610-02), REQ-006 (CHG-20260610-02), REQ-007 (CHG-20260610-02), REQ-028 (CHG-20260610-01), REQ-032 (CHG-20260610-07) | dcy-023 (issue #22) | [bat-034](./bat-034_20260610_drop-allocated-capital/) |
|
||||||
| BAT-035 | standard | confirmed | REQ-073, REQ-074, REQ-075, REQ-021 (CHG-20260611-01), REQ-034 (CHG-20260611-01), REQ-063 (CHG-20260611-01) | dcy-025 | [bat-035](./bat-035_20260611_strategy-read-surface-slots-req/) |
|
| BAT-035 | standard | confirmed | REQ-073, REQ-074, REQ-075, REQ-021 (CHG-20260611-01), REQ-034 (CHG-20260611-01), REQ-063 (CHG-20260611-01) | dcy-025 | [bat-035](./bat-035_20260611_strategy-read-surface-slots-req/) |
|
||||||
| BAT-036 | standard | confirmed | REQ-048 (CHG-20260611-01), REQ-005 (CHG-20260611-01) | dcy-024 | [bat-036](./bat-036_20260611_safety-sell-disable-mode/) |
|
| BAT-036 | standard | confirmed | REQ-048 (CHG-20260611-01), REQ-005 (CHG-20260611-01) | dcy-024 | [bat-036](./bat-036_20260611_safety-sell-disable-mode/) |
|
||||||
| BAT-037 | standard | draft | REQ-073 (CHG-20260612-01) | dcy-025 (REQ-073 change-req) | [bat-037](./bat-037_20260612_slots-rpc-available-slots/) |
|
| BAT-037 | standard | in-delivery | REQ-073 (CHG-20260612-01) | dcy-025 (REQ-073 change-req) | [bat-037](./bat-037_20260612_slots-rpc-available-slots/) |
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ def _wire(items=None, *, max_active_slots=None):
|
|||||||
snapshot_store=store,
|
snapshot_store=store,
|
||||||
pause_store=pause,
|
pause_store=pause,
|
||||||
watchdog_store=watchdog,
|
watchdog_store=watchdog,
|
||||||
|
max_active_slots=max_active_slots,
|
||||||
)
|
)
|
||||||
return repo, svc, store, pause, watchdog, handler
|
return repo, svc, store, pause, watchdog, handler
|
||||||
|
|
||||||
@@ -45,13 +46,12 @@ def _req(filter=None, spy=False):
|
|||||||
return RpcRequest(payload=payload, correlation_id="test-corr")
|
return RpcRequest(payload=payload, correlation_id="test-corr")
|
||||||
|
|
||||||
|
|
||||||
# REQ-073 AC-3: response keys only {slots/degraded/reason/message}
|
# REQ-073 AC-3: response keys include available_slots; assets/watchdog/strategy absent
|
||||||
def test_response_keys_only_slots_health():
|
def test_response_keys_only_slots_health():
|
||||||
_, _, _, _, _, handler = _wire()
|
_, _, _, _, _, handler = _wire()
|
||||||
resp = handler(_req())
|
resp = handler(_req())
|
||||||
assert set(resp.payload.keys()) == {"slots", "degraded", "reason", "message"}
|
assert set(resp.payload.keys()) == {"slots", "available_slots", "degraded", "reason", "message"}
|
||||||
assert "assets" not in resp.payload
|
assert "assets" not in resp.payload
|
||||||
assert "available_slots" not in resp.payload
|
|
||||||
assert "watchdog" not in resp.payload
|
assert "watchdog" not in resp.payload
|
||||||
|
|
||||||
|
|
||||||
@@ -210,3 +210,34 @@ def test_spy_false_updates_watchdog():
|
|||||||
sid = str(holding.slot_id)
|
sid = str(holding.slot_id)
|
||||||
handler(_req(filter="holding", spy=False))
|
handler(_req(filter="holding", spy=False))
|
||||||
assert watchdog.last_seen(sid) is not None # updated
|
assert watchdog.last_seen(sid) is not None # updated
|
||||||
|
|
||||||
|
|
||||||
|
# REQ-073 AC-6: available_slots capacity hint
|
||||||
|
def test_available_slots_null_when_uncapped():
|
||||||
|
_, _, _, _, _, handler = _wire(max_active_slots=None)
|
||||||
|
resp = handler(_req())
|
||||||
|
assert resp.payload["available_slots"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_available_slots_remaining_capacity():
|
||||||
|
repo, svc, _, _, _, handler = _wire(max_active_slots=2)
|
||||||
|
# 0 active → available = 2
|
||||||
|
assert handler(_req()).payload["available_slots"] == 2
|
||||||
|
# buy pf-A into holding → 1 active, available = 1
|
||||||
|
slot_a = repo.find_slot_by_portfolio("default", "pf-A")
|
||||||
|
pending = svc.transit_slot(slot_a.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
|
||||||
|
reason="buy", actor="test", symbol="005930")
|
||||||
|
svc.transit_slot(pending.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING,
|
||||||
|
reason="filled", actor="test", avg_price=100.0, qty=5.0)
|
||||||
|
assert handler(_req()).payload["available_slots"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_available_slots_zero_when_full():
|
||||||
|
repo, svc, _, _, _, handler = _wire(max_active_slots=1)
|
||||||
|
slot_a = repo.find_slot_by_portfolio("default", "pf-A")
|
||||||
|
pending = svc.transit_slot(slot_a.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
|
||||||
|
reason="buy", actor="test", symbol="005930")
|
||||||
|
svc.transit_slot(pending.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING,
|
||||||
|
reason="filled", actor="test", avg_price=100.0, qty=5.0)
|
||||||
|
# M=1, active=1 → available=0
|
||||||
|
assert handler(_req()).payload["available_slots"] == 0
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ def make_slots_rpc_handler(
|
|||||||
watchdog_store: Optional[WatchdogStore] = None,
|
watchdog_store: Optional[WatchdogStore] = None,
|
||||||
watchdog_timeout_sec: int = 120,
|
watchdog_timeout_sec: int = 120,
|
||||||
ledger_outbox=None,
|
ledger_outbox=None,
|
||||||
|
max_active_slots: Optional[int] = None,
|
||||||
):
|
):
|
||||||
"""Build the RPC handler for `trex.slots.req` (REQ-073/074).
|
"""Build the RPC handler for `trex.slots.req` (REQ-073/074).
|
||||||
|
|
||||||
@@ -184,9 +185,10 @@ def make_slots_rpc_handler(
|
|||||||
filter: "all"|"inactive"|"pending"|"holding"|"error" (default "all")
|
filter: "all"|"inactive"|"pending"|"holding"|"error" (default "all")
|
||||||
spy=true: watchdog not updated (operator view, REQ-034 CHG-01)
|
spy=true: watchdog not updated (operator view, REQ-034 CHG-01)
|
||||||
|
|
||||||
Response: {slots: [...], degraded: bool, reason: str|null, message: str|null}
|
Response: {slots: [...], available_slots: int|null, degraded: bool,
|
||||||
Each slot row carries slot/core state + hub asset data + watchdog + overlay
|
reason: str|null, message: str|null}
|
||||||
flags (strategy fields excluded — REQ-074 AC-5).
|
available_slots: capacity hint — null when uncapped, max(0, M-active) when capped
|
||||||
|
(REQ-073 AC-6). Does not replace the admission gate.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def handler(req: RpcRequest) -> RpcResponse:
|
def handler(req: RpcRequest) -> RpcResponse:
|
||||||
@@ -248,8 +250,14 @@ def make_slots_rpc_handler(
|
|||||||
reason = reason_code.value if reason_code is not None else None
|
reason = reason_code.value if reason_code is not None else None
|
||||||
message = msg
|
message = msg
|
||||||
|
|
||||||
|
if max_active_slots is not None and max_active_slots > 0:
|
||||||
|
available_slots = max(0, max_active_slots - slot_repo.count_active(instance_key))
|
||||||
|
else:
|
||||||
|
available_slots = None
|
||||||
|
|
||||||
return RpcResponse(payload={
|
return RpcResponse(payload={
|
||||||
"slots": slot_rows,
|
"slots": slot_rows,
|
||||||
|
"available_slots": available_slots,
|
||||||
"degraded": degraded,
|
"degraded": degraded,
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
"message": message,
|
"message": message,
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ class Daemon:
|
|||||||
watchdog_store=self.watchdog,
|
watchdog_store=self.watchdog,
|
||||||
watchdog_timeout_sec=self.config.safety_sell.watchdog_timeout_sec,
|
watchdog_timeout_sec=self.config.safety_sell.watchdog_timeout_sec,
|
||||||
ledger_outbox=self.outbox,
|
ledger_outbox=self.outbox,
|
||||||
|
max_active_slots=self.config.max_active_slots,
|
||||||
)
|
)
|
||||||
transport.consume_rpc_forever(
|
transport.consume_rpc_forever(
|
||||||
topology.slots_queue(self.config.instance_key), handler, self.stop)
|
topology.slots_queue(self.config.instance_key), handler, self.stop)
|
||||||
|
|||||||
Reference in New Issue
Block a user