fix: fence ledger outbox claims by instance

This commit is contained in:
Hermes Agent
2026-09-13 09:41:51 +09:00
parent f801c53e0c
commit 762a953c95
15 changed files with 316 additions and 88 deletions
@@ -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):