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

Improve performance by adding custom time source #1350

Merged
merged 3 commits into from
Oct 30, 2016
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
49 changes: 49 additions & 0 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import warnings
from collections import MutableSequence, namedtuple
from pathlib import Path
from time import gmtime, time
from urllib.parse import urlencode

from async_timeout import timeout
Expand Down Expand Up @@ -571,3 +572,51 @@ def insert(self, pos, item):
if self._frozen:
raise RuntimeError("Cannot modify frozen list.")
self._items.insert(pos, item)


class TimeService:
def __init__(self, loop):
self._loop = loop
self._time = time()
self._strtime = None
self._count = 0
self._cb = loop.call_later(1, self._on_cb)

def stop(self):
self._cb.cancel()
self._cb = None
self._loop = None

def _on_cb(self):
self._count += 1
if self._count >= 10*60:
# reset timer every 10 minutes
self._count = 0
self._time = time()
else:
self._time += 1
self._strtime = None
self._cb = self._loop.call_later(1, self._on_cb)

def _format_date_time(self):
# Weekday and month names for HTTP date/time formatting;
# always English!
# Tuples are contants stored in codeobject!
_weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
_monthname = (None, # Dummy so we can use 1-based month numbers
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")

year, month, day, hh, mm, ss, wd, y, z = gmtime(self._time)
return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
_weekdayname[wd], day, _monthname[month], year, hh, mm, ss
)

def time(self):
return self._time

def strtime(self):
s = self._strtime
if s is None:
self._strtime = s = self._format_date_time()
return self._strtime
6 changes: 6 additions & 0 deletions aiohttp/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,12 @@ def _add_default_headers(self):
self.headers.setdefault(hdrs.SERVER, self.SERVER_SOFTWARE)


class WebResponse(Response):
"""For usage in aiohttp.web only"""
def _add_default_headers(self):
pass


class Request(HttpMessage):

HOP_HEADERS = ()
Expand Down
5 changes: 5 additions & 0 deletions aiohttp/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,13 @@ def make_mocked_request(method, path, headers=None, *,
if payload is sentinel:
payload = mock.Mock()

time_service = mock.Mock()
time_service.time.return_value = 12345
time_service.strtime.return_value = "Tue, 15 Nov 1994 08:12:31 GMT"

req = Request(message, payload,
transport, reader, writer,
time_service,
secure_proxy_ssl_header=secure_proxy_ssl_header)

match_info = UrlMappingMatchInfo({}, mock.Mock())
Expand Down
27 changes: 17 additions & 10 deletions aiohttp/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from . import hdrs, web_exceptions, web_reqrep, web_urldispatcher, web_ws
from .abc import AbstractMatchInfo, AbstractRouter
from .helpers import FrozenList, sentinel
from .helpers import FrozenList, TimeService, sentinel
from .log import access_logger, web_logger
from .protocol import HttpVersion # noqa
from .server import ServerHttpProtocol
Expand All @@ -30,21 +30,27 @@

class RequestHandler(ServerHttpProtocol):

_meth = 'none'
_path = 'none'
_request = None

def __init__(self, manager, app, router, *,
def __init__(self, manager, app, router, time_service, *,
secure_proxy_ssl_header=None, **kwargs):
super().__init__(**kwargs)

self._manager = manager
self._app = app
self._router = router
self._secure_proxy_ssl_header = secure_proxy_ssl_header
self._time_service = time_service

def __repr__(self):
if self._request is None:
meth = 'none'
path = 'none'
else:
meth = self._request.method
path = self._request.rel_url.raw_path
return "<{} {}:{} {}>".format(
self.__class__.__name__, self._meth, self._path,
self.__class__.__name__, meth, path,
'connected' if self.transport is not None else 'disconnected')

def connection_made(self, transport):
Expand All @@ -67,9 +73,9 @@ def handle_request(self, message, payload):
request = web_reqrep.Request(
message, payload,
self.transport, self.reader, self.writer,
self._time_service,
secure_proxy_ssl_header=self._secure_proxy_ssl_header)
self._meth = request.method
self._path = request.path
self._request = request
try:
match_info = yield from self._router.resolve(request)
assert isinstance(match_info, AbstractMatchInfo), match_info
Expand Down Expand Up @@ -112,8 +118,7 @@ def handle_request(self, message, payload):
self.log_access(message, None, resp_msg, self._loop.time() - now)

# for repr
self._meth = 'none'
self._path = 'none'
self._request = None


class RequestHandlerFactory:
Expand All @@ -130,6 +135,7 @@ def __init__(self, app, router, *,
self._kwargs = kwargs
self._kwargs.setdefault('logger', app.logger)
self._requests_count = 0
self._time_service = TimeService(self._loop)

@property
def requests_count(self):
Expand All @@ -156,10 +162,11 @@ def finish_connections(self, timeout=None):
coros = [conn.shutdown(timeout) for conn in self._connections]
yield from asyncio.gather(*coros, loop=self._loop)
self._connections.clear()
self._time_service.stop()

def __call__(self):
return self._handler(
self, self._app, self._router, loop=self._loop,
self, self._app, self._router, self._time_service, loop=self._loop,
secure_proxy_ssl_header=self._secure_proxy_ssl_header,
**self._kwargs)

Expand Down
28 changes: 22 additions & 6 deletions aiohttp/web_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from . import hdrs, multipart
from .helpers import reify, sentinel
from .protocol import Response as ResponseImpl
from .protocol import WebResponse as ResponseImpl
from .protocol import HttpVersion10, HttpVersion11
from .streams import EOF_MARKER

Expand Down Expand Up @@ -91,7 +91,8 @@ class Request(collections.MutableMapping, HeadersMixin):
POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT,
hdrs.METH_TRACE, hdrs.METH_DELETE}

def __init__(self, message, payload, transport, reader, writer, *,
def __init__(self, message, payload, transport, reader, writer,
time_service, *,
secure_proxy_ssl_header=None):
self._app = None
self._message = message
Expand All @@ -111,6 +112,7 @@ def __init__(self, message, payload, transport, reader, writer, *,
self._has_body = not payload.at_eof()

self._secure_proxy_ssl_header = secure_proxy_ssl_header
self._time_service = time_service
self._state = {}
self._cache = {}

Expand Down Expand Up @@ -751,16 +753,18 @@ def _start(self, request):
if keep_alive is None:
keep_alive = request.keep_alive
self._keep_alive = keep_alive
version = request.version

resp_impl = self._resp_impl = ResponseImpl(
request._writer,
self._status,
request.version,
version,
not keep_alive,
self._reason)

self._copy_cookies()

headers = self.headers
if self._compression:
self._start_compression(request)

Expand All @@ -772,10 +776,22 @@ def _start(self, request):
resp_impl.enable_chunked_encoding()
if self._chunk_size:
resp_impl.add_chunking_filter(self._chunk_size)
headers[hdrs.TRANSFER_ENCODING] = 'chunked'
else:
resp_impl.length = self.content_length

if hdrs.DATE not in headers:
headers[hdrs.DATE] = request._time_service.strtime()
headers.setdefault(hdrs.SERVER, resp_impl.SERVER_SOFTWARE)
if hdrs.CONNECTION not in headers:
if keep_alive:
if version == HttpVersion10:
headers[hdrs.CONNECTION] = 'keep-alive'
else:
if version == HttpVersion11:
headers[hdrs.CONNECTION] = 'close'

headers = self.headers.items()
for key, val in headers:
resp_impl.add_header(key, val)
resp_impl.headers = headers

self._send_headers(resp_impl)
return resp_impl
Expand Down
100 changes: 67 additions & 33 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,49 +247,46 @@ def test_logger_no_transport():
mock_logger.info.assert_called_with("-", extra={'remote_address': '-'})


# ----------------------------------------------------------
class TestReify:

def test_reify(self):
class A:
def __init__(self):
self._cache = {}

def test_reify():
class A:
def __init__(self):
self._cache = {}
@helpers.reify
def prop(self):
return 1

@helpers.reify
def prop(self):
return 1
a = A()
assert 1 == a.prop

a = A()
assert 1 == a.prop
def test_reify_class(self):
class A:
def __init__(self):
self._cache = {}

@helpers.reify
def prop(self):
"""Docstring."""
return 1

def test_reify_class():
class A:
def __init__(self):
self._cache = {}
assert isinstance(A.prop, helpers.reify)
assert 'Docstring.' == A.prop.__doc__

@helpers.reify
def prop(self):
"""Docstring."""
return 1
def test_reify_assignment(self):
class A:
def __init__(self):
self._cache = {}

assert isinstance(A.prop, helpers.reify)
assert 'Docstring.' == A.prop.__doc__
@helpers.reify
def prop(self):
return 1

a = A()

def test_reify_assignment():
class A:
def __init__(self):
self._cache = {}

@helpers.reify
def prop(self):
return 1

a = A()

with pytest.raises(AttributeError):
a.prop = 123
with pytest.raises(AttributeError):
a.prop = 123


def test_create_future_with_new_loop():
Expand Down Expand Up @@ -388,3 +385,40 @@ def test_is_ip_address_invalid_type():

with pytest.raises(TypeError):
helpers.is_ip_address(object())


@pytest.fixture
def time_service(loop):
return helpers.TimeService(loop)


class TestTimeService:
def test_ctor(self, time_service):
assert time_service._cb is not None
assert time_service._time is not None
assert time_service._strtime is None
assert time_service._count == 0

def test_stop(self, time_service):
time_service.stop()
assert time_service._cb is None
assert time_service._loop is None

def test_time(self, time_service):
t = time_service._time
assert t == time_service.time()

def test_strtime(self, time_service):
time_service._time = 1477797232
assert time_service.strtime() == 'Sun, 30 Oct 2016 03:13:52 GMT'
# second call should use cached value
assert time_service.strtime() == 'Sun, 30 Oct 2016 03:13:52 GMT'

def test_recalc_time(self, time_service):
time_service._time = 123
time_service._strtime = 'asd'
time_service._count = 1000000
time_service._on_cb()
assert time_service._strtime is None
assert time_service._count == 0
assert time_service._time > 1234
16 changes: 16 additions & 0 deletions tests/test_web_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -1170,3 +1170,19 @@ def on_signal(app):
yield from server.close()

assert [app, subapp1, subapp2] == order


@asyncio.coroutine
def test_custom_date_header(loop, test_client):

@asyncio.coroutine
def handler(request):
return web.Response(headers={'Date': 'Sun, 30 Oct 2016 03:13:52 GMT'})

app = web.Application(loop=loop)
app.router.add_get('/', handler)
client = yield from test_client(app)

resp = yield from client.get('/')
assert 200 == resp.status
assert resp.headers['Date'] == 'Sun, 30 Oct 2016 03:13:52 GMT'
Loading