amoghrajesh commented on code in PR #68974:
URL: https://github.com/apache/airflow/pull/68974#discussion_r3473883434
##########
providers/databricks/tests/unit/databricks/operators/test_databricks.py:
##########
@@ -1651,6 +1668,161 @@ def
test_inject_parent_job_info_preserves_existing_config(self, db_mock_class, m
assert
submitted["new_cluster"]["spark_conf"]["spark.openlineage.parentJobNamespace"]
== "ns"
[email protected](
+ not AIRFLOW_V_3_3_PLUS, reason="task_state_store (durable execution)
requires Airflow 3.3+"
+)
+class TestDatabricksSubmitRunOperatorDurable:
+ @staticmethod
+ def _context(task_store=None):
+ ctx: dict = {"ti": MagicMock(autospec=True)}
+ if task_store is not None:
+ ctx["task_state_store"] = task_store
+ return ctx
+
+ @staticmethod
+ def _state(lifecycle: str, result: str = ""):
+ return {"state": {"life_cycle_state": lifecycle, "result_state":
result, "state_message": ""}}
+
+
@mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
+ def test_persists_run_id_to_task_state_store_on_fresh_submit(self,
db_mock_class):
+ op = DatabricksSubmitRunOperator(
+ task_id=TASK_ID, json={"new_cluster": NEW_CLUSTER,
"notebook_task": NOTEBOOK_TASK}
+ )
+ db_mock = db_mock_class.return_value
+ db_mock.submit_run.return_value = RUN_ID
+ db_mock.get_run = make_run_with_state_mock("TERMINATED", "SUCCESS")
+ task_store = MagicMock()
+ task_store.get.return_value = None
+
+ op.execute(self._context(task_store))
+
+ db_mock.submit_run.assert_called_once()
+ task_store.set.assert_called_once_with("databricks_run_id", RUN_ID)
+ assert op.run_id == RUN_ID
+
+
@mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
+ def test_reconnects_to_active_run_without_resubmitting(self,
db_mock_class):
+ op = DatabricksSubmitRunOperator(
+ task_id=TASK_ID, json={"new_cluster": NEW_CLUSTER,
"notebook_task": NOTEBOOK_TASK}
+ )
+ db_mock = db_mock_class.return_value
+ # status check sees the stored run still RUNNING, the poll then sees
it finish.
+ db_mock.get_run.side_effect = [self._state("RUNNING"),
self._state("TERMINATED", "SUCCESS")]
+ task_store = MagicMock()
Review Comment:
Handled in 1cdec79f2a
##########
providers/databricks/tests/unit/databricks/operators/test_databricks.py:
##########
@@ -1651,6 +1668,161 @@ def
test_inject_parent_job_info_preserves_existing_config(self, db_mock_class, m
assert
submitted["new_cluster"]["spark_conf"]["spark.openlineage.parentJobNamespace"]
== "ns"
[email protected](
+ not AIRFLOW_V_3_3_PLUS, reason="task_state_store (durable execution)
requires Airflow 3.3+"
+)
+class TestDatabricksSubmitRunOperatorDurable:
+ @staticmethod
+ def _context(task_store=None):
+ ctx: dict = {"ti": MagicMock(autospec=True)}
+ if task_store is not None:
+ ctx["task_state_store"] = task_store
+ return ctx
+
+ @staticmethod
+ def _state(lifecycle: str, result: str = ""):
+ return {"state": {"life_cycle_state": lifecycle, "result_state":
result, "state_message": ""}}
+
+
@mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
+ def test_persists_run_id_to_task_state_store_on_fresh_submit(self,
db_mock_class):
+ op = DatabricksSubmitRunOperator(
+ task_id=TASK_ID, json={"new_cluster": NEW_CLUSTER,
"notebook_task": NOTEBOOK_TASK}
+ )
+ db_mock = db_mock_class.return_value
+ db_mock.submit_run.return_value = RUN_ID
+ db_mock.get_run = make_run_with_state_mock("TERMINATED", "SUCCESS")
+ task_store = MagicMock()
Review Comment:
Handled in 1cdec79f2a
##########
providers/databricks/tests/unit/databricks/operators/test_databricks.py:
##########
@@ -1651,6 +1668,161 @@ def
test_inject_parent_job_info_preserves_existing_config(self, db_mock_class, m
assert
submitted["new_cluster"]["spark_conf"]["spark.openlineage.parentJobNamespace"]
== "ns"
[email protected](
+ not AIRFLOW_V_3_3_PLUS, reason="task_state_store (durable execution)
requires Airflow 3.3+"
+)
+class TestDatabricksSubmitRunOperatorDurable:
+ @staticmethod
+ def _context(task_store=None):
+ ctx: dict = {"ti": MagicMock(autospec=True)}
+ if task_store is not None:
+ ctx["task_state_store"] = task_store
+ return ctx
Review Comment:
Handled in 1cdec79f2a
##########
providers/databricks/tests/unit/databricks/operators/test_databricks.py:
##########
@@ -1651,6 +1668,161 @@ def
test_inject_parent_job_info_preserves_existing_config(self, db_mock_class, m
assert
submitted["new_cluster"]["spark_conf"]["spark.openlineage.parentJobNamespace"]
== "ns"
[email protected](
+ not AIRFLOW_V_3_3_PLUS, reason="task_state_store (durable execution)
requires Airflow 3.3+"
+)
+class TestDatabricksSubmitRunOperatorDurable:
+ @staticmethod
+ def _context(task_store=None):
+ ctx: dict = {"ti": MagicMock(autospec=True)}
+ if task_store is not None:
+ ctx["task_state_store"] = task_store
+ return ctx
+
+ @staticmethod
+ def _state(lifecycle: str, result: str = ""):
+ return {"state": {"life_cycle_state": lifecycle, "result_state":
result, "state_message": ""}}
+
+
@mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
+ def test_persists_run_id_to_task_state_store_on_fresh_submit(self,
db_mock_class):
+ op = DatabricksSubmitRunOperator(
+ task_id=TASK_ID, json={"new_cluster": NEW_CLUSTER,
"notebook_task": NOTEBOOK_TASK}
+ )
+ db_mock = db_mock_class.return_value
+ db_mock.submit_run.return_value = RUN_ID
+ db_mock.get_run = make_run_with_state_mock("TERMINATED", "SUCCESS")
+ task_store = MagicMock()
+ task_store.get.return_value = None
+
+ op.execute(self._context(task_store))
+
+ db_mock.submit_run.assert_called_once()
+ task_store.set.assert_called_once_with("databricks_run_id", RUN_ID)
+ assert op.run_id == RUN_ID
+
+
@mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
+ def test_reconnects_to_active_run_without_resubmitting(self,
db_mock_class):
+ op = DatabricksSubmitRunOperator(
+ task_id=TASK_ID, json={"new_cluster": NEW_CLUSTER,
"notebook_task": NOTEBOOK_TASK}
+ )
+ db_mock = db_mock_class.return_value
+ # status check sees the stored run still RUNNING, the poll then sees
it finish.
+ db_mock.get_run.side_effect = [self._state("RUNNING"),
self._state("TERMINATED", "SUCCESS")]
+ task_store = MagicMock()
+ task_store.get.return_value = RUN_ID
+
+ op.execute(self._context(task_store))
+
+ db_mock.submit_run.assert_not_called()
+ task_store.set.assert_not_called()
+ assert op.run_id == RUN_ID
+
+
@mock.patch("airflow.providers.databricks.operators.databricks.DatabricksHook")
+ def test_skips_polling_when_stored_run_already_succeeded(self,
db_mock_class):
+ op = DatabricksSubmitRunOperator(
+ task_id=TASK_ID, json={"new_cluster": NEW_CLUSTER,
"notebook_task": NOTEBOOK_TASK}
+ )
+ db_mock = db_mock_class.return_value
+ db_mock.get_run = make_run_with_state_mock("TERMINATED", "SUCCESS")
+ task_store = MagicMock()
+ task_store.get.return_value = RUN_ID
+
+ op.execute(self._context(task_store))
+
+ db_mock.submit_run.assert_not_called()
+ db_mock.get_run_page_url.assert_not_called()
+ assert op.run_id == RUN_ID
Review Comment:
Handled in 1cdec79f2a
##########
providers/databricks/src/airflow/providers/databricks/operators/databricks.py:
##########
@@ -876,6 +909,39 @@ def
_inject_openlineage_properties_into_databricks_job(self, json: dict, context
)
return json
+ def submit_job(self, context: Context) -> int:
+ normalised = self._prepare_submit_json(context)
+ # Set run_id the instant the run exists so on_kill can cancel it even
if the worker dies
+ # before polling begins.
+ self.run_id = self._hook.submit_run(normalised)
+ self.log.info("Run submitted with run_id: %s", self.run_id)
+ return self.run_id
+
+ def get_job_status(self, external_id: int, context: Context) -> str:
+ # Databricks splits run state across life_cycle_state and
result_state; the mixin's status
+ # interface is a single string, so encode both as "LIFE_CYCLE:RESULT"
and decode them in
+ # is_job_active / is_job_succeeded.
+ run_info = self._hook.get_run(external_id)
+ state = RunState(**run_info["state"])
+ return f"{state.life_cycle_state}:{state.result_state}"
+
+ def is_job_active(self, status: str) -> bool:
+ life_cycle_state = status.split(":", 1)[0]
+ return life_cycle_state not in ("TERMINATED", "SKIPPED",
"INTERNAL_ERROR")
+
+ def is_job_succeeded(self, status: str) -> bool:
+ return status == "TERMINATED:SUCCESS"
+
+ def poll_until_complete(self, external_id: int, context: Context) -> None:
+ self.run_id = external_id
+ # The run already exists here (fresh submit logged in submit_job, or
reconnect logged by the
+ # mixin), so the poll helper must not announce a submission.
+ _handle_databricks_operator_execution(self, self._hook, self.log,
context, announce_submission=False)
+
+ def get_job_result(self, external_id: int, context: Context) -> None:
+ self.run_id = external_id
+ return None
Review Comment:
Handled in 1cdec79f2a
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]