shahar1 commented on code in PR #75396:
URL: https://github.com/apache/airflow/pull/75396#discussion_r4484303049


##########
airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py:
##########
@@ -766,14 +787,16 @@ def set_xcom(
         )
     try:
         # We expect serialised value from the caller - sdk, do not serialise 
in here
-        XComModel.set_for_attempt(
-            key=key,
-            value=value,
-            task_instance_id=owner,
-            serialize=False,
-            dag_result=dag_result,
-            mapped_length=mapped_length,
-            session=session,
+        await session.run_sync(

Review Comment:
   **major:** this write now runs on the async engine, which doesn't get the 
sync engine's MySQL session setup:
   
   - `configure_orm()` calls `setup_event_handlers(engine)` for the sync engine 
only, so `SET time_zone = '+00:00'` never runs on async connections.
   - `prepare_engine_args()` sets `isolation_level="READ COMMITTED"` for the 
sync engine only, so async sessions use the server default.
   
   I ran Airflow's own engines against MySQL 8.0 started with 
`--default-time-zone=+02:00`, writing a `UtcDateTime` column through each:
   
   ```text
   sync  session vars: ('+00:00', 'READ-COMMITTED')
   async session vars: ('+02:00', 'REPEATABLE-READ')
   async write read by sync session: wrote 17:48:52+00:00 read 15:48:52+00:00
   sync write  read by sync session: wrote 17:48:52+00:00 read 17:48:52+00:00
   ```
   
   This probe covers the engines rather than this route, but `set_for_attempt` 
binds `timestamp` the same way. On such a server, `xcom_v2.timestamp` for every 
pushed XCom would be off by the server's UTC offset. That value is shown in the 
UI and REST API, and `airflow db clean` uses it as the recency column for 
`xcom_v2`. A server on UTC hides this.
   
   The isolation level also matters on the read side: in `get_xcom` the owner 
check starts the snapshot, so the value read after the lookup can't see a row 
committed in between. The window is short, and I only expect a transient 404 
from it.
   
   In `_configure_async_session`, passing `isolation_level="READ COMMITTED"` 
for MySQL and calling `setup_event_handlers(async_engine.sync_engine)` fixed 
both on that server (`('+00:00', 'READ-COMMITTED')`, zero delta). I only tried 
this with `asyncmy` on MySQL. The handlers would also run for SQLite and 
PostgreSQL, so those need checking too.
   
   `ti_heartbeat` already writes through this engine, so a separate PR that 
this one rebases on seems cleanest.
   



##########
airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py:
##########
@@ -563,31 +581,34 @@ def head_xcom(
         ]
     ),
     description="Get a single XCom Value",
+    response_model=XComResponse,
 )
-def get_xcom(
+async def get_xcom(
     dag_id: str,
     run_id: str,
     task_id: str,
     key: Annotated[str, Path(min_length=1)],
-    session: SessionDep,
+    session: AsyncSessionDep,
     params: Annotated[GetXcomFilterParams, Query()],
     dag_bag: DagBagDep,
     token: ExecutionToken = _TriggerAwareToken,
-) -> XComResponse:
+) -> JSONResponse:
     """Get an Airflow XCom from database - not other XCom Backends."""
     owner = None
     if token.claims.scope == "execution":
-        owner = session.execute(
-            select(TaskInstance.id, TaskInstance.working_set)
-            .where(
-                TaskInstance.id == token.id,
-                TaskInstance.dag_id == dag_id,
-                TaskInstance.run_id == run_id,
-                TaskInstance.task_id == task_id,
-                *build_coordinate_filters(TaskInstance, 
map_index=params.map_index),
+        owner = (
+            await session.execute(
+                select(TaskInstance.id, TaskInstance.working_set)
+                .where(
+                    TaskInstance.id == token.id,
+                    TaskInstance.dag_id == dag_id,
+                    TaskInstance.run_id == run_id,
+                    TaskInstance.task_id == task_id,
+                    *build_coordinate_filters(TaskInstance, 
map_index=params.map_index),
+                )
+                .limit(1)
+                .execution_options(include_all_attempts=True)
             )
-            .limit(1)
-            .execution_options(include_all_attempts=True)
         ).first()

Review Comment:
   **minor:** the first row of your connection table comes from this query: its 
transaction stays open, so the async connection stays checked out and idle in 
transaction through the `run_in_threadpool` below, including a cold-cache Dag 
deserialization. Behind PgBouncer in transaction mode, that also pins a server 
connection.
   
   `owner` is a plain row, so `await session.rollback()` right after this read 
would release the connection, and this path would no longer hold two at once. 
The cost is another checkout for the value read, with a `pool_pre_ping` round 
trip, so it's your call whether that trade is worth it.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to