Source code for mersal_sqlalchemy.sqlalchemy_outbox_storage

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from sqlalchemy import insert, select, update
from sqlalchemy.orm import registry

from mersal.messages.message_headers import MessageHeaders
from mersal.outbox import OutboxMessage, OutboxMessageBatch, OutboxStorage
from mersal_sqlalchemy.orm import create_outbox_table_and_map

if TYPE_CHECKING:
    from collections.abc import Callable, Sequence

    from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

    from mersal.serialization import MessageHeadersSerializer
    from mersal.transport import OutgoingMessage, TransactionContext

__all__ = (
    "SQLAlchemyOutboxStorage",
    "SQLAlchemyOutboxStorageConfig",
)


[docs] @dataclass class SQLAlchemyOutboxStorageConfig: """Configuration for SQLAlchemyOutboxStorage.""" async_session_factory: async_sessionmaker[AsyncSession] """Session factory used to create a session responsible for the outbox table and fetch outbox messages. This is not used for creating the session responsible for saving the messages. """ table_name: str "Outbox table name." session_extractor: Callable[[TransactionContext], AsyncSession] "A callback used to obtain the SQLAlchemy session from the current TransactionContext." commit_on_save: bool = True """Commit session at the end of the save method. This defaults to True but should be set to False if something else is taking care of committing the session. Check the :doc:`documentation </usage/outbox>` for examples. """ close_session_on_save: bool = True """Close session at the end of the save method. This only applies if the session is also being committed. """ @property def storage(self) -> SQLAlchemyOutboxStorage: return SQLAlchemyOutboxStorage(self)
[docs] class SQLAlchemyOutboxStorage(OutboxStorage):
[docs] def __init__( self, config: SQLAlchemyOutboxStorageConfig, ) -> None: self._session_maker = config.async_session_factory self._table_name = config.table_name self._session_extractor = config.session_extractor self._commit_on_save = config.commit_on_save self._close_session_on_save = config.close_session_on_save self.headers_serializer: MessageHeadersSerializer
[docs] async def save( self, outgoing_messages: Sequence[OutgoingMessage], transaction_context: TransactionContext, ) -> None: session = self._session_extractor(transaction_context) await session.execute( insert(self.table), [ { "destination_address": om.destination_address, "headers": self.headers_serializer.serialize(om.transport_message.headers), "body": om.transport_message.body, } for om in outgoing_messages ], ) if self._commit_on_save: await session.commit() if self._close_session_on_save: await session.close()
[docs] async def get_next_message_batch(self) -> OutboxMessageBatch: session = self._session_maker() async with session: stmt = select(self.table).where(self.table.c.sent == False) # noqa: E712 data = (await session.execute(stmt)).all() result = [ OutboxMessage( outbox_message_id=datum.outbox_message_id, destination_address=datum.destination_address, headers=MessageHeaders(self.headers_serializer.deserialize(datum.headers)), body=datum.body, ) for datum in data ] async def completion() -> None: await self._update_messages_sent_status(result, session) async def close() -> None: pass return OutboxMessageBatch(result, completion, close)
async def __call__(self) -> None: self.table = create_outbox_table_and_map(self._table_name, registry()) async with self._session_maker() as session: await session.run_sync(lambda s: self.table.create(s.get_bind(), checkfirst=True)) async def _update_messages_sent_status( self, outbox_messages: Sequence[OutboxMessage], session: AsyncSession ) -> None: await session.execute( update(self.table) .where(self.table.c.outbox_message_id.in_([x.outbox_message_id for x in outbox_messages])) .values(sent=True), ) await session.commit()