Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bigquery: retry idempotent RPCs #4148

Merged
merged 1 commit into from
Oct 12, 2017
Merged

bigquery: retry idempotent RPCs #4148

merged 1 commit into from
Oct 12, 2017

Conversation

jba
Copy link
Contributor

@jba jba commented Oct 10, 2017

Add retry logic to every RPC for which it makes sense.

Following the BigQuery team, we ignore the error code and
use the "reason" field of the error to determine whether
to retry.

@jba jba requested a review from lukesneeringer as a code owner October 10, 2017 00:34
@googlebot googlebot added the cla: yes This human has signed the Contributor License Agreement. label Oct 10, 2017
@jba
Copy link
Contributor Author

jba commented Oct 10, 2017

This is a first cut at adding retry to the BigQuery client. My thinking is that most retry parameters are not interesting to users and should not be exposed, but the timeout (a.k.a. deadline) is important, and should be an optional parameter.

Another choice would be to allow an optional google.gax.CallOptions, as the generated code does.

@theacodes
Copy link
Contributor

theacodes commented Oct 10, 2017

@jba this is a good start, but needs to be slightly different (and I apologize for not documenting what the retry pattern should look like).

  1. All methods that retry should take a full retry argument that should be a retry instance.
  2. A sane default retry should be provided.
  3. Retry can be set to False or None to disable retrying.
  4. The connection's api call should handle raising the appropriate google.api.core.exceptions.GoogleAPICallError subclass. This way, you don't need custom should_retry and users will get the appropriate exception if retrying fails.

So get_dataset should look something like this:

def get_dataset(self, dataset_ref, retry=DEFAULT_RETRY):
    api_call = functools.partial(
        self._connection.api_request,
        method='GET',
        path=dataset_ref.path)

    if retry:
        api_call = retry(api_call)

    api_response = api_call()

    return Dataset.from_api_repr(api_response)

and self._connection.api_request should handle raising the appropriate exception on error.

(FYI: functools.partial is preferred to lambda because retry et al knows how to properly print this when errors occur)

@jba
Copy link
Contributor Author

jba commented Oct 10, 2017

Thanks for the explanation. The one thing I'm not clear about is the exception subclass. BigQuery retry ignores the error code and is based solely on the reason string in the error. Should I define a special BigQueryTransientError subclass to capture that, which wraps the original error? If so, I'll still need a custom predicate, won't I? And won't that be worse for users when retry fails, because they'll have to call RetryError.cause() and then call cause() on that again?

@theacodes
Copy link
Contributor

BigQuery retry ignores the error code and is based solely on the reason string in the error.

Yikes. Do the error codes at least match up with idempotent http statuses?

Should I define a special BigQueryTransientError subclass to capture that, which wraps the original error?

Yeah, that could be reasonable, something like:

class BigQueryTransientError(exceptions.BackendError):
     ...

If so, I'll still need a custom predicate, won't I?

Possibly, if the default predicate doesn't work (e.g. if not all BackendErrors are retryable, just the subset that ends up being BigQueryTransientErrors). If that's the case, you can make it global:

google.cloud.bigquery.default_retry_predicate

That way custom retries are a bit easier:

myretry = retry.Retry(bigquery.default_retry_predicate, deadline=60)

You can also make the default retry object itself a global or class constant, so users can do;

myretry = bigquery_client.DEFAULT_RETRY.with_deadline(60)

And won't that be worse for users when retry fails, because they'll have to call RetryError.cause() and then call cause() on that again?

If you sublcass one of the existing exception classes they should only need to call cause() once, unless I'm misunderstanding something.

@jba
Copy link
Contributor Author

jba commented Oct 10, 2017

Yikes. Do the error codes at least match up with idempotent http statuses?

The error codes are irrelevant. Only the reason field matters. So I guess the answer is no.

If so, I'll still need a custom predicate, won't I?

Possibly, if the default predicate doesn't work (e.g. if not all BackendErrors are retryable, just the subset that ends up being BigQueryTransientErrors).

The default predicate won't work, for the reason you said.

You can also make the default retry object itself a global or class constant, so users can do;

myretry = bigquery_client.DEFAULT_RETRY.with_deadline(60)

Done.

@jba
Copy link
Contributor Author

jba commented Oct 10, 2017

I don't understand the error I'm getting. It happens only under 2.7. Am I holding functools wrong?

Traceback (most recent call last):
  File "/var/code/gcp/bigquery/tests/unit/test_client.py", line 297, in test_get_dataset
    dataset = client.get_dataset(dataset_ref)
  File "/var/code/gcp/bigquery/google/cloud/bigquery/client.py", line 288, in get_dataset
    api_call = retry(api_call)
  File "/var/code/gcp/.nox/unit-2-7/lib/python2.7/site-packages/google/api/core/retry.py", line 247, in __call__
    @six.wraps(func)
  File "/var/code/gcp/.nox/unit-2-7/lib/python2.7/site-packages/six.py", line 811, in wrapper
    f = functools.wraps(wrapped, assigned, updated)(f)
  File "/usr/local/lib/python2.7/functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'functools.partial' object has no attribute '__module__'

api_response = self._connection.api_request(
method='GET', path=dataset_ref.path)
api_call = functools.partial(
self._connection.api_request,

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

@jba
Copy link
Contributor Author

jba commented Oct 12, 2017

PTAL. In the latest push I added retry to nearly all methods that can support it.

@@ -291,7 +293,8 @@ class HTTPIterator(Iterator):
def __init__(self, client, api_request, path, item_to_value,
items_key=_DEFAULT_ITEMS_KEY,
page_token=None, max_results=None, extra_params=None,
page_start=_do_nothing_page_start, next_token=_NEXT_TOKEN):
page_start=_do_nothing_page_start, next_token=_NEXT_TOKEN,
retry=None):

This comment was marked as spam.

This comment was marked as spam.

@lukesneeringer
Copy link
Contributor

lukesneeringer commented Oct 12, 2017

@jba

I don't understand the error I'm getting. It happens only under 2.7. Am I holding functools wrong?

Traceback (most recent call last):
  File "/var/code/gcp/bigquery/tests/unit/test_client.py", line 297, in test_get_dataset
    dataset = client.get_dataset(dataset_ref)
  File "/var/code/gcp/bigquery/google/cloud/bigquery/client.py", line 288, in get_dataset
    api_call = retry(api_call)
  File "/var/code/gcp/.nox/unit-2-7/lib/python2.7/site-packages/google/api/core/retry.py", line 247, in __call__
    @six.wraps(func)
  File "/var/code/gcp/.nox/unit-2-7/lib/python2.7/site-packages/six.py", line 811, in wrapper
    f = functools.wraps(wrapped, assigned, updated)(f)
  File "/usr/local/lib/python2.7/functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'functools.partial' object has no attribute '__module__'

When you create a function on the fly with functools.partial, it does not assign it a __module__ property, which is assigned when a function is built with the def keyword. @six.wraps (which is just an alias to functools.wraps) requires a __module__ property for aliasing the docstring.

You can get around it with:

# Make the partial that you were making before.
partial_func = functools.partial(func, *args, **kwargs)

# Assign it an appropriate __module__ property based on the function it wraps.
partial_func.__module__ = func.__module__

After that it should work with wraps.

@theacodes
Copy link
Contributor

@lukesneeringer No need, we already fixed that in core. :)

@theacodes
Copy link
Contributor

retry stuff LGTM

Add retry logic to every RPC for which it makes sense.

Following the BigQuery team, we ignore the error code and
use the "reason" field of the error to determine whether
to retry.

Outstanding issues:

- Resumable upload consists of an initial call to get a URL,
  followed by posts to that URL. Getting the retry right on that
  initial call requires modifying the ResumableUpload class. At the
  same time, the num_retries argument should be removed.

- Users can't modify the retry behavior of Job.result(), because
  PollingFuture.result() does not accept a retry argument.
@jba jba merged commit fd691a2 into googleapis:bigquery-b2 Oct 12, 2017
tswast pushed a commit that referenced this pull request Oct 16, 2017
Add retry logic to every RPC for which it makes sense.

Following the BigQuery team, we ignore the error code and
use the "reason" field of the error to determine whether
to retry.

Outstanding issues:

- Resumable upload consists of an initial call to get a URL,
  followed by posts to that URL. Getting the retry right on that
  initial call requires modifying the ResumableUpload class. At the
  same time, the num_retries argument should be removed.

- Users can't modify the retry behavior of Job.result(), because
  PollingFuture.result() does not accept a retry argument.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes This human has signed the Contributor License Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants