Repository navigation
perf: turn pool_pre_ping off by default and rely on postgres_retry - #88
Merged
Merged
Conversation
The handler tests terminate the backend a request session holds, the endpoint tests terminate an idle pooled connection on each engine. Both fail until the handlers retry inside a Transaction and pre-ping is off.
Every handler is decorated with postgres_retry and runs its queries inside a request-scoped db_retry.Transaction, committing before it leaves the block. Repositories switch to auto_commit=False so the handler owns the whole transaction. Requires db-retry 0.8.0.
app/use_cases/ holds one dataclass per operation, retried with postgres_retry and running inside its Transaction, as in chat-app. ioc splits into Database, Repositories and UseCases groups, and handlers only call a use case and convert the result.
Every use case has the same retry and Transaction shape, so ListDecksUseCase stands in for all of them. It runs against a connection lost while held by the session and one lost while idle in the pool. It only reads, so the committed fixture data and the truncation go away.
db-retry already tests a pooled connection closed while idle being retried through a Transaction against a real Postgres. The template test only ran the same library code through a trivial use case.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
pool_pre_pingis now off by default.postgres_retryfrom db-retry handles a pooled connection that died while idle, so a connection checkout no longer costs an extra round trip.DB_POOL_PRE_PING=truestill turns pre-ping back on.This follows modern-python/chat-app#54.
Why this is safe now
DBAPIErrorthat SQLAlchemy marksconnection_invalidated. That covers asyncpg'sInterfaceError: connection is closed, which is what a dead idle connection raises on first use.session.getbenchmark, a request took 183 to 193 µs with pre-ping and 173 to 181 µs withpostgres_retryandTransactioninstead. The gap grows with network latency to Postgres.What changes
db-retry>=0.8.0is a dependency.app/use_cases/, following chat-app: a frozen dataclass withtransaction: Transactionand a repository field, and a__call__decorated withpostgres_retry. Each runs its repository call insideasync with self.transaction:and commits before leaving the block, reads included. Without the commit,__aexit__rolls back, and the rollback expires the rows the handler is about to serialize.FromDI(UseCase).app/ioc.pyis split intoDatabase(engines, session,transaction),RepositoriesandUseCasesgroups, collected inALL_GROUPS.auto_commit=False, so the use case owns the whole transaction. Withauto_commit=True, every write method commits on its own. That works while there is one call. A use case with two writes would commit the first, and a retry after a lost connection would apply it a second time.app/use_cases/is exempt from ruff'sTCrules, asapp/resources/already was, because modern-di reads the dataclass field annotations at runtime.auto_commitcommits at session close. advanced-alchemy commits at the end of each write method.Tests
tests/test_settings.pychecks the new default. The existing HTTP tests cover every use case.There is no connection-loss test here. db-retry already tests this path against a real Postgres in
test_a_connection_closed_while_pooled_is_retried_through_a_transaction: it terminates an idle pooled backend and checks thatpostgres_retryrecovers through aTransaction. A template test would only run the same library code again. I did check it locally before dropping it. With the retry, a use case recovers both from a connection that was lost while the session held it and from one lost while idle in the pool. WithDB_RETRY_RETRIES_NUMBER=1both fail withInterfaceError: connection is closed.Ruff and ty pass, and all 35 tests pass with 100% coverage against a local Postgres 14.
Trade-offs
COMMIT, the write may already have landed, and the retry runs it again.create_deckandcreate_cardshave no idempotency key, so a retry at that point can duplicate the write. AGENTS.md says so.Not done
I didn't benchmark the template itself. The numbers above are db-retry's.