Repository navigation
Report language SDK runtime launch failures in the task log - #75561
Merged
jason810496 merged 16 commits intoOct 27, 2026
Merged
Conversation
amoghrajesh
reviewed
Oct 26, 2026
jason810496
approved these changes
Oct 27, 2026
uranusjr
reviewed
Oct 27, 2026
uranusjr
reviewed
Oct 27, 2026
uranusjr
reviewed
Oct 27, 2026
uranusjr
reviewed
Oct 27, 2026
Member
|
Also in oordinator.py, TaskLaunchError says "before it starts the runtime", and The real invariant, which |
jason810496
marked this pull request as draft
October 27, 2026 06:10
vatsrahul1001
marked this pull request as ready for review
October 27, 2026 06:26
jason810496
reviewed
Oct 27, 2026
ChickenBenny
commented
Oct 27, 2026
The lock and the scan roots are entered inside the launch-error guard but released only after wait() returns, which is the whole reason the stack is there rather than a plain with. Co-Authored-By: Claude <noreply@anthropic.com>
A worker whose Task SDK does not know the schema version an artifact speaks cannot learn it by trying again, so the task should not burn its retries on it. The check also moves out of the native Dag branch and now runs once for every task, so an artifact found by scanning the bundle is rejected before the runtime starts rather than by the attrs validator inside start(). Co-Authored-By: Claude <noreply@anthropic.com>
The output a runtime wrote before it failed to connect went into the error message, so the task log rendered it as one event whose continuation lines carry no timestamp or level. It now goes to the log the way a connected runtime's output does, one event per line under task.stdout and task.stderr, and the message stays on one line. The kept tail grows to 16 KiB, since a JVM or Node stack trace does not fit in 4 KiB, and says so when it drops earlier output. Draining a runtime that keeps printing while it never connects is capped, so it cannot hold up the failure. Co-Authored-By: Claude <noreply@anthropic.com>
The test matched the path with a wildcard, which hid whether the reason the OS gave survives the wrap. It now asserts the whole message, and covers a runtime that exists but cannot be executed as well as one that is missing. _connect_without_output moves above the class that uses it, where every other helper in the file sits. Co-Authored-By: Claude <noreply@anthropic.com>
The pipes and the accepted connections were untracked as the argument to _register_pipe_readers, so they were disowned before it ran. A failure in it, or in _on_child_started after it, left four sockets that nothing closed: the tracker no longer owned them and the half-built subprocess was discarded. They now stay with the tracker until the task instance has started, which is the point 'self' takes them over and closes them in wait(). Co-Authored-By: Claude <noreply@anthropic.com>
_report_startup_output decoded and logged the lines itself, which repeated forward_to_log with small differences. It now feeds forward_to_log instead, so a failed launch and a connected runtime split lines and decode them the same way. The loggers to forward to come from WatchedSubprocess._target_loggers, split out of _get_target_loggers so both callers share it: 'self' does not exist yet when the runtime fails to connect, and the module logger the tuple ends with belongs to supervisor.py. Co-Authored-By: Claude <noreply@anthropic.com>
TaskLaunchError said a coordinator raises it "before it starts the runtime", but a runtime that exits or times out before connecting has already been spawned. The invariant custom coordinators implement against is that the task instance has not been started, which is what the supervisor does itself when it handles the error. The command builders now also say to raise TaskLaunchError with an explicit retryable when the implementation knows the answer, rather than leaving it to be guessed from the exception type. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
"the log keeps output for a try that never ran" reads as if the output shows up against the next try. It does not: the log file is rendered per try, so the lines stay in the file of the try whose launch failed. What happens is narrower, and worth naming: the supervisor stops without reporting a state, so the file is never uploaded and is left on the worker. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The process group test landed on main while this branch was open, against the RuntimeError _accept_connections raised there. This branch raises TaskLaunchError instead, which is not a RuntimeError. Co-Authored-By: Claude <noreply@anthropic.com>
jason810496
force-pushed
the
fix-lang-sdk-launch-failure
branch
from
October 27, 2026 08:11
278ac48 to
180d7b7
Compare
jason810496
enabled auto-merge (squash)
October 27, 2026 08:13
uranusjr
approved these changes
Oct 27, 2026
_get_target_loggers is a hook DagFileProcessorProcess overrides, so a helper called _target_loggers reads like the thing to call. The startup path has to call it directly, since there is no instance yet, and would then miss an override. _default_target_loggers says which one is which. start() also took subprocess_logs_to_stdout out of **kwargs to resolve those loggers, which hid a value every caller already passes. It is an argument now. Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
Backport successfully created: v3-4-testNote: As of Merging PRs targeted for Airflow 3.X In matter of doubt please ask in #release-management Slack channel.
|
vatsrahul1001
pushed a commit
that referenced
this pull request
Oct 27, 2026
…og (#75561) (#75741) * Add tests for language SDK tasks that cannot launch * Report language SDK runtime launch failures in the task log * Resolve artifact bundles the same way as native Dag bundles * Fail coordinator bugs before launch without a retry * Say why execute_task uses an ExitStack The lock and the scan roots are entered inside the launch-error guard but released only after wait() returns, which is the whole reason the stack is there rather than a plain with. * Fail an unsupported supervisor schema version without a retry A worker whose Task SDK does not know the schema version an artifact speaks cannot learn it by trying again, so the task should not burn its retries on it. The check also moves out of the native Dag branch and now runs once for every task, so an artifact found by scanning the bundle is rejected before the runtime starts rather than by the attrs validator inside start(). * Write a failed launch's runtime output to the task log The output a runtime wrote before it failed to connect went into the error message, so the task log rendered it as one event whose continuation lines carry no timestamp or level. It now goes to the log the way a connected runtime's output does, one event per line under task.stdout and task.stderr, and the message stays on one line. The kept tail grows to 16 KiB, since a JVM or Node stack trace does not fit in 4 KiB, and says so when it drops earlier output. Draining a runtime that keeps printing while it never connects is capped, so it cannot hold up the failure. * Assert why a runtime that cannot be executed fails The test matched the path with a wildcard, which hid whether the reason the OS gave survives the wrap. It now asserts the whole message, and covers a runtime that exists but cannot be executed as well as one that is missing. _connect_without_output moves above the class that uses it, where every other helper in the file sits. * Keep the runtime's sockets tracked until the launch has finished The pipes and the accepted connections were untracked as the argument to _register_pipe_readers, so they were disowned before it ran. A failure in it, or in _on_child_started after it, left four sockets that nothing closed: the tracker no longer owned them and the half-built subprocess was discarded. They now stay with the tracker until the task instance has started, which is the point 'self' takes them over and closes them in wait(). * Forward a failed launch's output through the supervisor's forwarder _report_startup_output decoded and logged the lines itself, which repeated forward_to_log with small differences. It now feeds forward_to_log instead, so a failed launch and a connected runtime split lines and decode them the same way. The loggers to forward to come from WatchedSubprocess._target_loggers, split out of _get_target_loggers so both callers share it: 'self' does not exist yet when the runtime fails to connect, and the module logger the tuple ends with belongs to supervisor.py. * State the launch-error contract in terms of the task instance TaskLaunchError said a coordinator raises it "before it starts the runtime", but a runtime that exits or times out before connecting has already been spawned. The invariant custom coordinators implement against is that the task instance has not been started, which is what the supervisor does itself when it handles the error. The command builders now also say to raise TaskLaunchError with an explicit retryable when the implementation knows the answer, rather than leaving it to be guessed from the exception type. * Drop the comment on execute_task's ExitStack * Say what a cleared task does to a failed launch's output "the log keeps output for a try that never ran" reads as if the output shows up against the next try. It does not: the log file is rendered per try, so the lines stay in the file of the try whose launch failed. What happens is narrower, and worth naming: the supervisor stops without reporting a state, so the file is never uploaded and is left on the worker. * Keep a Dag file command's launch error and its retryability * Expect a launch error when a runtime dies before connecting The process group test landed on main while this branch was open, against the RuntimeError _accept_connections raised there. This branch raises TaskLaunchError instead, which is not a RuntimeError. --------- (cherry picked from commit 21a8a98) Co-authored-by: ChickenBenny <zxc123benny14159@gmail.com> Co-authored-by: ZHE YOU LIU <zhu424.dev@gmail.com>
This was referenced Oct 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
closes: #75557
supervise_taskfails a task whose runtime cannot launch, with the reason in its task log, only when the coordinator raisesTaskLaunchError.SubprocessCoordinatorraised it only for a native Dag file that is missing or cannot run. Any other failure before the runtime connected reached the executor before the task instance was started: a@task.stubartifact that cannot be found, a runtime that cannot be executed (javanot installed, wrong architecture), an unsupported supervisor schema version, or a runtime that exits or times out before connecting. The try then had no task log, the task was failed through the scheduler's "state mismatch" path, and the runtime's output, already read by_accept_connections, was dropped. A JVM that printedError: Could not find or load main class ...left that line in no log at all.Rather than catching each of these where it is raised,
SubprocessCoordinatornow raises any failure before the task instance is started asTaskLaunchError, through one context manager,_raise_as_launch_error, around two blocks:execute_task, resolving the Dag bundle and building the command;_PopenActivitySubprocess.start, starting the runtime until it connects._on_child_started, which starts the task instance, andwait()stay outside, because the supervisor starts the task instance itself when it handles aTaskLaunchError. ATaskLaunchErrorraised inside, such as a routing mistake that is not retryable, is kept as it is. The blocks are only indented, so the diff reads best with whitespace ignored.The runtime's output does not go into the error.
_accept_connectionswrites what the runtime printed before it failed to connect to the task log, one event per line undertask.stdoutandtask.stderr, through the same forwarder a connected runtime's output goes through, so each line keeps its own level and timestamp. It keeps the last 16 KiB of each stream, enough for a JVM or Node stack trace, and says so when it drops earlier output. Output still buffered in the sockets is read without blocking, because a runtime can exit before its last lines are read, and a runtime that keeps printing while it never connects is read no further than a fixed budget. Reusing that forwarder is whyWatchedSubprocess._target_loggersis split out of_get_target_loggers: the loggers to forward to have to be resolved before the subprocess object exists.Most of the new failures are retryable, like a native Dag file that is missing or cannot run: an artifact can appear once a bundle finishes syncing, and a runtime that timed out on a busy worker can start on the next try. A supervisor schema version this worker's Task SDK does not know is not retryable, since retrying cannot teach it one. Catching
Exceptionalso reports a coordinator bug as a launch failure, without a retry; its traceback is then in the task log rather than in no log.TaskLaunchErrorsaid a coordinator raises it before it starts the runtime, which no longer holds: a runtime that exits or times out before connecting is already pastPopen. It andBaseCoordinator.execute_tasknow state the contract custom coordinators implement against as the task instance not having been started, which is what the supervisor does itself when it handles the error. A coordinator that knows whether retrying helps should say so rather than leave it to the exception type: