1# Copyright 2020 Google LLC 
    2# 
    3# Licensed under the Apache License, Version 2.0 (the "License"); 
    4# you may not use this file except in compliance with the License. 
    5# You may obtain a copy of the License at 
    6# 
    7#     http://www.apache.org/licenses/LICENSE-2.0 
    8# 
    9# Unless required by applicable law or agreed to in writing, software 
    10# distributed under the License is distributed on an "AS IS" BASIS, 
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
    12# See the License for the specific language governing permissions and 
    13# limitations under the License. 
    14 
    15"""Helpers for configuring retries with exponential back-off. 
    16 
    17See [Retry Strategy for Google Cloud Storage](https://cloud.google.com/storage/docs/retry-strategy#client-libraries) 
    18""" 
    19 
    20import http 
    21 
    22import requests 
    23import requests.exceptions as requests_exceptions 
    24import urllib3 
    25 
    26from google.api_core import exceptions as api_exceptions 
    27from google.api_core import retry 
    28from google.auth import exceptions as auth_exceptions 
    29from google.cloud.storage.exceptions import InvalidResponse 
    30 
    31 
    32_RETRYABLE_TYPES = ( 
    33    api_exceptions.TooManyRequests,  # 429 
    34    api_exceptions.InternalServerError,  # 500 
    35    api_exceptions.BadGateway,  # 502 
    36    api_exceptions.ServiceUnavailable,  # 503 
    37    api_exceptions.GatewayTimeout,  # 504 
    38    ConnectionError, 
    39    requests.ConnectionError, 
    40    requests_exceptions.ChunkedEncodingError, 
    41    requests_exceptions.Timeout, 
    42    http.client.BadStatusLine, 
    43    http.client.IncompleteRead, 
    44    http.client.ResponseNotReady, 
    45    urllib3.exceptions.PoolError, 
    46    urllib3.exceptions.ProtocolError, 
    47    urllib3.exceptions.SSLError, 
    48    urllib3.exceptions.TimeoutError, 
    49) 
    50 
    51 
    52_RETRYABLE_STATUS_CODES = ( 
    53    http.client.TOO_MANY_REQUESTS,  # 429 
    54    http.client.REQUEST_TIMEOUT,  # 408 
    55    http.client.INTERNAL_SERVER_ERROR,  # 500 
    56    http.client.BAD_GATEWAY,  # 502 
    57    http.client.SERVICE_UNAVAILABLE,  # 503 
    58    http.client.GATEWAY_TIMEOUT,  # 504 
    59) 
    60 
    61 
    62def _should_retry(exc): 
    63    """Predicate for determining when to retry.""" 
    64    if isinstance(exc, _RETRYABLE_TYPES): 
    65        return True 
    66    elif isinstance(exc, api_exceptions.GoogleAPICallError): 
    67        return exc.code in _RETRYABLE_STATUS_CODES 
    68    elif isinstance(exc, InvalidResponse): 
    69        return exc.response.status_code in _RETRYABLE_STATUS_CODES 
    70    elif isinstance(exc, auth_exceptions.TransportError): 
    71        return _should_retry(exc.args[0]) 
    72    else: 
    73        return False 
    74 
    75 
    76DEFAULT_RETRY = retry.Retry(predicate=_should_retry) 
    77"""The default retry object. 
    78 
    79This retry setting will retry all _RETRYABLE_TYPES and any status codes from 
    80_ADDITIONAL_RETRYABLE_STATUS_CODES. 
    81 
    82To modify the default retry behavior, create a new retry object modeled after 
    83this one by calling it a ``with_XXX`` method. For example, to create a copy of 
    84DEFAULT_RETRY with a deadline of 30 seconds, pass 
    85``retry=DEFAULT_RETRY.with_deadline(30)``. See google-api-core reference 
    86(https://googleapis.dev/python/google-api-core/latest/retry.html) for details. 
    87""" 
    88 
    89 
    90class ConditionalRetryPolicy(object): 
    91    """A class for use when an API call is only conditionally safe to retry. 
    92 
    93    This class is intended for use in inspecting the API call parameters of an 
    94    API call to verify that any flags necessary to make the API call idempotent 
    95    (such as specifying an ``if_generation_match`` or related flag) are present. 
    96 
    97    It can be used in place of a ``retry.Retry`` object, in which case 
    98    ``_http.Connection.api_request`` will pass the requested api call keyword 
    99    arguments into the ``conditional_predicate`` and return the ``retry_policy`` 
    100    if the conditions are met. 
    101 
    102    :type retry_policy: class:`google.api_core.retry.Retry` 
    103    :param retry_policy: A retry object defining timeouts, persistence and which 
    104        exceptions to retry. 
    105 
    106    :type conditional_predicate: callable 
    107    :param conditional_predicate: A callable that accepts exactly the number of 
    108        arguments in ``required_kwargs``, in order, and returns True if the 
    109        arguments have sufficient data to determine that the call is safe to 
    110        retry (idempotent). 
    111 
    112    :type required_kwargs: list(str) 
    113    :param required_kwargs: 
    114        A list of keyword argument keys that will be extracted from the API call 
    115        and passed into the ``conditional predicate`` in order. For example, 
    116        ``["query_params"]`` is commmonly used for preconditions in query_params. 
    117    """ 
    118 
    119    def __init__(self, retry_policy, conditional_predicate, required_kwargs): 
    120        self.retry_policy = retry_policy 
    121        self.conditional_predicate = conditional_predicate 
    122        self.required_kwargs = required_kwargs 
    123 
    124    def get_retry_policy_if_conditions_met(self, **kwargs): 
    125        if self.conditional_predicate(*[kwargs[key] for key in self.required_kwargs]): 
    126            return self.retry_policy 
    127        return None 
    128 
    129 
    130def is_generation_specified(query_params): 
    131    """Return True if generation or if_generation_match is specified.""" 
    132    generation = query_params.get("generation") is not None 
    133    if_generation_match = query_params.get("ifGenerationMatch") is not None 
    134    return generation or if_generation_match 
    135 
    136 
    137def is_metageneration_specified(query_params): 
    138    """Return True if if_metageneration_match is specified.""" 
    139    if_metageneration_match = query_params.get("ifMetagenerationMatch") is not None 
    140    return if_metageneration_match 
    141 
    142 
    143def is_etag_in_data(data): 
    144    """Return True if an etag is contained in the request body. 
    145 
    146    :type data: dict or None 
    147    :param data: A dict representing the request JSON body. If not passed, returns False. 
    148    """ 
    149    return data is not None and "etag" in data 
    150 
    151 
    152def is_etag_in_json(data): 
    153    """ 
    154    ``is_etag_in_json`` is supported for backwards-compatibility reasons only; 
    155    please use ``is_etag_in_data`` instead. 
    156    """ 
    157    return is_etag_in_data(data) 
    158 
    159 
    160DEFAULT_RETRY_IF_GENERATION_SPECIFIED = ConditionalRetryPolicy( 
    161    DEFAULT_RETRY, is_generation_specified, ["query_params"] 
    162) 
    163"""Conditional wrapper for the default retry object. 
    164 
    165This retry setting will retry all _RETRYABLE_TYPES and any status codes from 
    166_ADDITIONAL_RETRYABLE_STATUS_CODES, but only if the request included an 
    167``ifGenerationMatch`` header. 
    168""" 
    169 
    170DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED = ConditionalRetryPolicy( 
    171    DEFAULT_RETRY, is_metageneration_specified, ["query_params"] 
    172) 
    173"""Conditional wrapper for the default retry object. 
    174 
    175This retry setting will retry all _RETRYABLE_TYPES and any status codes from 
    176_ADDITIONAL_RETRYABLE_STATUS_CODES, but only if the request included an 
    177``ifMetagenerationMatch`` header. 
    178""" 
    179 
    180DEFAULT_RETRY_IF_ETAG_IN_JSON = ConditionalRetryPolicy( 
    181    DEFAULT_RETRY, is_etag_in_json, ["data"] 
    182) 
    183"""Conditional wrapper for the default retry object. 
    184 
    185This retry setting will retry all _RETRYABLE_TYPES and any status codes from 
    186_ADDITIONAL_RETRYABLE_STATUS_CODES, but only if the request included an 
    187``ETAG`` entry in its payload. 
    188"""