Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Commit ef36bf2

Browse files
committed
Fixed retry test logic to better align to library standards
1 parent 5861ba2 commit ef36bf2

File tree

3 files changed

+89
-34
lines changed

3 files changed

+89
-34
lines changed
Binary file not shown.

tests/unit/test_job_retry.py

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import google.api_core.retry
2323
import freezegun
2424

25+
from google.cloud.bigquery.client import Client
26+
from google.cloud.bigquery import _job_helpers
27+
2528
from .helpers import make_connection
2629

2730

@@ -242,38 +245,63 @@ def test_raises_on_job_retry_on_result_with_non_retryable_jobs(client):
242245
job.result(job_retry=google.api_core.retry.Retry())
243246

244247

245-
@mock.patch("time.sleep")
246-
def test_retry_ddl_query_rate_limit_exceeded(sleep, client):
247-
"""
248-
Specific test for retrying DDL queries with "jobRateLimitExceeded" error
249-
"""
250-
251-
err = dict(reason="jobRateLimitExceeded")
252-
responses = [
253-
dict(status=dict(state="DONE", errors=[err], errorResult=err)),
254-
dict(status=dict(state="DONE")), # Retry succeeds on second attempt
255-
dict(rows=[{"f": [{"v": "DDL operation successful"}]}], totalRows="1"),
256-
]
257-
258-
def api_request(method, path, query_params=None, data=None, **kw):
259-
response = responses.pop(0)
260-
if data:
261-
response["jobReference"] = data["jobReference"]
262-
else:
263-
response["jobReference"] = dict(
264-
jobId=path.split("/")[-1], projectId="PROJECT"
265-
)
266-
return response
267-
268-
conn = client._connection = make_connection()
269-
conn.api_request.side_effect = api_request
270-
271-
job = client.query(
272-
"ALTER TABLE my_table ADD COLUMN new_column STRING",
273-
job_retry=google.api_core.retry.Retry(),
248+
def test_query_and_wait_retries_job():
249+
freezegun.freeze_time(auto_tick_seconds=100)
250+
client = mock.create_autospec(Client)
251+
client._call_api.__name__ = "_call_api"
252+
client._call_api.__qualname__ = "Client._call_api"
253+
client._call_api.__annotations__ = {}
254+
client._call_api.__type_params__ = ()
255+
client._call_api.side_effect = (
256+
google.api_core.exceptions.BadRequest("jobRateLimitExceeded"),
257+
google.api_core.exceptions.InternalServerError("jobRateLimitExceeded"),
258+
google.api_core.exceptions.BadRequest("jobRateLimitExceeded"),
259+
{
260+
"jobReference": {
261+
"projectId": "response-project",
262+
"jobId": "abc",
263+
"location": "response-location",
264+
},
265+
"jobComplete": True,
266+
"schema": {
267+
"fields": [
268+
{"name": "full_name", "type": "STRING", "mode": "REQUIRED"},
269+
{"name": "age", "type": "INT64", "mode": "NULLABLE"},
270+
],
271+
},
272+
"rows": [
273+
{"f": [{"v": "Whillma Phlyntstone"}, {"v": "27"}]},
274+
{"f": [{"v": "Bhetty Rhubble"}, {"v": "28"}]},
275+
{"f": [{"v": "Phred Phlyntstone"}, {"v": "32"}]},
276+
{"f": [{"v": "Bharney Rhubble"}, {"v": "33"}]},
277+
],
278+
},
274279
)
275-
result = job.result()
276-
277-
assert result.total_rows == 1
278-
assert not responses # All calls made
279-
assert len(sleep.mock_calls) == 1 # One retry attempt
280+
rows = _job_helpers.query_and_wait(
281+
client,
282+
query="SELECT 1",
283+
location="request-location",
284+
project="request-project",
285+
job_config=None,
286+
page_size=None,
287+
max_results=None,
288+
retry=google.api_core.retry.Retry(
289+
lambda exc: isinstance(exc, google.api_core.exceptions.BadRequest),
290+
multiplier=1.0,
291+
).with_deadline(
292+
200.0
293+
), # Since auto_tick_seconds is 100, we should get at least 1 retry.
294+
job_retry=google.api_core.retry.Retry(
295+
lambda exc: isinstance(exc, google.api_core.exceptions.InternalServerError),
296+
multiplier=1.0,
297+
).with_deadline(600.0),
298+
)
299+
assert len(list(rows)) == 4
300+
301+
# For this code path, where the query has finished immediately, we should
302+
# only be calling the jobs.query API and no other request path.
303+
request_path = "/projects/request-project/queries"
304+
for call in client._call_api.call_args_list:
305+
_, kwargs = call
306+
assert kwargs["method"] == "POST"
307+
assert kwargs["path"] == request_path

tests/unit/test_retry.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,30 @@ def test_DEFAULT_JOB_RETRY_deadline():
129129

130130
# Make sure we can retry the job at least once.
131131
assert DEFAULT_JOB_RETRY._deadline > DEFAULT_RETRY._deadline
132+
133+
134+
def test_DEFAULT_JOB_RETRY_job_rate_limit_exceeded_retry_predicate():
135+
"""Tests the retry predicate specifically for jobRateLimitExceeded."""
136+
from google.cloud.bigquery.retry import DEFAULT_JOB_RETRY
137+
from google.api_core.exceptions import ClientError
138+
139+
# Non-ClientError exceptions should never trigger a retry
140+
assert not DEFAULT_JOB_RETRY._predicate(TypeError())
141+
142+
# ClientError without specific reason shouldn't trigger a retry
143+
assert not DEFAULT_JOB_RETRY._predicate(ClientError("fail"))
144+
145+
# ClientError with generic reason "idk" shouldn't trigger a retry
146+
assert not DEFAULT_JOB_RETRY._predicate(
147+
ClientError("fail", errors=[dict(reason="idk")])
148+
)
149+
150+
# ClientError with reason "jobRateLimitExceeded" should trigger a retry
151+
assert DEFAULT_JOB_RETRY._predicate(
152+
ClientError("fail", errors=[dict(reason="jobRateLimitExceeded")])
153+
)
154+
155+
# Other retryable reasons should still work as expected
156+
assert DEFAULT_JOB_RETRY._predicate(
157+
ClientError("fail", errors=[dict(reason="backendError")])
158+
)

0 commit comments

Comments
 (0)