amoghrajesh opened a new pull request, #69174:
URL: https://github.com/apache/airflow/pull/69174

    <!-- SPDX-License-Identifier: Apache-2.0
         https://www.apache.org/licenses/LICENSE-2.0 -->
   
   <!--
   Thank you for contributing!
   
   Please provide above a brief description of the changes made in this pull 
request.
   Write a good git commit message following this guide: 
http://chris.beams.io/posts/git-commit/
   
   Please make sure that your code changes are covered with tests.
   And in case of new features or big changes remember to adjust the 
documentation.
   
   For user-facing UI changes, please attach before/after screenshots (or a 
short
   screen recording) so reviewers can assess the visual impact.
   
   Feel free to ping (in general) for the review if you do not see reaction for 
a few days
   (72 Hours is the minimum reaction time you can expect from volunteers) - we 
sometimes miss notifications.
   
   In case of an existing issue, reference it using one of the following:
   
   * closes: #ISSUE
   * related: #ISSUE
   -->
   
   ### Why we are doing this
   
   `DatabricksRunNowOperator` triggers a run of an existing Databricks job, 
gets back a `run_id`, and then polls synchronously on the worker until the run 
finishes. That `run_id` lives only in the worker process. If the worker crashes 
or is preempted mid-poll (eviction, OOM, a deploy, a spot reclaim), Airflow 
retries the task in a fresh process with no memory of the `run_id`, so it calls 
`run-now` again, triggering a second run of the same job. The original run 
keeps executing on Databricks, orphaned, while the retry runs a duplicate.
   
   This is the same crash-safety gap closed for `DatabricksSubmitRunOperator` 
in #68974, applied to the run-now path. For long-running jobs it means paying 
twice for the same work, and the only existing mitigation 
(`cancel_previous_runs=True`) cancels rather than reconnects.
   
   ### Benefits this will bring in
   
   - A worker crash and retry reconnects to the run already executing on 
Databricks instead of triggering a duplicate, so you do not pay twice for the 
same job.
   - If the prior run already finished successfully, the retry returns 
immediately without triggering a new run.
   - If the stored run no longer exists because its history expired, the retry 
degrades to a fresh run instead of failing the task.
   - Works on the existing synchronous operator with no Triggerer required, and 
is enabled by default on Airflow 3.3+ with no Dag changes.
   
   ### Approach
   
   The operator now builds on the AIP-103 task state store. On the first run it 
persists the Databricks run id to the task state store before polling begins. 
On a retry it reads that id back and inspects the run's current state: still 
running means reconnect and keep polling, already succeeded means return 
immediately, and terminally failed (or a run whose history has expired) means 
trigger a fresh run. Reconnect keys off the persisted `run_id`, so it works the 
same whether the job was identified by `job_id` or `job_name`. Deferrable mode 
is unchanged and takes precedence when enabled.
   
   ### Changes of note
   
   - The run-state decoding helpers (`get_job_status` / `is_job_active` / 
`is_job_succeeded`) are intentionally duplicated from 
`DatabricksSubmitRunOperator` rather than extracted into a shared base, to keep 
each operator self-contained and this PR scoped to RunNow. They are small and 
each operator has its own tests covering them.
   - The expired-run degradation reuses the typed `DatabricksApiError` 
(carrying `http_status_code`) introduced alongside the SubmitRun work, so it is 
a structured `404` check rather than message-string matching.
   - The run-now payload build is split into a side-effect-free 
`_build_run_now_payload()` (merge, validate, resolve `job_name` to `job_id`, 
inject params) and the `cancel_previous_runs` step. On reconnect the payload is 
rebuilt via the side-effect-free path, so `repair_run` has the resolved 
`job_id` it needs and works across the reconnect boundary for both `job_id` and 
`job_name`, without re-firing `cancel_previous_runs` against the run being 
reconnected to.
   
   ### Backcompat
   
   - Fully backward compatible. On a clean run the observable trigger-and-poll 
behavior is unchanged; the only addition is that the run id is also written to 
the task state store before polling.
   - The task state store is an Airflow 3.3+ capability. On Airflow 2.x / 
pre-3.3 the operator degrades to exactly the old behavior (always triggers a 
fresh run on retry) through a compatibility shim, so the provider keeps working 
on older Airflow.
   - If the task state store is unavailable at runtime, the operator logs that 
crash recovery is disabled and behaves as before.
   - One new optional flag is added; no existing parameters change and no 
migration is needed.
   
   ### How to opt out
   
   Set `durable=False` on the operator:
   
   ```python
   DatabricksRunNowOperator(..., durable=False)
   ```
   
   This restores the previous behavior: always trigger a fresh run on retry and 
never touch the task state store. It can also be set through `default_args` to 
opt out across a whole Dag or deployment.
   
   
   ### Testing
   
   DAG used:
   ```python
   from datetime import timedelta
   
   import pendulum
   
   from airflow.providers.databricks.operators.databricks import 
DatabricksRunNowOperator
   from airflow.sdk import DAG
   
   JOB_ID = 480676095338270
   
   with DAG(
       dag_id="databricks_runnow_repro",
       schedule=None,
       start_date=pendulum.datetime(2025, 1, 1, tz="UTC"),
       catchup=False,
   ):
       DatabricksRunNowOperator(
           task_id="run_now_sleepy",
           databricks_conn_id="databricks_default",
           job_id=JOB_ID,
           deferrable=False,
           do_xcom_push=True,
           retries=1,
           retry_delay=timedelta(seconds=10),
       )
   
   ```
   
   #### Before my changes
   
   Kill mid run:
   <img width="1725" height="944" alt="image" 
src="https://github.com/user-attachments/assets/6b26a26f-1a9a-461e-98d6-2332be260b30";
 />
   
   
   Worker comes back up:
   
   <img width="1725" height="944" alt="image" 
src="https://github.com/user-attachments/assets/b045db3b-79ca-4ace-ba97-ab85eba2cc25";
 />
   
   
   Duplicate submissions:
   
   <img width="1725" height="944" alt="image" 
src="https://github.com/user-attachments/assets/ef663058-48f3-46c2-ad1f-97952b56fbed";
 />
   
   
   #### After my changes
   
   Killed mid run:
   
   <img width="1725" height="944" alt="image" 
src="https://github.com/user-attachments/assets/bd287457-5359-4d25-bdfa-86ef397593b3";
 />
   
   
   Job on dbx:
   
   <img width="1725" height="944" alt="image" 
src="https://github.com/user-attachments/assets/4f097f7b-10d7-4740-9b0c-aefe1365dfeb";
 />
   
   
   Restored worker:
   
   <img width="1725" height="944" alt="image" 
src="https://github.com/user-attachments/assets/651fae44-0f07-4b8c-838d-40969d98c660";
 />
   
   No duplicate job:
   
   <img width="1725" height="944" alt="image" 
src="https://github.com/user-attachments/assets/be8b0a3d-2265-4a2d-8823-73cb0f2d3602";
 />
   
   
   Killed the job from airflow:
   
   <img width="1725" height="944" alt="image" 
src="https://github.com/user-attachments/assets/5f4e7493-e0ed-4fd7-b462-73ce66f286aa";
 />
   
   
   <img width="1725" height="944" alt="image" 
src="https://github.com/user-attachments/assets/07e67cdc-a4f1-4c2c-ac47-8aa5ad389577";
 />
   
   
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   <!--
   If generative AI tooling has been used in the process of authoring this PR, 
please
   change below checkbox to `[X]` followed by the name of the tool, uncomment 
the "Generated-by".
   -->
   
   - [ ] Yes (please specify the tool below)
   
   <!--
   Generated-by: [Tool Name] following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
   -->
   
   ---
   
   * Read the **[Pull Request 
Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)**
 for more information. Note: commit author/co-author name and email in commits 
become permanently public when merged.
   * For fundamental code changes, an Airflow Improvement Proposal 
([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals))
 is needed.
   * When adding dependency, check compliance with the [ASF 3rd Party License 
Policy](https://www.apache.org/legal/resolved.html#category-x).
   * For significant user-facing changes create newsfragment: 
`{pr_number}.significant.rst`, in 
[airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments).
 You can add this file in a follow-up commit after the PR is created so you 
know the PR number.
   


-- 
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]

Reply via email to