From 97ae8b38cf1d63015e55318e7d68c512adc5f7da Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 16 Oct 2026 21:39:05 +0300 Subject: [PATCH] perf: check token revocation without BEGIN and COMMIT round trips --- app/database/resources.py | 8 ++++++ app/ioc.py | 10 ++++++- tests/use_cases/test_check_token_revoked.py | 30 +++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/use_cases/test_check_token_revoked.py diff --git a/app/database/resources.py b/app/database/resources.py index 49a70d3..b6d16b5 100644 --- a/app/database/resources.py +++ b/app/database/resources.py @@ -1,6 +1,7 @@ import asyncio import typing +from db_retry import Transaction from sqlalchemy.ext import asyncio as sa from app.settings import settings @@ -34,3 +35,10 @@ def create_session(engine: sa.AsyncEngine | sa.AsyncConnection) -> sa.AsyncSessi async def close_session(session: sa.AsyncSession) -> None: task: typing.Final = asyncio.create_task(session.close()) await asyncio.shield(task) + + +def create_autocommit_transaction(session: sa.AsyncSession) -> Transaction: + # Isolation level can only be set before the session holds a connection; test sessions are bound to one. + if isinstance(session.bind, sa.AsyncEngine) and not session.in_transaction(): + return Transaction(session=session, isolation_level="AUTOCOMMIT") + return Transaction(session=session) diff --git a/app/ioc.py b/app/ioc.py index eea6a5f..76cd660 100644 --- a/app/ioc.py +++ b/app/ioc.py @@ -39,6 +39,12 @@ class Database(Group): creator=Transaction, kwargs={"session": database_session}, ) + autocommit_transaction = providers.Factory( + scope=Scope.REQUEST, + creator=database_resources.create_autocommit_transaction, + bound_type=None, + kwargs={"session": database_session}, + ) class Repositories(Group, scope=Scope.REQUEST): @@ -77,7 +83,9 @@ class UseCases(Group, scope=Scope.REQUEST): fetch_user_use_case = providers.Factory(creator=FetchUserUseCase) mark_read_use_case = providers.Factory(creator=MarkReadUseCase) revoke_token_use_case = providers.Factory(creator=RevokeTokenUseCase) - check_token_revoked_use_case = providers.Factory(creator=CheckTokenRevokedUseCase) + check_token_revoked_use_case = providers.Factory( + creator=CheckTokenRevokedUseCase, kwargs={"transaction": Database.autocommit_transaction} + ) ALL_GROUPS: typing.Final[list[type[Group]]] = [Database, Repositories, UseCases] diff --git a/tests/use_cases/test_check_token_revoked.py b/tests/use_cases/test_check_token_revoked.py new file mode 100644 index 0000000..4993f93 --- /dev/null +++ b/tests/use_cases/test_check_token_revoked.py @@ -0,0 +1,30 @@ +import asyncio +import typing + +import asyncpg +import modern_di + +from app import ioc +from app.database.resources import create_database_engine + + +async def test_revocation_check_sends_no_transaction_statements(di_container: modern_di.Container) -> None: + engine: typing.Final = create_database_engine() + di_container.override(ioc.Database.database_engine, engine) + queries: typing.Final[list[asyncpg.connection.LoggedQuery]] = [] + + try: + async with engine.connect() as connection: + driver_connection = (await connection.get_raw_connection()).driver_connection + assert isinstance(driver_connection, asyncpg.Connection) + driver_connection.add_query_logger(queries.append) + + async with di_container.build_child_container(scope=modern_di.Scope.REQUEST) as container: + check_token_revoked = container.resolve_provider(ioc.UseCases.check_token_revoked_use_case) + assert await check_token_revoked(jti="unknown") is False + await asyncio.sleep(0) + finally: + di_container.reset_override(ioc.Database.database_engine) + await engine.dispose() + + assert not {"BEGIN;", "COMMIT;", "ROLLBACK;"} & {logged.query for logged in queries}, queries