Files
payment-processing-test/app/outbox.py
T

94 lines
3.1 KiB
Python

import asyncio
import logging
from datetime import UTC, datetime
from faststream.rabbit import RabbitBroker
from sqlalchemy import select
from sqlalchemy.ext.asyncio import async_sessionmaker
from app.messaging import PAYMENTS_EXCHANGE, PAYMENTS_ROUTING_KEY
from app.models import OutboxEvent
logger = logging.getLogger(__name__)
class OutboxPublisher:
def __init__(
self,
session_factory: async_sessionmaker,
broker: RabbitBroker,
*,
batch_size: int,
poll_interval: float,
publish_timeout: float,
) -> None:
self._session_factory = session_factory
self._broker = broker
self._batch_size = batch_size
self._poll_interval = poll_interval
self._publish_timeout = publish_timeout
async def publish_batch(self, stop_event: asyncio.Event | None = None) -> int:
async with self._session_factory() as session:
event_ids = list(
await session.scalars(
select(OutboxEvent.id)
.where(OutboxEvent.published_at.is_(None))
.order_by(OutboxEvent.created_at)
.limit(self._batch_size)
)
)
published = 0
for event_id in event_ids:
if stop_event is not None and stop_event.is_set():
break
async with self._session_factory() as session:
event = await session.scalar(
select(OutboxEvent)
.where(
OutboxEvent.id == event_id,
OutboxEvent.published_at.is_(None),
)
.with_for_update(skip_locked=True)
)
if event is None:
continue
try:
await self._broker.publish(
event.payload,
queue=PAYMENTS_ROUTING_KEY,
exchange=PAYMENTS_EXCHANGE,
persist=True,
message_id=str(event.id),
message_type=event.event_type,
timeout=self._publish_timeout,
)
except Exception as exc:
event.attempts += 1
event.last_error = str(exc)[:2000]
logger.exception("Failed to publish outbox event %s", event.id)
else:
event.published_at = datetime.now(UTC)
event.attempts += 1
event.last_error = None
published += 1
await session.commit()
return published
async def run(self, stop_event: asyncio.Event) -> None:
while not stop_event.is_set():
try:
await self.publish_batch(stop_event)
except Exception:
logger.exception("Outbox polling iteration failed")
try:
await asyncio.wait_for(stop_event.wait(), timeout=self._poll_interval)
except TimeoutError:
pass