This is an automated email from the ASF dual-hosted git repository.

Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 5f41ff419c9 poll BigQuery matchers on missing tables (#40559)
5f41ff419c9 is described below

commit 5f41ff419c9dcd97ee70655eff4fb6005155dbc5
Author: Abdelrahman Ibrahim <[email protected]>
AuthorDate: Thu Oct 15 16:35:45 2026 +0300

    poll BigQuery matchers on missing tables (#40559)
---
 .../trigger_files/beam_PostCommit_Python_Arm.json  |  2 +-
 .github/workflows/beam_PostCommit_Python_Arm.yml   |  2 +-
 .../apache_beam/io/gcp/tests/bigquery_matcher.py   | 30 ++++++++--
 .../io/gcp/tests/bigquery_matcher_test.py          | 67 +++++++++++++++++++++-
 4 files changed, 92 insertions(+), 9 deletions(-)

diff --git a/.github/trigger_files/beam_PostCommit_Python_Arm.json 
b/.github/trigger_files/beam_PostCommit_Python_Arm.json
index bbdc3a3910e..5cf4f475f31 100644
--- a/.github/trigger_files/beam_PostCommit_Python_Arm.json
+++ b/.github/trigger_files/beam_PostCommit_Python_Arm.json
@@ -1,4 +1,4 @@
 {
     "comment": "Modify this file in a trivial way to cause this test suite to 
run",
-    "modification": 3
+    "modification": 7
 }
diff --git a/.github/workflows/beam_PostCommit_Python_Arm.yml 
b/.github/workflows/beam_PostCommit_Python_Arm.yml
index f34d63bf851..994e4e6f789 100644
--- a/.github/workflows/beam_PostCommit_Python_Arm.yml
+++ b/.github/workflows/beam_PostCommit_Python_Arm.yml
@@ -56,7 +56,7 @@ jobs:
     # apache-beam-testing Snapshots image tagged with sdk_version, plus an
     # aarch64 wheel from this checkout. Keep headroom for the IT suite and
     # qemu wheel build.
-    timeout-minutes: 180
+    timeout-minutes: 210
     strategy:
       fail-fast: false
       matrix:
diff --git a/sdks/python/apache_beam/io/gcp/tests/bigquery_matcher.py 
b/sdks/python/apache_beam/io/gcp/tests/bigquery_matcher.py
index ff48a564491..6a70850a780 100644
--- a/sdks/python/apache_beam/io/gcp/tests/bigquery_matcher.py
+++ b/sdks/python/apache_beam/io/gcp/tests/bigquery_matcher.py
@@ -48,7 +48,13 @@ _LOGGER = logging.getLogger(__name__)
 
 
 def retry_on_http_timeout_and_value_error(exception):
-  """Filter allowing retries on Bigquery errors and value error."""
+  """Filter allowing retries on Bigquery errors and value error.
+
+  A missing table (HTTP 404) is not retried here. Streaming inserts create
+  the table on the first write, and matchers with a timeout poll for it.
+  """
+  if getattr(exception, 'code', None) == 404:
+    return False
   return isinstance(
       exception,
       (GoogleCloudError, ValueError, concurrent.futures.TimeoutError))
@@ -87,12 +93,18 @@ class BigqueryMatcher(BaseMatcher):
   def _matches(self, _):
     @retry.with_exponential_backoff(
         num_retries=1000,
-        initial_delay_secs=0.5,
+        initial_delay_secs=5,
         max_delay_secs=30,
         stop_after_secs=self.timeout_secs,
     )
     def get_checksum():
-      response = self._query_with_retry()
+      try:
+        response = self._query_with_retry()
+      except GoogleCloudError as exc:
+        # Keep polling until timeout_secs. The query retry does not retry 404.
+        if self.timeout_secs and getattr(exc, 'code', None) == 404:
+          raise ValueError('BigQuery table is not visible yet: %s' % exc)
+        raise
       _LOGGER.info(
           'Read from given query (%s), total rows %d',
           self.query,
@@ -196,11 +208,19 @@ class 
BigqueryFullResultStreamingMatcher(BigqueryFullResultMatcher):
   def _get_query_result(self):
     start_time = time.time()
     while time.time() - start_time <= self.timeout:
-      response = self._query_with_retry()
+      try:
+        response = self._query_with_retry()
+      except GoogleCloudError as exc:
+        # The destination table does not exist until the first streaming write.
+        if getattr(exc, 'code', None) != 404:
+          raise
+        _LOGGER.info('BigQuery table is not visible yet: %s', exc)
+        response = []
       if len(response) >= len(self.expected_data):
         return response
       _LOGGER.debug('Query result contains %d rows' % len(response))
-      time.sleep(1)
+      # Space out polls so repeated 404s do not hit BigQuery too often.
+      time.sleep(5)
     raise TimeoutError('Timeout exceeded for matcher.')  # noqa: F821
 
 
diff --git a/sdks/python/apache_beam/io/gcp/tests/bigquery_matcher_test.py 
b/sdks/python/apache_beam/io/gcp/tests/bigquery_matcher_test.py
index f0cf14fedce..34657d24345 100644
--- a/sdks/python/apache_beam/io/gcp/tests/bigquery_matcher_test.py
+++ b/sdks/python/apache_beam/io/gcp/tests/bigquery_matcher_test.py
@@ -33,9 +33,11 @@ from apache_beam.testing.test_utils import patch_retry
 # pylint: disable=wrong-import-order, wrong-import-position
 try:
   from google.cloud import bigquery
+  from google.cloud.exceptions import InternalServerError
   from google.cloud.exceptions import NotFound
 except ImportError:
   bigquery = None
+  InternalServerError = None
   NotFound = None
 # pylint: enable=wrong-import-order, wrong-import-position
 
@@ -87,6 +89,16 @@ class BigqueryMatcherTest(unittest.TestCase):
     self.assertEqual(2, mock_query.call_count)
 
   def test_bigquery_matcher_query_error_retry(self, mock_bigquery):
+    mock_query = mock_bigquery.return_value.query
+    mock_query.side_effect = InternalServerError('internal')
+
+    matcher = bq_verifier.BigqueryMatcher(
+        'mock_project', 'mock_query', 'mock_checksum')
+    with self.assertRaises(InternalServerError):
+      hc_assert_that(self._mock_result, matcher)
+    self.assertEqual(bq_verifier.MAX_RETRIES + 1, mock_query.call_count)
+
+  def test_bigquery_matcher_not_found_without_timeout(self, mock_bigquery):
     mock_query = mock_bigquery.return_value.query
     mock_query.side_effect = NotFound('table not found')
 
@@ -94,7 +106,28 @@ class BigqueryMatcherTest(unittest.TestCase):
         'mock_project', 'mock_query', 'mock_checksum')
     with self.assertRaises(NotFound):
       hc_assert_that(self._mock_result, matcher)
-    self.assertEqual(bq_verifier.MAX_RETRIES + 1, mock_query.call_count)
+    self.assertEqual(1, mock_query.call_count)
+
+  def test_bigquery_matcher_not_found_retries_until_visible(
+      self, mock_bigquery):
+    mock_query_result = [mock.Mock(), mock.Mock(), mock.Mock()]
+    mock_query_result[0].values.return_value = []
+    mock_query_result[1].values.return_value = None
+    mock_query_result[2].values.return_value = None
+    ready = mock.Mock()
+    ready.result.return_value = mock_query_result
+
+    mock_query = mock_bigquery.return_value.query
+    mock_query.side_effect = [NotFound('table not found'), ready]
+
+    matcher = bq_verifier.BigqueryMatcher(
+        'mock_project',
+        'mock_query',
+        '59f9d6bdee30d67ea73b8aded121c3a0280f9cd8',
+        timeout_secs=5,
+    )
+    hc_assert_that(self._mock_result, matcher)
+    self.assertEqual(2, mock_query.call_count)
 
   def test_bigquery_matcher_query_error_checksum(self, mock_bigquery):
     empty_query_result = []
@@ -162,6 +195,11 @@ class BigqueryTableMatcherTest(unittest.TestCase):
 class BigqueryFullResultStreamingMatcherTest(unittest.TestCase):
   def setUp(self):
     self.timeout = 0.01
+    # The matcher waits between polls. Skip that wait here.
+    sleep_patcher = mock.patch(
+        'apache_beam.io.gcp.tests.bigquery_matcher.time.sleep')
+    sleep_patcher.start()
+    self.addCleanup(sleep_patcher.stop)
 
   def test__get_query_result_timeout(self, mock_bigquery):
     mock_query = mock_bigquery.return_value.query
@@ -169,7 +207,32 @@ class 
BigqueryFullResultStreamingMatcherTest(unittest.TestCase):
 
     matcher = bq_verifier.BigqueryFullResultStreamingMatcher(
         'some-project', 'some-query', [1, 2, 3], timeout=self.timeout)
-    with self.assertRaises(TimeoutError):  # noqa: F821
+    with self.assertRaises(TimeoutError):
+      matcher._get_query_result()
+
+  def test_get_query_result_retries_missing_table(self, mock_bigquery):
+    rows = []
+    for value in (1, 2, 3):
+      row = mock.Mock()
+      row.values.return_value = (value, )
+      rows.append(row)
+    ready = mock.Mock()
+    ready.result.return_value = rows
+    mock_query = mock_bigquery.return_value.query
+    mock_query.side_effect = [NotFound('table not found'), ready]
+
+    matcher = bq_verifier.BigqueryFullResultStreamingMatcher(
+        'some-project', 'some-query', [(1, ), (2, ), (3, )], timeout=5)
+    self.assertEqual([(1, ), (2, ), (3, )], matcher._get_query_result())
+    self.assertEqual(2, mock_query.call_count)
+
+  def test_get_query_result_missing_table_timeout(self, mock_bigquery):
+    mock_query = mock_bigquery.return_value.query
+    mock_query.side_effect = NotFound('table not found')
+
+    matcher = bq_verifier.BigqueryFullResultStreamingMatcher(
+        'some-project', 'some-query', [1, 2, 3], timeout=self.timeout)
+    with self.assertRaises(TimeoutError):
       matcher._get_query_result()
 
 

Reply via email to