WIP - Split RequestTimeout, ResponseTimout, and KeepAliveTimeout into different timeouts, with different callbacks.

This commit is contained in:
Ashley Sommer
2017-09-11 17:17:33 +10:00
parent 53a5bd2319
commit 2979e03148
7 changed files with 395 additions and 46 deletions

View File

@@ -0,0 +1,142 @@
from json import JSONDecodeError
from sanic import Sanic
from time import sleep as sync_sleep
import asyncio
from sanic.response import text
from sanic.config import Config
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
class ReuseableTCPConnector(TCPConnector):
def __init__(self, *args, **kwargs):
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
self.conn = None
@asyncio.coroutine
def connect(self, req):
if self.conn:
return self.conn
conn = yield from super(ReuseableTCPConnector, self).connect(req)
self.conn = conn
return conn
def close(self):
return super(ReuseableTCPConnector, self).close()
class ReuseableSanicTestClient(SanicTestClient):
def __init__(self, app):
super(ReuseableSanicTestClient, self).__init__(app)
self._tcp_connector = None
self._session = None
def _sanic_endpoint_test(
self, method='get', uri='/', gather_request=True,
debug=False, server_kwargs={},
*request_args, **request_kwargs):
results = [None, None]
exceptions = []
if gather_request:
def _collect_request(request):
if results[0] is None:
results[0] = request
self.app.request_middleware.appendleft(_collect_request)
@self.app.listener('after_server_start')
async def _collect_response(sanic, loop):
try:
response = await self._local_request(
method, uri, *request_args,
**request_kwargs)
results[-1] = response
except Exception as e:
log.error(
'Exception:\n{}'.format(traceback.format_exc()))
exceptions.append(e)
self.app.stop()
server = self.app.create_server(host=HOST, debug=debug, port=PORT, **server_kwargs)
self.app.listeners['after_server_start'].pop()
if exceptions:
raise ValueError(
"Exception during request: {}".format(exceptions))
if gather_request:
try:
request, response = results
return request, response
except:
raise ValueError(
"Request and response object expected, got ({})".format(
results))
else:
try:
return results[-1]
except:
raise ValueError(
"Request object expected, got ({})".format(results))
async def _local_request(self, method, uri, cookies=None, *args,
**kwargs):
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri)
if self._session:
session = self._session
else:
if self._tcp_connector:
conn = self._tcp_connector
else:
conn = ReuseableTCPConnector(verify_ssl=False)
self._tcp_connector = conn
session = aiohttp.ClientSession(cookies=cookies,
connector=conn)
self._session = session
async with getattr(session, method.lower())(
url, *args, **kwargs) as response:
try:
response.text = await response.text()
except UnicodeDecodeError:
response.text = None
try:
response.json = await response.json()
except (JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError):
response.json = None
response.body = await response.read()
return response
Config.KEEP_ALIVE_TIMEOUT = 30
Config.KEEP_ALIVE = True
keep_alive_timeout_app = Sanic('test_request_timeout')
@keep_alive_timeout_app.route('/1')
async def handler(request):
return text('OK')
def test_keep_alive_timeout():
client = ReuseableSanicTestClient(keep_alive_timeout_app)
headers = {
'Connection': 'keep-alive'
}
request, response = client.get('/1', headers=headers)
assert response.status == 200
#sync_sleep(2)
request, response = client.get('/1')
assert response.status == 200

View File

@@ -1,38 +1,97 @@
from json import JSONDecodeError
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.exceptions import RequestTimeout
from sanic.config import Config
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
class DelayableTCPConnector(TCPConnector):
class DelayableHttpRequest(object):
def __new__(cls, req, delay):
cls = super(DelayableTCPConnector.DelayableHttpRequest, cls).\
__new__(cls)
cls.req = req
cls.delay = delay
return cls
def __getattr__(self, item):
return getattr(self.req, item)
def send(self, *args, **kwargs):
if self.delay and self.delay > 0:
_ = yield from asyncio.sleep(self.delay)
self.req.send(*args, **kwargs)
def __init__(self, *args, **kwargs):
_post_connect_delay = kwargs.pop('post_connect_delay', 0)
_pre_request_delay = kwargs.pop('pre_request_delay', 0)
super(DelayableTCPConnector, self).__init__(*args, **kwargs)
self._post_connect_delay = _post_connect_delay
self._pre_request_delay = _pre_request_delay
@asyncio.coroutine
def connect(self, req):
req = DelayableTCPConnector.\
DelayableHttpRequest(req, self._pre_request_delay)
conn = yield from super(DelayableTCPConnector, self).connect(req)
if self._post_connect_delay and self._post_connect_delay > 0:
_ = yield from asyncio.sleep(self._post_connect_delay)
return conn
class DelayableSanicTestClient(SanicTestClient):
def __init__(self, app, request_delay=1):
super(DelayableSanicTestClient, self).__init__(app)
self._request_delay = request_delay
async def _local_request(self, method, uri, cookies=None, *args,
**kwargs):
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri)
conn = DelayableTCPConnector(pre_request_delay=self._request_delay,
verify_ssl=False)
async with aiohttp.ClientSession(
cookies=cookies, connector=conn) as session:
# Insert a delay after creating the connection
# But before sending the request.
async with getattr(session, method.lower())(
url, *args, **kwargs) as response:
try:
response.text = await response.text()
except UnicodeDecodeError:
response.text = None
try:
response.json = await response.json()
except (JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError):
response.json = None
response.body = await response.read()
return response
Config.REQUEST_TIMEOUT = 1
request_timeout_app = Sanic('test_request_timeout')
request_timeout_default_app = Sanic('test_request_timeout_default')
@request_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(2)
return text('OK')
@request_timeout_app.exception(RequestTimeout)
def handler_exception(request, exception):
return text('Request Timeout from error_handler.', 408)
def test_server_error_request_timeout():
request, response = request_timeout_app.test_client.get('/1')
assert response.status == 408
assert response.text == 'Request Timeout from error_handler.'
@request_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(2)
async def handler(request):
return text('OK')
def test_default_server_error_request_timeout():
request, response = request_timeout_default_app.test_client.get('/1')
client = DelayableSanicTestClient(request_timeout_default_app, 2)
request, response = client.get('/1')
assert response.status == 408
assert response.text == 'Error: Request Timeout'

View File

@@ -0,0 +1,38 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.exceptions import ServiceUnavailable
from sanic.config import Config
Config.RESPONSE_TIMEOUT = 1
response_timeout_app = Sanic('test_response_timeout')
response_timeout_default_app = Sanic('test_response_timeout_default')
@response_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(2)
return text('OK')
@response_timeout_app.exception(ServiceUnavailable)
def handler_exception(request, exception):
return text('Response Timeout from error_handler.', 503)
def test_server_error_response_timeout():
request, response = response_timeout_app.test_client.get('/1')
assert response.status == 503
assert response.text == 'Response Timeout from error_handler.'
@response_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(2)
return text('OK')
def test_default_server_error_response_timeout():
request, response = response_timeout_default_app.test_client.get('/1')
assert response.status == 503
assert response.text == 'Error: Response Timeout'