Fix TaskGroupWithTimeout teardown initialization and remove redundant .as_teardown() calls - #296
Fix TaskGroupWithTimeout teardown initialization and remove redundant .as_teardown() calls#296yuna-tzeng wants to merge 5 commits into
Conversation
…askGroupWithTimeout` lacking .as_teardown() issue
| from airflow.models.baseoperator import chain | ||
| from airflow.utils.task_group import TaskGroup | ||
| from airflow.utils.trigger_rule import TriggerRule | ||
| from airflow.operators.bash import BashOperator |
| # group and requires no interception. | ||
| return node | ||
|
|
||
| case BaseOperator(): |
There was a problem hiding this comment.
under this block, not in __exit__
as you can see in the doc string of those 2 context manager methods, those were overridden specifically for forcing a root node
in addition, we have different handling for different types of "node", check the instance there is redundant
on the other hand, if your thought was wanted to inject is_teardown = True for the root node, do that under the block L134
There was a problem hiding this comment.
we cannot set node.is_teardown = True inside the add() method, this causes an AttributeError: PythonOperator object has no attribute _is_setup.
When an Operator is instantiated, its __init__ method runs. Midway through this initialization, if Airflow detects an active TaskGroup context, it immediately calls task_group.add(self).
This means that when our custom add() method receives the node, the object is only partially initialized. Crucial internal attributes, such as _is_setup and _is_teardown, haven't been created yet because the init method hasn't finished executing.
When we attempt to set node.is_teardown = True inside add(), Airflow's property setter triggers a validation check to ensure a task isn't both a setup and a teardown task (if self.is_setup:). Since the underlying _is_setup attribute doesn't exist yet, it throws an AttributeError
Alternatively, we could bypass the AttributeError by manually injecting the missing attribute (e.g., node._is_setup = False) inside the add() method before setting node.is_teardown = True.
However, I feel this might not be a good choice.
Should I go with this approach?
There was a problem hiding this comment.
setting task.is_teardown = True inside wrapped_execute doesn't work because wrapped_execute only happens at runtime.
Airflow's Scheduler determines the DAG structure and task roles (like is_teardown) at parse-time. Therefore, any attribute changes made dynamically during execution remain invisible to the Scheduler when it calculates the final DAG state.
As a result, the tasks we intended to be teardowns will still be treated as regular tasks, which ultimately masks the upstream failures.
Test result: https://39b01025d8504fa5afa7226c9e54d883-dot-us-east1.composer.googleusercontent.com/dags/jobset_rollback_ttr/grid?dag_run_id=manual__2026-07-15T05%3A43%3A40.884284%2B00%3A00

There was a problem hiding this comment.
I see, let me think about it
There was a problem hiding this comment.
how about this
dags/common/task_group_with_timeout/task_group_with_timeout.py:
class TaskGroupWithTimeout(TaskGroup):
...
def __init__(
self,
group_id,
timeout: timedelta,
as_teardown_of: BaseOperator | None = None,
**kwargs,
):
super().__init__(group_id=group_id, **kwargs)
self.group_name = f"{self.__class__.__name__}: '{group_id}'"
self.timeout = timeout
self.setup_op = as_teardown_of
self._root_node = None
# remove __enter__
def __exit__(self, *args):
"""Wire `_root_node` as upstream of in-group root children on context exit.
Overridden to guarantee `_root_node` runs first. Only children with no
in-group sibling upstream get a direct edge; others inherit transitively.
"""
self.initialize_task_group_session()
self.mark_teardown()
return super().__exit__(*args)
def initialize_task_group_session(self):
"""
TBD
"""
self._root_node = PythonOperator(
task_id=self.ROOT_TASK_ID,
python_callable=lambda: datetime.now(timezone.utc).isoformat(),
).as_teardown(
setups=self.setup_op,
on_failure_fail_dagrun=True,
)
children_ids = set(self.children.keys())
for child in self.children.values():
if child is self._root_node:
continue
# If a sibling already chains into this child, the dependency on
# _root_node is satisfied transitively — no need to add a direct edge.
if child.upstream_task_ids & children_ids:
continue
child.set_upstream(self._root_node)
def mark_teardown(self):
"""
TBD
"""
for child in self.children.values():
match child:
case TaskGroup():
pass # A TaskGroup does not have a teardown attribute.
case AbstractOperator():
child.as_teardown(
setups=self.setup_op,
on_failure_fail_dagrun=True,
)
b588728 to
0e938b1
Compare
54f9a78 to
8e4ea82
Compare
Description
This PR addresses an issue where tasks (specifically the injected
provision_taskgroup_sessionroot node) within aTaskGroupWithTimeoutwere notcorrectly inheriting the
is_teardown=Trueproperty, and fixes anAttributeErrorcaused by premature initialization.Tests
Airflow/Composer
yuna-ml-auto(under GCP project:cloud-ml-auto-solutions)2.13.1Changes Made:
Checklist
Before submitting this PR, please make sure (put X in square brackets):