Skip to content

Commit 66de4cb

Browse files
committed
Read current task executions by exact region and try
A loop pass or a mapped expansion reuses a task id and map index, so reading a task instance, its XCom, logs, links, HITL detail or state by (task_id, map_index) no longer names one execution. The public readers take region_id and region_index and resolve them against the live producers. An index without a region is rejected because an index means nothing without the region it indexes. Responses return the region coordinates and a map_index derived from the pinned definition, so mapped tasks keep presenting their slot as before while a task inside a loop presents -1. Filters and sorts use the same derived value so paging and cursors agree with what the responses show. Retired attempts stay in task_instance under their own UUID, so try details, logs and external log links select one by try_number plus the same coordinates. The external log URL used to ignore the try number, which became ambiguous once several rows share a coordinate. Operator extra links read their XCom from the selected region and pinned definition, since the coordinates alone no longer say which execution produced it. The run-level execution listing returns the region ancestry beside each live task instance because its coordinates cannot be interpreted without it. The Gantt payload carries the attempt id and region coordinates so a bar identifies one attempt.
1 parent 56f29f5 commit 66de4cb

50 files changed

Lines changed: 4974 additions & 458 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎airflow-core/src/airflow/api_fastapi/common/parameters/misc.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from airflow.models.errors import ParseImportError
4040
from airflow.models.hitl import HITLDetail
4141
from airflow.models.pool import Pool
42+
from airflow.models.task_coordinates import public_map_index_expression
4243
from airflow.models.taskinstance import TaskInstance
4344
from airflow.models.variable import Variable
4445
from airflow.utils.state import TaskInstanceState
@@ -245,7 +246,7 @@ def _optional_boolean(value: bool | None) -> bool | None:
245246
FilterParam[int | None],
246247
Depends(
247248
filter_param_factory(
248-
TaskInstance.region_index,
249+
public_map_index_expression(TaskInstance),
249250
int | None,
250251
filter_name="map_index",
251252
)

‎airflow-core/src/airflow/api_fastapi/common/parameters/sort.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def __init__(
4444
self,
4545
allowed_attrs: list[str],
4646
model: Base,
47-
to_replace: dict[str, str | Column | list[Column]] | None = None,
47+
to_replace: dict[str, str | ColumnElement | list[ColumnElement]] | None = None,
4848
) -> None:
4949
super().__init__()
5050
self.allowed_attrs = allowed_attrs
@@ -78,7 +78,7 @@ def _resolve(self) -> list[tuple[str, ColumnElement, bool]]:
7878
# it back to the actual row accessor via ``to_replace`` when reading values
7979
# for cursor encoding.
8080
attr_name = lstriped_orderby
81-
column: Column | None = None
81+
column: ColumnElement | None = None
8282
if self.to_replace:
8383
replacement = self.to_replace.get(lstriped_orderby, lstriped_orderby)
8484
if isinstance(replacement, str):
@@ -91,6 +91,8 @@ def _resolve(self) -> list[tuple[str, ColumnElement, bool]]:
9191
is_desc = order_by_value.startswith("-")
9292
for col in replacement:
9393
col_attr_name = col.key
94+
if col_attr_name is None:
95+
raise ValueError("Compound sort expressions must have labels")
9496
resolved.append((col_attr_name, col, is_desc))
9597
continue
9698
else:

‎airflow-core/src/airflow/api_fastapi/common/parameters/task_instance.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
)
3838
from airflow.api_fastapi.compat import HTTP_422_UNPROCESSABLE_CONTENT
3939
from airflow.models.dag_version import DagVersion
40+
from airflow.models.task_coordinates import public_map_index_expression
4041
from airflow.models.taskinstance import TaskInstance
4142
from airflow.utils.state import TaskInstanceState
4243

@@ -237,7 +238,7 @@ def _transform_ti_states(states: list[str] | None) -> list[TaskInstanceState | N
237238
FilterParam[list[int]],
238239
Depends(
239240
filter_param_factory(
240-
TaskInstance.region_index,
241+
public_map_index_expression(TaskInstance),
241242
list[int],
242243
FilterOptionEnum.ANY_EQUAL,
243244
filter_name="map_index",

‎airflow-core/src/airflow/api_fastapi/core_api/datamodels/task_instance_history.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from datetime import datetime
2020
from typing import Annotated, cast
21+
from uuid import UUID
2122

2223
from pydantic import (
2324
AliasPath,
@@ -27,21 +28,25 @@
2728
)
2829

2930
from airflow._shared.secrets_masker import redact
31+
from airflow.api_fastapi.common.region import OmitsMissingRegion, RegionId, RegionIndex
3032
from airflow.api_fastapi.core_api.base import BaseModel
3133
from airflow.api_fastapi.core_api.datamodels.dag_versions import DagVersionResponse
3234
from airflow.utils.state import TaskInstanceState
3335

3436

35-
class TaskInstanceHistoryResponse(BaseModel):
37+
class TaskInstanceHistoryResponse(OmitsMissingRegion, BaseModel):
3638
"""TaskInstanceHistory serializer for responses."""
3739

40+
id: UUID
3841
task_id: str
3942
dag_id: str
4043

4144
# todo: this should not be aliased; it's ambiguous with dag run's "id" - airflow 3.0
4245
run_id: str = Field(alias="dag_run_id")
4346

4447
map_index: int
48+
region_id: RegionId = None
49+
region_index: RegionIndex = None
4550
start_date: datetime | None
4651
end_date: datetime | None
4752
duration: float | None
@@ -64,6 +69,7 @@ class TaskInstanceHistoryResponse(BaseModel):
6469
executor: str | None
6570
executor_config: Annotated[str, BeforeValidator(str)]
6671
dag_version: DagVersionResponse | None
72+
note: str | None = None
6773
state_reason: str | None = Field(
6874
default=None,
6975
validation_alias="retry_reason",
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
from __future__ import annotations
18+
19+
from datetime import datetime
20+
from uuid import UUID
21+
22+
from pydantic import Field
23+
24+
from airflow.api_fastapi.common.region import OmitsMissingRegion, RegionId, RegionIndex
25+
from airflow.api_fastapi.core_api.base import BaseModel
26+
from airflow.utils.state import TaskInstanceState
27+
28+
29+
class ExecutionTaskResponse(OmitsMissingRegion, BaseModel):
30+
"""A task try together with the coordinates that address it exactly."""
31+
32+
id: UUID
33+
dag_id: str
34+
run_id: str = Field(alias="dag_run_id")
35+
task_id: str
36+
task_display_name: str
37+
region_id: RegionId = None
38+
region_index: RegionIndex = None
39+
map_index: int
40+
try_number: int
41+
state: TaskInstanceState | None
42+
start_date: datetime | None
43+
end_date: datetime | None
44+
duration: float | None
45+
dag_version_id: UUID | None
46+
operator: str | None
47+
note: str | None = None
48+
49+
50+
class ExecutionRegionResponse(BaseModel):
51+
"""Immutable region structure for interpreting task coordinates."""
52+
53+
id: UUID
54+
node_id: str
55+
parent_region_id: UUID | None
56+
parent_region_index: int | None
57+
forked_from_region_id: UUID | None
58+
resumes_from_index: int
59+
60+
61+
class ExecutionCollectionResponse(BaseModel):
62+
"""A page of task executions with their region ancestry."""
63+
64+
task_instances: list[ExecutionTaskResponse]
65+
regions: list[ExecutionRegionResponse]
66+
total_entries: int

‎airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/gantt.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,20 @@
1818
from __future__ import annotations
1919

2020
from datetime import datetime
21+
from uuid import UUID
2122

23+
from airflow.api_fastapi.common.region import OmitsMissingRegion, RegionId, RegionIndex
2224
from airflow.api_fastapi.core_api.base import BaseModel
2325
from airflow.utils.state import TaskInstanceState
2426

2527

26-
class GanttTaskInstance(BaseModel):
28+
class GanttTaskInstance(OmitsMissingRegion, BaseModel):
2729
"""Task instance data for Gantt chart."""
2830

31+
id: UUID
32+
region_id: RegionId = None
33+
region_index: RegionIndex = None
34+
map_index: int
2935
task_id: str
3036
task_display_name: str
3137
try_number: int

‎airflow-core/src/airflow/api_fastapi/core_api/datamodels/xcom.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,24 @@
1919
from collections.abc import Iterable
2020
from datetime import datetime
2121
from typing import Any
22+
from uuid import UUID
2223

2324
from pydantic import AliasPath, Field, field_validator
2425

26+
from airflow.api_fastapi.common.region import OmitsMissingRegion, RegionId, RegionIndex
2527
from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel
2628
from airflow.api_fastapi.core_api.datamodels.common import find_reserved_keys
2729

2830

29-
class XComResponse(BaseModel):
31+
class XComResponse(OmitsMissingRegion, BaseModel):
3032
"""Serializer for a xcom item."""
3133

3234
key: str
3335
timestamp: datetime
3436
logical_date: datetime | None
3537
map_index: int
38+
region_id: RegionId = None
39+
region_index: RegionIndex = None
3640
task_id: str
3741
dag_id: str
3842
run_id: str
@@ -105,6 +109,8 @@ class XComCreateBody(StrictBaseModel):
105109
key: str
106110
value: Any
107111
map_index: int = -1
112+
region_id: UUID | None = None
113+
region_index: int | None = Field(default=None, ge=-1)
108114

109115
@field_validator("value")
110116
@classmethod
@@ -117,6 +123,8 @@ class XComUpdateBody(StrictBaseModel):
117123

118124
value: Any
119125
map_index: int = -1
126+
region_id: UUID | None = None
127+
region_index: int | None = Field(default=None, ge=-1)
120128

121129
@field_validator("value")
122130
@classmethod

0 commit comments

Comments
 (0)