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

pierrejeambrun pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-2-test by this push:
     new 7249b6cc5b8 UI: Serve grid TI summaries from shared cached DagBag 
(#65775) (#65966)
7249b6cc5b8 is described below

commit 7249b6cc5b81224441acb458e0b07313b0975ed4
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Tue Apr 28 09:58:46 2026 +0200

    UI: Serve grid TI summaries from shared cached DagBag (#65775) (#65966)
    
    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.
    
    (cherry picked from commit be41cb4031e38050314c9c9605885edee5dd1df0)
---
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |  6 +-
 .../airflow/api_fastapi/core_api/routes/ui/grid.py | 85 ++++++++++++----------
 .../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, 64 insertions(+), 52 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 0139c7e08d5..e4b4a43abf1 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
@@ -1267,9 +1267,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 2ee6ea30cb8..a4eb1198c07 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
@@ -20,14 +20,16 @@ from __future__ import annotations
 import collections
 from collections.abc import Generator, Sequence
 from typing import TYPE_CHECKING, Annotated, Any
+from uuid import UUID
 
 import structlog
 from fastapi import Depends, HTTPException, Query, status
 from fastapi.responses import StreamingResponse
 from sqlalchemy import exists, select
-from sqlalchemy.orm import joinedload, load_only, selectinload
+from sqlalchemy.orm import Session, joinedload, load_only, selectinload
 
 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.parameters import (
     QueryDagRunRunTypesFilter,
@@ -70,6 +72,10 @@ from airflow.models.taskinstance import TaskInstance
 from airflow.models.taskinstancehistory import TaskInstanceHistory
 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"])
 
@@ -91,32 +97,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 | 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
 
 
@@ -344,7 +346,12 @@ def get_grid_runs(
 
 
 def _build_ti_summaries(
-    dag_id: str, run_id: str, task_instances: Sequence, session, serdag: 
SerializedDagModel | None = None
+    dag_id: str,
+    run_id: str,
+    task_instances: Sequence,
+    session: Session,
+    *,
+    dag_bag: DBDagBag,
 ) -> dict:
     ti_details: dict = collections.defaultdict(list)
     for ti in task_instances:
@@ -356,19 +363,19 @@ def _build_ti_summaries(
                 "dag_version_number": getattr(ti, "version_number", None),
             }
         )
-    if serdag is None:
-        serdag = _get_serdag(
-            dag_id=dag_id,
-            dag_version_id=task_instances[0].dag_version_id,
-            session=session,
-        )
+    serdag = _get_serdag(
+        dag_bag,
+        dag_id,
+        task_instances[0].dag_version_id,
+        session,
+    )
     if TYPE_CHECKING:
         assert serdag
 
     def get_node_summaries():
         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,
         ):
@@ -430,6 +437,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:
     """
@@ -439,8 +447,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]:
@@ -449,7 +458,6 @@ def get_grid_ti_summaries_stream(
         # released between yields.  This prevents a slow client from holding a
         # database connection open for the entire stream duration.
         # See https://github.com/apache/airflow/issues/65010.
-        serdag_cache: dict = {}
         for run_id in run_ids or []:
             with create_session(scoped=False) as session:
                 tis = session.execute(
@@ -468,10 +476,7 @@ def get_grid_ti_summaries_stream(
                 ).all()
                 if not tis:
                     continue
-                version_id = tis[0].dag_version_id
-                if version_id not in serdag_cache:
-                    serdag_cache[version_id] = _get_serdag(dag_id, version_id, 
session)
-                summary = _build_ti_summaries(dag_id, run_id, tis, session, 
serdag=serdag_cache[version_id])
+                summary = _build_ti_summaries(dag_id, run_id, tis, session, 
dag_bag=dag_bag)
             yield GridTISummaries.model_validate(summary).model_dump_json() + 
"\n"
 
     return StreamingResponse(content=_generate(), 
media_type="application/x-ndjson")
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 55f9930a7c6..95e878c4809 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -1865,8 +1865,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 184e0468f6d..9107c9372c3 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -1865,8 +1865,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 26a0fff8f3c..76621317ac4 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -1865,8 +1865,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 099ccca4095..396d36a8f8a 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -1865,8 +1865,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 3db366caea3..03f2c29a498 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
@@ -4363,8 +4363,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