You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
56 lines
2.5 KiB
56 lines
2.5 KiB
"""Real PostgreSQL competing writers, with independent thread connections.""" |
|
|
|
from concurrent.futures import ThreadPoolExecutor |
|
from datetime import date |
|
from decimal import Decimal |
|
from threading import Barrier |
|
|
|
import pytest |
|
from django.db import close_old_connections, connection, connections |
|
|
|
from finance.models import AuditEvent, Transaction, Transfer |
|
from finance.services import Actor, DomainError, create_account, match_transfer, record_transaction |
|
|
|
pytestmark = pytest.mark.django_db(transaction=True) |
|
|
|
|
|
def test_competing_matches_cannot_reuse_one_outgoing_leg(): |
|
assert connection.vendor == "postgresql" |
|
actor = Actor(kind="AGENT", identifier="synthetic-concurrency-test") |
|
bank = create_account(name="Synthetic Outgoing", kind="BANK", actor=actor) |
|
other = create_account(name="Synthetic Incoming", kind="BANK", actor=actor) |
|
common = dict(kind="TRANSFER", posting_date=date(2026, 1, 15), actor=actor) |
|
outgoing = record_transaction(account_id=bank.pk, amount=Decimal("-25"), **common) |
|
incoming = [record_transaction(account_id=other.pk, amount=Decimal("25"), **common) for _ in range(2)] |
|
barrier = Barrier(2) |
|
audit_ids = set(AuditEvent.objects.values_list("pk", flat=True)) |
|
|
|
def attempt(incoming_id): |
|
close_old_connections() |
|
try: |
|
# Opening connections before the barrier ensures two real sessions. |
|
with connection.cursor() as cursor: |
|
cursor.execute("SELECT pg_backend_pid()") |
|
backend_pid = cursor.fetchone()[0] |
|
barrier.wait(timeout=15) |
|
try: |
|
result = match_transfer(outgoing_id=outgoing.pk, incoming_id=incoming_id, actor=actor) |
|
return backend_pid, "success", result.pk |
|
except DomainError as error: |
|
return backend_pid, "failure", error.code |
|
finally: |
|
connections.close_all() |
|
|
|
with ThreadPoolExecutor(max_workers=2) as pool: |
|
results = list(pool.map(attempt, [row.pk for row in incoming])) |
|
|
|
assert len({result[0] for result in results}) == 2 |
|
assert sorted(result[1] for result in results) == ["failure", "success"] |
|
assert all(result[2] for result in results) |
|
assert Transfer.objects.count() == 1 |
|
paired = Transfer.objects.get() |
|
assert paired.outgoing_id == outgoing.pk |
|
assert paired.incoming_id in {row.pk for row in incoming} |
|
assert Transaction.objects.count() == 3 |
|
outcomes = list(AuditEvent.objects.exclude(pk__in=audit_ids).values_list("outcome", flat=True)) |
|
assert sorted(outcomes) == ["FAILURE", "SUCCESS"]
|
|
|