bingqin2 commented on issue #74459:
URL: https://github.com/apache/airflow/issues/74459#issuecomment-6084344914

   Tested #74305 and #72953 in `apache-airflow-providers-google==22.7.0rc1`. 
Both work as expected.
   
   Setup: a clean virtualenv with Python 3.12.13, `apache-airflow==3.3.2` and 
the RC from PyPI. I ran the same checks against 22.6.0 for comparison. I don't 
have a GCP project, so the BigQuery job states and the GCS blob calls are 
faked. The triggers, the operator, the tenacity retry policy and the transform 
subprocess are the installed code. shahar1's before/after run against real 
BigQuery is in #74356.
   
   | Check | 22.6.0 | 22.7.0rc1 |
   |---|---|---|
   | #74305: value check, job `RUNNING` on the first poll | fails with `Job 
running` | keeps polling, succeeds |
   | #74305: interval check, first job `RUNNING` on the first poll | fails with 
`Job completed` (the second job's message) | keeps polling, succeeds |
   | #74305: interval check, first job fails while the second runs | reports 
`Job running` | reports the first job's error |
   | #72953: download fails twice, `download_num_attempts=3` | fails after 1 
call | succeeds on call 3, after 2 s and 4 s waits |
   | #72953: upload fails once, `upload_num_attempts=2` | fails after 1 call | 
succeeds on call 2 |
   | #72953: download fails 3 times, `download_num_attempts=3` | raises after 1 
call | raises after 3 calls |
   
   <details><summary>Script</summary>
   
   ```python
   """Check #74305 and #72953 against an installed 
apache-airflow-providers-google wheel.
   
   No GCP access: BigQuery job states and GCS blob operations are faked, 
everything else
   (triggers, operator, tenacity retry policy, transform subprocess) is the 
installed code.
   """
   
   from __future__ import annotations
   
   import asyncio
   import importlib.metadata
   import logging
   import os
   import stat
   import sys
   import tempfile
   from pathlib import Path
   from unittest import mock
   
   import pendulum
   from google.api_core.exceptions import ServiceUnavailable
   
   logging.basicConfig(level=logging.WARNING, format="    log: %(message)s")
   
   VERSION = importlib.metadata.version("apache-airflow-providers-google")
   print(f"apache-airflow {importlib.metadata.version('apache-airflow')}, "
         f"apache-airflow-providers-google {VERSION}, Python 
{sys.version.split()[0]}")
   
   from airflow.providers.google.cloud.triggers.bigquery import (  # noqa: E402
       BigQueryIntervalCheckTrigger,
       BigQueryValueCheckTrigger,
   )
   
   RUNNING = {"status": "running", "message": "Job running"}
   DONE = {"status": "success", "message": "Job completed"}
   FAILED = {"status": "error", "message": "Syntax error: Unexpected keyword 
FROM at [1:8]"}
   
   
   def run_trigger(trigger, job_states):
       with 
mock.patch("airflow.providers.google.cloud.triggers.bigquery.BigQueryAsyncHook")
 as hook_cls:
           hook = hook_cls.return_value
           hook.get_job_status = mock.AsyncMock(side_effect=job_states)
           hook.get_sync_hook = mock.AsyncMock(
               
return_value=mock.MagicMock(is_default_universe=mock.MagicMock(return_value=True))
           )
           hook.get_job_output = mock.AsyncMock(return_value={})
           hook.get_records = mock.MagicMock(side_effect=lambda _: [[2]])
   
           async def collect():
               return [event.payload async for event in trigger.run()]
   
           events = asyncio.run(collect())
           return events[0], hook.get_job_status.await_count
   
   
   def show(name, expected, event, polls):
       got = f"{event['status']}: {event['message']}"
       verdict = "OK " if got == expected else "BAD"
       print(f"  [{verdict}] {name}\n        expected {expected!r}\n        got 
     {got!r} ({polls} status calls)")
   
   
   print("\n#74305 BigQuery check triggers")
   value = BigQueryValueCheckTrigger(
       conn_id="google_cloud_default", sql="SELECT COUNT(*) FROM t", 
pass_value=2,
       job_id="job_1", project_id="p", poll_interval=0.01,
   )
   event, polls = run_trigger(value, [RUNNING, DONE])
   show("value check, job RUNNING on the first poll", "success: Job completed", 
event, polls)
   
   
   def interval():
       return BigQueryIntervalCheckTrigger(
           conn_id="google_cloud_default", first_job_id="job_1", 
second_job_id="job_2", project_id="p",
           table="t", metrics_thresholds={"COUNT(*)": 1.5}, poll_interval=0.01,
       )
   
   
   # get_job_status is awaited for the first job, then the second job, on every 
poll.
   event, polls = run_trigger(interval(), [RUNNING, DONE, DONE, DONE])
   show("interval check, first job RUNNING on the first poll", "success: Job 
completed", event, polls)
   event, polls = run_trigger(interval(), [FAILED, RUNNING])
   show("interval check, first job FAILED while the second runs", f"error: 
{FAILED['message']}", event, polls)
   
   print("\n#72953 GCSTimeSpanFileTransformOperator retry attempts")
   from airflow.providers.google.cloud.operators.gcs import 
GCSTimeSpanFileTransformOperator  # noqa: E402
   
   
   class FakeGCS:
       """A GCS client whose first download_failures downloads and 
upload_failures uploads fail."""
   
       def __init__(self, download_failures, upload_failures):
           self.left = {"download": download_failures, "upload": 
upload_failures}
           self.calls = {"download": 0, "upload": 0}
           self.uploaded = []
   
       def _maybe_fail(self, kind):
           self.calls[kind] += 1
           if self.left[kind] > 0:
               self.left[kind] -= 1
               raise ServiceUnavailable(f"{kind} attempt {self.calls[kind]}: 
503 backend unavailable")
   
       def bucket(self, bucket_name):
           return self
   
       def blob(self, blob_name, chunk_size=None):
           gcs = self
   
           class Blob:
               def download_to_filename(self, filename):
                   gcs._maybe_fail("download")
                   Path(filename).write_text("hello\n")
   
               def upload_from_filename(self, filename):
                   gcs._maybe_fail("upload")
                   gcs.uploaded.append(blob_name)
   
           return Blob()
   
   
   def run_operator(download_failures, upload_failures, **attempts):
       gcs = FakeGCS(download_failures, upload_failures)
       hook = mock.MagicMock(project_id="p")
       hook.list_by_timespan.return_value = ["in/2026-10-09/a.txt"]
       hook.get_conn.return_value = gcs
       with tempfile.TemporaryDirectory() as tmp:
           script = Path(tmp) / "transform.sh"
           script.write_text('#!/bin/sh\ncp -R "$1"/. "$2"/\n')  # copy input 
to output
           script.chmod(script.stat().st_mode | stat.S_IEXEC)
           op = GCSTimeSpanFileTransformOperator(
               task_id="transform", source_bucket="src", 
source_prefix="in/%Y-%m-%d/",
               source_gcp_conn_id="google_cloud_default", 
destination_bucket="dst",
               destination_prefix="out/", 
destination_gcp_conn_id="google_cloud_default",
               transform_script=str(script), **attempts,
           )
           context = {
               "data_interval_start": pendulum.datetime(2026, 10, 9),
               "data_interval_end": pendulum.datetime(2026, 10, 10),
               "ti": mock.MagicMock(),
           }
           with (
               
mock.patch("airflow.providers.google.cloud.operators.gcs.GCSHook", 
return_value=hook),
               mock.patch("tenacity.nap.time.sleep"),  # keep the retry waits, 
without waiting
           ):
               try:
                   result = op.execute(context)
                   outcome = f"success, uploaded {result}"
               except Exception as e:
                   outcome = f"{type(e).__name__}: {e}"
       return outcome, gcs.calls
   
   
   for name, failures, attempts, expect in [
       ("download fails twice, download_num_attempts=3", (2, 0), 
{"download_num_attempts": 3}, "success"),
       ("upload fails once, upload_num_attempts=2", (0, 1), 
{"upload_num_attempts": 2}, "success"),
       ("download fails 3 times, download_num_attempts=3", (3, 0), 
{"download_num_attempts": 3}, "ServiceUnavailable"),
   ]:
       outcome, calls = run_operator(*failures, **attempts)
       verdict = "OK " if outcome.startswith(expect) else "BAD"
       print(f"  [{verdict}] {name}\n        expected {expect}\n        got     
 {outcome} "
             f"(download calls {calls['download']}, upload calls 
{calls['upload']})")
   ```
   
   </details>
   


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