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

    <!-- 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
   
   `SnowflakeSqlApiOperator` submits one or more SQL statements to the 
Snowflake SQL API and polls their statement handles to completion on the 
worker. On a worker crash or preemption mid-poll, Airflow retries the task by 
calling `execute()` again which re-submits the SQL from scratch, since nothing 
about the in-flight statement handles is persisted across attempts.
   
   ### Current behaviour
   
   A retry after a crash always resubmits the full SQL, even if the original 
statements are still running (or already finished) in Snowflake. For 
non-idempotent SQL (`INSERT`, `UPDATE`, `CREATE TABLE`, etc.) this risks 
duplicate writes; for expensive queries it's wasted warehouse compute, since 
the orphaned original execution keeps running with nobody polling it.
   
   ### Proposed change
   
   Adds `ResumableJobMixin` support (Airflow 3.3+) to 
`SnowflakeSqlApiOperator`, following the same pattern already completed for 
`DatabricksSubmitRunOperator`/`DatabricksRunNowOperator`. Before polling 
begins, the submitted statement handles are persisted to `task_state_store`. On 
retry, the operator reads them back and:
   
   - reconnects and keeps polling if any handle is still running 
(already-finished handles are not re-run, only the ones still in progress are 
waited on)
   - returns immediately without resubmitting if every handle already succeeded
   - submits the SQL fresh if a handle failed, or its handle expired past 
Snowflake's retention window (404)
   
   `durable=True` is the default; set `durable=False` to keep the old "always 
submit fresh on retry" behavior. `deferrable=True` takes precedence over 
`durable` — the Triggerer already tracks handles across the wait in that mode.
   
   Includes a companion fix (already merged separately, #69450) that removed an 
unconditional per-handle sleep in `poll_on_queries()`, which this port relies 
on to stay latency-neutral on the already-resolved case.
   
   ### Changes of Note
   
   - `get_job_status` aggregates across the list of statement handles into the 
single status string the mixin's interface expects: any `error` wins over any 
`running`, which wins over all-`success` — matching Snowflake's all-or-nothing 
submission semantics (there's no per-statement repair, unlike Databricks' 
`repair_run`).
   - The mixin calls `poll_until_complete` alone on reconnect — 
`get_job_result` is never invoked on that path. Since `get_job_result` is where 
`check_query_output` (the actual result fetch/log) lives, `check_query_output` 
was moved into `poll_until_complete` itself, guarded by a flag so the 
fresh-submit path (which calls both methods) doesn't fetch/push twice.
   - A 404 from an expired/unknown statement handle is caught in 
`get_job_status` and treated as a `not_found` sentinel, degrading to a fresh 
resubmit rather than failing the task.
   
   ### User implications / backcompat
   
   No breaking change. `durable` defaults to `True` on Airflow 3.3+; on earlier 
versions it's a no-op stub and the operator always submits fresh, exactly as 
before. If `task_state_store` isn't available at runtime, the operator logs 
that crash recovery is disabled and falls back to the same fresh-submit 
behavior.
   
   One minor, intentional behavior shift: a fresh submission that is still 
running on its very first status check now incurs one `poll_interval` of 
latency it didn't before (the old code had a sleep-free pre-check before 
entering the poll loop; the durable path goes straight into the sleep-guarded 
loop). One-time cost, not compounding, and documented in the test suite.
   
   ### Testing
   
   Follow this to create a snowflake connection using RSA: 
https://medium.com/@chik0di/using-the-snowflakesqlapi-operator-in-airflow-0206632db2a3
   
   DAG used:
   ```python
   from datetime import datetime, timedelta
   
   from airflow.providers.snowflake.operators.snowflake import 
SnowflakeSqlApiOperator
   from airflow.sdk import DAG
   
   with DAG(
       dag_id="snowflake_sql_api",
       schedule=None,
       start_date=datetime(2024, 1, 1),
       catchup=False,
   ) as dag:
       multi_stmt = SnowflakeSqlApiOperator(
           task_id="multi_statement",
           snowflake_conn_id="snowflake_default",
           sql=(
               "SELECT CURRENT_TIMESTAMP(); "
               "CALL SYSTEM$WAIT(30); "
               "SELECT CURRENT_ACCOUNT();"
           ),
           statement_count=3,
           deferrable=False,
           retries=2,
           retry_delay=timedelta(seconds=5),
       )
   
   ```
   
   #### Before changes
   
   Before:
   
   Initial try where worker crashed:
   
   <img width="1660" height="874" alt="image" 
src="https://github.com/user-attachments/assets/74fe5f6e-c047-446b-a6c6-d65d05716d23";
 />
   
   
   Later try repeats the same effort which was already done:
   
   <img width="1660" height="917" alt="image" 
src="https://github.com/user-attachments/assets/c2fbf8e7-2e39-467b-8928-16bf93865b9c";
 />
   
   
   Duplicate effort:
   
   <img width="1471" height="86" alt="image" 
src="https://github.com/user-attachments/assets/a442b318-5456-4bc5-9303-3c96fd65fcdb";
 />
   
   
   First try suceeded in snowflake at 5:37:43 even when airflow task failed at: 
5:37:17
   
   <img width="1471" height="794" alt="image" 
src="https://github.com/user-attachments/assets/0b6d58d4-faa5-4ea7-9238-e606df389619";
 />
   
   <img width="1471" height="794" alt="image" 
src="https://github.com/user-attachments/assets/516bc3e2-e7a2-4d6f-b50a-9d8cafdaefd8";
 />
   
   #### After changes
   
   Initial try where worker crashed
   <img width="1663" height="919" alt="image" 
src="https://github.com/user-attachments/assets/02ece83b-4d9a-4c82-a8d9-a1dbec6223c9";
 />
   
   
   Worker is back up but job has already completed:
   <img width="1663" height="919" alt="image" 
src="https://github.com/user-attachments/assets/507d32b5-dcf6-42d2-9129-d6355e5bfc14";
 />
   
   
   <img width="1663" height="919" alt="image" 
src="https://github.com/user-attachments/assets/36296e0d-a46e-494d-b0a8-9c8dd464e2e1";
 />
   
   <img width="1663" height="919" alt="image" 
src="https://github.com/user-attachments/assets/9548e343-6533-450d-8aef-db7408fe0c10";
 />
   
   Since I had a custom backend configured, data stored is:
   
   ```shell
   [Breeze:3.10.20] root@62f2e50bd27b:/opt/airflow$ cat 
/tmp/airflow_state/ti_multi_statement/snowflake_query_ids.json
   ["01c5873f-061b-2e62-0068-2101308e710b"]
   ```
   
   No duplicate resubmission.
   
   
   ---
   
   * 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