diff --git a/src/sentry/db/deletion.py b/src/sentry/db/deletion.py index 1543be9ea9b7f2..c3778fb4d4e389 100644 --- a/src/sentry/db/deletion.py +++ b/src/sentry/db/deletion.py @@ -7,6 +7,7 @@ from django.db import connections, router from django.utils import timezone +from sentry.db.postgres.transactions import unset_statement_timeout from sentry.utils.query import RangeQuerySetWrapper @@ -108,5 +109,7 @@ def iterator(self, chunk_size=100, batch_size=10000) -> Generator[tuple[int, ... result_value_getter=lambda item: item[1], ) - for batch in itertools.batched(wrapper, chunk_size): - yield tuple(item[0] for item in batch) + # Disable statement_timeout for long-running cleanup queries + with unset_statement_timeout(self.using): + for batch in itertools.batched(wrapper, chunk_size): + yield tuple(item[0] for item in batch) diff --git a/src/sentry/db/postgres/transactions.py b/src/sentry/db/postgres/transactions.py index 5e6cc61eacb65e..ef7c3a40e06b1a 100644 --- a/src/sentry/db/postgres/transactions.py +++ b/src/sentry/db/postgres/transactions.py @@ -115,3 +115,34 @@ def enforce_constraints(transaction: Atomic) -> Generator[None]: with transaction: yield get_connection(transaction.using or "default").check_constraints() + + +@contextlib.contextmanager +def unset_statement_timeout(using: str) -> Generator[None]: + """ + Temporarily disables the statement_timeout for long-running database operations. + + This is useful for operations like cleanup tasks that need to run expensive queries + that might exceed the default statement_timeout configured on the database. + + The previous timeout value is restored after the context exits. + """ + connection = connections[using] + + # Only PostgreSQL databases support statement_timeout + if connection.vendor != "postgresql": + yield + return + + with connection.cursor() as cursor: + # Save the current statement_timeout value + cursor.execute("SHOW statement_timeout") + previous_timeout = cursor.fetchone()[0] + + try: + # Disable statement_timeout (0 means unlimited) + cursor.execute("SET statement_timeout = 0") + yield + finally: + # Restore the previous timeout value + cursor.execute(f"SET statement_timeout = '{previous_timeout}'")