Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recover from deadlock in session_truncate fixture #1885

Merged
merged 1 commit into from
Sep 27, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 45 additions & 14 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import re
import time
import typing as t
import warnings
from contextlib import ExitStack
Expand Down Expand Up @@ -842,20 +843,50 @@ def event_after_transaction_end(_session, transaction):

def _truncate_all_tables(engine: sa.Engine) -> None:
"""Truncate all tables in the given database engine."""
with engine.begin() as transaction:
transaction.execute(
sa.text(
'''
DO $$
DECLARE tablenames text;
BEGIN
tablenames := string_agg(
quote_ident(schemaname) || '.' || quote_ident(tablename), ', ')
FROM pg_tables WHERE schemaname = 'public';
EXECUTE 'TRUNCATE TABLE ' || tablenames || ' RESTART IDENTITY';
END; $$'''
)
)
deadlock_retries = 0
while True:
try:
with engine.begin() as transaction:
transaction.execute(
sa.text(
'''
DO $$
DECLARE tablenames text;
BEGIN
tablenames := string_agg(
quote_ident(schemaname)
|| '.'
|| quote_ident(tablename), ', '
) FROM pg_tables WHERE schemaname = 'public';
EXECUTE
'TRUNCATE TABLE ' || tablenames || ' RESTART IDENTITY';
END; $$'''
)
)
break
except sa.exc.OperationalError:
# The TRUNCATE TABLE call will occasionally have a deadlock when the
# background server process has not finalised the transaction. SQLAlchemy
# recasts :exc:`psycopg.errors.DeadlockDetected` as
# :exc:`sqlalchemy.exc.OperationalError`. Pytest will show as::
#
# ERROR <filename> - sqlalchemy.exc.OperationalError:
# (psycopg.errors.DeadlockDetected) deadlock detected
# DETAIL: Process <pid1> waits for AccessExclusiveLock on relation
# <rel1> of database <db>; blocked by process <pid2>. Process <pid2>
# waits for AccessShareLock on relation <rel2> of database <db>;
# blocked by process <pid1>.
#
# We overcome the deadlock by rolling back the transaction, sleeping a
# second and attempting to truncate again, retrying two more times. If the
# deadlock remains unresolved, we raise the error to pytest. We are not
# explicitly checking for OperationalError wrapping DeadlockDetected on the
# assumption that this retry is safe for all operational errors. Any new
# type of non-transient error will be reported by the final raise.
if (deadlock_retries := deadlock_retries + 1) > 3:
raise
transaction.rollback()
time.sleep(1)


@pytest.fixture(scope='session')
Expand Down