Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import base64 | 5 import base64 |
| 6 import functools | 6 import functools |
| 7 import json | 7 import json |
| 8 import logging | 8 import logging |
| 9 import os | 9 import os |
| 10 import random | 10 import random |
| 11 import shutil | 11 import shutil |
| 12 import sys | 12 import sys |
| 13 import tarfile | 13 import tarfile |
| 14 import tempfile | 14 import tempfile |
| 15 import time | 15 import time |
| 16 | 16 |
| 17 # Add third party paths. | 17 # Add third party paths. |
| 18 from . import env | 18 from . import env |
| 19 from . import requests_ssl | 19 from . import requests_ssl |
| 20 from .requests_ssl import requests | 20 from .requests_ssl import requests |
| 21 | 21 |
| 22 import subprocess42 | 22 import subprocess42 |
| 23 from google.protobuf import text_format | 23 from google.protobuf import text_format |
| 24 | 24 |
| 25 from . import package_pb2 | 25 from . import package_pb2 |
| 26 | 26 |
| 27 class RetryFetch(Exception): | |
| 28 """Raised to indicate that the fetch operation should be retried""" | |
| 29 def __init__(self, e): | |
| 30 super(RetryFetch, self).__init__( | |
| 31 'Retrying fetch because of exception: %r' % e) | |
| 32 self.exc = e | |
| 27 | 33 |
| 28 class FetchError(Exception): | 34 class FetchError(Exception): |
| 29 pass | 35 pass |
| 30 | 36 |
| 31 | 37 |
| 32 class UncleanFilesystemError(FetchError): | 38 class UncleanFilesystemError(FetchError): |
| 33 pass | 39 pass |
| 34 | 40 |
| 35 | 41 |
| 36 class FetchNotAllowedError(FetchError): | 42 class FetchNotAllowedError(FetchError): |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 52 | 58 |
| 53 | 59 |
| 54 def _retry(f): | 60 def _retry(f): |
| 55 @functools.wraps(f) | 61 @functools.wraps(f) |
| 56 def wrapper(*args, **kwargs): | 62 def wrapper(*args, **kwargs): |
| 57 delay = random.uniform(2, 5) | 63 delay = random.uniform(2, 5) |
| 58 for _ in range(5): | 64 for _ in range(5): |
| 59 try: | 65 try: |
| 60 return f(*args, **kwargs) | 66 return f(*args, **kwargs) |
| 61 except (requests.exceptions.RequestException, | 67 except (requests.exceptions.RequestException, |
| 62 subprocess42.CalledProcessError): | 68 subprocess42.CalledProcessError, |
| 69 RetryFetch) as e: | |
| 63 # Only retry specific errors that may be transient. | 70 # Only retry specific errors that may be transient. |
| 64 logging.exception('retrying') | 71 logging.warning('retrying because of %s', e) |
| 65 time.sleep(delay) | 72 time.sleep(delay) |
| 66 delay *= 2 | 73 delay *= 2 |
| 67 return f(*args, **kwargs) | 74 return f(*args, **kwargs) |
| 68 return wrapper | 75 return wrapper |
| 69 | 76 |
| 70 | 77 |
| 71 class Backend(object): | 78 class Backend(object): |
| 72 @property | 79 @property |
| 73 def repo_type(self): | 80 def repo_type(self): |
| 74 """Returns repo type (see package_pb2.DepSpec).""" | 81 """Returns repo type (see package_pb2.DepSpec).""" |
| (...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 183 'need to download %s from gitiles but fetch not allowed' % repo) | 190 'need to download %s from gitiles but fetch not allowed' % repo) |
| 184 | 191 |
| 185 revision = self._resolve_revision(repo, revision) | 192 revision = self._resolve_revision(repo, revision) |
| 186 | 193 |
| 187 shutil.rmtree(checkout_dir, ignore_errors=True) | 194 shutil.rmtree(checkout_dir, ignore_errors=True) |
| 188 | 195 |
| 189 recipes_cfg_url = '%s/+/%s/infra/config/recipes.cfg?format=TEXT' % ( | 196 recipes_cfg_url = '%s/+/%s/infra/config/recipes.cfg?format=TEXT' % ( |
| 190 repo, requests.utils.quote(revision)) | 197 repo, requests.utils.quote(revision)) |
| 191 logging.info('fetching %s' % recipes_cfg_url) | 198 logging.info('fetching %s' % recipes_cfg_url) |
| 192 recipes_cfg_request = requests.get(recipes_cfg_url) | 199 recipes_cfg_request = requests.get(recipes_cfg_url) |
| 193 recipes_cfg_text = base64.b64decode(recipes_cfg_request.text) | 200 try: |
| 201 recipes_cfg_text = base64.b64decode(recipes_cfg_request.text) | |
| 202 except UnicodeEncodeError as e: | |
| 203 # UnicodeEncodeError can happen sometimes, if gitiles gives us some weird | |
| 204 # response. Just retry the whole thing. | |
| 205 logging.exception( | |
| 206 "got strange response from gitiles: %s", recipes_cfg_request.text) | |
| 207 raise RetryFetch(e) | |
| 208 | |
| 194 recipes_cfg_proto = package_pb2.Package() | 209 recipes_cfg_proto = package_pb2.Package() |
| 195 text_format.Merge(recipes_cfg_text, recipes_cfg_proto) | 210 text_format.Merge(recipes_cfg_text, recipes_cfg_proto) |
| 196 recipes_path_rel = recipes_cfg_proto.recipes_path | 211 recipes_path_rel = recipes_cfg_proto.recipes_path |
| 197 | 212 |
| 198 # Re-create recipes.cfg in |checkout_dir| so that the repo's recipes.py | 213 # Re-create recipes.cfg in |checkout_dir| so that the repo's recipes.py |
| 199 # can look it up. | 214 # can look it up. |
| 200 recipes_cfg_path = os.path.join( | 215 recipes_cfg_path = os.path.join( |
| 201 checkout_dir, 'infra', 'config', 'recipes.cfg') | 216 checkout_dir, 'infra', 'config', 'recipes.cfg') |
| 202 os.makedirs(os.path.dirname(recipes_cfg_path)) | 217 os.makedirs(os.path.dirname(recipes_cfg_path)) |
| 203 with open(recipes_cfg_path, 'w') as f: | 218 with open(recipes_cfg_path, 'w') as f: |
| (...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 270 """Returns a git sha corresponding to given revision. | 285 """Returns a git sha corresponding to given revision. |
| 271 | 286 |
| 272 Examples of non-sha revision: origin/master, HEAD.""" | 287 Examples of non-sha revision: origin/master, HEAD.""" |
| 273 rev_json = self._revision_metadata(repo, revision) | 288 rev_json = self._revision_metadata(repo, revision) |
| 274 logging.info('resolved %s to %s', revision, rev_json['commit']) | 289 logging.info('resolved %s to %s', revision, rev_json['commit']) |
| 275 return rev_json['commit'] | 290 return rev_json['commit'] |
| 276 | 291 |
| 277 def _fetch_gitiles_json(self, url): | 292 def _fetch_gitiles_json(self, url): |
| 278 """Fetches JSON from Gitiles and returns parsed result.""" | 293 """Fetches JSON from Gitiles and returns parsed result.""" |
| 279 logging.info('fetching %s', url) | 294 logging.info('fetching %s', url) |
| 280 raw = requests.get(url).text | 295 resp = requests.get(url) |
| 296 raw = resp.text | |
| 281 if not raw.startswith(')]}\'\n'): | 297 if not raw.startswith(')]}\'\n'): |
| 282 raise FetchError('Unexpected gitiles response: %s' % raw) | 298 logging.warning("Unexpected gitiles response: %s", raw) |
| 299 raise FetchError( | |
|
martiniss
2016/09/23 00:42:58
A (maybe unfounded) hypothesis is that the excepti
| |
| 300 'Unexpected gitiles response (code %d): %s... (truncated, see log for' | |
| 301 'full details)' % (resp.status_code, raw[:100])) | |
| 302 | |
| 283 return json.loads(raw.split('\n', 1)[1]) | 303 return json.loads(raw.split('\n', 1)[1]) |
| OLD | NEW |