fix: converge broker failures without retry
This commit is contained in:
@@ -1,18 +1,12 @@
|
||||
"""rejected SellIntent `retryable` flag (issue #27 / REQ-007 CHG-20260615-01).
|
||||
|
||||
Additive: only on intent_type="sell" + status="rejected". Maps reason→retryable
|
||||
via the core-owned table (unrecognized → True, safety default; bad_request → False).
|
||||
admitted / processing / buy responses must NOT carry the field.
|
||||
"""
|
||||
"""Strategy RPC responses never expose broker retryability (GitHub #60)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore
|
||||
from trex_core.application.intents import IntentService
|
||||
from trex_core.application.messaging import RpcRequest
|
||||
from trex_core.application.messaging_handlers import (
|
||||
_sell_reject_retryable,
|
||||
make_intent_rpc_handler,
|
||||
)
|
||||
from trex_core.application.messaging_handlers import make_intent_rpc_handler
|
||||
from trex_core.domain.slot import SlotService, SlotState
|
||||
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
|
||||
|
||||
@@ -22,9 +16,14 @@ INST = "default"
|
||||
def _wire():
|
||||
repo = InMemorySlotRepository()
|
||||
svc = SlotService(repo, instance_key=INST)
|
||||
intents = IntentService(svc, repo, InMemoryIntentStore(),
|
||||
resolve_symbol=lambda pid: "005930",
|
||||
instance_key=INST, max_active_slots=5)
|
||||
intents = IntentService(
|
||||
svc,
|
||||
repo,
|
||||
InMemoryIntentStore(),
|
||||
resolve_symbol=lambda pid: "005930",
|
||||
instance_key=INST,
|
||||
max_active_slots=5,
|
||||
)
|
||||
repo.reconcile_universe(INST, {"PF-1": "SYM"})
|
||||
handler = make_intent_rpc_handler(intents)
|
||||
slot = repo.list_slots(INST)[0]
|
||||
@@ -32,72 +31,65 @@ def _wire():
|
||||
|
||||
|
||||
def _sell(handler, slot_id, intent_id="s1"):
|
||||
return handler(RpcRequest(payload={
|
||||
"intent_type": "sell", "intent_id": intent_id, "slot_id": str(slot_id),
|
||||
}, correlation_id=intent_id))
|
||||
return handler(
|
||||
RpcRequest(
|
||||
payload={
|
||||
"intent_type": "sell",
|
||||
"intent_id": intent_id,
|
||||
"slot_id": str(slot_id),
|
||||
},
|
||||
correlation_id=intent_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestRetryableMapping:
|
||||
def test_each_reason_maps(self):
|
||||
assert _sell_reject_retryable("slot_state_changed") is True
|
||||
assert _sell_reject_retryable("slot_not_holding:pending_buy") is True
|
||||
assert _sell_reject_retryable("duplicate_in_flight") is True
|
||||
assert _sell_reject_retryable("slot_not_found") is False
|
||||
assert _sell_reject_retryable("slot_not_holding:inactive") is False
|
||||
assert _sell_reject_retryable("slot_not_holding:pending_sell") is False
|
||||
assert _sell_reject_retryable("slot_not_holding:error") is False
|
||||
|
||||
def test_unrecognized_defaults_true(self):
|
||||
assert _sell_reject_retryable("some_future_reason") is True
|
||||
assert _sell_reject_retryable(None) is True
|
||||
|
||||
def test_bad_request_is_false(self):
|
||||
assert _sell_reject_retryable("bad_request:KeyError('slot_id')") is False
|
||||
def test_rejected_sell_does_not_expose_retryability():
|
||||
_, _, _, handler, slot = _wire()
|
||||
response = _sell(handler, slot.slot_id)
|
||||
assert response.payload["status"] == "rejected"
|
||||
assert response.payload["reason"] == "slot_not_holding:inactive"
|
||||
assert "retryable" not in response.payload
|
||||
|
||||
|
||||
class TestResponseField:
|
||||
def test_rejected_sell_inactive_slot_has_retryable_false(self):
|
||||
# slot is inactive (never bought) → slot_not_holding:inactive → terminal
|
||||
repo, svc, intents, handler, slot = _wire()
|
||||
resp = _sell(handler, slot.slot_id)
|
||||
assert resp.payload["status"] == "rejected"
|
||||
assert resp.payload["reason"] == "slot_not_holding:inactive"
|
||||
assert resp.payload["retryable"] is False
|
||||
def test_unknown_slot_sell_does_not_expose_retryability():
|
||||
_, _, _, handler, _ = _wire()
|
||||
response = _sell(handler, uuid4())
|
||||
assert response.payload["status"] == "rejected"
|
||||
assert response.payload["reason"] == "slot_not_found"
|
||||
assert "retryable" not in response.payload
|
||||
|
||||
def test_rejected_sell_unknown_slot_false(self):
|
||||
from uuid import uuid4
|
||||
_, _, _, handler, _ = _wire()
|
||||
resp = _sell(handler, uuid4())
|
||||
assert resp.payload["status"] == "rejected"
|
||||
assert resp.payload["reason"] == "slot_not_found"
|
||||
assert resp.payload["retryable"] is False
|
||||
|
||||
def test_bad_request_sell_carries_retryable_false(self):
|
||||
_, _, _, handler, _ = _wire()
|
||||
resp = handler(RpcRequest(payload={
|
||||
"intent_type": "sell", "intent_id": "x", # missing slot_id
|
||||
}, correlation_id="x"))
|
||||
assert resp.payload["status"] == "rejected"
|
||||
assert resp.payload["reason"].startswith("bad_request")
|
||||
assert resp.payload["retryable"] is False
|
||||
def test_malformed_sell_does_not_expose_retryability():
|
||||
_, _, _, handler, _ = _wire()
|
||||
response = handler(
|
||||
RpcRequest(
|
||||
payload={"intent_type": "sell", "intent_id": "x"},
|
||||
correlation_id="x",
|
||||
)
|
||||
)
|
||||
assert response.payload["status"] == "rejected"
|
||||
assert response.payload["reason"].startswith("bad_request")
|
||||
assert "retryable" not in response.payload
|
||||
|
||||
def test_admitted_sell_has_no_retryable(self):
|
||||
repo, svc, intents, handler, slot = _wire()
|
||||
# bring slot to holding so the sell is admitted
|
||||
svc.transit_slot(slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
|
||||
reason="buy", actor="t")
|
||||
svc.transit_slot(slot.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING,
|
||||
reason="filled", actor="t", qty=10, avg_price=100)
|
||||
resp = _sell(handler, slot.slot_id)
|
||||
assert resp.payload["status"] == "admitted"
|
||||
assert "retryable" not in resp.payload
|
||||
|
||||
def test_buy_rejection_has_no_retryable(self):
|
||||
# buy on a non-existent slot → rejected, but buy responses omit retryable
|
||||
from uuid import uuid4
|
||||
_, _, _, handler, _ = _wire()
|
||||
resp = handler(RpcRequest(payload={
|
||||
"intent_type": "buy", "intent_id": "b1", "slot_id": str(uuid4()),
|
||||
}, correlation_id="b1"))
|
||||
assert resp.payload["status"] == "rejected"
|
||||
assert "retryable" not in resp.payload
|
||||
def test_admitted_sell_does_not_expose_retryability():
|
||||
_, svc, _, handler, slot = _wire()
|
||||
svc.transit_slot(
|
||||
slot.slot_id,
|
||||
SlotState.INACTIVE,
|
||||
SlotState.PENDING_BUY,
|
||||
reason="buy",
|
||||
actor="test",
|
||||
)
|
||||
svc.transit_slot(
|
||||
slot.slot_id,
|
||||
SlotState.PENDING_BUY,
|
||||
SlotState.HOLDING,
|
||||
reason="filled",
|
||||
actor="test",
|
||||
qty=10,
|
||||
avg_price=100,
|
||||
)
|
||||
response = _sell(handler, slot.slot_id)
|
||||
assert response.payload["status"] == "admitted"
|
||||
assert "retryable" not in response.payload
|
||||
|
||||
Reference in New Issue
Block a user