|
2 | 2 | import datetime as dt |
3 | 3 |
|
4 | 4 | from db_retry import Transaction, postgres_retry |
5 | | -from sqlalchemy import func, insert, select |
| 5 | +from sqlalchemy import Table, func, insert, select, text |
6 | 6 | from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker |
7 | 7 |
|
8 | 8 | from sqlalchemy_inbox import Inbox |
9 | | -from tests.conftest import Tables |
| 9 | +from tests.conftest import Tables, count_rows |
| 10 | + |
| 11 | + |
| 12 | +@dataclasses.dataclass(kw_only=True, frozen=True, slots=True) |
| 13 | +class CreateChat: |
| 14 | + idempotency_id: str |
| 15 | + |
| 16 | + |
| 17 | +class ChatsRepository: |
| 18 | + def __init__(self, session: AsyncSession, table: Table, *, failures: int = 0) -> None: |
| 19 | + self._session = session |
| 20 | + self._table = table |
| 21 | + self._failures = failures |
| 22 | + self.calls = 0 |
| 23 | + |
| 24 | + async def create(self, command: CreateChat) -> None: |
| 25 | + self.calls += 1 |
| 26 | + await self._session.execute(insert(self._table).values(message_id=command.idempotency_id)) |
| 27 | + if self.calls <= self._failures: |
| 28 | + await self._session.execute( |
| 29 | + text("DO $$ BEGIN RAISE EXCEPTION 'injected' USING ERRCODE = 'serialization_failure'; END $$") |
| 30 | + ) |
| 31 | + |
| 32 | + |
| 33 | +@dataclasses.dataclass(kw_only=True, frozen=True, slots=True) |
| 34 | +class CreateChatUseCase: |
| 35 | + transaction: Transaction |
| 36 | + inbox: Inbox |
| 37 | + chats_repository: ChatsRepository |
| 38 | + |
| 39 | + @postgres_retry |
| 40 | + async def __call__(self, *, command: CreateChat) -> None: |
| 41 | + async with self.transaction: |
| 42 | + if not await self.inbox.claim(scope="chats.create-chat.v1", message_id=command.idempotency_id): |
| 43 | + return |
| 44 | + await self.chats_repository.create(command) |
| 45 | + await self.transaction.commit() |
| 46 | + |
| 47 | + |
| 48 | +def _create_chat_use_case( |
| 49 | + session_maker: async_sessionmaker[AsyncSession], tables: Tables, *, failures: int = 0 |
| 50 | +) -> CreateChatUseCase: |
| 51 | + session = session_maker() |
| 52 | + return CreateChatUseCase( |
| 53 | + transaction=Transaction(session=session), |
| 54 | + inbox=Inbox(session, tables.inbox), |
| 55 | + chats_repository=ChatsRepository(session, tables.business, failures=failures), |
| 56 | + ) |
| 57 | + |
| 58 | + |
| 59 | +async def test_readme_processing_use_case_retries_on_the_same_session( |
| 60 | + pg_engine: AsyncEngine, tables: Tables, session_maker: async_sessionmaker[AsyncSession] |
| 61 | +) -> None: |
| 62 | + use_case = _create_chat_use_case(session_maker, tables, failures=1) |
| 63 | + |
| 64 | + await use_case(command=CreateChat(idempotency_id="m-1")) |
| 65 | + |
| 66 | + assert use_case.chats_repository.calls == 2 |
| 67 | + assert await count_rows(pg_engine, tables.inbox) == 1 |
| 68 | + assert await count_rows(pg_engine, tables.business) == 1 |
| 69 | + |
| 70 | + |
| 71 | +async def test_readme_processing_use_case_skips_a_duplicate_and_handles_the_next_message( |
| 72 | + pg_engine: AsyncEngine, tables: Tables, session_maker: async_sessionmaker[AsyncSession] |
| 73 | +) -> None: |
| 74 | + use_case = _create_chat_use_case(session_maker, tables) |
| 75 | + |
| 76 | + await use_case(command=CreateChat(idempotency_id="m-1")) |
| 77 | + await use_case(command=CreateChat(idempotency_id="m-1")) |
| 78 | + await use_case(command=CreateChat(idempotency_id="m-2")) |
| 79 | + |
| 80 | + assert use_case.chats_repository.calls == 2 |
| 81 | + assert not use_case.transaction.session.in_transaction() |
| 82 | + assert await count_rows(pg_engine, tables.inbox) == 2 |
| 83 | + assert await count_rows(pg_engine, tables.business) == 2 |
10 | 84 |
|
11 | 85 |
|
12 | 86 | @dataclasses.dataclass(kw_only=True, frozen=True, slots=True) |
|
0 commit comments