Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add connection error support #377

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions bravado/exception.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
# -*- coding: utf-8 -*-
from six import with_metaclass

try:
from builtins import TimeoutError as base_exception
from builtins import ConnectionError as base_connection_error
except ImportError:
# TimeoutError was introduced in python 3.3+
base_exception = Exception
# ConnectionError was introduced in python 3.3+
base_connection_error = OSError


from six import with_metaclass
try:
from builtins import TimeoutError as base_timeout_error
except ImportError:
# TimeoutError was introduced in python 3.3+
base_timeout_error = OSError


# Dictionary of HTTP status codes to exception classes
status_map = {}
Expand Down Expand Up @@ -332,7 +339,11 @@ class HTTPNetworkAuthenticationRequired(HTTPServerError):
status_code = 511


class BravadoTimeoutError(base_exception):
class BravadoTimeoutError(base_timeout_error):
pass


class BravadoConnectionError(base_connection_error):
pass


Expand Down
10 changes: 9 additions & 1 deletion bravado/fido_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import requests
import requests.structures
import six
import twisted.internet.error
import twisted.web.client
from bravado_core.response import IncomingResponse
from yelp_bytes import to_bytes

Expand Down Expand Up @@ -166,7 +168,13 @@ class FidoFutureAdapter(FutureAdapter):
retrieve results.
"""

timeout_errors = [fido.exceptions.HTTPTimeoutError]
timeout_errors = (fido.exceptions.HTTPTimeoutError,)
connection_errors = (
fido.exceptions.TCPConnectionError,
twisted.internet.error.ConnectingCancelledError,
twisted.internet.error.DNSLookupError,
twisted.web.client.RequestNotSent,
)

def __init__(self, eventual_result):
self._eventual_result = eventual_result
Expand Down
57 changes: 34 additions & 23 deletions bravado/http_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from msgpack import unpackb

from bravado.config import RequestConfig
from bravado.exception import BravadoConnectionError
from bravado.exception import BravadoTimeoutError
from bravado.exception import ForcedFallbackResultError
from bravado.exception import HTTPServerError
Expand Down Expand Up @@ -43,7 +44,31 @@ class FutureAdapter(object):
"""

# Make sure to define the timeout errors associated with your http client
timeout_errors = []
timeout_errors = ()
connection_errors = ()

def _raise_error(self, base_exception_class, class_name_suffix, exception):
error = type(
'{}{}'.format(self.__class__.__name__, class_name_suffix),
(exception.__class__, base_exception_class),
dict(
# Small hack to allow all exceptions to be generated even if they have parameters in the signature
exception.__dict__,
__init__=lambda *args, **kwargs: None,
),
)()

six.reraise(
error.__class__,
error,
sys.exc_info()[2],
)

def _raise_timeout_error(self, exception):
self._raise_error(BravadoTimeoutError, 'Timeout', exception)

def _raise_connection_error(self, exception):
self._raise_error(BravadoConnectionError, 'ConnectionError', exception)

def result(self, timeout=None):
"""
Expand All @@ -58,32 +83,18 @@ def result(self, timeout=None):


def reraise_errors(func):

@wraps(func)
def wrapper(self, *args, **kwargs):
timeout_errors = tuple(getattr(self.future, 'timeout_errors', None) or ())

# Make sure that timeout error type for a specific future adapter is generated only once
if timeout_errors and getattr(self.future, '__timeout_error_type', None) is None:
setattr(
self.future, '__timeout_error_type',
type(
'{}Timeout'.format(self.future.__class__.__name__),
tuple(list(timeout_errors) + [BravadoTimeoutError]),
dict(),
),
)
timeout_errors = tuple(self.future.timeout_errors or ())
connection_errors = tuple(self.future.connection_errors or ())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make this code backwards compatible? I think something like

connection_errors = getattr(self.future, 'connection_errors', ())

I don't think it's a problem to use whatever the value of that attribute is if it is defined.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure ... keeping backward compatibility is something to aim for :)
I'm not against using getattr but I would understand how the change could provide backward incompatible changes (so next time I'll think to it too)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about it and well the change is backward compatible as self.future in an instance of bravado.http_future. FutureAdapter which defines those attributes.
So even if http clients are not defining them the attribute will be available with an empty tuple as default value


if timeout_errors:
try:
return func(self, *args, **kwargs)
except timeout_errors as exception:
six.reraise(
self.future.__timeout_error_type,
self.future.__timeout_error_type(*exception.args),
sys.exc_info()[2],
)
else:
try:
return func(self, *args, **kwargs)
except timeout_errors as exception:
self.future._raise_timeout_error(exception)
except connection_errors as exception:
self.future._raise_connection_error(exception)

return wrapper

Expand Down
3 changes: 2 additions & 1 deletion bravado/requests_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ class RequestsFutureAdapter(FutureAdapter):
HTTP calls with the Requests library in a future-y sort of way.
"""

timeout_errors = [requests.exceptions.ReadTimeout]
timeout_errors = (requests.exceptions.ReadTimeout,)
connection_errors = (requests.exceptions.ConnectionError,)

def __init__(self, session, request, misc_options):
"""Kicks API call for Requests client
Expand Down
6 changes: 5 additions & 1 deletion tests/http_future/HttpFuture/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@

@pytest.fixture
def mock_future_adapter():
return mock.Mock(spec=FutureAdapter, timeout_errors=None)
return mock.Mock(
spec=FutureAdapter,
timeout_errors=None,
connection_errors=None,
)
27 changes: 27 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,27 @@
},
},
},
'/sleep': {
'get': {
'operationId': 'sleep',
'produces': ['application/json'],
'parameters': [
{
'in': 'query',
'name': 'sec',
'type': 'number',
'format': 'float',
'required': True,
}
],
'responses': {
'200': {
'description': 'HTTP/200',
'schema': {'$ref': '#/definitions/api_response'},
},
},
},
},
},
}

Expand Down Expand Up @@ -216,3 +237,9 @@ def wait_unit_service_starts(url, max_wait_time=10):
yield server_address
finally:
web_service_process.terminate()


@pytest.fixture(scope='session')
def not_answering_http_server():
# Nobody could listen on such address, so TCP 3 way handshake will fail
yield 'http://0.0.0.0'
15 changes: 14 additions & 1 deletion tests/integration/fido_client_test.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
# -*- coding: utf-8 -*-
import fido.exceptions
import twisted.internet.error
import twisted.web.client

from bravado.fido_client import FidoClient
from bravado.fido_client import FidoFutureAdapter
from tests.integration import requests_client_test


class TestServerFidoClient(requests_client_test.TestServerRequestsClient):
class TestServerFidoClient(requests_client_test.ServerClientGeneric):

http_client_type = FidoClient
http_future_adapter_type = FidoFutureAdapter
connection_errors_exceptions = {
fido.exceptions.TCPConnectionError(),
twisted.internet.error.ConnectingCancelledError('address'),
twisted.internet.error.DNSLookupError(),
twisted.web.client.RequestNotSent(),
}

@classmethod
def encode_expected_response(cls, response):
return response

def cancel_http_future(self, http_future):
http_future.future._eventual_result.cancel()
Loading