Skip to content

Commit 7f4b6bf

Browse files
OscarLigthartEmmanuela Opurum
authored andcommitted
feat: enable queue up new tasks (apache#63484)
* feat: enable queue up new tasks * chore: remove redundant disable flag from button * feat: create and pass NewTaskCollectionResponse * cleanup: unused translations * fix: remove additional return type and simplify code * remove redundancy * fix: remove unused var * fix: mypy and tests * fix: more mypy * cleanup * fix: mypy * fix: pnpm lint * cleanup
1 parent 78346be commit 7f4b6bf

12 files changed

Lines changed: 337 additions & 26 deletions

File tree

airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from collections.abc import Iterable
2121
from datetime import datetime
2222
from enum import Enum
23-
from typing import TYPE_CHECKING
23+
from typing import TYPE_CHECKING, Any
2424

2525
from pydantic import AliasPath, AwareDatetime, Field, NonNegativeInt, model_validator
2626

@@ -55,11 +55,23 @@ class DAGRunClearBody(StrictBaseModel):
5555

5656
dry_run: bool = True
5757
only_failed: bool = False
58+
only_new: bool = Field(
59+
default=False,
60+
description="Only queue newly added tasks in the latest DAG version without clearing existing tasks.",
61+
)
5862
run_on_latest_version: bool = Field(
5963
default=False,
6064
description="(Experimental) Run on the latest bundle version of the Dag after clearing the Dag Run.",
6165
)
6266

67+
@model_validator(mode="before")
68+
@classmethod
69+
def validate_model(cls, data: Any) -> Any:
70+
"""Validate clear DAG run form."""
71+
if data.get("only_new") and data.get("only_failed"):
72+
raise ValueError("only_new and only_failed are mutually exclusive")
73+
return data
74+
6375

6476
class DAGRunResponse(BaseModel):
6577
"""DAG Run serializer for responses."""

airflow-core/src/airflow/api_fastapi/core_api/datamodels/task_instances.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
AliasPath,
2626
AwareDatetime,
2727
BeforeValidator,
28+
Discriminator,
2829
Field,
2930
NonNegativeInt,
3031
StringConstraints,
32+
Tag,
3133
ValidationError,
3234
field_validator,
3335
model_validator,
@@ -40,6 +42,13 @@
4042
from airflow.utils.state import TaskInstanceState
4143

4244

45+
class NewTaskResponse(BaseModel):
46+
"""Lightweight response for new tasks that don't have TaskInstances yet."""
47+
48+
task_id: str
49+
task_display_name: str
50+
51+
4352
class TaskInstanceResponse(BaseModel):
4453
"""TaskInstance serializer for responses."""
4554

@@ -112,6 +121,28 @@ class TaskInstanceCollectionResponse(BaseModel):
112121
)
113122

114123

124+
def _task_instance_discriminator(v: Any) -> str:
125+
"""Discriminate between TaskInstanceResponse and NewTaskResponse in the union."""
126+
if isinstance(v, NewTaskResponse):
127+
return "new"
128+
if isinstance(v, dict):
129+
return "new" if "id" not in v else "full"
130+
# ORM objects and TaskInstanceResponse instances
131+
return "full"
132+
133+
134+
class ClearTaskInstanceCollectionResponse(BaseModel):
135+
"""Response for clear dag run dry run, which may contain new tasks without full TaskInstance data."""
136+
137+
task_instances: Iterable[
138+
Annotated[
139+
Annotated[TaskInstanceResponse, Tag("full")] | Annotated[NewTaskResponse, Tag("new")],
140+
Discriminator(_task_instance_discriminator),
141+
]
142+
]
143+
total_entries: int
144+
145+
115146
class TaskDependencyResponse(BaseModel):
116147
"""Task Dependency serializer for responses."""
117148

airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1987,7 +1987,7 @@ paths:
19871987
application/json:
19881988
schema:
19891989
anyOf:
1990-
- $ref: '#/components/schemas/TaskInstanceCollectionResponse'
1990+
- $ref: '#/components/schemas/ClearTaskInstanceCollectionResponse'
19911991
- $ref: '#/components/schemas/DAGRunResponse'
19921992
title: Response Clear Dag Run
19931993
'401':
@@ -9903,6 +9903,25 @@ components:
99039903
- action
99049904
- entities
99059905
title: BulkUpdateAction[VariableBody]
9906+
ClearTaskInstanceCollectionResponse:
9907+
properties:
9908+
task_instances:
9909+
items:
9910+
oneOf:
9911+
- $ref: '#/components/schemas/TaskInstanceResponse'
9912+
- $ref: '#/components/schemas/NewTaskResponse'
9913+
type: array
9914+
title: Task Instances
9915+
total_entries:
9916+
type: integer
9917+
title: Total Entries
9918+
type: object
9919+
required:
9920+
- task_instances
9921+
- total_entries
9922+
title: ClearTaskInstanceCollectionResponse
9923+
description: Response for clear dag run dry run, which may contain new tasks
9924+
without full TaskInstance data.
99069925
ClearTaskInstancesBody:
99079926
properties:
99089927
dry_run:
@@ -10709,6 +10728,12 @@ components:
1070910728
type: boolean
1071010729
title: Only Failed
1071110730
default: false
10731+
only_new:
10732+
type: boolean
10733+
title: Only New
10734+
description: Only queue newly added tasks in the latest DAG version without
10735+
clearing existing tasks.
10736+
default: false
1071210737
run_on_latest_version:
1071310738
type: boolean
1071410739
title: Run On Latest Version
@@ -12098,6 +12123,21 @@ components:
1209812123
type: object
1209912124
title: MaterializeAssetBody
1210012125
description: Materialize asset request.
12126+
NewTaskResponse:
12127+
properties:
12128+
task_id:
12129+
type: string
12130+
title: Task Id
12131+
task_display_name:
12132+
type: string
12133+
title: Task Display Name
12134+
type: object
12135+
required:
12136+
- task_id
12137+
- task_display_name
12138+
title: NewTaskResponse
12139+
description: Lightweight response for new tasks that don't have TaskInstances
12140+
yet.
1210112141
PatchTaskInstanceBody:
1210212142
properties:
1210312143
new_state:

airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@
7575
TriggerDAGRunPostBody,
7676
)
7777
from airflow.api_fastapi.core_api.datamodels.task_instances import (
78-
TaskInstanceCollectionResponse,
78+
ClearTaskInstanceCollectionResponse,
79+
NewTaskResponse,
7980
TaskInstanceResponse,
8081
)
8182
from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc
@@ -286,7 +287,7 @@ def clear_dag_run(
286287
body: DAGRunClearBody,
287288
dag_bag: DagBagDep,
288289
session: SessionDep,
289-
) -> TaskInstanceCollectionResponse | DAGRunResponse:
290+
) -> ClearTaskInstanceCollectionResponse | DAGRunResponse:
290291
dag_run = session.scalar(
291292
select(DagRun).filter_by(dag_id=dag_id, run_id=dag_run_id).options(joinedload(DagRun.dag_model))
292293
)
@@ -298,27 +299,39 @@ def clear_dag_run(
298299

299300
dag = dag_bag.get_dag_for_run(dag_run, session=session)
300301

302+
if not dag:
303+
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")
304+
301305
if body.dry_run:
302-
if not dag:
303-
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")
304-
task_instances = dag.clear(
306+
task_instances_or_ids = dag.clear(
305307
run_id=dag_run_id,
306308
task_ids=None,
309+
only_new=body.only_new,
307310
only_failed=body.only_failed,
308311
run_on_latest_version=body.run_on_latest_version,
309312
dry_run=True,
310313
session=session,
311314
)
312315

313-
return TaskInstanceCollectionResponse(
314-
task_instances=cast("list[TaskInstanceResponse]", task_instances),
316+
if body.only_new:
317+
# Create lightweight NewTaskResponse objects for new tasks
318+
new_task_ids = cast("set[str]", task_instances_or_ids)
319+
task_instances: list[TaskInstanceResponse | NewTaskResponse] = [
320+
NewTaskResponse(task_id=task_id, task_display_name=task_id)
321+
for task_id in sorted(new_task_ids)
322+
]
323+
else:
324+
task_instances = cast("list[TaskInstanceResponse | NewTaskResponse]", task_instances_or_ids)
325+
326+
return ClearTaskInstanceCollectionResponse(
327+
task_instances=task_instances,
315328
total_entries=len(task_instances),
316329
)
317-
if not dag:
318-
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id {dag_id} was not found")
330+
319331
dag.clear(
320332
run_id=dag_run_id,
321333
task_ids=None,
334+
only_new=body.only_new,
322335
only_failed=body.only_failed,
323336
run_on_latest_version=body.run_on_latest_version,
324337
session=session,

airflow-core/src/airflow/models/taskinstance.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,14 @@ def _get_new_task_ids(
260260
if not latest_dag:
261261
raise ValueError(f"Latest DAG version for '{dag_id}' not found")
262262

263-
current_dag = scheduler_dagbag.get_dag_for_run(dag_run=dag_run, session=session)
263+
# Use created_dag_version_id directly to get the DAG version the run was
264+
# originally created with. We cannot use get_dag_for_run here because it
265+
# falls back to the latest version when bundle_version is not set (e.g.
266+
# LocalDagBundle), which would make current_dag == latest_dag and the diff
267+
# always empty.
268+
current_dag = None
269+
if dag_run.created_dag_version_id:
270+
current_dag = scheduler_dagbag.get_dag(version_id=dag_run.created_dag_version_id, session=session)
264271
new_task_ids = set(latest_dag.task_ids) - set(current_dag.task_ids) if current_dag else set()
265272

266273
return list(new_task_ids)

airflow-core/src/airflow/serialization/definitions/dag.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,23 @@ def clear(
947947
run_on_latest_version: bool = False,
948948
) -> list[TaskInstance]: ... # pragma: no cover
949949

950+
@overload
951+
def clear(
952+
self,
953+
*,
954+
dry_run: Literal[True],
955+
task_ids: Collection[str | tuple[str, int]] | None = None,
956+
run_id: str,
957+
only_failed: bool = False,
958+
only_running: bool = False,
959+
only_new: bool,
960+
dag_run_state: DagRunState = DagRunState.QUEUED,
961+
session: Session = NEW_SESSION,
962+
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
963+
exclude_run_ids: frozenset[str] | None = frozenset(),
964+
run_on_latest_version: bool = False,
965+
) -> set[str] | list[TaskInstance]: ... # pragma: no cover
966+
950967
@overload
951968
def clear(
952969
self,

airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,6 +1270,33 @@ export const $BulkUpdateAction_VariableBody_ = {
12701270
title: 'BulkUpdateAction[VariableBody]'
12711271
} as const;
12721272

1273+
export const $ClearTaskInstanceCollectionResponse = {
1274+
properties: {
1275+
task_instances: {
1276+
items: {
1277+
oneOf: [
1278+
{
1279+
'$ref': '#/components/schemas/TaskInstanceResponse'
1280+
},
1281+
{
1282+
'$ref': '#/components/schemas/NewTaskResponse'
1283+
}
1284+
]
1285+
},
1286+
type: 'array',
1287+
title: 'Task Instances'
1288+
},
1289+
total_entries: {
1290+
type: 'integer',
1291+
title: 'Total Entries'
1292+
}
1293+
},
1294+
type: 'object',
1295+
required: ['task_instances', 'total_entries'],
1296+
title: 'ClearTaskInstanceCollectionResponse',
1297+
description: 'Response for clear dag run dry run, which may contain new tasks without full TaskInstance data.'
1298+
} as const;
1299+
12731300
export const $ClearTaskInstancesBody = {
12741301
properties: {
12751302
dry_run: {
@@ -2499,6 +2526,12 @@ export const $DAGRunClearBody = {
24992526
title: 'Only Failed',
25002527
default: false
25012528
},
2529+
only_new: {
2530+
type: 'boolean',
2531+
title: 'Only New',
2532+
description: 'Only queue newly added tasks in the latest DAG version without clearing existing tasks.',
2533+
default: false
2534+
},
25022535
run_on_latest_version: {
25032536
type: 'boolean',
25042537
title: 'Run On Latest Version',
@@ -4601,6 +4634,23 @@ export const $MaterializeAssetBody = {
46014634
description: 'Materialize asset request.'
46024635
} as const;
46034636

4637+
export const $NewTaskResponse = {
4638+
properties: {
4639+
task_id: {
4640+
type: 'string',
4641+
title: 'Task Id'
4642+
},
4643+
task_display_name: {
4644+
type: 'string',
4645+
title: 'Task Display Name'
4646+
}
4647+
},
4648+
type: 'object',
4649+
required: ['task_id', 'task_display_name'],
4650+
title: 'NewTaskResponse',
4651+
description: "Lightweight response for new tasks that don't have TaskInstances yet."
4652+
} as const;
4653+
46044654
export const $PatchTaskInstanceBody = {
46054655
properties: {
46064656
new_state: {

airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,14 @@ export type BulkUpdateAction_VariableBody_ = {
400400
action_on_non_existence?: BulkActionNotOnExistence;
401401
};
402402

403+
/**
404+
* Response for clear dag run dry run, which may contain new tasks without full TaskInstance data.
405+
*/
406+
export type ClearTaskInstanceCollectionResponse = {
407+
task_instances: Array<(TaskInstanceResponse | NewTaskResponse)>;
408+
total_entries: number;
409+
};
410+
403411
/**
404412
* Request body for Clear Task Instances endpoint.
405413
*/
@@ -655,6 +663,10 @@ export type DAGResponse = {
655663
export type DAGRunClearBody = {
656664
dry_run?: boolean;
657665
only_failed?: boolean;
666+
/**
667+
* Only queue newly added tasks in the latest DAG version without clearing existing tasks.
668+
*/
669+
only_new?: boolean;
658670
/**
659671
* (Experimental) Run on the latest bundle version of the Dag after clearing the Dag Run.
660672
*/
@@ -1160,6 +1172,14 @@ export type MaterializeAssetBody = {
11601172
partition_key?: string | null;
11611173
};
11621174

1175+
/**
1176+
* Lightweight response for new tasks that don't have TaskInstances yet.
1177+
*/
1178+
export type NewTaskResponse = {
1179+
task_id: string;
1180+
task_display_name: string;
1181+
};
1182+
11631183
/**
11641184
* Request body for Clear Task Instances endpoint.
11651185
*/
@@ -2618,7 +2638,7 @@ export type ClearDagRunData = {
26182638
requestBody: DAGRunClearBody;
26192639
};
26202640

2621-
export type ClearDagRunResponse = TaskInstanceCollectionResponse | DAGRunResponse;
2641+
export type ClearDagRunResponse = ClearTaskInstanceCollectionResponse | DAGRunResponse;
26222642

26232643
export type GetDagRunsData = {
26242644
bundleVersion?: string | null;
@@ -4741,7 +4761,7 @@ export type $OpenApiTs = {
47414761
/**
47424762
* Successful Response
47434763
*/
4744-
200: TaskInstanceCollectionResponse | DAGRunResponse;
4764+
200: ClearTaskInstanceCollectionResponse | DAGRunResponse;
47454765
/**
47464766
* Unauthorized
47474767
*/

0 commit comments

Comments
 (0)