Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9c6cd0e
Read JAR manifests per the JAR specification
jason810496 Sep 28, 2026
1f361c2
Build the JVM command that parses a Dag JAR
jason810496 Sep 28, 2026
c1a0bd2
Parse native Java Dags with their coordinator
jason810496 Sep 28, 2026
f6b87a7
Treat an unreadable JAR like one without a manifest
jason810496 Sep 28, 2026
bb23794
Test the Java Dag source placeholder for a JAR without a manifest
jason810496 Sep 28, 2026
1e85316
Name the JAR manifest attributes the Java coordinator reads
jason810496 Sep 28, 2026
a28bdee
Document native Java Dag parsing
jason810496 Sep 28, 2026
45761df
Run a task on its run's version when dag_bundle_name names its bundle
jason810496 Oct 2, 2026
801e5a1
Pass the task's Dag file to the coordinator's command builder
jason810496 Oct 2, 2026
4c9eaf9
Report why a Java Dag JAR cannot be read
jason810496 Oct 2, 2026
e6933d8
Run the JAR a native Java Dag was parsed from
jason810496 Oct 2, 2026
1e5f033
Reject a Java Dag JAR whose SDK cannot answer a parse
jason810496 Oct 2, 2026
cb7ff03
Stop promising that Java dependency JARs are not parsed
jason810496 Oct 2, 2026
2f3c1b7
Keep the Java parse_dag test from changing the pytest worker
jason810496 Oct 2, 2026
f950fb8
Hand out the Java Dag importer through get_dag_importer
jason810496 Oct 2, 2026
3cd507c
Read Java Dag sources from the bundle's sources.json index
jason810496 Oct 2, 2026
e63ffc3
Let the Java Dag importer find its coordinator
jason810496 Oct 3, 2026
d1ffa7d
Fail a task cleanly when its runtime cannot be launched
jason810496 Oct 3, 2026
03ac851
Run a native Dag's task from its own Dag file
jason810496 Oct 3, 2026
b5f144d
Fail a native task cleanly when its file or coordinator class cannot …
jason810496 Oct 3, 2026
7a243c1
Say tasks list runs a native Java Dag's main class
jason810496 Oct 3, 2026
3b6a9d0
Say a JRE parses native Java Dags, and keep main's bundle test mocks
jason810496 Oct 5, 2026
37759fb
Put the JAR a Dag was parsed from first on its classpath
jason810496 Oct 6, 2026
9f08bce
Keep a JAR whose manifest cannot be read, not only for OSError and Ba…
jason810496 Oct 6, 2026
8ecee9e
Let the first executable JAR's Main-Class win, not the last one found
jason810496 Oct 6, 2026
bce7efc
Say the native Java ignore bullet applies with one coordinator too
jason810496 Oct 6, 2026
87236e5
Assert execute_task never asks for the parsing coordinator
jason810496 Oct 6, 2026
705c315
Autospec the bundles this test class mocks
jason810496 Oct 6, 2026
d243b2b
Move a deferred secrets_masker import to the top of the module
jason810496 Oct 6, 2026
cab56f4
Show both .airflowignore cases and syntaxes for the task-handler JAR
jason810496 Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 128 additions & 2 deletions airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,32 @@ the Dag processor resolve ``task_handler_bundle_name`` through it, and wherever
it is rejected if the name is missing there. The Dag processor still receives ``sales_pipeline.py`` through
the separate Dag delivery process.

A Dag processor with this ``[sdk]`` configuration also parses the executable JARs of every Dag bundle,
and needs a JRE to do so (see :ref:`java-sdk/native-dag-parsing`). Keep it from parsing a bundle's JARs
with that bundle's ``.airflowignore``, whose pattern syntax follows ``[core] dag_ignore_file_syntax``.

With ``task_handler_bundle_name`` set to its own bundle, as above, ``java-task-handlers`` holds only the
JARs the Python Dag's tasks run, so ignoring everything in it is safe:

.. code-block:: bash

# [core] dag_ignore_file_syntax = glob (the default)
echo '*' > /opt/airflow/jars/sales-pipeline/.airflowignore
Comment thread
jason810496 marked this conversation as resolved.

# [core] dag_ignore_file_syntax = regexp; a bare * is not a valid pattern and is dropped
echo '.' > /opt/airflow/jars/sales-pipeline/.airflowignore

With ``task_handler_bundle_name`` unset, the JAR sits in the same Dag bundle as ``sales_pipeline.py``, so
ignore only the JAR, not the whole bundle:

.. code-block:: bash

# [core] dag_ignore_file_syntax = glob (the default)
echo '*.jar' >> /opt/airflow/dags/.airflowignore

# [core] dag_ignore_file_syntax = regexp
echo '\.jar$' >> /opt/airflow/dags/.airflowignore

After Airflow has parsed the Dag, trigger it from the UI or command line:

.. code-block:: bash
Expand Down Expand Up @@ -606,6 +632,69 @@ Durations and date-times are ISO-8601 strings in annotations (``retryDelay = "PT
``java.time.OffsetDateTime`` values in ``config`` calls. An unknown key or a mismatched value type
fails the build for an annotation, and the ``config`` call itself for an object.

.. _java-sdk/native-dag-parsing:

Parsing native Java Dags
~~~~~~~~~~~~~~~~~~~~~~~~

To have Airflow parse the Dags a bundle JAR declares,
put the JAR in a Dag bundle and configure a :class:`~airflow.sdk.coordinators.java.JavaCoordinator`.
The Dag processor runs the JAR's main class to list its Dags, so it needs a Java executable, as the workers do:

.. code-block:: ini

[sdk]
coordinators = {
"java-native": {
"classpath": "airflow.sdk.coordinators.java.JavaCoordinator",
"kwargs": {"java_executable": "/usr/lib/jvm/java-17-openjdk/bin/java"}
}
}
queue_to_coordinator = {"java-native": "java-native"}

Once a ``JavaCoordinator`` is configured, the Dag processor parses the executable JARs of every Dag bundle,
so it needs this ``[sdk]`` configuration and a JRE. With one ``JavaCoordinator``, it parses them all.
With several, map each Dag bundle that holds native Java Dags to one of them in ``[sdk] dag_bundle_to_coordinator``.
A JAR in a bundle that has no entry, or an entry that names no ``JavaCoordinator``, fails to parse with an import error:

.. code-block:: ini

[sdk]
dag_bundle_to_coordinator = {"dags-folder": "java-native"}

A Dag bundle that holds only the JARs that Python Dags' tasks run should list ``*`` in its ``.airflowignore``
either way: with one ``JavaCoordinator`` its JARs are still parsed on every loop, and a JAR built with a Java
SDK older than schema ``2026-10-30``, every released one today, fails to parse as an import error. With
several Java coordinators, an unmapped bundle's JARs fail to parse too.

* Every JAR in the bundle whose manifest sets ``Main-Class`` is parsed. Each Dag its main class
Comment thread
jason810496 marked this conversation as resolved.
declares, through ``Bundle.register`` of a ``DagDef`` or an ``@Builder.Dag`` class, is stored with
that JAR as its file. Task handlers for a Python Dag are not Dags. A JAR without ``Main-Class`` is
skipped, but many dependency JARs set one (the PostgreSQL JDBC driver and H2 do, for example), so a
thin bundle should set ``main_class`` or list its dependency JARs in ``.airflowignore``.
* Parsing needs a JAR built with a Java SDK whose supervisor schema version (the
``Airflow-Supervisor-Schema-Version`` manifest attribute) is ``2026-10-30`` or later. An older JAR
fails to parse, so list it in ``.airflowignore``; its tasks still find it, because tasks do not read
``.airflowignore``.
* Do not declare a Dag in Java that a Python file in the same bundle also defines.
* Keep one executable JAR per bundle, or set ``main_class``, so that only JARs with that
``Main-Class`` are parsed. List JARs that should not be parsed in ``.airflowignore``.
* Two JARs in the bundle that set the same ``Main-Class`` fail to parse, because the JVM would load
the classes of only one of them. Keep one in the bundle.
* Set ``queue`` on every task, with ``@Builder.Task(queue = "java-native")`` or
``TaskDef.config("queue", "java-native")``, so it runs on the coordinator's queue. There is no
Dag-level queue yet.
* A task runs the JAR of its Dag on the ``JavaCoordinator`` that its queue routes to,
which need not be the one that parsed the JAR. For example, a queue can route to a coordinator that uses another JDK.
A task whose queue routes to another kind of coordinator fails without retries. A task whose JAR is missing,
or that the coordinator cannot run (for example, because ``main_class`` does not match the JAR's ``Main-Class``),
fails and retries while it has retries left.
* The Code view shows the source of the JAR's main class, which the Gradle plugin packs into the JAR.
* Cluster policies (``dag_policy``, ``task_policy``) are not applied to a native Java Dag.
* ``airflow dags reserialize`` does not store the Dags of a JAR, which only the Dag processor stores.
``airflow dags test``, ``tasks test`` and ``tasks render`` refuse a native Java Dag.
``airflow tasks list`` lists its tasks by running the JAR's main class, so it needs a JRE.

.. _java-sdk/logging:

Logging
Expand Down Expand Up @@ -873,6 +962,11 @@ The ``build/bundle/`` directory contains all required JAR(s). Copy or mount it i
:class:`~airflow.sdk.coordinators.java.JavaCoordinator` scans that Dag bundle recursively and builds the
classpath automatically.

The plugin also packs the source file of ``mainClass``, and of each class that declares a Dag in Java,
into the bundle JAR, so the Airflow UI can show the source of a native Java Dag (see
:ref:`java-sdk/native-dag-parsing`). To find those classes, the plugin runs ``mainClass`` once at build
time. If that run fails, the build logs a warning and packs only the ``mainClass`` source.

.. note::

You only need the ``annotationProcessor`` entry if you use the annotation-based API. It is not needed for
Expand Down Expand Up @@ -1046,6 +1140,32 @@ directory into the Dag bundle named by ``task_handler_bundle_name``.
Unlike the Gradle plugin, Maven has no equivalent of the ``verifyBundleMainClass`` validation step.
A wrong ``<mainClass>`` value will not be caught until runtime.

To show the source of a native Java Dag in the Airflow UI, pack the main class's source file under
``META-INF/airflow/sources/`` with an index that names it, ``src/main/resources/META-INF/airflow/sources.json``:

.. code-block:: json

{"entrypoint_path": "com/example/Main.java"}

.. code-block:: xml

<build>
<resources>
<!-- Declaring resources replaces the default, so keep it. -->
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java/com/example</directory>
<includes><include>Main.java</include></includes>
<targetPath>META-INF/airflow/sources/com/example</targetPath>
</resource>
</resources>
</build>

Then add ``<Airflow-Java-SDK-Sources>META-INF/airflow/sources.json</Airflow-Java-SDK-Sources>`` to the
``manifestEntries`` of ``maven-shade-plugin`` or ``maven-jar-plugin`` shown above.

.. _java-sdk/coordinator-config:

:class:`~airflow.sdk.coordinators.java.JavaCoordinator` configuration
Expand Down Expand Up @@ -1077,8 +1197,10 @@ All ``kwargs`` in the ``coordinators`` config entry are passed to the
* - ``main_class``
- *(auto-detect)*
- Explicit entry-point class. If omitted, the coordinator scans the Dag bundle for a JAR
whose manifest sets ``Main-Class``. If more than one JAR in that Dag bundle sets it, which
one runs is non-deterministic, so set ``main_class`` explicitly in that case.
whose manifest sets ``Main-Class``. If more than one JAR in that Dag bundle sets it, the first
by path is used, so set ``main_class`` explicitly in that case.
A task of a native Java Dag runs the JAR the Dag was parsed from.
When the coordinator parses native Java Dags, only JARs with this ``Main-Class`` are parsed.
* - ``task_startup_timeout``
- ``10.0``
- Seconds to wait for the JVM subprocess to connect after launch. Increase this if your
Expand All @@ -1098,6 +1220,10 @@ All ``kwargs`` in the ``coordinators`` config entry are passed to the
dependencies. To isolate conflicting dependency versions, put the handlers in a second Dag bundle
served by a second coordinator on its own queue.

A task of a native Java Dag ignores ``task_handler_bundle_name``:
it runs the JAR of its Dag from the Dag's own bundle, at the version the run was created with.
See :ref:`java-sdk/native-dag-parsing`.

.. note::

The ``[sdk]`` configuration is read at startup, so changes to ``coordinators`` or
Expand Down
4 changes: 3 additions & 1 deletion task-sdk/src/airflow/sdk/coordinators/_dag_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
from airflow.sdk.coordinators._subprocess import SubprocessCoordinator
from airflow.sdk.importers.base import DagDefinition

COORDINATOR_DAG_IMPORTERS: Final[tuple[str, ...]] = ()
COORDINATOR_DAG_IMPORTERS: Final[tuple[str, ...]] = (
"airflow.sdk.coordinators.java._dag_importer.JavaDagImporter",
)
"""
The classpaths of the :class:`CoordinatorDagImporter` subclasses a Dag bundle's registry may hold.

Expand Down
Loading