This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun 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 be41cb4031e UI: Serve grid TI summaries from shared cached DagBag 
(#65775)
be41cb4031e is described below

commit be41cb4031e38050314c9c9605885edee5dd1df0
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Mon Apr 27 17:54:17 2026 +0200

    UI: Serve grid TI summaries from shared cached DagBag (#65775)
    
    The grid TI summaries streaming endpoint used a per-request dict to
    dedupe serialized Dag loads within a single response. Switch to the
    app-wide DBDagBag so the serialized Dag is cached across requests (via
    the LRU+TTL configured by [api] dag_cache_size / dag_cache_ttl) and
    the in-memory representation is the lightweight SerializedDAG instead
    of the SerializedDagModel ORM row holding the full JSON blob.
---
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |  6 +-
 .../airflow/api_fastapi/core_api/routes/ui/grid.py | 82 ++++++++++------------
 .../ui/openapi-gen/queries/ensureQueryData.ts      |  5 +-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |  5 +-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |  5 +-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |  5 +-
 .../ui/openapi-gen/requests/services.gen.ts        |  5 +-
 7 files changed, 55 insertions(+), 58 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index ebf0975307a..364ecd38f95 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -1427,9 +1427,11 @@ paths:
         progressively without waiting for all runs to complete.
 
 
-        The serialized Dag structure is loaded once and reused for all runs 
that
+        The serialized Dag structure is served from the app-wide ``DBDagBag`` 
cache
 
-        share the same ``dag_version_id``, avoiding repeated deserialization.'
+        (keyed by ``dag_version_id``), which avoids repeated deserialization 
across
+
+        runs of the same version *and* across requests.'
       operationId: get_grid_ti_summaries_stream
       security:
       - OAuth2PasswordBearer: []
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py
index d266dcf76d7..d3ddf8a2a52 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py
@@ -19,6 +19,7 @@ from __future__ import annotations
 
 from collections.abc import Generator, Iterable
 from typing import TYPE_CHECKING, Annotated, Any
+from uuid import UUID
 
 import structlog
 from fastapi import Depends, HTTPException, Query, status
@@ -27,6 +28,7 @@ from sqlalchemy import exists, select
 from sqlalchemy.orm import Session, joinedload, load_only
 
 from airflow.api_fastapi.auth.managers.models.resource_details import 
DagAccessEntity
+from airflow.api_fastapi.common.dagbag import DagBagDep
 from airflow.api_fastapi.common.db.common import SessionDep, paginated_select
 from airflow.api_fastapi.common.db.dag_runs import attach_dag_versions_to_runs
 from airflow.api_fastapi.common.parameters import (
@@ -69,6 +71,10 @@ from airflow.models.serialized_dag import SerializedDagModel
 from airflow.models.taskinstance import TaskInstance
 from airflow.utils.session import create_session
 
+if TYPE_CHECKING:
+    from airflow.models.dagbag import DBDagBag
+    from airflow.serialization.definitions.dag import SerializedDAG
+
 log = structlog.get_logger(logger_name=__name__)
 grid_router = AirflowRouter(prefix="/grid", tags=["Grid"])
 
@@ -90,32 +96,28 @@ def _get_latest_serdag(dag_id, session):
     return serdag
 
 
-def _get_serdag(dag_id, dag_version_id, session) -> SerializedDagModel | None:
-    # this is a simplification - we account for structure based on the first 
task
-    version = session.scalar(
-        select(DagVersion)
-        .where(DagVersion.id == dag_version_id)
-        .options(joinedload(DagVersion.serialized_dag))
+def _get_serdag(
+    dag_bag: DBDagBag,
+    dag_id: str,
+    dag_version_id: UUID | str | None,
+    session: Session,
+) -> SerializedDAG | None:
+    """Resolve the serialized Dag for a grid TI summary via the shared 
(cached) ``DBDagBag``."""
+    if dag_version_id is not None:
+        serdag = dag_bag.get_dag(dag_version_id, session=session)
+        if serdag is None:
+            log.error("No serialized dag found", dag_id=dag_id, 
version_id=dag_version_id)
+        return serdag
+
+    # Fallback: pre-3.0 upgrade — pick the oldest DagVersion for this dag_id.
+    oldest_version_id = session.scalar(
+        select(DagVersion.id).where(DagVersion.dag_id == 
dag_id).order_by(DagVersion.id).limit(1)
     )
-    if not version:
-        version = session.scalar(
-            select(DagVersion)
-            .where(
-                DagVersion.dag_id == dag_id,
-            )
-            .options(joinedload(DagVersion.serialized_dag))
-            .order_by(DagVersion.id)  # ascending cus this is mostly for 
pre-3.0 upgrade
-            .limit(1)
-        )
-    if not version:
+    if oldest_version_id is None:
         return None
-    if not (serdag := version.serialized_dag):
-        log.error(
-            "No serialized dag found",
-            dag_id=dag_id,
-            version_id=version.id,
-            version_number=version.version_number,
-        )
+    serdag = dag_bag.get_dag(oldest_version_id, session=session)
+    if serdag is None:
+        log.error("No serialized dag found", dag_id=dag_id, 
version_id=oldest_version_id)
     return serdag
 
 
@@ -356,12 +358,12 @@ def _build_ti_summaries(
     task_instances: Iterable[Any],
     session: Session,
     *,
-    serdag: SerializedDagModel | None = None,
-    serdag_cache: dict[Any, SerializedDagModel | None] | None = None,
+    dag_bag: DBDagBag,
 ) -> dict[str, Any] | None:
     ti_details: dict[str, GridNodeAgg] = {}
     dag_version_id = None
     for ti in task_instances:
+        # this is a simplification - we account for structure based on the 
first task
         dag_version_id = dag_version_id or ti.dag_version_id
         summary = ti_details.get(ti.task_id)
         if summary is None:
@@ -374,28 +376,15 @@ def _build_ti_summaries(
         )
     if not ti_details:
         return None
-    if serdag is None:
-        if serdag_cache is not None:
-            if dag_version_id not in serdag_cache:
-                serdag_cache[dag_version_id] = _get_serdag(
-                    dag_id=dag_id,
-                    dag_version_id=dag_version_id,
-                    session=session,
-                )
-            serdag = serdag_cache[dag_version_id]
-        else:
-            serdag = _get_serdag(
-                dag_id=dag_id,
-                dag_version_id=dag_version_id,
-                session=session,
-            )
+
+    serdag = _get_serdag(dag_bag, dag_id, dag_version_id, session)
     if TYPE_CHECKING:
         assert serdag
 
     def get_node_summaries() -> Iterable[dict[str, Any]]:
         yielded_task_ids: set[str] = set()
         for node, _ in _find_aggregates(
-            node=serdag.dag.task_group,
+            node=serdag.task_group,
             parent_node=None,
             ti_details=ti_details,
         ):
@@ -457,6 +446,7 @@ def _build_ti_summaries(
 )
 def get_grid_ti_summaries_stream(
     dag_id: str,
+    dag_bag: DagBagDep,
     run_ids: Annotated[list[str] | None, Query()] = None,
 ) -> StreamingResponse:
     """
@@ -466,8 +456,9 @@ def get_grid_ti_summaries_stream(
     run's task instances have been processed, so the client can render columns
     progressively without waiting for all runs to complete.
 
-    The serialized Dag structure is loaded once and reused for all runs that
-    share the same ``dag_version_id``, avoiding repeated deserialization.
+    The serialized Dag structure is served from the app-wide ``DBDagBag`` cache
+    (keyed by ``dag_version_id``), which avoids repeated deserialization across
+    runs of the same version *and* across requests.
     """
 
     def _generate() -> Generator[str, None, None]:
@@ -476,7 +467,6 @@ def get_grid_ti_summaries_stream(
         # database connection open for the entire stream duration.
         # See https://github.com/apache/airflow/issues/65010.
 
-        serdag_cache: dict[Any, SerializedDagModel | None] = {}
         for run_id in run_ids or []:
             with create_session(scoped=False) as session:
                 tis = session.execute(
@@ -499,7 +489,7 @@ def get_grid_ti_summaries_stream(
                     run_id,
                     tis,
                     session,
-                    serdag_cache=serdag_cache,
+                    dag_bag=dag_bag,
                 )
             if summary is None:
                 continue
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index 2808db355cf..1958854733a 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -1903,8 +1903,9 @@ export const ensureUseGridServiceGetGridRunsData = 
(queryClient: QueryClient, {
 * run's task instances have been processed, so the client can render columns
 * progressively without waiting for all runs to complete.
 *
-* The serialized Dag structure is loaded once and reused for all runs that
-* share the same ``dag_version_id``, avoiding repeated deserialization.
+* The serialized Dag structure is served from the app-wide ``DBDagBag`` cache
+* (keyed by ``dag_version_id``), which avoids repeated deserialization across
+* runs of the same version *and* across requests.
 * @param data The data for the request.
 * @param data.dagId
 * @param data.runIds
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index e1782283f52..71436325670 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -1903,8 +1903,9 @@ export const prefetchUseGridServiceGetGridRuns = 
(queryClient: QueryClient, { da
 * run's task instances have been processed, so the client can render columns
 * progressively without waiting for all runs to complete.
 *
-* The serialized Dag structure is loaded once and reused for all runs that
-* share the same ``dag_version_id``, avoiding repeated deserialization.
+* The serialized Dag structure is served from the app-wide ``DBDagBag`` cache
+* (keyed by ``dag_version_id``), which avoids repeated deserialization across
+* runs of the same version *and* across requests.
 * @param data The data for the request.
 * @param data.dagId
 * @param data.runIds
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index 4c2bf1a86c6..f7c589343fc 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -1903,8 +1903,9 @@ export const useGridServiceGetGridRuns = <TData = 
Common.GridServiceGetGridRunsD
 * run's task instances have been processed, so the client can render columns
 * progressively without waiting for all runs to complete.
 *
-* The serialized Dag structure is loaded once and reused for all runs that
-* share the same ``dag_version_id``, avoiding repeated deserialization.
+* The serialized Dag structure is served from the app-wide ``DBDagBag`` cache
+* (keyed by ``dag_version_id``), which avoids repeated deserialization across
+* runs of the same version *and* across requests.
 * @param data The data for the request.
 * @param data.dagId
 * @param data.runIds
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index a6d1d118db4..2a29df44c2a 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -1903,8 +1903,9 @@ export const useGridServiceGetGridRunsSuspense = <TData = 
Common.GridServiceGetG
 * run's task instances have been processed, so the client can render columns
 * progressively without waiting for all runs to complete.
 *
-* The serialized Dag structure is loaded once and reused for all runs that
-* share the same ``dag_version_id``, avoiding repeated deserialization.
+* The serialized Dag structure is served from the app-wide ``DBDagBag`` cache
+* (keyed by ``dag_version_id``), which avoids repeated deserialization across
+* runs of the same version *and* across requests.
 * @param data The data for the request.
 * @param data.dagId
 * @param data.runIds
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index 1bb87669cee..a92d59a7be8 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -4415,8 +4415,9 @@ export class GridService {
      * run's task instances have been processed, so the client can render 
columns
      * progressively without waiting for all runs to complete.
      *
-     * The serialized Dag structure is loaded once and reused for all runs that
-     * share the same ``dag_version_id``, avoiding repeated deserialization.
+     * The serialized Dag structure is served from the app-wide ``DBDagBag`` 
cache
+     * (keyed by ``dag_version_id``), which avoids repeated deserialization 
across
+     * runs of the same version *and* across requests.
      * @param data The data for the request.
      * @param data.dagId
      * @param data.runIds

Reply via email to