This is an automated email from the ASF dual-hosted git repository.
o-nikolas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new b788d83d215 Adds Airflow 3 compatibility in `try_adopt_task_instances`
for BatchExec (#68027)
b788d83d215 is described below
commit b788d83d215ec9fe6b289c3021a726997ca3d8a3
Author: Alfonso Manuel Baena González
<[email protected]>
AuthorDate: Mon Jun 22 23:47:35 2026 +0200
Adds Airflow 3 compatibility in `try_adopt_task_instances` for BatchExec
(#68027)
Build either an Airflow 2 `airflow` CLI command or an Airflow 3 python
execution of task execution with a json payload representing this task.
---
.../amazon/aws/executors/batch/batch_executor.py | 47 +++++++++---
.../aws/executors/batch/test_batch_executor.py | 87 +++++++++++++++++++++-
2 files changed, 122 insertions(+), 12 deletions(-)
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/executors/batch/batch_executor.py
b/providers/amazon/src/airflow/providers/amazon/aws/executors/batch/batch_executor.py
index e04464883c0..74614961f89 100644
---
a/providers/amazon/src/airflow/providers/amazon/aws/executors/batch/batch_executor.py
+++
b/providers/amazon/src/airflow/providers/amazon/aws/executors/batch/batch_executor.py
@@ -414,15 +414,7 @@ class AwsBatchExecutor(BaseExecutor):
if isinstance(command[0], workloads.ExecuteTask) or (
AIRFLOW_V_3_3_PLUS and isinstance(command[0],
workloads.ExecuteCallback)
):
- workload = command[0]
- ser_input = workload.model_dump_json()
- command = [
- "python",
- "-m",
- "airflow.sdk.execution_time.execute_workload",
- "--json-string",
- ser_input,
- ]
+ command = self._serialize_workload_to_command(command[0])
else:
raise ValueError(
f"BatchExecutor doesn't know how to handle workload of
type: {type(command[0])}"
@@ -509,6 +501,39 @@ class AwsBatchExecutor(BaseExecutor):
)
return submit_kwargs
+ @staticmethod
+ def _serialize_workload_to_command(workload) -> CommandType:
+ """
+ Serialize a workload into a command for the Task SDK.
+
+ :param workload: ExecuteTask or ExecuteCallback workload to serialize
+ :return: Command as list of strings for Task SDK execution
+ """
+ return [
+ "python",
+ "-m",
+ "airflow.sdk.execution_time.execute_workload",
+ "--json-string",
+ workload.model_dump_json(),
+ ]
+
+ def _build_task_command(self, ti: TaskInstance) -> CommandType:
+ """
+ Build task command for execution based on Airflow version.
+
+ For Airflow 3.x+, generates an ExecuteTask workload with JSON
serialization.
+ For Airflow 2.x, uses the legacy command_as_list() method.
+
+ :param ti: TaskInstance to build command for
+ :return: Command as list of strings
+ """
+ if AIRFLOW_V_3_0_PLUS:
+ from airflow.executors.workloads import ExecuteTask
+
+ workload = ExecuteTask.make(ti)
+ return self._serialize_workload_to_command(workload)
+ return ti.command_as_list()
+
def try_adopt_task_instances(self, tis: Sequence[TaskInstance]) ->
Sequence[TaskInstance]:
"""
Adopt task instances which have an external_executor_id (the Batch job
ID).
@@ -523,10 +548,12 @@ class AwsBatchExecutor(BaseExecutor):
for batch_job in batch_jobs:
ti = next(ti for ti in tis if ti.external_executor_id ==
batch_job.job_id)
+ command = self._build_task_command(ti)
+
self.active_workers.add_job(
job_id=batch_job.job_id,
airflow_workload_key=ti.key,
- airflow_cmd=ti.command_as_list(),
+ airflow_cmd=command,
queue=ti.queue,
exec_config=ti.executor_config,
attempt_number=ti.try_number,
diff --git
a/providers/amazon/tests/unit/amazon/aws/executors/batch/test_batch_executor.py
b/providers/amazon/tests/unit/amazon/aws/executors/batch/test_batch_executor.py
index 4695ca1d47a..e990c657b9e 100644
---
a/providers/amazon/tests/unit/amazon/aws/executors/batch/test_batch_executor.py
+++
b/providers/amazon/tests/unit/amazon/aws/executors/batch/test_batch_executor.py
@@ -22,6 +22,7 @@ import logging
import os
from unittest import mock
from unittest.mock import patch
+from uuid import uuid4
import pytest
import yaml
@@ -776,7 +777,6 @@ class TestAwsBatchExecutor:
}
executor.batch.describe_jobs.return_value = {"jobs": [after_batch_job]}
- @pytest.mark.skip(reason="Adopting task instances hasn't been ported over
to Airflow 3 yet")
def test_try_adopt_task_instances(self, mock_executor):
"""Test that executor can adopt orphaned task instances from a
SchedulerJob shutdown event."""
mock_executor.batch.describe_jobs.return_value = {
@@ -794,8 +794,38 @@ class TestAwsBatchExecutor:
orphaned_tasks[0].external_executor_id = "001" # Matches a running
task_arn
orphaned_tasks[1].external_executor_id = "002" # Matches a running
task_arn
orphaned_tasks[2].external_executor_id = None # One orphaned task has
no external_executor_id
- for task in orphaned_tasks:
+
+ for idx, task in enumerate(orphaned_tasks):
task.try_number = 1
+ task.key = mock.Mock(spec=TaskInstanceKey)
+ task.queue = "default"
+ task.executor_config = {}
+ task.id = uuid4()
+ task.dag_version_id = uuid4()
+ task.task_id = f"task_{idx}"
+ task.dag_id = "test_dag"
+ task.run_id = "test_run"
+ task.map_index = -1
+ task.pool_slots = 1
+ task.priority_weight = 1
+ task.context_carrier = {}
+ task.queued_dttm = dt.datetime(2024, 1, 1, tzinfo=dt.timezone.utc)
+ task.dag_model = mock.Mock()
+ task.dag_model.bundle_name = "test_bundle"
+ task.dag_model.relative_fileloc = "test_dag.py"
+ task.dag_run = mock.Mock()
+ task.dag_run.bundle_version = "1.0.0"
+ task.dag_run.context_carrier = {}
+
+ if not AIRFLOW_V_3_0_PLUS:
+ task.command_as_list.return_value = [
+ "airflow",
+ "tasks",
+ "run",
+ "dag",
+ f"task_{idx}",
+ "2024-01-01",
+ ]
not_adopted_tasks =
mock_executor.try_adopt_task_instances(orphaned_tasks)
@@ -805,6 +835,59 @@ class TestAwsBatchExecutor:
# The remaining one task is unable to be adopted.
assert len(not_adopted_tasks) == 1
+ @pytest.mark.skipif(not AIRFLOW_V_3_0_PLUS, reason="Test requires Airflow
3+")
+ def test_serialize_workload_to_command(self, mock_executor):
+ """Test that _serialize_workload_to_command properly serializes a Task
SDK workload."""
+ from airflow.executors.workloads import ExecuteTask
+
+ workload = mock.Mock(spec=ExecuteTask)
+ ser_workload = json.dumps({"test_key": "test_value"})
+ workload.model_dump_json.return_value = ser_workload
+
+ command = mock_executor._serialize_workload_to_command(workload)
+
+ assert command == [
+ "python",
+ "-m",
+ "airflow.sdk.execution_time.execute_workload",
+ "--json-string",
+ ser_workload,
+ ]
+ workload.model_dump_json.assert_called_once()
+
+ @pytest.mark.skipif(not AIRFLOW_V_3_0_PLUS, reason="Test requires Airflow
3+")
+ @mock.patch("airflow.executors.workloads.ExecuteTask")
+ def test_build_task_command_airflow3(self, mock_execute_task_class,
mock_executor):
+ """Test _build_task_command for Airflow 3.x+ using Task SDK."""
+ mock_ti = mock.Mock(spec=TaskInstance)
+ mock_workload = mock.Mock()
+ ser_workload = json.dumps({"task": "data"})
+ mock_workload.model_dump_json.return_value = ser_workload
+ mock_execute_task_class.make.return_value = mock_workload
+
+ command = mock_executor._build_task_command(mock_ti)
+
+ mock_execute_task_class.make.assert_called_once_with(mock_ti)
+ assert command == [
+ "python",
+ "-m",
+ "airflow.sdk.execution_time.execute_workload",
+ "--json-string",
+ ser_workload,
+ ]
+
+ @pytest.mark.skipif(AIRFLOW_V_3_0_PLUS, reason="Test requires Airflow 2.x")
+ def test_build_task_command_airflow2(self, mock_executor):
+ """Test _build_task_command for Airflow 2.x using command_as_list."""
+ mock_ti = mock.Mock(spec=TaskInstance)
+ expected_command = ["airflow", "tasks", "run", "dag_id", "task_id",
"execution_date"]
+ mock_ti.command_as_list.return_value = expected_command
+
+ command = mock_executor._build_task_command(mock_ti)
+
+ mock_ti.command_as_list.assert_called_once()
+ assert command == expected_command
+
@pytest.mark.skipif(not AIRFLOW_V_3_1_PLUS, reason="Multi-team support
requires Airflow 3.1+")
def test_team_config(self):
"""Test that the executor uses team-specific configuration when
provided via self.conf."""