fix: normalize snapshot read failures to unavailable in slots RPC path
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
f394f1560d
commit
d4a968ee1a
@@ -36,6 +36,11 @@ class FakeAccountBroker:
|
|||||||
return self.cash
|
return self.cash
|
||||||
|
|
||||||
|
|
||||||
|
class BrokenLoadStore(InMemoryAccountSnapshotStore):
|
||||||
|
def load(self, account_hash: str) -> AccountSnapshot | None:
|
||||||
|
raise RuntimeError("db read failed")
|
||||||
|
|
||||||
|
|
||||||
def _wire():
|
def _wire():
|
||||||
now = [datetime(2026, 7, 29, tzinfo=UTC)]
|
now = [datetime(2026, 7, 29, tzinfo=UTC)]
|
||||||
broker = FakeAccountBroker()
|
broker = FakeAccountBroker()
|
||||||
@@ -177,3 +182,19 @@ def test_slots_rpc_uses_snapshot_only_with_sell_ttl_freshness():
|
|||||||
with pytest.raises(AccountSnapshotUnavailable) as raised:
|
with pytest.raises(AccountSnapshotUnavailable) as raised:
|
||||||
service.holdings_for_slots_rpc()
|
service.holdings_for_slots_rpc()
|
||||||
assert raised.value.code == "expired"
|
assert raised.value.code == "expired"
|
||||||
|
|
||||||
|
|
||||||
|
def test_slots_rpc_normalizes_snapshot_store_read_failure():
|
||||||
|
broker = FakeAccountBroker()
|
||||||
|
service = AccountSnapshotService(
|
||||||
|
broker=broker,
|
||||||
|
store=BrokenLoadStore(),
|
||||||
|
buy_max_age_sec=600,
|
||||||
|
sell_max_age_sec=3600,
|
||||||
|
clock=lambda: datetime(2026, 7, 29, tzinfo=UTC),
|
||||||
|
rng=lambda: 0.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AccountSnapshotUnavailable) as raised:
|
||||||
|
service.holdings_for_slots_rpc()
|
||||||
|
assert raised.value.code == "read_failed"
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
from trex_core.application.account_snapshot import AccountSnapshotUnavailable
|
from trex_core.application.account_snapshot import (
|
||||||
|
AccountSnapshot,
|
||||||
|
AccountSnapshotService,
|
||||||
|
AccountSnapshotUnavailable,
|
||||||
|
InMemoryAccountSnapshotStore,
|
||||||
|
)
|
||||||
from trex_core.application.broker_port import BalanceItem
|
from trex_core.application.broker_port import BalanceItem
|
||||||
from trex_core.application.messaging import RpcRequest
|
from trex_core.application.messaging import RpcRequest
|
||||||
from trex_core.application.messaging_handlers import make_slots_rpc_handler
|
from trex_core.application.messaging_handlers import make_slots_rpc_handler
|
||||||
@@ -32,9 +38,29 @@ class _Reader:
|
|||||||
return list(self._holdings), self._captured_at
|
return list(self._holdings), self._captured_at
|
||||||
|
|
||||||
|
|
||||||
|
class _SlotsReader(Protocol):
|
||||||
|
def holdings_for_slots_rpc(self) -> tuple[list[BalanceItem], datetime]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _BrokenLoadStore(InMemoryAccountSnapshotStore):
|
||||||
|
def load(self, account_hash: str) -> AccountSnapshot | None:
|
||||||
|
raise RuntimeError("db read failed")
|
||||||
|
|
||||||
|
|
||||||
|
class _Broker:
|
||||||
|
def account_scope_hash(self) -> str:
|
||||||
|
return "a" * 64
|
||||||
|
|
||||||
|
def get_balance(self) -> list[BalanceItem]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_available_cash(self, *, price_check_code: str = "005930") -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _holding_handler(
|
def _holding_handler(
|
||||||
*,
|
*,
|
||||||
broker_reader: _Reader,
|
broker_reader: _SlotsReader,
|
||||||
instance_key: str = "default",
|
instance_key: str = "default",
|
||||||
portfolio_id: str = "pf-A",
|
portfolio_id: str = "pf-A",
|
||||||
symbol: str = "104620",
|
symbol: str = "104620",
|
||||||
@@ -131,6 +157,24 @@ def test_holding_qty_stale_snapshot_is_unavailable():
|
|||||||
assert row["broker_quantity_available"] is False
|
assert row["broker_quantity_available"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_holding_qty_store_read_failure_is_unavailable():
|
||||||
|
_, handler = _holding_handler(
|
||||||
|
broker_reader=AccountSnapshotService(
|
||||||
|
broker=_Broker(),
|
||||||
|
store=_BrokenLoadStore(),
|
||||||
|
buy_max_age_sec=600,
|
||||||
|
sell_max_age_sec=3600,
|
||||||
|
clock=lambda: datetime(2026, 8, 24, 12, 0, tzinfo=UTC),
|
||||||
|
rng=lambda: 0.5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
row = handler(_req_holding()).payload["slots"][0]
|
||||||
|
assert row["qty"] is None
|
||||||
|
assert row["qty_source"] == "unavailable"
|
||||||
|
assert row["broker_quantity_available"] is False
|
||||||
|
assert row["broker_quantity_captured_at"] is None
|
||||||
|
|
||||||
|
|
||||||
def test_broker_quantity_reader_is_instance_bound_per_handler():
|
def test_broker_quantity_reader_is_instance_bound_per_handler():
|
||||||
repo = InMemorySlotRepository()
|
repo = InMemorySlotRepository()
|
||||||
svc = SlotService(repo)
|
svc = SlotService(repo)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class AccountSnapshotUnavailable(RuntimeError):
|
|||||||
"account_mismatch",
|
"account_mismatch",
|
||||||
"expired",
|
"expired",
|
||||||
"live_read_failed",
|
"live_read_failed",
|
||||||
|
"read_failed",
|
||||||
})
|
})
|
||||||
|
|
||||||
def __init__(self, code: str) -> None:
|
def __init__(self, code: str) -> None:
|
||||||
@@ -189,7 +190,15 @@ class AccountSnapshotService:
|
|||||||
account_hash = self._broker.account_scope_hash()
|
account_hash = self._broker.account_scope_hash()
|
||||||
if not account_hash:
|
if not account_hash:
|
||||||
raise AccountSnapshotUnavailable("account_mismatch")
|
raise AccountSnapshotUnavailable("account_mismatch")
|
||||||
|
try:
|
||||||
snapshot = self._store.load(account_hash)
|
snapshot = self._store.load(account_hash)
|
||||||
|
except Exception as exc:
|
||||||
|
_log.warning(
|
||||||
|
"account snapshot load failed: account_hash=%s",
|
||||||
|
account_hash,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
raise AccountSnapshotUnavailable("read_failed") from exc
|
||||||
if snapshot is None or snapshot.account_hash != account_hash:
|
if snapshot is None or snapshot.account_hash != account_hash:
|
||||||
raise AccountSnapshotUnavailable("account_mismatch")
|
raise AccountSnapshotUnavailable("account_mismatch")
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|||||||
Reference in New Issue
Block a user