retry_condition.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. # Copyright 2019 Alibaba Cloud Inc. All rights reserved.
  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. import jmespath
  15. import logging
  16. import aliyunsdkcore.utils
  17. import aliyunsdkcore.utils.validation as validation
  18. from aliyunsdkcore.acs_exception.exceptions import ClientException, ServerException
  19. import aliyunsdkcore.acs_exception.error_code as error_code
  20. logger = logging.getLogger(__name__)
  21. def _find_data_in_retry_config(key_name, request, retry_config):
  22. if request.get_product() is None:
  23. return None
  24. path = '"{0}"."{1}"."{2}"'.format(request.get_product().lower(),
  25. request.get_version(),
  26. key_name)
  27. return jmespath.search(path, retry_config)
  28. class RetryCondition(object):
  29. BLANK_STATUS = 0
  30. NO_RETRY = 1
  31. SHOULD_RETRY = 2
  32. SHOULD_RETRY_WITH_CLIENT_TOKEN = 4
  33. SHOULD_RETRY_WITH_THROTTLING_BACKOFF = 8
  34. def should_retry(self, retry_policy_context):
  35. """Decide whether the previous request should be retried."""
  36. pass
  37. class NoRetryCondition(RetryCondition):
  38. def should_retry(self, retry_policy_context):
  39. return RetryCondition.NO_RETRY
  40. class MaxRetryTimesCondition(RetryCondition):
  41. def __init__(self, max_retry_times):
  42. validation.assert_integer_positive(max_retry_times, "max_retry_times")
  43. self.max_retry_times = max_retry_times
  44. def should_retry(self, retry_policy_context):
  45. if retry_policy_context.retries_attempted < self.max_retry_times:
  46. return RetryCondition.SHOULD_RETRY
  47. else:
  48. logger.debug("Reached the maximum number of retry. Attempts:%d",
  49. retry_policy_context.retries_attempted)
  50. return RetryCondition.NO_RETRY
  51. class RetryOnExceptionCondition(RetryCondition):
  52. def __init__(self, retry_config):
  53. self.retry_config = retry_config
  54. def should_retry(self, retry_policy_context):
  55. request = retry_policy_context.original_request
  56. exception = retry_policy_context.exception
  57. if isinstance(exception, ClientException):
  58. if exception.get_error_code() == error_code.SDK_HTTP_ERROR:
  59. logger.debug("Retryable ClientException occurred. ClientException:%s",
  60. exception)
  61. return RetryCondition.SHOULD_RETRY
  62. if isinstance(exception, ServerException):
  63. error_code_ = exception.get_error_code()
  64. normal_errors = _find_data_in_retry_config("RetryableNormalErrors",
  65. request,
  66. self.retry_config)
  67. if isinstance(normal_errors, list) and error_code_ in normal_errors:
  68. logger.debug("Retryable ServerException occurred. ServerException:%s",
  69. exception)
  70. return RetryCondition.SHOULD_RETRY
  71. throttling_errors = _find_data_in_retry_config("RetryableThrottlingErrors",
  72. request,
  73. self.retry_config)
  74. if isinstance(throttling_errors, list) and error_code_ in throttling_errors:
  75. logger.debug("Retryable ThrottlingError occurred. ThrottlingError:%s",
  76. exception)
  77. return RetryCondition.SHOULD_RETRY | \
  78. RetryCondition.SHOULD_RETRY_WITH_THROTTLING_BACKOFF
  79. return RetryCondition.NO_RETRY
  80. class RetryOnHttpStatusCondition(RetryCondition):
  81. DEFAULT_RETRYABLE_HTTP_STATUS_LIST = [
  82. 500, 502, 503, 504
  83. ]
  84. def __init__(self, retryable_http_status_list=None):
  85. if retryable_http_status_list:
  86. self.retryable_http_status_list = retryable_http_status_list
  87. else:
  88. self.retryable_http_status_list = self.DEFAULT_RETRYABLE_HTTP_STATUS_LIST
  89. def should_retry(self, retry_policy_context):
  90. if retry_policy_context.http_status_code in self.retryable_http_status_list:
  91. logger.debug(
  92. "Retryable HTTP error occurred. HTTP status code: %s",
  93. retry_policy_context.http_status_code)
  94. return RetryCondition.SHOULD_RETRY
  95. else:
  96. return RetryCondition.NO_RETRY
  97. class RetryOnApiCondition(RetryCondition):
  98. def __init__(self, retry_config):
  99. self.retry_config = retry_config
  100. def should_retry(self, retry_policy_context):
  101. request = retry_policy_context.original_request
  102. retryable_apis = _find_data_in_retry_config("RetryableAPIs", request, self.retry_config)
  103. if isinstance(retryable_apis, list) and request.get_action_name() in retryable_apis:
  104. return RetryCondition.SHOULD_RETRY
  105. else:
  106. return RetryCondition.NO_RETRY
  107. class RetryOnApiWithClientTokenCondition(RetryCondition):
  108. def __init__(self, retry_config):
  109. self.retry_config = retry_config
  110. def should_retry(self, retry_policy_context):
  111. request = retry_policy_context.original_request
  112. retryable_apis = _find_data_in_retry_config(
  113. "RetryableAPIsWithClientToken", request, self.retry_config)
  114. if isinstance(retryable_apis, list) and request.get_action_name() in retryable_apis:
  115. return RetryCondition.SHOULD_RETRY | RetryCondition.SHOULD_RETRY_WITH_THROTTLING_BACKOFF
  116. else:
  117. return RetryCondition.NO_RETRY
  118. class AndRetryCondition(RetryCondition):
  119. def __init__(self, conditions):
  120. self.conditions = conditions
  121. def should_retry(self, retry_policy_context):
  122. retryable = RetryCondition.BLANK_STATUS
  123. for condition in self.conditions:
  124. retryable |= condition.should_retry(retry_policy_context)
  125. return retryable
  126. class OrRetryCondition(RetryCondition):
  127. def __init__(self, conditions):
  128. self.conditions = conditions
  129. def should_retry(self, retry_policy_context):
  130. retryable = RetryCondition.BLANK_STATUS
  131. no_retry_flag = RetryCondition.NO_RETRY
  132. mask = RetryCondition.SHOULD_RETRY
  133. mask |= RetryCondition.SHOULD_RETRY_WITH_CLIENT_TOKEN
  134. mask |= RetryCondition.SHOULD_RETRY_WITH_THROTTLING_BACKOFF
  135. for condition in self.conditions:
  136. ret = condition.should_retry(retry_policy_context)
  137. retryable |= ret & mask
  138. no_retry_flag &= ret & RetryCondition.NO_RETRY
  139. return retryable | no_retry_flag
  140. class MixedRetryCondition(RetryCondition):
  141. def __init__(self, max_retry_times, retry_config):
  142. RetryCondition.__init__(self)
  143. self._inner_condition = AndRetryCondition([
  144. MaxRetryTimesCondition(max_retry_times),
  145. OrRetryCondition([
  146. RetryOnApiCondition(retry_config),
  147. RetryOnApiWithClientTokenCondition(retry_config),
  148. ]),
  149. OrRetryCondition([
  150. RetryOnExceptionCondition(retry_config),
  151. RetryOnHttpStatusCondition(),
  152. ]),
  153. ])
  154. def should_retry(self, retry_policy_context):
  155. return self._inner_condition.should_retry(retry_policy_context)
  156. class DefaultConfigRetryCondition(MixedRetryCondition):
  157. DEFAULT_MAX_RETRY_TIMES = 3
  158. RETRY_CONFIG_FILE = "retry_config.json"
  159. _loaded_retry_config = None
  160. def __init__(self, max_retry_times=None):
  161. if not self._loaded_retry_config:
  162. self._loaded_retry_config = aliyunsdkcore.utils._load_json_from_data_dir(
  163. self.RETRY_CONFIG_FILE)
  164. if max_retry_times is None:
  165. max_retry_times = self.DEFAULT_MAX_RETRY_TIMES
  166. MixedRetryCondition.__init__(self, max_retry_times, self._loaded_retry_config)