Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
DEFAULT_GET_JOB_TIMEOUT,
DEFAULT_JOB_RETRY,
DEFAULT_RETRY,
INSERT_ROWS_DEFAULT_RETRY,
DEFAULT_TIMEOUT,
POLLING_DEFAULT_VALUE,
)
Expand Down Expand Up @@ -3956,7 +3957,7 @@ def insert_rows_json(
skip_invalid_rows: Optional[bool] = None,
ignore_unknown_values: Optional[bool] = None,
template_suffix: Optional[str] = None,
retry: retries.Retry = DEFAULT_RETRY,
retry: retries.Retry = INSERT_ROWS_DEFAULT_RETRY,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The insert_rows method (defined elsewhere in this file) also has retry: retries.Retry = DEFAULT_RETRY as its default parameter value and delegates to insert_rows_json by passing retry=retry. Because of this, calling client.insert_rows(...) without a retry argument will explicitly pass DEFAULT_RETRY to insert_rows_json, bypassing the new INSERT_ROWS_DEFAULT_RETRY default. Please update the default value of the retry parameter in insert_rows to INSERT_ROWS_DEFAULT_RETRY as well to ensure the SSLError carve-out is correctly applied to the standard insert_rows path.

timeout: TimeoutType = DEFAULT_TIMEOUT,
) -> Sequence[dict]:
"""Insert rows into a table without applying local type conversions.
Expand Down
29 changes: 28 additions & 1 deletion packages/google-cloud-bigquery/google/cloud/bigquery/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@
# Exceptions that are subclasses of types in _UNSTRUCTURED_RETRYABLE_TYPES
# but should not be retried because they typically indicate persistent
# configuration or security issues.
_UNSTRUCTURED_NON_RETRYABLE_TYPES = (requests.exceptions.SSLError,)
#
# NOTE: requests.exceptions.SSLError is deliberately NOT listed here. It is a
# subclass of requests.exceptions.ConnectionError, and transient TLS resets
# (e.g. SSLEOFError during a pooled-connection handshake) are transport errors
# that the client retried before #17489. That PR's carve-out belongs to the
# streaming-insert path only (see INSERT_ROWS_DEFAULT_RETRY); making it
# global broke jobs.get / result() polling on transient resets.
_UNSTRUCTURED_NON_RETRYABLE_TYPES = ()

# Ambiguous errors (e.g. internalError, backendError, rateLimitExceeded) retry
# until the full `_DEFAULT_RETRY_DEADLINE`. This is because the
Expand Down Expand Up @@ -88,6 +95,26 @@ def _should_retry(exc):


DEFAULT_RETRY = retry.Retry(predicate=_should_retry, deadline=_DEFAULT_RETRY_DEADLINE)


def _should_retry_insert_rows(exc):
"""Predicate for the streaming-insert (insertAll) path.

A connection-level SSLError there is usually the transport rejecting a
malformed payload (e.g. an invalid table schema), which does not resolve
on retry. Scope that carve-out here rather than globally: jobs.get and
result() polling must keep retrying transient TLS resets.
"""
if isinstance(exc, requests.exceptions.SSLError):
return False
return _should_retry(exc)


# Streaming inserts keep the SSLError carve-out from #17489, scoped to the
# insertAll path only.
INSERT_ROWS_DEFAULT_RETRY = retry.Retry(
predicate=_should_retry_insert_rows, deadline=_DEFAULT_RETRY_DEADLINE
)
"""The default retry object.

Any method with a ``retry`` parameter will be retried automatically,
Expand Down
42 changes: 41 additions & 1 deletion packages/google-cloud-bigquery/tests/unit/test_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_w_unstructured_requests_connectionerror(self):

def test_w_unstructured_requests_sslerror(self):
exc = requests.exceptions.SSLError()
self.assertFalse(self._call_fut(exc))
self.assertTrue(self._call_fut(exc))

def test_w_unstructured_requests_chunked_encoding_error(self):
exc = requests.exceptions.ChunkedEncodingError()
Expand Down Expand Up @@ -160,3 +160,43 @@ def test_DEFAULT_JOB_RETRY_job_rate_limit_exceeded_retry_predicate():
assert DEFAULT_JOB_RETRY._predicate(
ClientError("fail", errors=[dict(reason="backendError")])
)


class Test_should_retry_insert_rows(unittest.TestCase):
def _call_fut(self, exc):
from google.cloud.bigquery.retry import _should_retry_insert_rows

return _should_retry_insert_rows(exc)

def test_w_sslerror(self):
exc = requests.exceptions.SSLError()
self.assertFalse(self._call_fut(exc))

def test_w_unstructured_connectionerror(self):
exc = requests.exceptions.ConnectionError()
self.assertTrue(self._call_fut(exc))

def test_w_rate_limited(self):
exc = mock.Mock(errors=[{"reason": "rateLimitExceeded"}], spec=["errors"])
self.assertTrue(self._call_fut(exc))


class Test_insert_rows_default_retry(unittest.TestCase):
def test_insert_rows_json_defaults_to_scoped_retry(self):
from types import MethodType
from google.cloud.bigquery.retry import INSERT_ROWS_DEFAULT_RETRY, _should_retry_insert_rows

# The default retry object on insert_rows_json is the scoped one,
# so transient SSLErrors on polling paths stay retryable while the
# streaming-insert carve-out is preserved.
self.assertIs(
INSERT_ROWS_DEFAULT_RETRY._predicate,
_should_retry_insert_rows,
)

def test_scoped_predicate_keeps_connection_errors_retryable(self):
from google.cloud.bigquery.retry import _should_retry_insert_rows

self.assertTrue(
_should_retry_insert_rows(requests.exceptions.ConnectionError())
)
Loading