Merge branch 'master' into 4823-python-sdk-writable-collection-api
[arvados.git] / sdk / python / arvados / retry.py
index a5a303fa830b490bc7398b9ac0a55fea5ff2d021..52a68faa6f6b511bf78378cc944aa6e4c5914c33 100644 (file)
@@ -1,11 +1,16 @@
 #!/usr/bin/env python
 
+import functools
+import inspect
 import time
 
 from collections import deque
 
 import arvados.errors
 
+_HTTP_SUCCESSES = set(xrange(200, 300))
+_HTTP_CAN_RETRY = set([408, 409, 422, 423, 500, 502, 503, 504])
+
 class RetryLoop(object):
     """Coordinate limited retries of code.
 
@@ -102,3 +107,52 @@ class RetryLoop(object):
         except IndexError:
             raise arvados.errors.AssertionError(
                 "queried loop results before any were recorded")
+
+
+def check_http_response_success(result):
+    """Convert a 'requests' response to a loop control flag.
+
+    Pass this method a requests.Response object.  It returns True if
+    the response indicates success, None if it indicates temporary
+    failure, and False otherwise.  You can use this as the
+    success_check for a RetryLoop.
+
+    Implementation details:
+    * Any 2xx result returns True.
+    * A select few status codes, or any malformed responses, return None.
+      422 Unprocessable Entity is in this category.  This may not meet the
+      letter of the HTTP specification, but the Arvados API server will
+      use it for various server-side problems like database connection
+      errors.
+    * Everything else returns False.  Note that this includes 1xx and
+      3xx status codes.  They don't indicate success, and you can't
+      retry those requests verbatim.
+    """
+    try:
+        status = result.status_code
+    except Exception:
+        return None
+    if status in _HTTP_SUCCESSES:
+        return True
+    elif status in _HTTP_CAN_RETRY:
+        return None
+    elif 100 <= status < 600:
+        return False
+    else:
+        return None  # Get well soon, server.
+
+def retry_method(orig_func):
+    """Provide a default value for a method's num_retries argument.
+
+    This is a decorator for instance and class methods that accept a
+    num_retries argument, with a None default.  When the method is called
+    without a value for num_retries, it will be set from the underlying
+    instance or class' num_retries attribute.
+    """
+    @functools.wraps(orig_func)
+    def num_retries_setter(self, *args, **kwargs):
+        arg_vals = inspect.getcallargs(orig_func, self, *args, **kwargs)
+        if arg_vals['num_retries'] is None:
+            kwargs['num_retries'] = self.num_retries
+        return orig_func(self, *args, **kwargs)
+    return num_retries_setter