Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1232)

Unified Diff: recipe_engine/fetch.py

Issue 2362993002: More strategic retries in fetch. (Closed)
Patch Set: Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | recipe_engine/unittests/fetch_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: recipe_engine/fetch.py
diff --git a/recipe_engine/fetch.py b/recipe_engine/fetch.py
index a221155cba1a24c5897e017a9cc0738a19ce7f6f..31415e13fa070e14e1c0801b769176a1cc3e76e4 100644
--- a/recipe_engine/fetch.py
+++ b/recipe_engine/fetch.py
@@ -4,10 +4,10 @@
import base64
import functools
+import httplib
import json
import logging
import os
-import random
import shutil
import sys
import tarfile
@@ -17,6 +17,7 @@ import time
# Add third party paths.
from . import env
from . import requests_ssl
+from . import util
from .requests_ssl import requests
import subprocess42
@@ -29,10 +30,6 @@ class FetchError(Exception):
pass
-class UncleanFilesystemError(FetchError):
- pass
-
-
class FetchNotAllowedError(FetchError):
pass
@@ -51,30 +48,14 @@ def _run_git(checkout_dir, *args):
return subprocess42.check_output(cmd)
-def _retry(f):
- @functools.wraps(f)
- def wrapper(*args, **kwargs):
- delay = random.uniform(2, 5)
- for _ in range(5):
- try:
- return f(*args, **kwargs)
- except (requests.exceptions.RequestException,
- subprocess42.CalledProcessError):
- # Only retry specific errors that may be transient.
- logging.exception('retrying')
- time.sleep(delay)
- delay *= 2
- return f(*args, **kwargs)
- return wrapper
-
-
class Backend(object):
@property
def repo_type(self):
"""Returns repo type (see package_pb2.DepSpec)."""
raise NotImplementedError()
- def branch_spec(self, branch):
+ @staticmethod
dnj 2016/09/23 00:42:46 This was a pylint failure b/c it's static in child
+ def branch_spec(branch):
"""Returns branch spec for given branch suitable for given git backend."""
raise NotImplementedError()
@@ -103,6 +84,14 @@ class Backend(object):
raise NotImplementedError()
+class UncleanFilesystemError(FetchError):
dnj 2016/09/23 00:42:46 (Moved closer to GitBackend)
+ pass
+
+
+class GitFetchError(FetchError):
+ pass
+
+
class GitBackend(Backend):
"""GitBackend uses a local git checkout."""
@@ -114,7 +103,7 @@ class GitBackend(Backend):
def branch_spec(branch):
return 'origin/%s' % branch
- @_retry
+ @util.exponential_retry(condition=lambda e: isinstance(e, GitFetchError))
def checkout(self, repo, revision, checkout_dir, allow_fetch):
logging.info('Freshening repository %s in %s', repo, checkout_dir)
@@ -135,7 +124,14 @@ class GitBackend(Backend):
if not allow_fetch:
raise FetchNotAllowedError(
'need to fetch %s but fetch not allowed' % repo)
- _run_git(checkout_dir, 'fetch')
+
+ # Fetch from the remote Git repository. Wrap this in a GitFetchError
+ # for exponential retry on failure.
+ try:
+ _run_git(checkout_dir, 'fetch')
+ except subprocess42.CalledProcessError as e:
+ raise GitFetchError(e.message)
+
_run_git(checkout_dir, 'reset', '-q', '--hard', revision)
def updates(self, repo, revision, checkout_dir, allow_fetch,
@@ -161,9 +157,29 @@ class GitBackend(Backend):
}
+class GitilesFetchError(FetchError):
+ """An HTTP error that occurred during Gitiles fetching."""
+
+ def __init__(self, status, message):
+ super(GitilesFetchError, self).__init__(
+ 'Gitiles error code (%d): %s' % (status, message))
+ self.status = status
+ self.message = message
+
+ @staticmethod
+ def transient(e):
+ """Returns (bool): True "e" is a GitilesFetchError with transient HTTP code.
martiniss 2016/09/23 00:49:50 nit: docs formatting
dnj 2016/09/23 01:40:55 Done.
+ """
+ return (isinstance(e, GitilesFetchError) and
+ e.status >= httplib.INTERNAL_SERVER_ERROR)
+
+
class GitilesBackend(Backend):
"""GitilesBackend uses a repo served by Gitiles."""
+ # Header at the beginning of Gerrit/Gitiles JSON API responses.
+ _GERRIT_XSRF_HEADER = ')]}\'\n'
+
@property
def repo_type(self):
return package_pb2.DepSpec.GITILES
@@ -172,7 +188,6 @@ class GitilesBackend(Backend):
def branch_spec(branch):
return branch
- @_retry
def checkout(self, repo, revision, checkout_dir, allow_fetch):
requests_ssl.check_requests_ssl()
logging.info('Freshening repository %s in %s', repo, checkout_dir)
@@ -188,9 +203,8 @@ class GitilesBackend(Backend):
recipes_cfg_url = '%s/+/%s/infra/config/recipes.cfg?format=TEXT' % (
repo, requests.utils.quote(revision))
- logging.info('fetching %s' % recipes_cfg_url)
- recipes_cfg_request = requests.get(recipes_cfg_url)
- recipes_cfg_text = base64.b64decode(recipes_cfg_request.text)
+ recipes_cfg_text = base64.b64decode(
martiniss 2016/09/23 00:49:50 Can you add something like what I did: try:
dnj 2016/09/23 01:40:55 Does Gitiles actually return 200 w/ a UnicodeError
+ self._fetch_gitiles(recipes_cfg_url).text)
recipes_cfg_proto = package_pb2.Package()
text_format.Merge(recipes_cfg_text, recipes_cfg_proto)
recipes_path_rel = recipes_cfg_proto.recipes_path
@@ -209,10 +223,9 @@ class GitilesBackend(Backend):
archive_url = '%s/+archive/%s/%s.tar.gz' % (
repo, requests.utils.quote(revision), recipes_path_rel)
- logging.info('fetching %s' % archive_url)
- archive_request = requests.get(archive_url)
+ archive_response = self._fetch_gitiles(archive_url)
with tempfile.NamedTemporaryFile(delete=False) as f:
- f.write(archive_request.content)
+ f.write(archive_response.content)
f.close()
try:
@@ -274,10 +287,27 @@ class GitilesBackend(Backend):
logging.info('resolved %s to %s', revision, rev_json['commit'])
return rev_json['commit']
- def _fetch_gitiles_json(self, url):
+ @staticmethod
+ @util.exponential_retry(condition=GitilesFetchError.transient)
+ def _fetch_gitiles(url):
+ """Fetches a remote URL and returns the response object on success."""
+ logging.info('fetching %s' % url)
+ resp = requests.get(url)
+ if resp.status_code != httplib.OK:
+ raise GitilesFetchError(resp.status_code, resp.text)
+ return resp
+
+ @classmethod
+ @util.exponential_retry(condition=GitilesFetchError.transient)
+ def _fetch_gitiles_json(cls, url):
"""Fetches JSON from Gitiles and returns parsed result."""
logging.info('fetching %s', url)
- raw = requests.get(url).text
- if not raw.startswith(')]}\'\n'):
- raise FetchError('Unexpected gitiles response: %s' % raw)
- return json.loads(raw.split('\n', 1)[1])
+
+ resp = requests.get(url)
+ if resp.status_code != httplib.OK:
+ raise GitilesFetchError(resp.status_code, resp.text)
+
+ if not resp.text.startswith(cls._GERRIT_XSRF_HEADER):
+ raise GitilesFetchError(resp.status_code, 'Missing XSRF header')
+
+ return json.loads(resp.text[len(cls._GERRIT_XSRF_HEADER):])
« no previous file with comments | « no previous file | recipe_engine/unittests/fetch_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698