This is an automated email from the ASF dual-hosted git repository.
eladkal 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 dc0e0bb084d Add on_kill() to DatabricksTaskBaseOperator to cancel runs
on task kill (#69442)
dc0e0bb084d is described below
commit dc0e0bb084d685e2cb475e312f729fbb2a262efa
Author: Victory <[email protected]>
AuthorDate: Thu Aug 6 13:46:27 2026 +0800
Add on_kill() to DatabricksTaskBaseOperator to cancel runs on task kill
(#69442)
DatabricksSubmitRunOperator and DatabricksRunNowOperator both implement
on_kill() to cancel the Databricks run when an Airflow task is killed
(SIGTERM or execution_timeout). DatabricksTaskBaseOperator — the base for
DatabricksTaskOperator and DatabricksNotebookOperator — was missing the
same implementation, leaving Databricks jobs running after the Airflow task
was killed and orphaning compute resources.
DatabricksWorkflowTaskGroup received on_kill() in #42115; this PR closes
the remaining gap for standalone task operators.
For workflow members self.databricks_run_id is the shared parent run ID;
cancelling it would stop all sibling tasks. on_kill() therefore calls
_get_current_databricks_task()["run_id"] to target only the current task's
own child run, mirroring monitor_databricks_job. Standalone operators
continue to cancel via self.databricks_run_id directly.
If resolving the child run_id fails (API error, task_key mismatch), on_kill
logs the exception and returns without cancelling anything — falling back to
the parent run_id would stop sibling tasks, defeating the purpose.
Unit tests cover: cancel called for standalone operator, no-op when
databricks_run_id is None, workflow-member cancels child run (parent=1,
child=999, asserts cancel_run(999)), and workflow-member where
_get_current_databricks_task raises asserts cancel_run not called.
---
.../providers/databricks/operators/databricks.py | 25 +++++++++
.../unit/databricks/operators/test_databricks.py | 64 ++++++++++++++++++++++
2 files changed, 89 insertions(+)
diff --git
a/providers/databricks/src/airflow/providers/databricks/operators/databricks.py
b/providers/databricks/src/airflow/providers/databricks/operators/databricks.py
index f6bf1cf6aca..a64acb78598 100644
---
a/providers/databricks/src/airflow/providers/databricks/operators/databricks.py
+++
b/providers/databricks/src/airflow/providers/databricks/operators/databricks.py
@@ -1973,6 +1973,31 @@ class DatabricksTaskBaseOperator(BaseOperator, ABC):
errors = event.get("errors", [])
self._handle_terminal_run_state(run_state, errors)
+ def on_kill(self) -> None:
+ if self.databricks_run_id is None:
+ return
+ if self._databricks_workflow_task_group:
+ # Workflow member: cancel only this task's child run, not the
shared parent workflow run.
+ # Cancelling the parent would also stop all sibling tasks.
+ # If the child run_id cannot be resolved, log and bail out — do
NOT fall back to the
+ # parent run_id as that would cancel sibling tasks.
+ try:
+ run_id_to_cancel =
self._get_current_databricks_task()["run_id"]
+ except Exception:
+ self.log.exception(
+ "Task: %s could not resolve child run_id; skipping cancel
to avoid stopping sibling tasks.",
+ self.task_id,
+ )
+ return
+ else:
+ run_id_to_cancel = self.databricks_run_id
+ self._hook.cancel_run(run_id_to_cancel)
+ self.log.info(
+ "Task: %s with run_id: %s was requested to be cancelled.",
+ self.task_id,
+ run_id_to_cancel,
+ )
+
class DatabricksNotebookOperator(DatabricksTaskBaseOperator):
"""
diff --git
a/providers/databricks/tests/unit/databricks/operators/test_databricks.py
b/providers/databricks/tests/unit/databricks/operators/test_databricks.py
index ad9d718e34f..65c990a5926 100644
--- a/providers/databricks/tests/unit/databricks/operators/test_databricks.py
+++ b/providers/databricks/tests/unit/databricks/operators/test_databricks.py
@@ -4090,3 +4090,67 @@ class TestDatabricksTaskOperator:
expected_task_key = "test_task_key"
assert expected_task_key == operator.databricks_task_key
+
+
@mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
+ def test_on_kill_cancels_run(self, db_mock_class):
+ operator = DatabricksTaskOperator(
+ task_id="task",
+ task_config={"sql_task": {"query": {"query_id": "abc"}}},
+ )
+ db_mock = db_mock_class.return_value
+ operator.databricks_run_id = 1
+ operator.on_kill()
+ db_mock.cancel_run.assert_called_once_with(1)
+
+ def test_on_kill_does_nothing_when_run_id_is_none(self):
+ operator = DatabricksTaskOperator(
+ task_id="task",
+ task_config={"sql_task": {"query": {"query_id": "abc"}}},
+ )
+ with
mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
as db_mock_class:
+ operator.on_kill()
+ db_mock_class.return_value.cancel_run.assert_not_called()
+
+
@mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
+ def test_on_kill_workflow_member_cancels_child_run(self, db_mock_class):
+ operator = DatabricksTaskOperator(
+ task_id="task",
+ task_config={"sql_task": {"query": {"query_id": "abc"}}},
+ )
+ db_mock = db_mock_class.return_value
+ operator.databricks_run_id = 1
+ with mock.patch.object(
+ operator,
+ "_get_current_databricks_task",
+ return_value={"run_id": 999, "task_key": "task"},
+ ):
+ with mock.patch(
+ "airflow.providers.databricks.operators.databricks"
+ ".DatabricksTaskBaseOperator._databricks_workflow_task_group",
+ new_callable=mock.PropertyMock,
+ return_value=object(),
+ ):
+ operator.on_kill()
+ db_mock.cancel_run.assert_called_once_with(999)
+
+
@mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
+ def
test_on_kill_workflow_member_get_task_raises_does_not_cancel_parent(self,
db_mock_class):
+ operator = DatabricksTaskOperator(
+ task_id="task",
+ task_config={"sql_task": {"query": {"query_id": "abc"}}},
+ )
+ db_mock = db_mock_class.return_value
+ operator.databricks_run_id = 1
+ with mock.patch.object(
+ operator,
+ "_get_current_databricks_task",
+ side_effect=Exception("API error"),
+ ):
+ with mock.patch(
+ "airflow.providers.databricks.operators.databricks"
+ ".DatabricksTaskBaseOperator._databricks_workflow_task_group",
+ new_callable=mock.PropertyMock,
+ return_value=object(),
+ ):
+ operator.on_kill()
+ db_mock.cancel_run.assert_not_called()