Repository navigation
Send the Python task's execution_timeout to the supervisor - #75387
ChickenBenny wants to merge 5 commits into
Conversation
d2facf4 to
cd4f712
Compare
jason810496
left a comment
There was a problem hiding this comment.
I agreed we should make the getter of execution time on both side consistent.
A [core] default_task_execution_timeout set only on the worker no longer applies to Python tasks. It now comes from where the Dag is serialized, same as for language SDK tasks.
Would it be better to still respect the config if we set then fallback to the field set on the TIRunContext?
Since this will introduce breaking change for user IIUC.
We intentionally avoid the value being passed from runner to supervisor in previous PR, but it's fine for the supervisor to respect the config set at the worker.
|
Thanks! Agreed that a worker-only I didn't make the worker config take precedence over Instead, the worker config is used only when the run context has no timeout:
The "equals the worker's default" check matters: the re-parsed value can also come from a Dag file changed since it was serialized, or from |
jason810496
left a comment
There was a problem hiding this comment.
Thanks for tracing this. I agree #74869 has a bug when the Dag processor and the worker resolve execution_timeout differently. It is already on v3-4-test (#75376) but not in any tag yet, so we should fix it before the next 3.4 pre-release.
On v3-4-test, the runner arms SIGALRM from task.execution_timeout, while the supervisor arms from TIRunContext.execution_timeout. When the server value is smaller (a different default_task_execution_timeout, a Dag file changed after serialization, pre_execute, or a mapped expand(execution_timeout=...)), the supervisor sends SIGTERM before the runner raises AirflowTaskTimeout, so on_kill, the retry policy, callbacks and listeners are skipped.
Making the run context the only source for Python tasks fixes that mismatch, but a few cases still end up with no timeout or two different ones:
expand(execution_timeout=...): the server only readspartial_kwargs, so it sendsNone. Values from.map()or from upstream XComs can't be resolved on the server at all.- A core that doesn't send the field: tasks that set
execution_timeoutthemselves get no timeout. The worker-default fallback only covers tasks whose value equals the worker default. BaseAsyncOperatorstill usestask.execution_timeoutinasyncio.wait_for, so it can disagree with SIGALRM.
How about splitting the two paths instead?
- Python tasks:
task.execution_timeoutis the only source. The runner arms SIGALRM with it and sends the same value to the supervisor. - Language SDK tasks: keep using
TIRunContext.execution_timeout, armed by the coordinator as today.
# comms.py
class StartExecutionTimeout(BaseModel):
timeout_seconds: Annotated[float, Field(gt=0)]
type: Literal["StartExecutionTimeout"] = "StartExecutionTimeout"
# supervisor.py
def _handle_start_execution_timeout(self, msg, log, req_id):
self._execution_timeout_seconds = msg.timeout_seconds
self._arm_execution_timeout()
return None, {}
# task_runner.py
SUPERVISOR_COMMS.send(StartExecutionTimeout(timeout_seconds=timeout_seconds))This is close to what #73806 did with SetExecutionTimeout. Both timers then use the same value, and mapped tasks, pre_execute and a worker-only default keep working as before. _resolve_execution_timeout, the supervisor fallback, the create_runtime_ti change and the significant newsfragment can then be dropped, and the TIRunContext.execution_timeout docstring would say it is for tasks run by a language SDK. A language SDK task mapped with expand(execution_timeout=...) would still get None, which we can note as a known limitation.
jason810496
left a comment
There was a problem hiding this comment.
Mark as request change in case of accidental merge.
…uage SDK (#74869)" (#75376) (#75469) This reverts commit 8326025. 3.4.0 closes today and #75376 needs #75387 alongside it, which needs more bandwidth than is left. On v3-4-test alone, the Python task runner arms SIGALRM from task.execution_timeout while the supervisor arms its deadline from TIRunContext.execution_timeout. When the server value is smaller, the supervisor sends SIGTERM before the runner raises AirflowTaskTimeout, so on_kill, the retry policy, callbacks and listeners are skipped. Reverting avoids shipping that regression in 3.4.0. #74869 stays on main. SetExecutionTimeout (#73806) is untouched, so Python tasks keep the supervisor backstop they had before #75376. Co-authored-by: Claude <noreply@anthropic.com>
|
Agree with Jason, and sorry for my incorrect suggestion in #74869. I think we can keep this PR to only "undo" the changes on the Python Task SDK path. Maybe we should also rename One thing we may need to document: with Jason's approach, Python tasks and language SDK tasks read
For language SDK tasks, the default is filled from the Dag processor's config when the Dag is parsed: (WOW it's kinda weird to the users isn't it) |
54b0a67 to
21af315
Compare
|
Thanks both! Reworked along Jason's suggestion, so my earlier reply about the worker-default fallback is outdated.
Two things worth a look:
|
Starting any task deserialized its whole Dag so the route could find out whether the task was a stub and read its arg_bindings. Denormalize the serialized is_stub flag onto task_instance, which ti_run already fetches and locks, and load the Dag only when it is set. execution_timeout is resolved from the same definition, so it is now sent for a lang-SDK task only; apache#75387 has a Python task send its own to the supervisor. Co-Authored-By: Claude <noreply@anthropic.com>
* Load the serialized Dag in ti_run only for stub tasks Starting any task deserialized its whole Dag so the route could find out whether the task was a stub and read its arg_bindings. Denormalize the serialized is_stub flag onto task_instance, which ti_run already fetches and locks, and load the Dag only when it is set. execution_timeout is resolved from the same definition, so it is now sent for a lang-SDK task only; #75387 has a Python task send its own to the supervisor. * Hold SQLite foreign keys off while the downgrade rebuilds Dropping a column rebuilds the table on SQLite, unlike adding one, so the downgrade copies task_instance rows while other tables still point at it. * Pin the bulk-insert path and reword the is_stub comments create_dagrun refreshes every row from its task right after the bulk write, so the existing test passed even when insert_mapping dropped the key. Assert the mapping itself instead, and record what a row keeps when it moves to a new Dag version without being refreshed. * Mark a Go or Java TriggerDagRun task a stub, as TypeScript does * Inline the table name in the is_stub migration ---------
jason810496
left a comment
There was a problem hiding this comment.
After revisit the end to end path, I think the best way is:
- Make this PR revert the loading serialized Dag part and add the
AIRFLOW__CORE__DEFAULT_TASK_EXECUTION_TIMEOUTvia coordinator to pass the env to SDK runtime.
airflow/task-sdk/src/airflow/sdk/coordinators/_subprocess.py
Lines 193 to 206 in b19bc48
- Make each SDK emit the
SetExecutionTimeoutjust like the normal Python task does, so that we can respect the value that user explicitly set, worker env and the default value if unset while we won't introduce the inconsistent behavior as mentioned in #75387 (we shouldn't allow this be happened)
Sorry, I didn't realized we will load serialized Dag for every task on execution API server back then.
Go and TS SDKs built from main now need Airflow 3.5, since a 3.4 supervisor rejects schema 2027-02-28. Is that OK?
Sure, it's fine.
A language SDK task mapped with expand(execution_timeout=...) still gets None. I listed it as a known limitation.
Yes, all the SDKs so far haven't supported the mapped task yet.
| A task that does not set ``execution_timeout`` itself gets ``[core] default_task_execution_timeout``, which is read in | ||
| different places depending on how the task runs: | ||
|
|
||
| - A Python task reads it on the worker, which parses the Dag file again before running the task. | ||
| - A task run by a language SDK reads it where the Dag processor runs, when the Dag is parsed and serialized. | ||
|
|
||
| Set ``[core] default_task_execution_timeout`` to the same value on the Dag processor and the workers, so both kinds | ||
| of task get the same default. | ||
|
|
There was a problem hiding this comment.
Let's remove this part, since this part should be fixed after the new plan I proposed above.
| The supervisor enforces the timeout of a task run by a language SDK with this value. A Python task | ||
| enforces its own ``execution_timeout`` and sends that to the supervisor instead, since the server |
A follow-up to #74869, reworked after review.
With #74869 on
main, the Python task runner arms SIGALRM fromtask.execution_timeout, while the supervisor arms its backstop fromTIRunContext.execution_timeout. When the two differ, the supervisor sends SIGTERM before the runner raisesAirflowTaskTimeout, soon_kill, the retry policy, callbacks and listeners are skipped. They differ more often than expected: a different[core] default_task_execution_timeouton the Dag processor and the worker, a Dag file changed after serialization,pre_execute, or a mappedexpand(execution_timeout=...), which the server cannot resolve and sends asNone.Making the run context the only source does not work either, since the server cannot resolve every value. So each kind of task now has a single source, and both timers use it:
task.execution_timeout, as in 3.3, and sends that value to the supervisor withSetExecutionTimeout(timeout_seconds). This restores the message from Enforce execution_timeout from the task supervisor side #73806, identical to what 3.4.0 ships, in place ofStartExecutionTimeout.TIRunContext.execution_timeoutand the coordinator arms it once the runtime has the task.#74869 was reverted from
v3-4-test(#75469), so 3.4.0 does not haveTIRunContext.execution_timeout. Its schema changes are therefore moved from2026-10-30to2027-02-28in this PR, since they ship together with the change above:AddExecutionTimeoutToTIRunContextmoves tov2027_02_28.py.v2027_02_28.py, the first 3.5 supervisor schema version, dated like the Execution API one, withAddExecutionTimeoutToSupervisorTIRunContext.SetExecutionTimeoutneeds no entry because it matches 3.4.0.Because the two kinds of task take the timeout from different places, a task that does not set
execution_timeoutreads[core] default_task_execution_timeoutin different places too:This is documented in the Timeouts section of
core-concepts/tasks.rstand in the option's description, which recommend setting the same value on the Dag processor and the workers.Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 5.5) following the guidelines