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

jason810496 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 85aa9b621f3 Defer Cadwyn import to keep FastAPI off the Task SDK 
worker path (#69016)
85aa9b621f3 is described below

commit 85aa9b621f361c461cb858a39f14ff10966d8fe6
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Fri Jun 26 19:52:45 2026 +0900

    Defer Cadwyn import to keep FastAPI off the Task SDK worker path (#69016)
    
    The Task SDK supervisor imports the execution-time schema package on every
    worker, and that package imported cadwyn at module load. cadwyn pulls in
    FastAPI, Starlette and Jinja2, which are only needed on the 
foreign-language-SDK
    schema-migration path and never for a pure-Python worker. Because the 
supervisor
    is imported lazily after the Celery prefork, each pool worker paid this 
import
    in private (non copy-on-write) pages, raising worker memory on Python 3.14.
    
    Defer the cadwyn import behind a cached bundle accessor and into 
_versioned_class
    so importing the schema package no longer loads FastAPI. The foreign-SDK 
path is
    unchanged; a regression test asserts the worker import graph stays 
cadwyn-free.
---
 .../airflow/sdk/execution_time/schema/__init__.py  | 13 +++++++-
 .../airflow/sdk/execution_time/schema/migrator.py  |  9 ++++--
 .../sdk/execution_time/schema/versions/__init__.py | 36 +++++++++++++++++++---
 .../execution_time/schema/test_migrator.py         | 35 +++++++++++++++++++++
 4 files changed, 85 insertions(+), 8 deletions(-)

diff --git a/task-sdk/src/airflow/sdk/execution_time/schema/__init__.py 
b/task-sdk/src/airflow/sdk/execution_time/schema/__init__.py
index 1963e8b8e66..585bc84a33e 100644
--- a/task-sdk/src/airflow/sdk/execution_time/schema/__init__.py
+++ b/task-sdk/src/airflow/sdk/execution_time/schema/__init__.py
@@ -52,7 +52,6 @@ from airflow.sdk.execution_time.schema.migrator import (
     SchemaVersionMigrator,
     get_schema_version_migrator,
 )
-from airflow.sdk.execution_time.schema.versions import bundle
 
 if TYPE_CHECKING:
     from collections.abc import Iterable
@@ -119,6 +118,18 @@ def resolve_body_class(body: Any) -> type[BaseModel] | 
None:
     return registered_models_by_name().get(name)
 
 
+def __getattr__(name: str) -> Any:
+    # Re-export ``bundle`` lazily so importing this package does not import 
``cadwyn`` (-> FastAPI/
+    # Starlette) until something actually accesses ``schema.bundle``. The Task 
SDK supervisor imports
+    # this package on every worker, but only the foreign-language-SDK 
migration path touches the
+    # bundle, so a pure-Python worker never pays the cadwyn import.
+    if name == "bundle":
+        from airflow.sdk.execution_time.schema.versions import get_bundle
+
+        return get_bundle()
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
 __all__ = [
     "SchemaVersionMigrator",
     "bundle",
diff --git a/task-sdk/src/airflow/sdk/execution_time/schema/migrator.py 
b/task-sdk/src/airflow/sdk/execution_time/schema/migrator.py
index 9560f6de419..d2a7a9e305f 100644
--- a/task-sdk/src/airflow/sdk/execution_time/schema/migrator.py
+++ b/task-sdk/src/airflow/sdk/execution_time/schema/migrator.py
@@ -33,7 +33,6 @@ import functools
 from typing import TYPE_CHECKING, Any, cast
 
 import attrs
-from cadwyn import generate_versioned_models
 
 if TYPE_CHECKING:
     from cadwyn import VersionBundle
@@ -103,6 +102,11 @@ class SchemaVersionMigrator:
 
     def _versioned_class(self, version: str, model: type[BaseModel]) -> 
type[BaseModel]:
         """Get the Cadwyn-generated class for *model* at *version*."""
+        # Imported here, not at module scope, so importing this module (and 
the parent ``schema``
+        # package the Task SDK supervisor pulls in) does not import ``cadwyn`` 
-> FastAPI/Starlette.
+        # Only the foreign-language-SDK migration path reaches this; a 
pure-Python worker never does.
+        from cadwyn import generate_versioned_models
+
         if self._versioned_models is None:
             self._versioned_models = generate_versioned_models(self._bundle)
         return self._versioned_models[version][model]
@@ -207,8 +211,9 @@ def get_schema_version_migrator() -> SchemaVersionMigrator:
     no per-call state, so concurrent callers can share a single
     instance safely.
     """
-    from airflow.sdk.execution_time.schema.versions import bundle
+    from airflow.sdk.execution_time.schema.versions import get_bundle
 
+    bundle = get_bundle()
     return SchemaVersionMigrator(bundle=bundle, 
supervisor_version=bundle.versions[0].value)
 
 
diff --git 
a/task-sdk/src/airflow/sdk/execution_time/schema/versions/__init__.py 
b/task-sdk/src/airflow/sdk/execution_time/schema/versions/__init__.py
index d8ae4e4580e..9491a8993fd 100644
--- a/task-sdk/src/airflow/sdk/execution_time/schema/versions/__init__.py
+++ b/task-sdk/src/airflow/sdk/execution_time/schema/versions/__init__.py
@@ -17,9 +17,35 @@
 
 from __future__ import annotations
 
-from cadwyn import HeadVersion, Version, VersionBundle
+import functools
+from typing import TYPE_CHECKING, Any
 
-bundle = VersionBundle(
-    HeadVersion(),
-    Version("2026-06-16"),
-)
+if TYPE_CHECKING:
+    from cadwyn import VersionBundle
+
+
[email protected]
+def get_bundle() -> VersionBundle:
+    """
+    Build the supervisor schema ``VersionBundle`` lazily.
+
+    The ``cadwyn`` import is deferred to here so importing this module -- and 
the parent ``schema``
+    package, which the Task SDK supervisor pulls in on every worker -- does 
not import ``cadwyn``,
+    which drags in FastAPI/Starlette/Jinja2. The bundle (and its cadwyn 
machinery) is only needed on
+    the foreign-language-SDK migration path; a pure-Python worker never builds 
it. Cached so the
+    bundle is constructed once per process.
+    """
+    from cadwyn import HeadVersion, Version, VersionBundle
+
+    return VersionBundle(
+        HeadVersion(),
+        Version("2026-06-16"),
+    )
+
+
+def __getattr__(name: str) -> Any:
+    # Keep ``from ...versions import bundle`` working for existing callers 
without forcing the
+    # cadwyn import at module load (only resolves the bundle when ``bundle`` 
is actually accessed).
+    if name == "bundle":
+        return get_bundle()
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/task-sdk/tests/task_sdk/execution_time/schema/test_migrator.py 
b/task-sdk/tests/task_sdk/execution_time/schema/test_migrator.py
index e051d046078..05218aded3d 100644
--- a/task-sdk/tests/task_sdk/execution_time/schema/test_migrator.py
+++ b/task-sdk/tests/task_sdk/execution_time/schema/test_migrator.py
@@ -28,6 +28,8 @@ schema diverges from head.
 
 from __future__ import annotations
 
+import subprocess
+import sys
 from typing import Literal
 
 import pytest
@@ -334,3 +336,36 @@ class TestResolveBodyClass:
     def test_extra_fields_in_body_do_not_affect_resolution(self, 
mock_registry):
         body = {"type": "_LangSdkRequest", "ti_id": "t1", "field_a": 7}
         assert resolve_body_class(body) is _LangSdkRequest
+
+
+class TestLazyCadwynImport:
+    """``cadwyn`` (which imports FastAPI/Starlette/Jinja2) must stay off the 
worker import path.
+
+    The Task SDK supervisor imports the schema package on every Celery pool 
worker, but ``cadwyn`` is
+    only needed on the foreign-language-SDK migration path, so its import is 
deferred. Each check runs
+    in a fresh interpreter because ``sys.modules`` is process-global and other 
tests import cadwyn.
+    """
+
+    @pytest.mark.parametrize(
+        "module",
+        [
+            "airflow.sdk.execution_time.schema",
+            "airflow.sdk.execution_time.supervisor",
+        ],
+    )
+    def test_importing_worker_path_does_not_load_cadwyn(self, module):
+        code = (
+            f"import sys; import {module}; "
+            "assert 'cadwyn' not in sys.modules, sorted(m for m in sys.modules 
if 'cadwyn' in m); "
+            "assert 'fastapi' not in sys.modules, 'fastapi was imported'"
+        )
+        subprocess.run([sys.executable, "-c", code], check=True, 
capture_output=True, text=True)
+
+    def test_accessing_bundle_loads_cadwyn(self):
+        # The foreign-SDK migration path does need cadwyn; accessing the 
bundle is where it loads.
+        code = (
+            "import sys; from airflow.sdk.execution_time.schema import bundle; 
"
+            "assert bundle.versions, 'bundle should be built'; "
+            "assert 'cadwyn' in sys.modules, 'cadwyn should load when the 
bundle is accessed'"
+        )
+        subprocess.run([sys.executable, "-c", code], check=True, 
capture_output=True, text=True)

Reply via email to