Add retry in downloading Android artifacts.

BUG=chromium:512668
TEST=local test

Change-Id: Ic084079fd9eac71ac4cca40a702f1337d9eca2aa
Reviewed-on: https://chromium-review.googlesource.com/312503
Commit-Ready: Dan Shi <dshi@google.com>
Tested-by: Dan Shi <dshi@google.com>
Reviewed-by: Dan Shi <dshi@google.com>
diff --git a/retry.py b/retry.py
new file mode 100644
index 0000000..a0b51fb
--- /dev/null
+++ b/retry.py
@@ -0,0 +1,79 @@
+# Copyright 2015 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Basic infrastructure for implementing retries.
+
+This code is adopted from autotest: client/common_lib/cros/retry.py
+This implementation removes the timeout feature as that requires the retry to
+be done in main thread. For devserver, the call is handled in a thread kicked
+off by cherrypy, so timeotu can't be supported.
+"""
+
+from __future__ import print_function
+
+import cherrypy
+import random
+import sys
+import time
+
+
+def retry(ExceptionToCheck, timeout_min=1.0, delay_sec=3, blacklist=None):
+  """Retry calling the decorated function using a delay with jitter.
+
+  Will raise RPC ValidationError exceptions from the decorated
+  function without retrying; a malformed RPC isn't going to
+  magically become good. Will raise exceptions in blacklist as well.
+
+  original from:
+    http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
+
+  Args:
+    ExceptionToCheck: the exception to check.  May be a tuple of exceptions to
+                      check.
+    timeout_min: timeout in minutes until giving up.
+    delay_sec: pre-jittered delay between retries in seconds.  Actual delays
+               will be centered around this value, ranging up to 50% off this
+               midpoint.
+    blacklist: a list of exceptions that will be raised without retrying
+  """
+  def deco_retry(func):
+    random.seed()
+
+    def delay():
+      """'Jitter' the delay, up to 50% in either direction."""
+      random_delay = random.uniform(.5 * delay_sec, 1.5 * delay_sec)
+      cherrypy.log('Retrying in %f seconds...' % random_delay)
+      time.sleep(random_delay)
+
+    def func_retry(*args, **kwargs):
+      # Used to cache exception to be raised later.
+      exc_info = None
+      delayed_enabled = False
+      exception_tuple = () if blacklist is None else tuple(blacklist)
+      start_time = time.time()
+      remaining_time = timeout_min * 60
+
+      while remaining_time > 0:
+        if delayed_enabled:
+          delay()
+        else:
+          delayed_enabled = True
+        try:
+          # Clear the cache
+          exc_info = None
+          return func(*args, **kwargs)
+        except exception_tuple:
+          raise
+        except ExceptionToCheck as e:
+          cherrypy.log('%s(%s)' % (e.__class__, e))
+          # Cache the exception to be raised later.
+          exc_info = sys.exc_info()
+
+          remaining_time = int(timeout_min*60 - (time.time() - start_time))
+
+      # Raise the cached exception with original backtrace.
+      raise exc_info[0], exc_info[1], exc_info[2]
+
+    return func_retry  # true decorator
+  return deco_retry