Index: swarm_client/utils/net.py |
=================================================================== |
--- swarm_client/utils/net.py (revision 235167) |
+++ swarm_client/utils/net.py (working copy) |
@@ -1,800 +0,0 @@ |
-# Copyright 2013 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-"""Classes and functions for generic network communication over HTTP.""" |
- |
-import cookielib |
-import cStringIO as StringIO |
-import httplib |
-import itertools |
-import logging |
-import math |
-import os |
-import random |
-import re |
-import socket |
-import ssl |
-import threading |
-import time |
-import urllib |
-import urllib2 |
-import urlparse |
- |
-from third_party import requests |
-from third_party.requests import adapters |
-from third_party.rietveld import upload |
- |
-from utils import zip_package |
- |
-# Hack out upload logging.info() |
-upload.logging = logging.getLogger('upload') |
-# Mac pylint choke on this line. |
-upload.logging.setLevel(logging.WARNING) # pylint: disable=E1103 |
- |
- |
-# TODO(vadimsh): Remove this once we don't have to support python 2.6 anymore. |
-def monkey_patch_httplib(): |
- """Patch httplib.HTTPConnection to have '_tunnel_host' attribute. |
- |
- 'requests' library (>= v2) accesses 'HTTPConnection._tunnel_host' attribute |
- added only in python 2.6.3. This function patches HTTPConnection to have it |
- on python 2.6.2 as well. |
- """ |
- conn = httplib.HTTPConnection('example.com') |
- if not hasattr(conn, '_tunnel_host'): |
- httplib.HTTPConnection._tunnel_host = None |
-monkey_patch_httplib() |
- |
- |
-# Big switch that controls what API to use to make HTTP requests. |
-# It's temporary here to simplify benchmarking of old vs new implementation. |
-USE_REQUESTS_LIB = True |
- |
-# The name of the key to store the count of url attempts. |
-COUNT_KEY = 'UrlOpenAttempt' |
- |
-# Default maximum number of attempts to trying opening a url before aborting. |
-URL_OPEN_MAX_ATTEMPTS = 30 |
- |
-# Default timeout when retrying. |
-URL_OPEN_TIMEOUT = 6*60. |
- |
-# Content type for url encoded POST body. |
-URL_ENCODED_FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded' |
- |
-# Default content type for POST body. |
-DEFAULT_CONTENT_TYPE = URL_ENCODED_FORM_CONTENT_TYPE |
- |
-# Content type -> function that encodes a request body. |
-CONTENT_ENCODERS = { |
- URL_ENCODED_FORM_CONTENT_TYPE: urllib.urlencode, |
-} |
- |
-# File to use to store all auth cookies. |
-COOKIE_FILE = os.path.join(os.path.expanduser('~'), '.isolated_cookies') |
- |
-# Google Storage URL regular expression. |
-GS_STORAGE_HOST_URL_RE = re.compile(r'https://.*\.storage\.googleapis\.com') |
- |
- |
-# Global (for now) map: server URL (http://example.com) -> HttpService instance. |
-# Used by get_http_service to cache HttpService instances. |
-_http_services = {} |
-_http_services_lock = threading.Lock() |
- |
-# CookieJar reused by all services + lock that protects its instantiation. |
-_cookie_jar = None |
-_cookie_jar_lock = threading.Lock() |
- |
-# Path to cacert.pem bundle file reused by all services. |
-_ca_certs = None |
-_ca_certs_lock = threading.Lock() |
- |
- |
-class NetError(IOError): |
- """Generic network related error.""" |
- |
- def __init__(self, inner_exc=None): |
- super(NetError, self).__init__(str(inner_exc or self.__doc__)) |
- self.inner_exc = inner_exc |
- |
- def format(self, verbose=False): |
- """Human readable description with detailed information about the error.""" |
- out = ['Exception: %s' % (self.inner_exc,)] |
- if verbose: |
- headers = None |
- body = None |
- if isinstance(self.inner_exc, urllib2.HTTPError): |
- headers = self.inner_exc.hdrs.items() |
- body = self.inner_exc.read() |
- elif isinstance(self.inner_exc, requests.HTTPError): |
- headers = self.inner_exc.response.headers.items() |
- body = self.inner_exc.response.content |
- if headers or body: |
- out.append('----------') |
- if headers: |
- for header, value in headers: |
- if not header.startswith('x-'): |
- out.append('%s: %s' % (header.capitalize(), value)) |
- out.append('') |
- out.append(body or '<empty body>') |
- out.append('----------') |
- return '\n'.join(out) |
- |
- |
-class TimeoutError(NetError): |
- """Timeout while reading HTTP response.""" |
- |
- |
-class ConnectionError(NetError): |
- """Failed to connect to the server.""" |
- |
- |
-class HttpError(NetError): |
- """Server returned HTTP error code.""" |
- |
- def __init__(self, code, inner_exc=None): |
- super(HttpError, self).__init__(inner_exc) |
- self.code = code |
- |
- |
-def url_open(url, **kwargs): |
- """Attempts to open the given url multiple times. |
- |
- |data| can be either: |
- - None for a GET request |
- - str for pre-encoded data |
- - list for data to be encoded |
- - dict for data to be encoded |
- |
- See HttpService.request for a full list of arguments. |
- |
- Returns HttpResponse object, where the response may be read from, or None |
- if it was unable to connect. |
- """ |
- urlhost, urlpath = split_server_request_url(url) |
- service = get_http_service(urlhost) |
- return service.request(urlpath, **kwargs) |
- |
- |
-def url_read(url, **kwargs): |
- """Attempts to open the given url multiple times and read all data from it. |
- |
- Accepts same arguments as url_open function. |
- |
- Returns all data read or None if it was unable to connect or read the data. |
- """ |
- kwargs['stream'] = False |
- response = url_open(url, **kwargs) |
- if not response: |
- return None |
- try: |
- return response.read() |
- except TimeoutError: |
- return None |
- |
- |
-def split_server_request_url(url): |
- """Splits the url into scheme+netloc and path+params+query+fragment.""" |
- url_parts = list(urlparse.urlparse(url)) |
- urlhost = '%s://%s' % (url_parts[0], url_parts[1]) |
- urlpath = urlparse.urlunparse(['', ''] + url_parts[2:]) |
- return urlhost, urlpath |
- |
- |
-def get_http_service(urlhost): |
- """Returns existing or creates new instance of HttpService that can send |
- requests to given base urlhost. |
- """ |
- # Ensure consistency. |
- urlhost = str(urlhost).lower().rstrip('/') |
- with _http_services_lock: |
- service = _http_services.get(urlhost) |
- if not service: |
- if GS_STORAGE_HOST_URL_RE.match(urlhost): |
- # For Google Storage URL create a dumber HttpService that doesn't modify |
- # requests with COUNT_KEY (since it breaks a signature) and doesn't try |
- # to 'login' into Google Storage (since it's impossible). |
- service = HttpService( |
- urlhost, |
- engine=create_request_engine(None), |
- authenticator=None, |
- use_count_key=False) |
- else: |
- # For other URLs (presumably App Engine), create a fancier HttpService |
- # with cookies, authentication and COUNT_KEY query parameter in retries. |
- cookie_jar = get_cookie_jar() |
- service = HttpService( |
- urlhost, |
- engine=create_request_engine(cookie_jar), |
- authenticator=AppEngineAuthenticator(urlhost, cookie_jar), |
- use_count_key=True) |
- _http_services[urlhost] = service |
- return service |
- |
- |
-def create_request_engine(cookie_jar): |
- """Returns a new instance of RequestEngine subclass. |
- |
- |cookie_jar| is an instance of ThreadSafeCookieJar class that holds all |
- cookies. It is optional and may be None (in that case cookies are not saved |
- on disk). |
- """ |
- if USE_REQUESTS_LIB: |
- return RequestsLibEngine(cookie_jar, get_cacerts_bundle()) |
- return Urllib2Engine(cookie_jar) |
- |
- |
-def get_cookie_jar(): |
- """Returns global CoookieJar object that stores cookies in the file.""" |
- global _cookie_jar |
- with _cookie_jar_lock: |
- if _cookie_jar is not None: |
- return _cookie_jar |
- jar = ThreadSafeCookieJar(COOKIE_FILE) |
- jar.load() |
- _cookie_jar = jar |
- return jar |
- |
- |
-def get_cacerts_bundle(): |
- """Returns path to a file with CA root certificates bundle.""" |
- global _ca_certs |
- with _ca_certs_lock: |
- if _ca_certs is not None and os.path.exists(_ca_certs): |
- return _ca_certs |
- _ca_certs = zip_package.extract_resource(requests, 'cacert.pem') |
- return _ca_certs |
- |
- |
-class HttpService(object): |
- """Base class for a class that provides an API to HTTP based service: |
- - Provides 'request' method. |
- - Supports automatic request retries. |
- - Supports persistent cookies. |
- - Thread safe. |
- """ |
- def __init__(self, urlhost, engine, authenticator=None, use_count_key=True): |
- self.urlhost = urlhost |
- self.engine = engine |
- self.authenticator = authenticator |
- self.use_count_key = use_count_key |
- |
- @staticmethod |
- def is_transient_http_error(code, retry_404, retry_50x): |
- """Returns True if given HTTP response code is a transient error.""" |
- # Google Storage can return this and it should be retried. |
- if code == 408: |
- return True |
- # Retry 404 only if allowed by the caller. |
- if code == 404: |
- return retry_404 |
- # All other 4** errors are fatal. |
- if code < 500: |
- return False |
- # Retry >= 500 error only if allowed by the caller. |
- return retry_50x |
- |
- @staticmethod |
- def encode_request_body(body, content_type): |
- """Returns request body encoded according to its content type.""" |
- # No body or it is already encoded. |
- if body is None or isinstance(body, str): |
- return body |
- # Any body should have content type set. |
- assert content_type, 'Request has body, but no content type' |
- encoder = CONTENT_ENCODERS.get(content_type) |
- assert encoder, ('Unknown content type %s' % content_type) |
- return encoder(body) |
- |
- def request( |
- self, |
- urlpath, |
- data=None, |
- content_type=None, |
- max_attempts=URL_OPEN_MAX_ATTEMPTS, |
- retry_404=False, |
- retry_50x=True, |
- timeout=URL_OPEN_TIMEOUT, |
- read_timeout=None, |
- stream=True, |
- method=None): |
- """Attempts to open the given url multiple times. |
- |
- |urlpath| is relative to the server root, i.e. '/some/request?param=1'. |
- |
- |data| can be either: |
- - None for a GET request |
- - str for pre-encoded data |
- - list for data to be form-encoded |
- - dict for data to be form-encoded |
- |
- - Optionally retries HTTP 404 and 50x. |
- - Retries up to |max_attempts| times. If None or 0, there's no limit in the |
- number of retries. |
- - Retries up to |timeout| duration in seconds. If None or 0, there's no |
- limit in the time taken to do retries. |
- - If both |max_attempts| and |timeout| are None or 0, this functions retries |
- indefinitely. |
- |
- If |method| is given it can be 'GET', 'POST' or 'PUT' and it will be used |
- when performing the request. By default it's GET if |data| is None and POST |
- if |data| is not None. |
- |
- If |read_timeout| is not None will configure underlying socket to |
- raise TimeoutError exception whenever there's no response from the server |
- for more than |read_timeout| seconds. It can happen during any read |
- operation so once you pass non-None |read_timeout| be prepared to handle |
- these exceptions in subsequent reads from the stream. |
- |
- Returns a file-like object, where the response may be read from, or None |
- if it was unable to connect. If |stream| is False will read whole response |
- into memory buffer before returning file-like object that reads from this |
- memory buffer. |
- """ |
- assert urlpath and urlpath[0] == '/', urlpath |
- |
- if data is not None: |
- assert method in (None, 'POST', 'PUT') |
- method = method or 'POST' |
- content_type = content_type or DEFAULT_CONTENT_TYPE |
- body = self.encode_request_body(data, content_type) |
- else: |
- assert method in (None, 'GET') |
- method = method or 'GET' |
- body = None |
- assert not content_type, 'Can\'t use content_type on GET' |
- |
- # Prepare request info. |
- parsed = urlparse.urlparse('/' + urlpath.lstrip('/')) |
- resource_url = urlparse.urljoin(self.urlhost, parsed.path) |
- query_params = urlparse.parse_qsl(parsed.query) |
- |
- # Prepare headers. |
- headers = {} |
- if body is not None: |
- headers['Content-Length'] = len(body) |
- if content_type: |
- headers['Content-Type'] = content_type |
- |
- last_error = None |
- auth_attempted = False |
- |
- for attempt in retry_loop(max_attempts, timeout): |
- # Log non-first attempt. |
- if attempt.attempt: |
- logging.warning( |
- 'Retrying request %s, attempt %d/%d...', |
- resource_url, attempt.attempt, max_attempts) |
- |
- try: |
- # Prepare and send a new request. |
- request = HttpRequest(method, resource_url, query_params, body, |
- headers, read_timeout, stream) |
- self.prepare_request(request, attempt.attempt) |
- response = self.engine.perform_request(request) |
- logging.debug('Request %s succeeded', request.get_full_url()) |
- return response |
- |
- except (ConnectionError, TimeoutError) as e: |
- last_error = e |
- logging.warning( |
- 'Unable to open url %s on attempt %d.\n%s', |
- request.get_full_url(), attempt.attempt, e.format()) |
- continue |
- |
- except HttpError as e: |
- last_error = e |
- |
- # Access denied -> authenticate. |
- if e.code in (401, 403): |
- logging.error( |
- 'Authentication is required for %s on attempt %d.\n%s', |
- request.get_full_url(), attempt.attempt, e.format()) |
- # Try to authenticate only once. If it doesn't help, then server does |
- # not support app engine authentication. |
- if not auth_attempted: |
- auth_attempted = True |
- if self.authenticator and self.authenticator.authenticate(): |
- # Success! Run request again immediately. |
- attempt.skip_sleep = True |
- # Also refresh cookies used by request engine. |
- self.engine.reload_cookies() |
- continue |
- # Authentication attempt was unsuccessful. |
- logging.error( |
- 'Unable to authenticate to %s.\n%s', |
- request.get_full_url(), e.format(verbose=True)) |
- return None |
- |
- # Hit a error that can not be retried -> stop retry loop. |
- if not self.is_transient_http_error(e.code, retry_404, retry_50x): |
- # This HttpError means we reached the server and there was a problem |
- # with the request, so don't retry. |
- logging.error( |
- 'Able to connect to %s but an exception was thrown.\n%s', |
- request.get_full_url(), e.format(verbose=True)) |
- return None |
- |
- # Retry all other errors. |
- logging.warning( |
- 'Server responded with error on %s on attempt %d.\n%s', |
- request.get_full_url(), attempt.attempt, e.format()) |
- continue |
- |
- logging.error( |
- 'Unable to open given url, %s, after %d attempts.\n%s', |
- request.get_full_url(), max_attempts, last_error.format(verbose=True)) |
- return None |
- |
- def prepare_request(self, request, attempt): # pylint: disable=R0201 |
- """Modify HttpRequest before sending it by adding COUNT_KEY parameter.""" |
- # Add COUNT_KEY only on retries. |
- if self.use_count_key and attempt: |
- request.params += [(COUNT_KEY, attempt)] |
- |
- |
-class HttpRequest(object): |
- """Request to HttpService.""" |
- |
- def __init__(self, method, url, params, body, headers, timeout, stream): |
- """Arguments: |
- |method| - HTTP method to use |
- |url| - relative URL to the resource, without query parameters |
- |params| - list of (key, value) pairs to put into GET parameters |
- |body| - encoded body of the request (None or str) |
- |headers| - dict with request headers |
- |timeout| - socket read timeout (None to disable) |
- |stream| - True to stream response from socket |
- """ |
- self.method = method |
- self.url = url |
- self.params = params[:] |
- self.body = body |
- self.headers = headers.copy() |
- self.timeout = timeout |
- self.stream = stream |
- |
- def get_full_url(self): |
- """Resource URL with url-encoded GET parameters.""" |
- if not self.params: |
- return self.url |
- else: |
- return '%s?%s' % (self.url, urllib.urlencode(self.params)) |
- |
- def make_fake_response(self, content=''): |
- """Makes new fake HttpResponse to this request, useful in tests.""" |
- return HttpResponse.get_fake_response(content, self.get_full_url()) |
- |
- |
-class HttpResponse(object): |
- """Response from HttpService.""" |
- |
- def __init__(self, stream, url, headers): |
- self._stream = stream |
- self._url = url |
- self._headers = headers |
- self._read = 0 |
- |
- @property |
- def content_length(self): |
- """Total length to the response or None if not known in advance.""" |
- length = self._headers.get('Content-Length') |
- return int(length) if length is not None else None |
- |
- def read(self, size=None): |
- """Reads up to |size| bytes from the stream and returns them. |
- |
- If |size| is None reads all available bytes. |
- |
- Raises TimeoutError on read timeout. |
- """ |
- try: |
- # cStringIO has a bug: stream.read(None) is not the same as stream.read(). |
- data = self._stream.read() if size is None else self._stream.read(size) |
- self._read += len(data) |
- return data |
- except (socket.timeout, ssl.SSLError, requests.Timeout) as e: |
- logging.error('Timeout while reading from %s, read %d of %s: %s', |
- self._url, self._read, self.content_length, e) |
- raise TimeoutError(e) |
- |
- @classmethod |
- def get_fake_response(cls, content, url): |
- """Returns HttpResponse with predefined content, useful in tests.""" |
- return cls(StringIO.StringIO(content), |
- url, {'content-length': len(content)}) |
- |
- |
-class RequestEngine(object): |
- """Base class for objects that know how to execute HttpRequests.""" |
- |
- def perform_request(self, request): |
- """Sends a HttpRequest to the server and reads back the response. |
- |
- Returns HttpResponse. |
- |
- Raises: |
- ConnectionError - failed to establish connection to the server. |
- TimeoutError - timeout while connecting or reading response. |
- HttpError - server responded with >= 400 error code. |
- """ |
- raise NotImplementedError() |
- |
- def reload_cookies(self): |
- """Reloads cookies from original cookie jar.""" |
- # This method is optional. |
- pass |
- |
- |
-class Authenticator(object): |
- """Base class for objects that know how to authenticate into http services.""" |
- |
- def authenticate(self): |
- """Authenticates in the app engine service.""" |
- raise NotImplementedError() |
- |
- |
-class Urllib2Engine(RequestEngine): |
- """Class that knows how to execute HttpRequests via urllib2.""" |
- |
- def __init__(self, cookie_jar): |
- super(Urllib2Engine, self).__init__() |
- self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar)) |
- |
- def perform_request(self, request): |
- try: |
- req = self.make_urllib2_request(request) |
- if request.timeout: |
- resp = self.opener.open(req, timeout=request.timeout) |
- else: |
- resp = self.opener.open(req) |
- return HttpResponse(resp, req.get_full_url(), resp.headers) |
- except urllib2.HTTPError as e: |
- raise HttpError(e.code, e) |
- except (urllib2.URLError, httplib.HTTPException, |
- socket.timeout, ssl.SSLError) as e: |
- raise ConnectionError(e) |
- |
- @staticmethod |
- def make_urllib2_request(request): |
- """Converts HttpRequest to urllib2.Request.""" |
- result = urllib2.Request(request.get_full_url(), data=request.body) |
- for header, value in request.headers.iteritems(): |
- result.add_header(header, value) |
- return result |
- |
- |
-class RequestsLibEngine(RequestEngine): |
- """Class that knows how to execute HttpRequests via requests library.""" |
- |
- # Preferred number of connections in a connection pool. |
- CONNECTION_POOL_SIZE = 64 |
- # If True will not open more than CONNECTION_POOL_SIZE connections. |
- CONNECTION_POOL_BLOCK = False |
- # Maximum number of internal connection retries in a connection pool. |
- CONNECTION_RETRIES = 0 |
- |
- def __init__(self, cookie_jar, ca_certs): |
- super(RequestsLibEngine, self).__init__() |
- self.session = requests.Session() |
- self.cookie_jar = cookie_jar |
- # Configure session. |
- self.session.trust_env = False |
- if cookie_jar: |
- self.session.cookies = cookie_jar |
- self.session.verify = ca_certs |
- # Configure connection pools. |
- for protocol in ('https://', 'http://'): |
- self.session.mount(protocol, adapters.HTTPAdapter( |
- pool_connections=self.CONNECTION_POOL_SIZE, |
- pool_maxsize=self.CONNECTION_POOL_SIZE, |
- max_retries=self.CONNECTION_RETRIES, |
- pool_block=self.CONNECTION_POOL_BLOCK)) |
- |
- def perform_request(self, request): |
- try: |
- response = self.session.request( |
- method=request.method, |
- url=request.url, |
- params=request.params, |
- data=request.body, |
- headers=request.headers, |
- timeout=request.timeout, |
- stream=request.stream) |
- response.raise_for_status() |
- if request.stream: |
- stream = response.raw |
- else: |
- stream = StringIO.StringIO(response.content) |
- return HttpResponse(stream, request.get_full_url(), response.headers) |
- except requests.Timeout as e: |
- raise TimeoutError(e) |
- except requests.HTTPError as e: |
- raise HttpError(e.response.status_code, e) |
- except (requests.ConnectionError, socket.timeout, ssl.SSLError) as e: |
- raise ConnectionError(e) |
- |
- def reload_cookies(self): |
- if self.cookie_jar: |
- self.session.cookies = self.cookie_jar |
- |
- |
-class AppEngineAuthenticator(Authenticator): |
- """Helper class to perform AppEngine authentication dance via upload.py.""" |
- |
- # This lock ensures that user won't be confused with multiple concurrent |
- # login prompts. |
- _auth_lock = threading.Lock() |
- |
- def __init__(self, urlhost, cookie_jar, email=None, password=None): |
- super(AppEngineAuthenticator, self).__init__() |
- self.urlhost = urlhost |
- self.cookie_jar = cookie_jar |
- self.email = email |
- self.password = password |
- self._keyring = None |
- |
- def authenticate(self): |
- """Authenticates in the app engine application. |
- |
- Mutates |self.cookie_jar| in place by adding all required cookies. |
- |
- Returns True on success. |
- """ |
- # To be used from inside AuthServer. |
- cookie_jar = self.cookie_jar |
- # RPC server that uses AuthenticationSupport's cookie jar. |
- class AuthServer(upload.AbstractRpcServer): |
- def _GetOpener(self): |
- # Authentication code needs to know about 302 response. |
- # So make OpenerDirector without HTTPRedirectHandler. |
- opener = urllib2.OpenerDirector() |
- opener.add_handler(urllib2.ProxyHandler()) |
- opener.add_handler(urllib2.UnknownHandler()) |
- opener.add_handler(urllib2.HTTPHandler()) |
- opener.add_handler(urllib2.HTTPDefaultErrorHandler()) |
- opener.add_handler(urllib2.HTTPSHandler()) |
- opener.add_handler(urllib2.HTTPErrorProcessor()) |
- opener.add_handler(urllib2.HTTPCookieProcessor(cookie_jar)) |
- return opener |
- def PerformAuthentication(self): |
- self._Authenticate() |
- return self.authenticated |
- with cookie_jar: |
- with self._auth_lock: |
- rpc_server = AuthServer(self.urlhost, self.get_credentials) |
- return rpc_server.PerformAuthentication() |
- |
- def get_credentials(self): |
- """Called during authentication process to get the credentials. |
- May be called multiple times if authentication fails. |
- Returns tuple (email, password). |
- """ |
- if self.email and self.password: |
- return (self.email, self.password) |
- self._keyring = self._keyring or upload.KeyringCreds(self.urlhost, |
- self.urlhost, self.email) |
- return self._keyring.GetUserCredentials() |
- |
- |
-class ThreadSafeCookieJar(cookielib.MozillaCookieJar): |
- """MozillaCookieJar with thread safe load and save.""" |
- |
- def __enter__(self): |
- """Context manager interface.""" |
- return self |
- |
- def __exit__(self, *_args): |
- """Saves cookie jar when exiting the block.""" |
- self.save() |
- return False |
- |
- def load(self, filename=None, ignore_discard=False, ignore_expires=False): |
- """Loads cookies from the file if it exists.""" |
- filename = os.path.expanduser(filename or self.filename) |
- with self._cookies_lock: |
- if os.path.exists(filename): |
- try: |
- cookielib.MozillaCookieJar.load( |
- self, filename, ignore_discard, ignore_expires) |
- logging.debug('Loaded cookies from %s', filename) |
- except (cookielib.LoadError, IOError): |
- pass |
- else: |
- try: |
- fd = os.open(filename, os.O_CREAT, 0600) |
- os.close(fd) |
- except OSError: |
- logging.debug('Failed to create %s', filename) |
- try: |
- os.chmod(filename, 0600) |
- except OSError: |
- logging.debug('Failed to fix mode for %s', filename) |
- |
- def save(self, filename=None, ignore_discard=False, ignore_expires=False): |
- """Saves cookies to the file, completely overwriting it.""" |
- logging.debug('Saving cookies to %s', filename or self.filename) |
- with self._cookies_lock: |
- try: |
- cookielib.MozillaCookieJar.save( |
- self, filename, ignore_discard, ignore_expires) |
- except OSError: |
- logging.error('Failed to save %s', filename) |
- |
- |
-class RetryAttempt(object): |
- """Contains information about current retry attempt. |
- |
- Yielded from retry_loop. |
- """ |
- |
- def __init__(self, attempt, remaining): |
- """Information about current attempt in retry loop: |
- |attempt| - zero based index of attempt. |
- |remaining| - how much time is left before retry loop finishes retries. |
- """ |
- self.attempt = attempt |
- self.remaining = remaining |
- self.skip_sleep = False |
- |
- |
-def calculate_sleep_before_retry(attempt, max_duration): |
- """How long to sleep before retrying an attempt in retry_loop.""" |
- # Maximum sleeping time. We're hammering a cloud-distributed service, it'll |
- # survive. |
- MAX_SLEEP = 10. |
- # random.random() returns [0.0, 1.0). Starts with relatively short waiting |
- # time by starting with 1.5/2+1.5^-1 median offset. |
- duration = (random.random() * 1.5) + math.pow(1.5, (attempt - 1)) |
- assert duration > 0.1 |
- duration = min(MAX_SLEEP, duration) |
- if max_duration: |
- duration = min(max_duration, duration) |
- return duration |
- |
- |
-def sleep_before_retry(attempt, max_duration): |
- """Sleeps for some amount of time when retrying the attempt in retry_loop. |
- |
- To be mocked in tests. |
- """ |
- time.sleep(calculate_sleep_before_retry(attempt, max_duration)) |
- |
- |
-def current_time(): |
- """Used by retry loop to get current time. |
- |
- To be mocked in tests. |
- """ |
- return time.time() |
- |
- |
-def retry_loop(max_attempts=None, timeout=None): |
- """Yields whenever new attempt to perform some action is needed. |
- |
- Yields instances of RetryAttempt class that contains information about current |
- attempt. Setting |skip_sleep| attribute of RetryAttempt to True will cause |
- retry loop to run next attempt immediately. |
- """ |
- start = current_time() |
- for attempt in itertools.count(): |
- # Too many attempts? |
- if max_attempts and attempt == max_attempts: |
- break |
- # Retried for too long? |
- remaining = (timeout - (current_time() - start)) if timeout else None |
- if remaining is not None and remaining < 0: |
- break |
- # Kick next iteration. |
- attemp_obj = RetryAttempt(attempt, remaining) |
- yield attemp_obj |
- if attemp_obj.skip_sleep: |
- continue |
- # Only sleep if we are going to try again. |
- if max_attempts and attempt != max_attempts - 1: |
- remaining = (timeout - (current_time() - start)) if timeout else None |
- if remaining is not None and remaining < 0: |
- break |
- sleep_before_retry(attempt, remaining) |