This is an automated email from the ASF dual-hosted git repository.
kaxil pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 38c11037bc1 Take the Postgres migration lock without a bound parameter
(#76976)
38c11037bc1 is described below
commit 38c11037bc11e57f17849629253447a2960fa849
Author: Rahul Vats <[email protected]>
AuthorDate: Thu Nov 12 20:24:52 2026 +0530
Take the Postgres migration lock without a bound parameter (#76976)
The migration lock connection stays idle in transaction for the whole
migration. Under psycopg3 the parameterized `pg_advisory_lock(:id)` keeps
its
snapshot on that idle transaction, so a migration running CREATE INDEX
CONCURRENTLY (e.g. from an external DB manager) waits on its own process's
lock connection and `airflow db migrate` hangs forever.
Inline the lock id instead. The transaction stays open, so behind a
transaction-pooling PgBouncer the lock and unlock still run on the same
server connection.
---
airflow-core/src/airflow/utils/db.py | 16 ++++++++++++----
airflow-core/tests/unit/utils/test_db.py | 22 +++++++++++++++++++++-
2 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/airflow-core/src/airflow/utils/db.py
b/airflow-core/src/airflow/utils/db.py
index 5e88b05c137..32291556837 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -1627,7 +1627,12 @@ def _create_global_lock_mysql(lock: DBLocks,
lock_timeout: int) -> Generator[Non
def _create_global_lock_postgresql(
session: Session, lock: DBLocks, lock_timeout: int
) -> Generator[None, None, None]:
- """Create a global advisory lock for PostgreSQL using transactional
advisory locks."""
+ """
+ Create a global advisory lock for PostgreSQL using session-level advisory
locks.
+
+ The lock connection stays in an open transaction while the lock is held,
which keeps it
+ on one server connection behind a transaction-pooling PgBouncer.
+ """
bind = session.get_bind()
if hasattr(bind, "connect"):
conn = bind.connect()
@@ -1642,10 +1647,13 @@ def _create_global_lock_postgresql(
text("SELECT set_config('lock_timeout', :timeout, false)"),
{"timeout": str(lock_timeout)},
)
- conn.execute(text("SELECT pg_advisory_lock(:id)"), {"id":
lock.value})
else:
conn.execute(text("SET LOCK_TIMEOUT to :timeout"), {"timeout":
lock_timeout})
- conn.execute(text("SELECT pg_advisory_lock(:id)"), {"id":
lock.value})
+ # The last statement run on this connection before the yield must have
no bound parameter:
+ # under psycopg3 a parameterized statement keeps its snapshot while
the connection sits idle
+ # in transaction, and CREATE INDEX CONCURRENTLY run by a migration in
this process would
+ # wait on it forever.
+ conn.execute(text(f"SELECT pg_advisory_lock({int(lock.value)})"))
yield
finally:
@@ -1679,7 +1687,7 @@ def create_global_lock(
- GET_LOCK is session-level, not transaction-level
- DDL operations cause implicit commits that would break transaction
wrappers
- For PostgreSQL, uses transactional advisory locks as before.
+ For PostgreSQL, uses a session-level advisory lock, held while the
connection stays in a transaction.
"""
dialect_name = get_dialect_name(session)
diff --git a/airflow-core/tests/unit/utils/test_db.py
b/airflow-core/tests/unit/utils/test_db.py
index 6af67dec69a..786310e3e11 100644
--- a/airflow-core/tests/unit/utils/test_db.py
+++ b/airflow-core/tests/unit/utils/test_db.py
@@ -30,7 +30,7 @@ from alembic.config import Config
from alembic.migration import MigrationContext
from alembic.runtime.environment import EnvironmentContext
from alembic.script import ScriptDirectory
-from sqlalchemy import Column, Integer, MetaData, String, Table, delete, select
+from sqlalchemy import Column, Integer, MetaData, String, Table, delete,
select, text
from sqlalchemy.orm import declarative_base
from airflow import settings
@@ -38,6 +38,7 @@ from airflow.models import Base as airflow_base
from airflow.models.team import Team
from airflow.utils.db import (
AutocommitEngineForMySQL,
+ DBLocks,
LazySelectSequence,
_get_alembic_config,
_get_current_revision,
@@ -46,6 +47,7 @@ from airflow.utils.db import (
check_team_names_can_be_lower_cased,
compare_server_default,
compare_type,
+ create_global_lock,
downgrade,
get_default_connections,
initdb,
@@ -501,6 +503,24 @@ class TestReflectTablesSchemaQualified:
session.execute.assert_called()
[email protected]("postgres")
+def test_postgres_migration_lock_does_not_block_create_index_concurrently():
+ """The held migration lock must not block CREATE INDEX CONCURRENTLY run by
migrations."""
+ engine = settings.get_engine()
+ table = "test_migration_lock_cic"
+ with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as
ddl:
+ ddl.execute(text(f"DROP TABLE IF EXISTS {table}"))
+ ddl.execute(text(f"CREATE TABLE {table} (id INTEGER)"))
+ try:
+ with create_session() as session,
create_global_lock(session=session, lock=DBLocks.MIGRATIONS):
+ # Fails with a statement timeout instead of hanging if the
lock connection blocks it.
+ ddl.execute(text("SET statement_timeout = '10s'"))
+ ddl.execute(text(f"CREATE INDEX CONCURRENTLY idx_{table} ON
{table} (id)"))
+ finally:
+ ddl.execute(text("RESET statement_timeout"))
+ ddl.execute(text(f"DROP TABLE IF EXISTS {table}"))
+
+
class TestAutocommitEngineForMySQL:
"""Test the AutocommitEngineForMySQL context manager."""