Chromium Code Reviews| 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):]) |