fix: fence ledger outbox claims by instance
This commit is contained in:
@@ -118,7 +118,7 @@ def test_migration_replay_converts_existing_auto_venue_row_to_sor(pg_clean):
|
||||
|
||||
|
||||
def test_migration_024_separates_legacy_sor_and_is_replay_safe(pg_clean):
|
||||
outbox = PostgreSQLLedgerOutbox(pg_clean)
|
||||
outbox = PostgreSQLLedgerOutbox(pg_clean, instance_key="test-instance")
|
||||
normal = LedgerTrade(
|
||||
asset_id="PF-NORMAL",
|
||||
order_id="O-NORMAL",
|
||||
@@ -177,7 +177,7 @@ def test_migration_024_separates_legacy_sor_and_is_replay_safe(pg_clean):
|
||||
)
|
||||
|
||||
|
||||
def test_migration_028_fail_closes_unowned_legacy_outbox_rows(pg_clean):
|
||||
def test_migration_029_fail_closes_unowned_legacy_outbox_rows(pg_clean):
|
||||
outbox = PostgreSQLLedgerOutbox(pg_clean, instance_key="inst-a")
|
||||
trade = LedgerTrade(
|
||||
asset_id="PF-LEGACY",
|
||||
|
||||
@@ -4,12 +4,16 @@ Skipped without TREX_POSTGRESQL_TEST_DSN (same gate as other PG suites).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from trex_core.adapters.ledger.in_memory import InMemoryLedger
|
||||
from trex_core.adapters.persistence.postgresql import PostgreSQLLedgerOutbox
|
||||
from trex_core.application.ledger_outbox import (
|
||||
LedgerWriter,
|
||||
OutboxClaimLostError,
|
||||
OutboxOutcome,
|
||||
WorkItemStatus,
|
||||
)
|
||||
@@ -196,18 +200,27 @@ def test_stale_claim_cannot_overwrite_new_owner_outcome(pg_clean):
|
||||
expected_claim_expires_at=claim_b.claim_expires_at,
|
||||
)
|
||||
|
||||
stored = outbox_a.mark_outcome(
|
||||
row.correlation_id,
|
||||
OutboxOutcome.RETRYABLE_FAILURE,
|
||||
error="stale owner",
|
||||
next_retry_at=now["t"],
|
||||
expected_claim_expires_at=claim_a.claim_expires_at,
|
||||
)
|
||||
with pytest.raises(OutboxClaimLostError):
|
||||
outbox_a.mark_outcome(
|
||||
row.correlation_id,
|
||||
OutboxOutcome.RETRYABLE_FAILURE,
|
||||
error="stale owner",
|
||||
next_retry_at=now["t"],
|
||||
expected_claim_expires_at=claim_a.claim_expires_at,
|
||||
)
|
||||
|
||||
stored = outbox_a.get(row.correlation_id)
|
||||
assert stored is not None
|
||||
assert stored.status is WorkItemStatus.COMPLETED
|
||||
assert stored.last_outcome is OutboxOutcome.COMPLETED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("instance_key", ["", " "])
|
||||
def test_postgresql_outbox_rejects_empty_instance_before_connect(pg_clean, instance_key):
|
||||
with pytest.raises(ValueError, match="non-empty"):
|
||||
PostgreSQLLedgerOutbox(pg_clean, instance_key=instance_key)
|
||||
|
||||
|
||||
def test_original_owner_can_finalize_expired_unreclaimed_claim(pg_clean):
|
||||
now = {"t": datetime(2026, 6, 4, tzinfo=UTC)}
|
||||
outbox = PostgreSQLLedgerOutbox(
|
||||
@@ -255,6 +268,22 @@ def test_writer_records_retryable_403_without_cross_instance_delivery(pg_clean):
|
||||
assert outbox_a.get(row_a.correlation_id).status is WorkItemStatus.PENDING
|
||||
|
||||
|
||||
def test_concurrent_workers_claim_each_row_at_most_once(pg_clean):
|
||||
outbox_a = PostgreSQLLedgerOutbox(
|
||||
pg_clean, instance_key="inst-a", worker_id="worker-a"
|
||||
)
|
||||
outbox_b = PostgreSQLLedgerOutbox(
|
||||
pg_clean, instance_key="inst-a", worker_id="worker-b"
|
||||
)
|
||||
row = outbox_a.enqueue(_trade(execution_id="pg-concurrent"))
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
claims = list(pool.map(lambda box: box.claim_pending(), (outbox_a, outbox_b)))
|
||||
|
||||
claimed_ids = [item.correlation_id for batch in claims for item in batch]
|
||||
assert claimed_ids == [row.correlation_id]
|
||||
|
||||
|
||||
def test_writer_marks_duplicate_completed_from_adapter_response_on_pg(pg_clean):
|
||||
class _DuplicateLedger:
|
||||
def record_trade(self, _trade):
|
||||
|
||||
Reference in New Issue
Block a user