You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
postgres_retry costs ~21 µs per call on the success path #71
postgres_retry costs about 21 µs per call even when the first attempt succeeds, which is almost every call. That is more than some of the database work it wraps.
Measurements
Wrapping a coroutine that returns immediately, on Python 3.14 with tenacity 9.1.4, 50,000 calls:
µs per call
bare await
0.05
postgres_retry
21.5
calling a prebuilt AsyncRetrying
15.3
building AsyncRetrying alone
1.1
get_retries_number() (env read)
0.45
try/except around the call
0.10
Most of the cost is tenacity's attempt loop, which runs even for a single successful attempt. Building the retryer for each call is only about 1 µs. I did not break down the remaining few µs.
In a real request loop the cost is larger. Reading one row by primary key through SQLAlchemy and asyncpg, against a local Postgres on the autocommit engine, took 167 µs per request without the decorator and 210 µs with it. In the same setup pool_pre_ping=True costs 25 µs. So with a nearby database, replacing pre-ping with postgres_retry (now possible after #69) makes requests slower rather than faster.
Proposal
Call the function once directly, and build the tenacity retryer only after that first attempt raises a retriable error:
await f(*args, **kwargs). On success, return.
On an exception that is not retriable, or when the attempt budget is 1, re-raise.
Otherwise wait the first backoff and run the remaining attempts through AsyncRetrying, with the budget reduced by one.
Constraints from the existing invariants:
The attempt count is still resolved for each call, from DB_RETRY_RETRIES_NUMBER or retries=N (ADR 0001, test_the_default_attempt_count_is_re_read_on_every_call). It can now be read after the first failure instead of before the first attempt.
A retry never re-runs immediately. The wait between the first and second attempt has to stay (test_a_retry_never_re_runs_the_body_immediately).
The total number of attempts and the retriable predicate stay the same.
One visible change: before_log writes a DEBUG line at the start of every attempt, including the first. With the fast path, a call that succeeds first time logs nothing. That seems acceptable, but it belongs in the release notes.
Expected result: the success path drops to roughly the cost of a try/except, about 0.1 µs, from 21 µs.
Activity
perf: replace tenacity with a plain retry loop (#71) (#72)