This is an automated email from the ASF dual-hosted git repository.
shahar1 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 ed918278f2f Use READ COMMITTED for async MySQL metadata sessions
(#76694)
ed918278f2f is described below
commit ed918278f2f09dc729a9fa3a45ad5ffba9a6b549
Author: Dev-iL <[email protected]>
AuthorDate: Thu Nov 12 20:37:26 2026 +0200
Use READ COMMITTED for async MySQL metadata sessions (#76694)
The async engine never got the isolation level the sync engine sets for
MySQL, so its sessions ran at the server default, REPEATABLE READ. A
query repeated in one async transaction could then read a stale snapshot
that the sync engine's sessions would not, and task heartbeats, audit
log rows and other writes that already go through the async engine
behaved differently from their sync counterparts.
---
airflow-core/src/airflow/settings.py | 24 +++++++++++++++++-------
airflow-core/tests/unit/core/test_settings.py | 12 ++++++++++++
2 files changed, 29 insertions(+), 7 deletions(-)
diff --git a/airflow-core/src/airflow/settings.py
b/airflow-core/src/airflow/settings.py
index f5a2c460268..d049df5520f 100644
--- a/airflow-core/src/airflow/settings.py
+++ b/airflow-core/src/airflow/settings.py
@@ -499,6 +499,7 @@ def _configure_async_session() -> None:
engine_args["pool_recycle"] = conf.getint("database",
"SQL_ALCHEMY_POOL_RECYCLE", fallback=1800)
engine_args["pool_pre_ping"] = conf.getboolean("database",
"SQL_ALCHEMY_POOL_PRE_PING", fallback=True)
engine_args["max_overflow"] = conf.getint("database",
"SQL_ALCHEMY_MAX_OVERFLOW", fallback=10)
+ engine_args.update(_get_isolation_level_args(SQL_ALCHEMY_CONN_ASYNC))
async_engine = create_async_metadata_engine(
SQL_ALCHEMY_CONN_ASYNC,
@@ -673,17 +674,26 @@ def prepare_engine_args(disable_connection_pool=False,
pool_class=None):
engine_args["pool_pre_ping"] = pool_pre_ping
engine_args["max_overflow"] = max_overflow
- # The default isolation level for MySQL (REPEATABLE READ) can introduce
inconsistencies when
- # running multiple schedulers, as repeated queries on the same session may
read from stale snapshots.
- # 'READ COMMITTED' is the default value for PostgreSQL.
- # More information here:
- #
https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html
- if SQL_ALCHEMY_CONN.startswith("mysql"):
- engine_args["isolation_level"] = "READ COMMITTED"
+ engine_args.update(_get_isolation_level_args(SQL_ALCHEMY_CONN))
return engine_args
+def _get_isolation_level_args(sql_alchemy_conn: str) -> dict[str, str]:
+ """
+ Return the isolation level engine argument for the metadata database
connection.
+
+ The default isolation level for MySQL (REPEATABLE READ) can introduce
inconsistencies when
+ running multiple schedulers, as repeated queries on the same session may
read from stale snapshots.
+ 'READ COMMITTED' is the default value for PostgreSQL.
+ More information here:
+
https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html
+ """
+ if sql_alchemy_conn.startswith("mysql"):
+ return {"isolation_level": "READ COMMITTED"}
+ return {}
+
+
def dispose_orm(do_log: bool = True):
"""Properly close pooled database connections."""
global Session, engine, NonScopedSession, async_engine, AsyncSession
diff --git a/airflow-core/tests/unit/core/test_settings.py
b/airflow-core/tests/unit/core/test_settings.py
index 28f9d074578..962aa08ad57 100644
--- a/airflow-core/tests/unit/core/test_settings.py
+++ b/airflow-core/tests/unit/core/test_settings.py
@@ -784,6 +784,18 @@ def
test_async_pool_is_closed_before_process_shutdown(driver):
pytest.param(
"mysql+asyncmy", "SELECT @@session.time_zone", "+00:00",
marks=pytest.mark.backend("mysql")
),
+ pytest.param(
+ "mysql+aiomysql",
+ "SELECT @@session.transaction_isolation",
+ "READ-COMMITTED",
+ marks=pytest.mark.backend("mysql"),
+ ),
+ pytest.param(
+ "mysql+asyncmy",
+ "SELECT @@session.transaction_isolation",
+ "READ-COMMITTED",
+ marks=pytest.mark.backend("mysql"),
+ ),
pytest.param("sqlite+aiosqlite", "PRAGMA foreign_keys", 1,
marks=pytest.mark.backend("sqlite")),
],
)