feat: add messaging transport rollout
This commit is contained in:
@@ -1,112 +0,0 @@
|
||||
"""HTTP /v1 API tests via in-memory adapters (REQ-005/007/008).
|
||||
|
||||
No PostgreSQL needed: the app is wired with the in-memory slot repo + intent
|
||||
store, exercising the full request → admission → transition → response path.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from trex_core.adapters.api.app import create_app
|
||||
from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore
|
||||
from trex_core.application.intents import IntentService
|
||||
from trex_core.domain.slot import SlotService, SlotState
|
||||
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
|
||||
|
||||
DAY = date(2026, 6, 3)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ctx():
|
||||
repo = InMemorySlotRepository()
|
||||
svc = SlotService(repo)
|
||||
intents = IntentService(svc, repo, InMemoryIntentStore())
|
||||
svc.preallocate_daily_slots(DAY, 3)
|
||||
app = create_app(
|
||||
slot_service=svc, slot_repo=repo, intent_service=intents, default_total_slots=3
|
||||
)
|
||||
return repo, svc, intents, TestClient(app)
|
||||
|
||||
|
||||
def _buy(client, intent_id, symbol="005930"):
|
||||
return client.post(
|
||||
"/v1/intents/buy",
|
||||
json={"intent_id": intent_id, "trading_date": DAY.isoformat(), "symbol": symbol, "qty": 10},
|
||||
)
|
||||
|
||||
|
||||
def test_healthz(ctx):
|
||||
_, _, _, client = ctx
|
||||
r = client.get("/v1/healthz")
|
||||
assert r.status_code == 200 and r.json()["status"] == "ok"
|
||||
|
||||
|
||||
def test_buy_intent_admits_and_transitions(ctx):
|
||||
repo, _, _, client = ctx
|
||||
r = _buy(client, "buy-1")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["status"] == "admitted" and body["slot_id"]
|
||||
slot = repo.fetch_slot(__import__("uuid").UUID(body["slot_id"]))
|
||||
assert slot.status is SlotState.PENDING_BUY and slot.symbol == "005930"
|
||||
|
||||
|
||||
def test_buy_intent_idempotent_resubmit(ctx):
|
||||
repo, _, _, client = ctx
|
||||
r1 = _buy(client, "buy-dup")
|
||||
r2 = _buy(client, "buy-dup") # same intent_id
|
||||
assert r1.json() == r2.json() # identical decision
|
||||
# only ONE slot moved out of idle
|
||||
pending = [s for s in repo.list_slots(DAY) if s.status is SlotState.PENDING_BUY]
|
||||
assert len(pending) == 1
|
||||
|
||||
|
||||
def test_buy_intent_rejected_when_no_idle(ctx):
|
||||
_, _, _, client = ctx
|
||||
for i in range(3):
|
||||
assert _buy(client, f"buy-fill-{i}").json()["status"] == "admitted"
|
||||
r = _buy(client, "buy-overflow")
|
||||
assert r.json()["status"] == "rejected" and r.json()["reason"] == "no_idle_slot"
|
||||
|
||||
|
||||
def test_sell_intent_requires_holding(ctx):
|
||||
repo, _, _, client = ctx
|
||||
# a freshly bought slot is pending_buy, not holding -> sell rejected
|
||||
slot_id = _buy(client, "buy-for-sell").json()["slot_id"]
|
||||
r = client.post("/v1/intents/sell", json={"intent_id": "sell-1", "slot_id": slot_id})
|
||||
assert r.json()["status"] == "rejected" and r.json()["reason"].startswith("slot_not_holding")
|
||||
|
||||
|
||||
def test_sell_intent_admits_from_holding(ctx):
|
||||
repo, svc, _, client = ctx
|
||||
import uuid
|
||||
|
||||
slot_id = uuid.UUID(_buy(client, "buy-h").json()["slot_id"])
|
||||
svc.transit_slot(slot_id, SlotState.PENDING_BUY, SlotState.HOLDING, reason="filled", actor="t")
|
||||
r = client.post("/v1/intents/sell", json={"intent_id": "sell-h", "slot_id": str(slot_id)})
|
||||
assert r.json()["status"] == "admitted"
|
||||
assert repo.fetch_slot(slot_id).status is SlotState.PENDING_SELL
|
||||
|
||||
|
||||
def test_get_intent_roundtrip(ctx):
|
||||
_, _, _, client = ctx
|
||||
_buy(client, "buy-q")
|
||||
r = client.get("/v1/intents/buy-q")
|
||||
assert r.status_code == 200 and r.json()["status"] == "admitted"
|
||||
assert client.get("/v1/intents/does-not-exist").status_code == 404
|
||||
|
||||
|
||||
def test_list_slots_and_capacity(ctx):
|
||||
_, _, _, client = ctx
|
||||
_buy(client, "buy-c")
|
||||
slots = client.get(
|
||||
"/v1/slots", params={"status": "pending_buy", "trading_date": DAY.isoformat()}
|
||||
).json()
|
||||
assert len(slots) == 1 and slots[0]["status"] == "pending_buy"
|
||||
cap = client.get(
|
||||
"/v1/capacity", params={"total_slots": 3, "trading_date": DAY.isoformat()}
|
||||
).json()
|
||||
assert cap["idle_count"] == 2 and cap["pending_count"] == 1 and cap["total_slots"] == 3
|
||||
@@ -0,0 +1,88 @@
|
||||
"""InMemoryTransport stub tests (REQ-022 AC-1 + REQ-024 degrade)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from trex_core.adapters.messaging.in_memory import InMemoryTransport
|
||||
from trex_core.adapters.messaging import topology
|
||||
from trex_core.application.messaging import (
|
||||
RpcRequest,
|
||||
RpcResponse,
|
||||
RpcTimeoutError,
|
||||
TransportHealth,
|
||||
TransportUnavailableError,
|
||||
)
|
||||
|
||||
|
||||
def test_publish_and_subscribe_routes_by_pattern():
|
||||
t = InMemoryTransport()
|
||||
received: list[tuple[str, dict]] = []
|
||||
stop = threading.Event()
|
||||
th = threading.Thread(
|
||||
target=t.consume_events_forever,
|
||||
args=("trex.events.strategy.A", topology.strategy_event_binding_pattern("A"),
|
||||
lambda rk, p: received.append((rk, p)), stop),
|
||||
daemon=True,
|
||||
)
|
||||
th.start()
|
||||
time.sleep(0.05)
|
||||
# Routed to A's subscriber
|
||||
t.publish_event(topology.event_routing_key("slot_filled_buy", "A", "005930"),
|
||||
{"slot": "1"})
|
||||
# NOT routed to A's subscriber (strategy B)
|
||||
t.publish_event(topology.event_routing_key("slot_filled_buy", "B", "005930"),
|
||||
{"slot": "2"})
|
||||
time.sleep(0.05)
|
||||
stop.set()
|
||||
th.join(timeout=1.0)
|
||||
assert len(received) == 1
|
||||
assert received[0][0] == "slot_filled_buy.A.005930"
|
||||
assert received[0][1] == {"slot": "1"}
|
||||
|
||||
|
||||
def test_rpc_call_dispatches_to_registered_handler():
|
||||
t = InMemoryTransport()
|
||||
|
||||
def handler(req: RpcRequest) -> RpcResponse:
|
||||
return RpcResponse(payload={"echo": req.payload, "cid": req.correlation_id})
|
||||
|
||||
stop = threading.Event()
|
||||
th = threading.Thread(
|
||||
target=t.consume_rpc_forever,
|
||||
args=("trex.intent.req", handler, stop),
|
||||
daemon=True,
|
||||
)
|
||||
th.start()
|
||||
time.sleep(0.05)
|
||||
resp = t.call_rpc("trex.intent.req", {"x": 1}, correlation_id="cid-1", timeout=0.5)
|
||||
assert resp["echo"] == {"x": 1}
|
||||
assert resp["cid"] == "cid-1"
|
||||
stop.set()
|
||||
th.join(timeout=1.0)
|
||||
|
||||
|
||||
def test_unavailable_transport_raises_on_publish_and_rpc():
|
||||
t = InMemoryTransport()
|
||||
t.set_available(False)
|
||||
assert t.health() is TransportHealth.DISCONNECTED
|
||||
with pytest.raises(TransportUnavailableError):
|
||||
t.publish_event("e.A.S", {"x": 1})
|
||||
with pytest.raises(TransportUnavailableError):
|
||||
t.call_rpc("trex.intent.req", {"x": 1}, correlation_id="cid", timeout=0.1)
|
||||
|
||||
|
||||
def test_rpc_without_registered_consumer_times_out():
|
||||
t = InMemoryTransport()
|
||||
with pytest.raises(RpcTimeoutError):
|
||||
t.call_rpc("trex.intent.req", {}, correlation_id="cid", timeout=0.05)
|
||||
|
||||
|
||||
def test_publish_event_with_no_subscriber_routes_to_dlq():
|
||||
t = InMemoryTransport()
|
||||
t.publish_event("slot_filled_buy.A.005930", {"slot": "1"})
|
||||
dlq = t.dlq_messages()
|
||||
assert len(dlq) == 1
|
||||
assert dlq[0][0] == "slot_filled_buy.A.005930"
|
||||
@@ -0,0 +1,127 @@
|
||||
"""End-to-end intent RPC against RabbitMQ + intent_log dedup (REQ-023 AC-1/AC-3).
|
||||
|
||||
Skipped without TREX_RABBITMQ_TEST_URL. Wires the real RabbitMQTransport to
|
||||
the application-layer intent handler and proves that:
|
||||
- publishing the same correlation_id twice triggers slot transition only ONCE
|
||||
- the durable RPC queue is declared with the DLQ pointer (AC-3 topology check)
|
||||
- intent_log persists the dedup decision
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from trex_core.adapters.messaging import topology
|
||||
from trex_core.adapters.messaging.rabbitmq import RabbitMQTransport
|
||||
from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore
|
||||
from trex_core.application.intents import IntentService
|
||||
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
|
||||
|
||||
DAY = date(2026, 6, 5)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mq_url() -> str:
|
||||
url = os.environ.get("TREX_RABBITMQ_TEST_URL")
|
||||
if not url:
|
||||
pytest.skip("TREX_RABBITMQ_TEST_URL not set")
|
||||
return url
|
||||
|
||||
|
||||
def _drain(url: str, queue: str) -> None:
|
||||
import pika
|
||||
conn = pika.BlockingConnection(pika.URLParameters(url))
|
||||
try:
|
||||
ch = conn.channel()
|
||||
try:
|
||||
ch.queue_delete(queue=queue)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_intent_admission_rpc_dedups_duplicate_correlation_id_over_rabbitmq(mq_url):
|
||||
"""REQ-023 AC-1: same correlation_id processed twice ⇒ slot transitions ONCE."""
|
||||
repo = InMemorySlotRepository()
|
||||
svc = SlotService(repo)
|
||||
intents = IntentService(svc, repo, InMemoryIntentStore())
|
||||
svc.preallocate_daily_slots(DAY, 3)
|
||||
handler = make_intent_rpc_handler(intents)
|
||||
|
||||
queue = f"trex.test.intent.{uuid.uuid4().hex[:8]}"
|
||||
server = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
client = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
try:
|
||||
# Pre-declare with DLQ pointer (mirrors production declare_topology shape)
|
||||
server._ensure_connected()
|
||||
server._channel.queue_declare(
|
||||
queue, durable=True,
|
||||
arguments={"x-dead-letter-exchange": topology.EVENTS_DLQ_EXCHANGE},
|
||||
)
|
||||
|
||||
stop = threading.Event()
|
||||
th = threading.Thread(
|
||||
target=server.consume_rpc_forever, args=(queue, handler, stop), daemon=True,
|
||||
)
|
||||
th.start()
|
||||
time.sleep(0.2)
|
||||
|
||||
cid = "buy-dup-rmq"
|
||||
payload = {
|
||||
"intent_type": "buy", "intent_id": cid,
|
||||
"trading_date": DAY.isoformat(), "symbol": "005930", "qty": 10,
|
||||
}
|
||||
r1 = client.call_rpc(queue, payload, correlation_id=cid, timeout=3.0)
|
||||
r2 = client.call_rpc(queue, payload, correlation_id=cid, timeout=3.0)
|
||||
assert r1 == r2
|
||||
assert r1["status"] == "admitted"
|
||||
|
||||
pending = [s for s in repo.list_slots(DAY) if s.status is SlotState.PENDING_BUY]
|
||||
assert len(pending) == 1, "intent_log dedup must prevent double slot transition"
|
||||
|
||||
stop.set()
|
||||
th.join(timeout=2.0)
|
||||
finally:
|
||||
server.close()
|
||||
client.close()
|
||||
_drain(mq_url, queue)
|
||||
|
||||
|
||||
def test_intent_queue_topology_uses_dlq_pointer(mq_url):
|
||||
"""REQ-023 AC-3 / REQ-022 AC-2 topology assertion."""
|
||||
import pika
|
||||
t = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
try:
|
||||
t.declare_topology()
|
||||
# Re-declare with the SAME args is no-op; declaring with DIFFERENT args
|
||||
# would raise PRECONDITION_FAILED. Use that as a topology assertion.
|
||||
conn = pika.BlockingConnection(pika.URLParameters(mq_url))
|
||||
try:
|
||||
ch = conn.channel()
|
||||
# Same args → ok
|
||||
ch.queue_declare(
|
||||
topology.INTENT_REQ_QUEUE, durable=True,
|
||||
arguments={"x-dead-letter-exchange": topology.EVENTS_DLQ_EXCHANGE},
|
||||
)
|
||||
ch.close()
|
||||
# Different args → must raise
|
||||
ch2 = conn.channel()
|
||||
with pytest.raises(pika.exceptions.ChannelClosedByBroker):
|
||||
ch2.queue_declare(
|
||||
topology.INTENT_REQ_QUEUE, durable=False,
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
finally:
|
||||
t.close()
|
||||
@@ -0,0 +1,171 @@
|
||||
"""RabbitMQTransport integration tests (REQ-022 AC-2, REQ-023, REQ-024).
|
||||
|
||||
Skipped without TREX_RABBITMQ_TEST_URL. Each test uses a unique uuid in
|
||||
queue/routing keys to avoid colliding with other tests or runs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from trex_core.adapters.messaging import topology
|
||||
from trex_core.adapters.messaging.rabbitmq import RabbitMQTransport
|
||||
from trex_core.application.messaging import (
|
||||
RpcRequest,
|
||||
RpcResponse,
|
||||
RpcTimeoutError,
|
||||
TransportHealth,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mq_url() -> str:
|
||||
url = os.environ.get("TREX_RABBITMQ_TEST_URL")
|
||||
if not url:
|
||||
pytest.skip("TREX_RABBITMQ_TEST_URL not set; skipping RabbitMQ integration test")
|
||||
return url
|
||||
|
||||
|
||||
def _drain_queue(url: str, queue: str) -> None:
|
||||
import pika
|
||||
conn = pika.BlockingConnection(pika.URLParameters(url))
|
||||
try:
|
||||
ch = conn.channel()
|
||||
try:
|
||||
ch.queue_delete(queue=queue)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_declare_topology_creates_core_entities(mq_url):
|
||||
t = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
try:
|
||||
t.declare_topology()
|
||||
# If declare_topology succeeded, exchanges/queues now exist. Re-declare
|
||||
# is idempotent (RabbitMQ no-op on existing matching definitions).
|
||||
t.declare_topology()
|
||||
assert t.health() is TransportHealth.CONNECTED
|
||||
finally:
|
||||
t.close()
|
||||
|
||||
|
||||
def test_rpc_roundtrip(mq_url):
|
||||
queue = f"trex.test.rpc.{uuid.uuid4().hex[:8]}"
|
||||
server = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
client = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
try:
|
||||
# Pre-declare RPC queue (server-side will redeclare too).
|
||||
ch = server._channel
|
||||
if server._conn is None:
|
||||
server._connect()
|
||||
ch = server._channel
|
||||
ch.queue_declare(queue, durable=True,
|
||||
arguments={"x-dead-letter-exchange": topology.EVENTS_DLQ_EXCHANGE})
|
||||
|
||||
def handler(req: RpcRequest) -> RpcResponse:
|
||||
return RpcResponse(payload={"echo": req.payload, "cid": req.correlation_id})
|
||||
|
||||
stop = threading.Event()
|
||||
th = threading.Thread(
|
||||
target=server.consume_rpc_forever,
|
||||
args=(queue, handler, stop),
|
||||
daemon=True,
|
||||
)
|
||||
th.start()
|
||||
time.sleep(0.2)
|
||||
resp = client.call_rpc(queue, {"hello": "world"}, correlation_id="cid-1", timeout=3.0)
|
||||
assert resp["echo"] == {"hello": "world"}
|
||||
assert resp["cid"] == "cid-1"
|
||||
stop.set()
|
||||
th.join(timeout=2.0)
|
||||
finally:
|
||||
server.close()
|
||||
client.close()
|
||||
_drain_queue(mq_url, queue)
|
||||
|
||||
|
||||
def test_event_publish_and_per_strategy_consume(mq_url):
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
strategy_a = f"A-{suffix}"
|
||||
strategy_b = f"B-{suffix}"
|
||||
queue_a = topology.strategy_event_queue_name(strategy_a)
|
||||
queue_b = topology.strategy_event_queue_name(strategy_b)
|
||||
|
||||
publisher = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
consumer_a = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
consumer_b = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
received_a: list[tuple[str, dict]] = []
|
||||
received_b: list[tuple[str, dict]] = []
|
||||
stop = threading.Event()
|
||||
try:
|
||||
publisher.declare_topology()
|
||||
# Pre-declare per-strategy queues so the publisher's message is not
|
||||
# discarded before the consumers start.
|
||||
publisher.declare_strategy_queue(queue_a, topology.strategy_event_binding_pattern(strategy_a))
|
||||
publisher.declare_strategy_queue(queue_b, topology.strategy_event_binding_pattern(strategy_b))
|
||||
|
||||
th_a = threading.Thread(
|
||||
target=consumer_a.consume_events_forever,
|
||||
args=(queue_a, topology.strategy_event_binding_pattern(strategy_a),
|
||||
lambda rk, p: received_a.append((rk, p)), stop),
|
||||
daemon=True,
|
||||
)
|
||||
th_b = threading.Thread(
|
||||
target=consumer_b.consume_events_forever,
|
||||
args=(queue_b, topology.strategy_event_binding_pattern(strategy_b),
|
||||
lambda rk, p: received_b.append((rk, p)), stop),
|
||||
daemon=True,
|
||||
)
|
||||
th_a.start()
|
||||
th_b.start()
|
||||
time.sleep(0.3)
|
||||
|
||||
rk_a = topology.event_routing_key("slot_filled_buy", strategy_a, "005930")
|
||||
rk_b = topology.event_routing_key("slot_filled_buy", strategy_b, "005930")
|
||||
publisher.publish_event(rk_a, {"slot": "1", "for": "A"})
|
||||
publisher.publish_event(rk_b, {"slot": "2", "for": "B"})
|
||||
|
||||
deadline = time.monotonic() + 3.0
|
||||
while time.monotonic() < deadline and (not received_a or not received_b):
|
||||
time.sleep(0.05)
|
||||
|
||||
assert any(rk == rk_a for rk, _ in received_a)
|
||||
assert all(rk != rk_b for rk, _ in received_a), "A queue must not receive B events"
|
||||
assert any(rk == rk_b for rk, _ in received_b)
|
||||
assert all(rk != rk_a for rk, _ in received_b), "B queue must not receive A events"
|
||||
finally:
|
||||
stop.set()
|
||||
time.sleep(0.2)
|
||||
publisher.close()
|
||||
consumer_a.close()
|
||||
consumer_b.close()
|
||||
_drain_queue(mq_url, queue_a)
|
||||
_drain_queue(mq_url, queue_b)
|
||||
|
||||
|
||||
def test_call_rpc_timeout(mq_url):
|
||||
queue = f"trex.test.rpc.timeout.{uuid.uuid4().hex[:8]}"
|
||||
client = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
try:
|
||||
# Declare a queue with no consumer; RPC must time out.
|
||||
client._ensure_connected()
|
||||
client._channel.queue_declare(queue, durable=True)
|
||||
with pytest.raises(RpcTimeoutError):
|
||||
client.call_rpc(queue, {"x": 1}, correlation_id="cid-timeout", timeout=0.5)
|
||||
finally:
|
||||
client.close()
|
||||
_drain_queue(mq_url, queue)
|
||||
|
||||
|
||||
def test_close_marks_disconnected(mq_url):
|
||||
t = RabbitMQTransport(mq_url, timeout=5.0)
|
||||
t.declare_topology()
|
||||
assert t.health() is TransportHealth.CONNECTED
|
||||
t.close()
|
||||
assert t.health() is TransportHealth.DISCONNECTED
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Topology constants + routing key builders (REQ-021)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from trex_core.adapters.messaging import topology
|
||||
|
||||
|
||||
def test_topology_constants():
|
||||
assert topology.SNAPSHOT_REQ_QUEUE == "trex.snapshot.req"
|
||||
assert topology.INTENT_REQ_QUEUE == "trex.intent.req"
|
||||
assert topology.EVENTS_EXCHANGE == "trex.events"
|
||||
assert topology.EVENTS_DLQ_EXCHANGE == "trex.events.dlq"
|
||||
|
||||
|
||||
def test_event_routing_key_format():
|
||||
assert topology.event_routing_key("slot_filled_buy", "A", "005930") == \
|
||||
"slot_filled_buy.A.005930"
|
||||
# dots/wildcards in inputs are sanitized to underscores
|
||||
assert topology.event_routing_key("e.x", "y*z", "a#b") == "e_x.y_z.a_b"
|
||||
# None / empty strategy → "_"
|
||||
assert topology.event_routing_key("e", None, "S") == "e._.S"
|
||||
|
||||
|
||||
def test_strategy_queue_name_and_binding_pattern():
|
||||
assert topology.strategy_event_queue_name("A") == "trex.events.strategy.A"
|
||||
assert topology.strategy_event_binding_pattern("A") == "*.A.#"
|
||||
|
||||
|
||||
def test_strategy_queue_args_have_ttl_and_max_length():
|
||||
args = topology.STRATEGY_QUEUE_ARGS
|
||||
assert args["x-message-ttl"] > 0
|
||||
assert args["x-max-length"] > 0
|
||||
assert args["x-overflow"] == "drop-head"
|
||||
assert args["x-dead-letter-exchange"] == topology.EVENTS_DLQ_EXCHANGE
|
||||
Reference in New Issue
Block a user