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

    <!-- 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
   -->
   
   ---
   
   ##### 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)
   -->
   
   ### What
   
   `BigQueryInsertJobOperator` submits a BigQuery job and polls it on the 
worker. If the worker crashes mid-poll and the task is retried, there was no 
reliable way to reconnect to the still running job & the retry submits a brand 
new one, orphaning the original (which keeps running and costing money) instead 
of reusing it.
   
   ### Current behaviour
   
   The operator already has a partial, fragile reattach mechanism: it 
optimistically resubmits with a **recomputed** job id and relies on BigQuery 
rejecting the duplicate (`Conflict`/HTTP 409) to detect and reattach to an 
existing job. This only works when `force_rerun=False` (or a fixed `job_id`) 
and `reattach_states` is populated but `force_rerun=True` is the operator's own 
default, which makes `generate_job_id` return a fresh random id on every 
attempt, so by default the retry's recomputed id never matches and reattach 
never triggers. A crash-and-retry under default settings always submits a 
duplicate job and orphans the first one.
   
   ### Proposed change
   
   Adopt `ResumableJobMixin` (from `airflow.sdk`, AIP-103) so the operator 
persists the *actual* submitted job id to task state and reads it back on 
retry, rather than trying to recompute an identical id. This makes reattach 
work regardless of `force_rerun`:
   
   - `submit_job`: extracted from the old `execute()`: generates/submits the 
job (still honoring the existing `Conflict`/`reattach_states`/429-retry rules 
on the fresh-submit path) and persists the job id.
   - `get_job_status`: fetches the job by id (`get_job`), mapping `DONE` to 
`"success"`/`"error"` based on `error_result`, with `NotFound` degrading to a 
`"not_found"` sentinel so an expired/unknown id triggers a fresh submit instead 
of crashing.
   - `is_job_active` / `is_job_succeeded`: simple predicates over that status.
   - `poll_until_complete` / `get_job_result`: wait on the job and return its 
id, reusing the existing link/XCom bookkeeping (`_persist_job_links`) so 
behavior is identical whether this attempt submitted the job or reconnected to 
it.
   - `execute()` now checks `deferrable` first (unchanged) and routes the 
synchronous path through `execute_resumable`; `submit_job` is shared as-is by 
the deferrable branch since its logic doesn't depend on `deferrable` at all.
   
   ### Changes of Note
   
   - **`self._configured_job_id`**: `get_job_status` (called during a durable 
resubmit's initial status check) mutates `self.job_id` to the *old* failed 
job's id via `_persist_job_links`, before `submit_job` runs again. If 
`submit_job` used `self.job_id` as input to `generate_job_id`, it would compute 
a corrupted id from the stale value. Fixed by capturing the user's original 
`job_id` input once in `__init__` as `self._configured_job_id` (never mutated) 
and using that instead.
   - **`poll_until_complete` must return the job id**: on the mixin's reconnect 
path, `execute_resumable` returns `poll_until_complete`'s result directly, 
skipping `get_job_result` entirely, an easy bug to reintroduce (also hit 
independently in the Redshift port).
   - **`get_job_status` self-resolves `self.hook`/`self.project_id`**: these 
are normally set by `submit_job`, which is skipped entirely on the reconnect 
path, so `get_job_status` resolves them itself when unset.
   - The existing `reattach_states`/`Conflict`/429/`DONE`-can't-reattach 
special cases are preserved unchanged on the fresh-submit path 
(`durable=False`, or pre-Airflow-3.3).
   
   ### User implications / backcompat
   
   New `durable` parameter, defaulting to `True` on Airflow 3.3+. On older 
Airflow the `ResumableJobMixin` import is stubbed and the operator falls back 
to today's regenerate-id + `Conflict`-reattach behavior — no behavior change 
for those versions. `force_rerun`'s default is unchanged; durable execution 
makes its non-determinism irrelevant to crash-recovery specifically, but it's 
still relevant to idempotency across separate DAG runs (task state is scoped 
per task instance), so `force_rerun` is not being deprecated.
   
   ### Testing
   
   Create a connection with your GCP credentials, like so:
   ```shell
   airflow connections add google_cloud_default   --conn-type 
google_cloud_platform   --conn-extra '{"key_path": 
"/opt/airflow/dev/sigma-night-269811-6107ef06a57f.json", "project": 
"sigma-night-269811", "scope": 
"https://www.googleapis.com/auth/cloud-platform"}'
   ```
   
   DAG:
   ```python
   from __future__ import annotations
   
   from datetime import timedelta
   
   import pendulum
   
   from airflow.providers.google.cloud.operators.bigquery import 
BigQueryInsertJobOperator
   from airflow.sdk import DAG
   
   with DAG(
       dag_id="bigquery_insert_job_demo",
       start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
       schedule=None,
       catchup=False,
   ):
       # A slow-ish query so there's a real window to kill the worker mid-poll.
       # Plain aggregation over a cross join finishes too fast -- BigQuery 
parallelizes it
       # across many slots. A JS UDF forces genuinely slow, hard-to-parallelize 
per-row work,
       # so wall-clock time is controlled by ROWS * LOOP_ITERS, not just row 
count.
       # Tune ROWS (currently 3000 -> 9M rows) or the JS loop bound (1000) 
up/down to hit
       # whatever poll window you want.
       run_job = BigQueryInsertJobOperator(
           task_id="run_job",
           configuration={
               "query": {
                   "query": (
                       "CREATE TEMP FUNCTION slow_calc(x INT64) RETURNS FLOAT64 
LANGUAGE js AS '''\n"
                       "  var acc = 0;\n"
                       "  for (var i = 0; i < 1000; i++) { acc += Math.sqrt(x + 
i); }\n"
                       "  return acc;\n"
                       "''';\n"
                       "SELECT COUNT(*) FROM UNNEST(GENERATE_ARRAY(1, 3000)) AS 
a "
                       "CROSS JOIN UNNEST(GENERATE_ARRAY(1, 3000)) AS b "
                       "WHERE MOD(CAST(slow_calc(a * b) AS INT64), 7) = 0"
                   ),
                   "useLegacySql": False,
               }
           },
           location="US",
           # durable=True,
           retries=2,
           retry_delay=timedelta(seconds=5),
       )
   
   ```
   
   #### Before changes
   
   First try where worker was killed:
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/31e5bb51-9a24-465d-8a39-9135fc0aefbf";
 />
   
   
   This job started running:
   
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/e2c162b0-9184-41b6-b558-97947b9ead48";
 />
   
   
   When worker comes back up, a new bigquery job gets submitted:
   
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/6ae58edf-0c0b-44d6-bc2f-613aedca3232";
 />
   
   
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/e6879318-6cfd-42dc-9b44-b233e241b010";
 />
   
   Hence lot of compute wastage
   
   ### Before changes but with `force_rerun=False`
   
   Try 1
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/15798fb3-4428-46f1-95ba-eb0e27fe4779";
 />
   
   
   Worker killed and brought back up
   
   Try 2:
   
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/efc121ae-0fee-4afc-aba0-f1f6c7ed1f8d";
 />
   
   
   Try 3:
   
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/22269680-5542-4895-a199-f4401a283f79";
 />
   
   
   You can see that:
   - Try 1's job is still running in BigQuery, nobody ever read its result, and 
Try 3 submitted a second, independent job for the same task instance - one 
orphaned job, one "real" one. This is precisely the 
`force_rerun=False`-without-reattach_states fragility, and it's why the mixin's 
persisted-id-and-reconnect approach (on bigquery-durable-execution) is needed, 
it reconnects on Try 2 directly instead of depending on reattach_states being 
populated correctly.
   
   #### After Changes:
   
   Try 1:
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/762843b2-570b-40a9-ab35-e8d2aaf7ba75";
 />
   
   
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/bf9567f0-5005-4bc9-b537-82654f1397a8";
 />
   
   
   Worker came back up, try 2:
   
   <img width="1725" height="977" alt="image" 
src="https://github.com/user-attachments/assets/ac2758e9-6f7e-430c-a8aa-aee4e7f92e99";
 />
   
   
   
   
   ### What's next
   
   Identified several other BigQuery operators with the same 
crash-vulnerability shape (`BigQueryCheckOperator`, 
`BigQueryValueCheckOperator`, `BigQueryIntervalCheckOperator`, 
`BigQueryColumnCheckOperator`, `BigQueryTableCheckOperator`), will pick them 
and work one by one.
   
   
   
   ---
   
   * 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