Retry support for resourcegroup tagging apis.

    The resource group tagging api typically returns a 200 status code
    with embedded resource specific errors. To enable resource specific
    retry on throttles, we extract those, perform backoff w/ jitter and
    continue. Other errors are immediately raised.

    We do not aggregate unified resource responses across retries, only the
    last successful response is returned for a subset of the resources if
    a retry is performed.

def universal_retry(method, ResourceARNList, **kw):
    """Retry support for resourcegroup tagging apis.

    The resource group tagging api typically returns a 200 status code
    with embedded resource specific errors. To enable resource specific
    retry on throttles, we extract those, perform backoff w/ jitter and
    continue. Other errors are immediately raised.

    We do not aggregate unified resource responses across retries, only the
    last successful response is returned for a subset of the resources if
    a retry is performed.
    """
    max_attempts = 6

    for idx, delay in enumerate(
            utils.backoff_delays(1.5, 2 ** 8, jitter=True)):
        response = method(ResourceARNList=ResourceARNList, **kw)
        failures = response.get('FailedResourcesMap', {})
        if not failures:
            return response

        errors = {}
        throttles = set()

        for f_arn in failures:
            error_code = failures[f_arn]['ErrorCode']
            if error_code == 'ThrottlingException':
                throttles.add(f_arn)
            elif error_code == 'ResourceNotFoundException':
                continue
            else:
                errors[f_arn] = error_code

        if errors:
            raise Exception("Resource Tag Errors %s" % (errors))

        if idx == max_attempts - 1:
            raise Exception("Resource Tag Throttled %s" % (", ".join(throttles)))

        time.sleep(delay)
        ResourceARNList = list(throttles)