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


##########
airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py:
##########
@@ -178,88 +183,144 @@ def get_order(self, entity: AliasedClass, *, descending: 
bool = False) -> tuple[
         return tuple(column.desc() if descending else column.asc() for column 
in columns)
 
 
+def _render_json(content: JsonValue) -> JSONResponse:
+    """
+    Render an XCom read's response on a worker thread.
+
+    The values come from a JSON column, so returning a ``JSONResponse`` skips 
FastAPI's re-validation of
+    them as ``JsonValue``, which for large values costs several times more 
than encoding them. Cadwyn still
+    decodes and re-encodes a returned ``JSONResponse`` body on the event loop
+    (https://github.com/zmievsa/cadwyn/issues/51), so only the first encoding 
runs on the worker thread.
+    """
+    return JSONResponse(content)
+
+
+def _reject_archived_caller(
+    token: ExecutionToken, *, dag_id: str, run_id: str, task_id: str, 
map_index: int | None, session: Session
+) -> None:
+    """Refuse a task's read of its own XCom once its attempt has been 
archived."""
+    if token.claims.scope != "execution":
+        return
+    caller = 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=map_index),
+        )
+        .limit(1)
+        .execution_options(include_all_attempts=True)
+    ).first()
+    if caller is None or caller.working_set is not None:
+        return
+    identify_archived = IdentifyArchivedTaskStateUpdates.is_applied
+    raise HTTPException(
+        status_code=status.HTTP_410_GONE if identify_archived else 
status.HTTP_404_NOT_FOUND,
+        detail={
+            "reason": "not_found",
+            "message": (
+                "Task Instance not found in the working set; its attempt has 
been archived"
+                if identify_archived
+                else "Task Instance not found"
+            ),
+        },
+    )
+
+
 def _build_xcom_read(
     *,
     dag_id: str,
     run_id: str,
     task_id: str,
     key: str,
-    session: Session,
     dag_bag: DBDagBag,
     token: ExecutionToken,
     map_index: int | None = None,
     include_prior_dates: bool = False,
     previous_iteration: bool = False,
     sequence: bool = False,
+    reject_archived_caller: bool = False,
 ) -> _XComRead:
     """
     Select the XCom rows of the producers visible to the calling task instance.
 
     A ``sequence`` read returns several values in order. When it targets a 
loop task from outside that
     loop, it covers every iteration of the task rather than the one a caller 
inside the loop would see.
+
+    Routes run this on a worker thread: resolving producers can deserialize a 
Dag on a DagBag cache miss,
+    CPU work that would stall the event loop. A session cannot cross threads, 
so this opens its own.
+
+    ``reject_archived_caller`` makes a caller reading its own XCom get the 
archived-attempt error first. The
+    check runs here rather than on the route's session, so it leaves no route 
connection idle during the lookup.
     """
-    resolver = TaskCoordinateResolver(dag_bag, session)
-    read = partial(
-        XComModel.get_many,
-        dag_ids=dag_id,
-        run_id=run_id,
-        task_ids=task_id,
-        key=key,
-        include_prior_dates=include_prior_dates,
-    )
-    try:
-        if previous_iteration and include_prior_dates:
-            raise ValueError("previous_iteration cannot be combined with prior 
dates")
-        if not previous_iteration and not resolver.has_regions(
-            dag_id, None if include_prior_dates else run_id, task_id
-        ):
-            return _XComRead(read(region_id=SENTINEL_REGION_ID, 
map_indexes=map_index))
-        caller = session.get(TaskInstance, token.id) if token.claims.scope == 
"execution" else None
-        if include_prior_dates:
-            task = None
-            if session.scalar(select(DagRun.id).where(DagRun.dag_id == dag_id, 
DagRun.run_id == run_id)):
-                with suppress(TaskNotFound):
-                    task = resolver.get_task(
-                        dag_id,
-                        run_id,
-                        task_id,
-                        dag_version_id=caller.dag_version_id
-                        if caller and (caller.dag_id, caller.run_id) == 
(dag_id, run_id)
-                        else None,
-                    )
-            if task is not None and enclosing_loop(task) is not None:
-                raise ValueError("Prior-date lookups are not supported for 
tasks inside a loop")
-            return _XComRead(
-                read(region_id=SENTINEL_REGION_ID, include_node_regions=True, 
map_indexes=map_index)
+    with create_session(scoped=False) as session:

Review Comment:
   **[warning]** The common read never touches the DagBag, but it still pays 
the worker thread and the second connection.
   
   For a task that is not in a loop or a mapped region (and without 
`previous_iteration`), `_build_xcom_read` only does this before it returns:
   
   - `_reject_archived_caller`: one `SELECT` on `task_instance`;
   - `resolver.has_regions(...)`: one `SELECT EXISTS` on `task_instance` 
(`task_coordinates.py:571-582`), no Dag lookup;
   - `XComModel.get_many(...)`: builds a statement, no query.
   
   So on that path nothing can deserialize a Dag, which is the reason given for 
the worker thread. The request still does a thread hop, a sync checkout with 
its pre-ping, two of its three queries on the worker thread, and then an async 
checkout with a second pre-ping for the one `SELECT` that is awaited. That fits 
the small-value rows of the benchmark table (own small value on PostgreSQL: 6.2 
ms on `main`, 8.7 and 7.3 ms here), and it is the case most XCom reads are. The 
goal in #67799 is that database waits leave the threadpool; here most of them 
stay in it, and the "two connections per request" note in the description 
applies to every read.
   
   Proposal: run the archived-caller check and the `has_regions` query on the 
route's `AsyncSession` and go to `run_in_threadpool` only when the task has 
regions or `previous_iteration` is set. The common read would then use one 
connection and no worker thread, and the bridge stays for the path that needs 
it. The same holds for `_find_writer_id` (line 892): a task writing its own 
XCom returns after `session.get` and `get_public_map_index`, which runs no 
query when the task is not in a region, before the resolver is built.
   
   The cost is an async form of those two queries next to the sync ones the 
resolver keeps. I have not measured this; if you prefer to keep this PR a plain 
conversion, a follow-up is fine with me, but then I would say in the 
description that the common path is the one that pays for the bridge.



##########
airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py:
##########
@@ -129,8 +131,11 @@ def has_xcom_access(
         _team_name_for_ti_stmt,
     )
 
-    requester_team = session.scalar(_team_name_for_ti_stmt(token.id))
-    target_team = session.scalar(_team_name_for_dag_stmt(dag_id))
+    # Cadwyn solves this router dependency in two passes, and the first pass's 
request session would stay
+    # checked out until the response; a session of its own returns the 
connection before the producer lookup.
+    async with create_session_async() as session:

Review Comment:
   **[nit]** With `[core] multi_team` on, this check still runs in both of 
Cadwyn's dependency passes.
   
   The comment above says Cadwyn solves this router dependency twice. Now that 
the check opens its own session, each request checks out two async connections 
and runs the two team queries twice (your reply on the second thread counts 3 
async checkouts per multi-team read). `_resolve_trigger_token` below already 
skips the first pass with `Depends(current_dependency_solver)` and 
`dependency_solver != "cadwyn"`. The same guard here would bring a multi-team 
request back to one access check, and the 403 would still be raised in the pass 
the endpoint runs with.



-- 
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