This is an automated email from the ASF dual-hosted git repository.
jason810496 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new 93a5e20f168 [v3-4-test] Don't retry Java tasks if classpath can't be
built (#76707) (#76719)
93a5e20f168 is described below
commit 93a5e20f168219c64192be4aced6295acc7dc3d4
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Nov 9 08:21:14 2026 +0000
[v3-4-test] Don't retry Java tasks if classpath can't be built (#76707)
(#76719)
A colon inside a Dag bundle, a temporary directory whose path holds one,
and an unsafe directory of classpath links now raise a non-retryable
TaskLaunchError instead of ValueError or PermissionError, which the
coordinator retried although a retry cannot fix them.
(cherry picked from commit e7ca5f1c2d383dccf35b7c38c59eb115f3dbdd7f)
Co-authored-by: Tzu-ping Chung <[email protected]>
---
.../language-sdks/java.rst | 6 ++---
.../airflow/sdk/coordinators/java/coordinator.py | 26 +++++++++++++---------
.../task_sdk/coordinators/java/test_coordinator.py | 10 ++++++---
3 files changed, 25 insertions(+), 17 deletions(-)
diff --git a/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
b/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
index 0cc9981b52d..5039f0e6a23 100644
--- a/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
+++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
@@ -1072,11 +1072,11 @@ The ``java`` command cannot load a classpath containing
paths that use the ``:``
systems). The version directory of a Dag bundle may contain one, for example
when it is named after a
timestamp. The coordinator tries to handle this automatically by reading such
a bundle through a symbolic link
created in a temporary directory. A file or directory *inside* the Dag bundle
still cannot contain the colon
-separator.
+separator. Such a Dag bundle fails to parse, and its tasks fail without
retrying.
This approach should generally work. On the off chance it does not, however,
all such links are located in
-``airflow-java-classpath-<uid>`` under the temporary directory (``$TMPDIR`` on
Linux, usually ``/tmp``).
-Airflow logs should also report which link corresponds to which bundle.
+``airflow-java-classpath-<uid>`` under the temporary directory (``/tmp`` by
default on Linux, or ``$TMPDIR``
+if set). Airflow logs should also report which link corresponds to which
bundle.
Each link is named after the first 16 hexadecimal digits of the SHA-256 hash
of the bundle version's absolute
path, so the same path always gets the same link. Links are not removed
immediately when they are not needed,
diff --git a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py
b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py
index 18b6b75923b..7fdc6864276 100644
--- a/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py
+++ b/task-sdk/src/airflow/sdk/coordinators/java/coordinator.py
@@ -40,6 +40,7 @@ from airflow.sdk.coordinators.java._jar_manifest import (
SUPERVISOR_SCHEMA_VERSION,
read_main_attributes,
)
+from airflow.sdk.execution_time.coordinator import TaskLaunchError
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Sequence
@@ -100,24 +101,25 @@ def _get_classpath_link_dir() -> pathlib.Path:
"""
Return this user's directory of classpath links, creating it if needed.
- :raises ValueError: when the directory's path holds ``os.pathsep``.
- :raises PermissionError: when the directory is not a real directory owned
by this user, or
- others can write to it. Anyone who can write there could point a link
at their own JARs.
+ :raises TaskLaunchError: when the directory's path holds ``os.pathsep``, or
+ it is not a real directory owned by this user, or writable by others.
"""
uid = os.getuid()
path = pathlib.Path(tempfile.gettempdir(), f"airflow-java-classpath-{uid}")
if os.pathsep in os.fspath(path):
- raise ValueError(
+ raise TaskLaunchError(
f"Cannot keep Java classpath links in {path}, because its path
holds {os.pathsep!r}. "
- "Point TMPDIR to a directory whose path does not."
+ "Point TMPDIR to a directory whose path does not.",
+ retryable=False,
)
with contextlib.suppress(FileExistsError):
path.mkdir(mode=0o700)
st = path.lstat()
if not stat.S_ISDIR(st.st_mode) or st.st_uid != uid or st.st_mode & 0o022:
- raise PermissionError(
+ raise TaskLaunchError(
f"Cannot keep Java classpath links in {path}: it must be a
directory owned by "
- "the current user that no other user can write to."
+ "the current user that no other user can write to.",
+ retryable=False,
)
return path
@@ -167,8 +169,8 @@ def _calculate_classpath(roots: Sequence[pathlib.Path], *,
primary: pathlib.Path
timestamp, is reached through a symlink from :func:`_get_classpath_link`,
which this creates
if needed.
- :raises ValueError: when a JAR's path below its root still holds
``os.pathsep``, or when the
- temporary directory's path holds it.
+ :raises TaskLaunchError: when a JAR's path below its root still holds
``os.pathsep``, or the
+ directory of links cannot be used. Retrying cannot fix either.
"""
jars = sorted(_find_jars(roots), key=lambda p: p.as_posix())
if primary is not None:
@@ -176,9 +178,10 @@ def _calculate_classpath(roots: Sequence[pathlib.Path], *,
primary: pathlib.Path
jars = [primary, *(p for p in jars if p.resolve() != target)]
placed = [(p, _find_root(p, roots)) for p in jars]
if bad := [p for p, r in placed if os.pathsep in os.fspath(p if r is None
else p.relative_to(r))]:
- raise ValueError(
+ raise TaskLaunchError(
f"Cannot put these JARs on a Java classpath, because
{os.pathsep!r} separates its "
- f"entries: {', '.join(map(os.fspath, bad))}. Rename or move them."
+ f"entries: {', '.join(map(os.fspath, bad))}. Rename or move them.",
+ retryable=False,
)
linked_roots = {r for _, r in placed if r is not None and os.pathsep in
os.fspath(r)}
links = {r: _get_classpath_link(r) for r in linked_roots}
@@ -391,6 +394,7 @@ class JavaCoordinator(SubprocessCoordinator):
JARs go on the classpath, so another JAR that sets the same
``Main-Class`` is rejected.
:raises ValueError: when the JAR cannot run.
+ :raises TaskLaunchError: when the bundle's JARs cannot be put on a
classpath.
"""
main_class, schema_version = _read_executable_jar(path)
if self.main_class and main_class != self.main_class:
diff --git a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py
b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py
index e4a0e07df32..e3b1e3dadac 100644
--- a/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py
+++ b/task-sdk/tests/task_sdk/coordinators/java/test_coordinator.py
@@ -309,10 +309,12 @@ class TestClasspathLink:
with (
patch("airflow.sdk.coordinators.java.coordinator.os.getuid",
autospec=True, return_value=uid),
- pytest.raises(PermissionError, match="Cannot keep Java classpath
links"),
+ pytest.raises(TaskLaunchError, match="Cannot keep Java classpath
links") as ctx,
):
_get_classpath_link(root)
+ assert ctx.value.retryable is False
+
def test_rejects_a_link_dir_holding_the_separator(self, tmp_path):
root = _make_timestamped_bundle(tmp_path)
tmp = tmp_path / f"tmp{os.pathsep}dir"
@@ -324,10 +326,11 @@ class TestClasspathLink:
autospec=True,
return_value=os.fspath(tmp),
),
- pytest.raises(ValueError, match="Cannot keep Java classpath
links"),
+ pytest.raises(TaskLaunchError, match="Cannot keep Java classpath
links") as ctx,
):
_calculate_classpath([root])
+ assert ctx.value.retryable is False
assert list(tmp.iterdir()) == []
def test_separator_inside_the_bundle_raises_without_linking(self,
tmp_path, link_dir):
@@ -336,9 +339,10 @@ class TestClasspathLink:
nested.mkdir()
jar = _make_jar(nested / "app.jar")
- with pytest.raises(ValueError, match="Cannot put these JARs on a Java
classpath") as ctx:
+ with pytest.raises(TaskLaunchError, match="Cannot put these JARs on a
Java classpath") as ctx:
_calculate_classpath([root])
+ assert ctx.value.retryable is False
assert os.fspath(jar) in str(ctx.value)
assert not link_dir.exists()