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 4824280c895 Support `extra` field for Coordinator  (#68694)
4824280c895 is described below

commit 4824280c895ff40b1d0f304b26e5917ab9753be1
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Mon Jun 22 23:21:15 2026 +0900

    Support `extra` field for Coordinator  (#68694)
    
    * Add optional extra metadata to lang-SDK coordinator config entries
    
    Coordinator config entries need a place to carry deployment metadata that
    other components consume without coupling it to the coordinator instance.
    The coordinator itself never receives this metadata; components such as
    KubernetesExecutor read it by queue when launching worker pods.
    
    * Address Wei's comment
---
 .../language-sdks/index.rst                        | 11 ++-
 .../src/airflow/config_templates/config.yml        | 18 ++++-
 .../src/airflow/sdk/execution_time/coordinator.py  | 29 ++++++++
 .../task_sdk/execution_time/test_coordinator.py    | 79 ++++++++++++++++++++++
 4 files changed, 135 insertions(+), 2 deletions(-)

diff --git a/airflow-core/docs/authoring-and-scheduling/language-sdks/index.rst 
b/airflow-core/docs/authoring-and-scheduling/language-sdks/index.rst
index e32a70d0ffc..faba410b325 100644
--- a/airflow-core/docs/authoring-and-scheduling/language-sdks/index.rst
+++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/index.rst
@@ -138,7 +138,8 @@ Coordinators are registered in ``airflow.cfg`` (or via 
environment variables) un
         coordinators = {
             "my-coordinator": {
                 "classpath": "path.to.CoordinatorClass",
-                "kwargs": {}
+                "kwargs": {},
+                "extra": {}
             }
         }
 
@@ -147,6 +148,14 @@ Coordinators are registered in ``airflow.cfg`` (or via 
environment variables) un
     of each coordinator (e.g. :ref:`java-sdk/coordinator-config` for
     :class:`~airflow.sdk.coordinators.java.JavaCoordinator`).
 
+    ``extra`` is an optional object for any additional information you want to 
associate with a
+    coordinator without coupling it to the coordinator instance. The 
coordinator itself never
+    receives it; other components read it as needed. For example, 
KubernetesExecutor reads
+    ``extra.pod_template_file`` to launch a queue's worker pod from a specific 
pod template, and
+    ``extra.worker_container_repository`` + ``extra.worker_container_tag`` to 
override that queue's
+    worker base image (both keys are required), e.g. an image that bundles the 
JVM for a Java
+    coordinator.
+
 ``queue_to_coordinator``
     A JSON object mapping Celery queue names to coordinator names:
 
diff --git a/airflow-core/src/airflow/config_templates/config.yml 
b/airflow-core/src/airflow/config_templates/config.yml
index 86e422451a0..d8bbd8442b6 100644
--- a/airflow-core/src/airflow/config_templates/config.yml
+++ b/airflow-core/src/airflow/config_templates/config.yml
@@ -2045,13 +2045,29 @@ sdk:
         multiple times under different names with different ``kwargs`` (for
         example, two ``JavaCoordinator`` instances pinned to different JDK
         versions).
+
+        An entry may also carry an optional ``extra`` object for additional
+        information associated with the coordinator that the coordinator itself
+        does not receive; other components read it as needed. For example,
+        KubernetesExecutor reads ``extra.pod_template_file`` to launch a 
queue's
+        worker pod from a specific pod template, and
+        ``extra.worker_container_repository`` + ``extra.worker_container_tag`` 
to
+        override the worker base image for that queue (both are required).
       version_added: 3.3.0
       type: string
       example: |
         {
           "jdk-17": {
             "classpath": "airflow.sdk.coordinators.java.JavaCoordinator",
-            "kwargs": {"java_executable": 
"/usr/lib/jvm/java-17-openjdk/bin/java", "jvm_args": ["-Xmx1024m"]}
+            "kwargs": {
+              "java_executable": "/usr/lib/jvm/java-17-openjdk/bin/java",
+              "jvm_args": ["-Xmx1024m"]
+            },
+            "extra": {
+              "pod_template_file": "/opt/airflow/pod_templates/java.yaml",
+              "worker_container_repository": "apache/airflow",
+              "worker_container_tag": "3.3.0"
+            }
           }
         }
       default: ~
diff --git a/task-sdk/src/airflow/sdk/execution_time/coordinator.py 
b/task-sdk/src/airflow/sdk/execution_time/coordinator.py
index e233ab8e4ae..55d1654a7fe 100644
--- a/task-sdk/src/airflow/sdk/execution_time/coordinator.py
+++ b/task-sdk/src/airflow/sdk/execution_time/coordinator.py
@@ -109,6 +109,9 @@ class BaseCoordinator:
 class _CoordinatorSpec(pydantic.BaseModel):
     classpath: str
     kwargs: dict[str, Any] = pydantic.Field(default_factory=dict)
+    # Optional metadata read by other components; kept separate from ``kwargs``
+    # so it is never passed to the coordinator constructor.
+    extra: dict[str, Any] | None = None
 
 
 class _PythonCoordinator(BaseCoordinator):
@@ -184,6 +187,18 @@ class CoordinatorManager:
     routed to a JDK 11 coordinator, and a ``"modern-java"`` queue routed to a
     JDK 17 coordinator).
 
+    A coordinator entry may also carry an optional ``extra`` mapping: metadata
+    that other components read as needed. It is kept separate from ``kwargs`` 
and
+    never passed to the coordinator constructor::
+
+        {
+            "java": {
+                "classpath": "airflow.sdk.coordinators.java.JavaCoordinator",
+                "kwargs": {...},
+                "extra": {"pod_template_file": 
"/opt/airflow/pod_templates/java.yaml"},
+            }
+        }
+
     :meta private:
     """
 
@@ -234,6 +249,20 @@ class CoordinatorManager:
         log.debug("Coordinator found for queue", coordinator=coordinator, 
queue=queue)
         return coordinator
 
+    def extra_for_queue(self, queue: str) -> dict[str, Any] | None:
+        """
+        Return the optional ``extra`` mapping configured for *queue*'s 
coordinator.
+
+        Returns ``None`` when the queue is not routed to a coordinator or its
+        coordinator declares no ``extra``. Only the declarative spec is read; 
the
+        coordinator is never instantiated.
+        """
+        if (key := self._queue_to_coordinator.get(queue)) is None:
+            return None
+        if (spec := self._coordinator_specs.get(key)) is None:
+            return None
+        return spec.extra
+
 
 @functools.cache
 def get_coordinator_manager() -> CoordinatorManager:
diff --git a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py 
b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py
index 8e3bcfa1021..475b587bf7f 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_coordinator.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_coordinator.py
@@ -41,6 +41,11 @@ class _CoordinatorB(BaseCoordinator):
     pass
 
 
+class _ExplodingCoordinator(BaseCoordinator):
+    def __init__(self):
+        raise RuntimeError("This coordinator must not be instantiated")
+
+
 @pytest.fixture
 def sdk_config(monkeypatch):
     """Set the ``[sdk]`` env vars consumed by 
:meth:`CoordinatorManager.from_config`.
@@ -120,3 +125,77 @@ class TestCoordinatorManager:
         m1 = get_coordinator_manager()
         m2 = get_coordinator_manager()
         assert m1 is m2
+
+    @pytest.mark.parametrize(
+        ("queue", "expected"),
+        [
+            pytest.param(
+                "queue-java",
+                {"pod_template_file": "/opt/airflow/pod_templates/java.yaml"},
+                id="mapped-with-extra",
+            ),
+            pytest.param("queue-go", None, id="mapped-without-extra"),
+            pytest.param("queue-unmapped", None, id="unmapped-queue"),
+        ],
+    )
+    def test_extra_for_queue(self, sdk_config, queue, expected):
+        sdk_config(
+            coordinators=json.dumps(
+                {
+                    "java": {
+                        "classpath": 
f"{_CoordinatorA.__module__}._CoordinatorA",
+                        "extra": {"pod_template_file": 
"/opt/airflow/pod_templates/java.yaml"},
+                    },
+                    "go": {"classpath": 
f"{_CoordinatorB.__module__}._CoordinatorB"},
+                }
+            ),
+            queue_to_coordinator=json.dumps({"queue-java": "java", "queue-go": 
"go"}),
+        )
+        manager = CoordinatorManager.from_config()
+        # Resolving the extra must not instantiate the coordinator.
+        assert manager.extra_for_queue(queue) == expected
+        assert manager._created_coordinators == {}
+
+    def test_extra_not_forwarded_to_constructor(self, sdk_config):
+        """``extra`` is kept separate from ``kwargs`` and never reaches the 
coordinator constructor."""
+        sdk_config(
+            coordinators=json.dumps(
+                {
+                    "java": {
+                        "classpath": 
f"{_CoordinatorA.__module__}._CoordinatorA",
+                        "kwargs": {"label": "java-label"},
+                        "extra": {"pod_template_file": 
"/opt/airflow/pod_templates/java.yaml"},
+                    },
+                }
+            ),
+            queue_to_coordinator=json.dumps({"queue-java": "java"}),
+        )
+        manager = CoordinatorManager.from_config()
+        # _CoordinatorA only accepts ``label``; construction would raise 
TypeError
+        # if ``extra`` were passed through.
+        coordinator = manager.for_queue("queue-java")
+        assert isinstance(coordinator, _CoordinatorA)
+        assert coordinator.label == "java-label"
+        # The extra is still readable from the spec without instantiation cost.
+        assert manager.extra_for_queue("queue-java") == {
+            "pod_template_file": "/opt/airflow/pod_templates/java.yaml"
+        }
+
+    def test_extra_for_queue_does_not_instantiate_coordinator(self, 
sdk_config):
+        """Reading ``extra`` reads only the spec; a failing constructor must 
never run."""
+        sdk_config(
+            coordinators=json.dumps(
+                {
+                    "boom": {
+                        "classpath": 
f"{_ExplodingCoordinator.__module__}._ExplodingCoordinator",
+                        "extra": {"pod_template_file": 
"/opt/airflow/pod_templates/boom.yaml"},
+                    },
+                }
+            ),
+            queue_to_coordinator=json.dumps({"queue-boom": "boom"}),
+        )
+        manager = CoordinatorManager.from_config()
+        assert manager.extra_for_queue("queue-boom") == {
+            "pod_template_file": "/opt/airflow/pod_templates/boom.yaml"
+        }
+        assert manager._created_coordinators == {}

Reply via email to