Skip to content

Commit d77c6ea

Browse files
authored
perf: run the single-statement reads at AUTOCOMMIT (#64)
* test: check that the single-statement reads send no BEGIN or COMMIT * perf: run the single-statement reads at AUTOCOMMIT
1 parent e27c2e2 commit d77c6ea

4 files changed

Lines changed: 63 additions & 41 deletions

File tree

‎AGENTS.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,11 @@ a session, a transaction or a repository directly. Provider scopes:
109109
- `Database.database_session` — request-scoped, finalizer closes the session.
110110
- `Database.transaction` — request-scoped `db_retry.Transaction`, the object
111111
every use case wraps its work in.
112-
- `Database.autocommit_transaction` — the same, opened at `AUTOCOMMIT` so the
113-
token revocation check sends no BEGIN or COMMIT. `rolled_back_db_session`
112+
- `Database.autocommit_transaction` — the same, opened at `AUTOCOMMIT` so a
113+
use case that sends exactly one read statement (login, `GET /auth/me/`, the
114+
chat list and detail, the token revocation check) sends no BEGIN or COMMIT.
115+
Reads of two or more statements keep `Database.transaction`, so they see
116+
one snapshot. `rolled_back_db_session`
114117
overrides `Database.autocommit_isolation_level` to `None`, because switching
115118
the shared test connection would roll back its outer transaction.
116119
- `Repositories.*` — request-scoped, `auto_commit=False` (see above).

‎app/ioc.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,23 @@ class Repositories(Group, scope=Scope.REQUEST):
7575

7676
class UseCases(Group, scope=Scope.REQUEST):
7777
register_user_use_case = providers.Factory(creator=RegisterUserUseCase)
78-
authenticate_user_use_case = providers.Factory(creator=AuthenticateUserUseCase)
78+
authenticate_user_use_case = providers.Factory(
79+
creator=AuthenticateUserUseCase, kwargs={"transaction": Database.autocommit_transaction}
80+
)
7981
create_chat_use_case = providers.Factory(creator=CreateChatUseCase)
80-
fetch_chat_use_case = providers.Factory(creator=FetchChatUseCase)
82+
fetch_chat_use_case = providers.Factory(
83+
creator=FetchChatUseCase, kwargs={"transaction": Database.autocommit_transaction}
84+
)
8185
create_message_use_case = providers.Factory(creator=CreateMessageUseCase)
8286
fetch_messages_use_case = providers.Factory(creator=FetchMessagesUseCase)
8387
edit_message_use_case = providers.Factory(creator=EditMessageUseCase)
8488
delete_message_use_case = providers.Factory(creator=DeleteMessageUseCase)
85-
fetch_chats_use_case = providers.Factory(creator=FetchChatsUseCase)
86-
fetch_user_use_case = providers.Factory(creator=FetchUserUseCase)
89+
fetch_chats_use_case = providers.Factory(
90+
creator=FetchChatsUseCase, kwargs={"transaction": Database.autocommit_transaction}
91+
)
92+
fetch_user_use_case = providers.Factory(
93+
creator=FetchUserUseCase, kwargs={"transaction": Database.autocommit_transaction}
94+
)
8795
mark_read_use_case = providers.Factory(creator=MarkReadUseCase)
8896
revoke_token_use_case = providers.Factory(creator=RevokeTokenUseCase)
8997
check_token_revoked_use_case = providers.Factory(

tests/use_cases/test_check_token_revoked.py renamed to tests/use_cases/test_autocommit_reads.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
1+
import contextlib
12
import typing
23

34
import modern_di
5+
import pytest
46
import sqlalchemy as sa
57
from sqlalchemy import event
68

79
from app import ioc
10+
from app.actor import Actor
811
from app.database.resources import create_database_engine
12+
from app.exceptions import PermissionDeniedError
913

1014

11-
async def test_revocation_check_sends_no_begin_or_commit(di_container: modern_di.Container) -> None:
15+
UNKNOWN_ACTOR: typing.Final = Actor(id=-1)
16+
17+
18+
@pytest.mark.parametrize(
19+
("provider", "kwargs"),
20+
[
21+
(ioc.UseCases.check_token_revoked_use_case, {"jti": "unknown"}),
22+
(ioc.UseCases.authenticate_user_use_case, {"username": "nobody", "password": "hunter2hunter2"}),
23+
(ioc.UseCases.fetch_user_use_case, {"actor": UNKNOWN_ACTOR}),
24+
(ioc.UseCases.fetch_chats_use_case, {"actor": UNKNOWN_ACTOR}),
25+
(ioc.UseCases.fetch_chat_use_case, {"actor": UNKNOWN_ACTOR, "chat_id": -1}),
26+
],
27+
)
28+
async def test_single_statement_read_sends_no_begin_or_commit(
29+
di_container: modern_di.Container,
30+
provider: modern_di.providers.Factory[typing.Any],
31+
kwargs: dict[str, typing.Any],
32+
) -> None:
1233
engine: typing.Final = create_database_engine()
1334
in_transaction: typing.Final[list[bool]] = []
1435

@@ -21,8 +42,9 @@ def record(connection: sa.Connection, *_: object) -> None:
2142
di_container.override(ioc.Database.database_engine, engine)
2243
try:
2344
async with di_container.build_child_container(scope=modern_di.Scope.REQUEST) as container:
24-
use_case: typing.Final = container.resolve_provider(ioc.UseCases.check_token_revoked_use_case)
25-
assert await use_case(jti="unknown") is False
45+
use_case: typing.Final = container.resolve_provider(provider)
46+
with contextlib.suppress(PermissionDeniedError):
47+
await use_case(**kwargs)
2648
finally:
2749
di_container.reset_override(ioc.Database.database_engine)
2850
await engine.dispose()

‎tests/use_cases/test_connection_loss.py‎

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -71,32 +71,19 @@ async def lost_pooled_connection(request_container: modern_di.Container) -> None
7171
await _terminate_backend(engine, *watched)
7272

7373

74-
_READ_CASES: typing.Final = [
75-
(ioc.UseCases.authenticate_user_use_case, {"username": "nobody", "password": "hunter2hunter2"}, None),
76-
(ioc.UseCases.fetch_chats_use_case, {"actor": UNKNOWN_ACTOR}, []),
77-
(ioc.UseCases.fetch_user_use_case, {"actor": UNKNOWN_ACTOR}, None),
78-
]
79-
READ_USE_CASES: typing.Final = pytest.mark.parametrize(("provider", "kwargs", "expected"), _READ_CASES)
80-
READ_USE_CASES_WITH_REVOCATION_CHECK: typing.Final = pytest.mark.parametrize(
74+
AUTOCOMMIT_READ_USE_CASES: typing.Final = pytest.mark.parametrize(
8175
("provider", "kwargs", "expected"),
82-
[*_READ_CASES, (ioc.UseCases.check_token_revoked_use_case, {"jti": "unknown"}, False)],
76+
[
77+
(ioc.UseCases.authenticate_user_use_case, {"username": "nobody", "password": "hunter2hunter2"}, None),
78+
(ioc.UseCases.fetch_chats_use_case, {"actor": UNKNOWN_ACTOR}, []),
79+
(ioc.UseCases.fetch_user_use_case, {"actor": UNKNOWN_ACTOR}, None),
80+
(ioc.UseCases.check_token_revoked_use_case, {"jti": "unknown"}, False),
81+
],
8382
)
8483

8584

86-
@pytest.mark.usefixtures("lost_connection")
87-
@READ_USE_CASES
88-
async def test_read_use_case_recovers_from_a_lost_connection(
89-
request_container: modern_di.Container,
90-
provider: modern_di.providers.Factory[typing.Any],
91-
kwargs: dict[str, typing.Any],
92-
expected: object,
93-
) -> None:
94-
use_case: typing.Final = request_container.resolve_provider(provider)
95-
assert await use_case(**kwargs) == expected
96-
97-
9885
@pytest.mark.usefixtures("lost_pooled_connection")
99-
@READ_USE_CASES_WITH_REVOCATION_CHECK
86+
@AUTOCOMMIT_READ_USE_CASES
10087
async def test_read_use_case_recovers_from_a_connection_lost_while_pooled(
10188
request_container: modern_di.Container,
10289
provider: modern_di.providers.Factory[typing.Any],
@@ -107,22 +94,24 @@ async def test_read_use_case_recovers_from_a_connection_lost_while_pooled(
10794
assert await use_case(**kwargs) == expected
10895

10996

97+
_TRANSACTIONAL_MEMBERSHIP_GATED_CASES: typing.Final = [
98+
(ioc.UseCases.fetch_messages_use_case, {}),
99+
(
100+
ioc.UseCases.create_message_use_case,
101+
{"data": SendMessageRequest(idempotency_key=uuid.uuid4(), text="hello")},
102+
),
103+
(ioc.UseCases.mark_read_use_case, {"data": MarkReadRequest(last_read_message_id=1)}),
104+
]
105+
TRANSACTIONAL_MEMBERSHIP_GATED_USE_CASES: typing.Final = pytest.mark.parametrize(
106+
("provider", "kwargs"), _TRANSACTIONAL_MEMBERSHIP_GATED_CASES
107+
)
110108
MEMBERSHIP_GATED_USE_CASES: typing.Final = pytest.mark.parametrize(
111-
("provider", "kwargs"),
112-
[
113-
(ioc.UseCases.fetch_chat_use_case, {}),
114-
(ioc.UseCases.fetch_messages_use_case, {}),
115-
(
116-
ioc.UseCases.create_message_use_case,
117-
{"data": SendMessageRequest(idempotency_key=uuid.uuid4(), text="hello")},
118-
),
119-
(ioc.UseCases.mark_read_use_case, {"data": MarkReadRequest(last_read_message_id=1)}),
120-
],
109+
("provider", "kwargs"), [(ioc.UseCases.fetch_chat_use_case, {}), *_TRANSACTIONAL_MEMBERSHIP_GATED_CASES]
121110
)
122111

123112

124113
@pytest.mark.usefixtures("lost_connection")
125-
@MEMBERSHIP_GATED_USE_CASES
114+
@TRANSACTIONAL_MEMBERSHIP_GATED_USE_CASES
126115
async def test_membership_gated_use_case_recovers_from_a_lost_connection(
127116
request_container: modern_di.Container,
128117
provider: modern_di.providers.Factory[typing.Any],

0 commit comments

Comments
 (0)