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

Implement web.run_app utility function #734

Merged
merged 20 commits into from
Jan 13, 2016
Merged
Show file tree
Hide file tree
Changes from 19 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
35 changes: 35 additions & 0 deletions aiohttp/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,38 @@ def __call__(self):

def __repr__(self):
return "<Application>"


def run_app(app, *, host='0.0.0.0', port=None, loop=None,
shutdown_timeout=60.0, ssl_context=None):
"""Run an app locally"""
if port is None:
if not ssl_context:
port = 8080
else:
port = 8443

if loop is None:
Copy link
Contributor

Choose a reason for hiding this comment

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

Is an explicit loop parameter necessary -- Wouldn't loop = app.loop suffice here?
Is there a use case where the App would run on a different loop than the Server?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point!

loop = asyncio.get_event_loop()

handler = app.make_handler()
srv = loop.run_until_complete(loop.create_server(handler, host, port,
ssl=ssl_context))

scheme = 'https' if ssl_context else 'http'
prompt = '127.0.0.1' if host == '0.0.0.0' else host
Copy link
Member

Choose a reason for hiding this comment

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

How should someone bind to public interface?

Copy link
Member

Choose a reason for hiding this comment

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

sorry, read from end to top))

Copy link
Member

Choose a reason for hiding this comment

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

Use :: for IPv6 (:

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I don't follow.
Isn't knowledge of host/port info enough?

Copy link
Member

Choose a reason for hiding this comment

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

As I already said -- I read diff from bottom to top) thought that you replaced '0.0.0.0' with localhost and bind to it)

print("======== Running on {scheme}://{prompt}:{port}/ ========\n"
"(Press CTRL+C to quit)".format(
scheme=scheme, prompt=prompt, port=port))

try:
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
srv.close()
loop.run_until_complete(srv.wait_closed())
loop.run_until_complete(app.shutdown())
loop.run_until_complete(handler.finish_connections(shutdown_timeout))
loop.run_until_complete(app.finish())
loop.close()
41 changes: 22 additions & 19 deletions docs/web.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,16 @@ particular *HTTP method* and *path*::
app = web.Application()
app.router.add_route('GET', '/', hello)

After that, create a server and run the *asyncio loop* as usual::
After that, run the application by :func:`run_app` call::

loop = asyncio.get_event_loop()
handler = app.make_handler()
f = loop.create_server(handler, '0.0.0.0', 8080)
srv = loop.run_until_complete(f)
print('serving on', srv.sockets[0].getsockname())
try:
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
srv.close()
loop.run_until_complete(srv.wait_closed())
loop.run_until_complete(app.on_shutdown.send())
loop.run_until_complete(handler.finish_connections(1.0))
loop.run_until_complete(app.finish())
loop.close()
run_app(app)

That's it. Now, head over to ``http://localhost:8080/`` to see the results.

.. seealso:: :ref:`aiohttp-web-graceful-shutdown` section
explains what :func:`run_app` does and how implement
complex server initialization/finalization from scratch.


.. _aiohttp-web-handler:

Expand Down Expand Up @@ -834,9 +823,22 @@ Signal handler may looks like:

app.on_shutdown.append(on_shutdown)

Proper finalization procedure has three steps:

Server finalizer should raise shutdown signal by
:meth:`Application.shutdown` call::
1. Stop accepting new client connections by
:meth:`asyncio.Server.close` and
:meth:`asyncio.Server.wait_closed` calls.

2. Fire :meth:`Application.shutdown` event.

3. Close accepted connections from clients by
:meth:`RequestHandlerFactory.finish_connections` call with
reasonable small delay.

4. Call registered application finalizers by :meth:`Application.finish`.

The following code snippet performs proper application start, run and
finalizing. It's pretty close to :func:`run_app` utility function::

loop = asyncio.get_event_loop()
handler = app.make_handler()
Expand All @@ -857,6 +859,7 @@ Server finalizer should raise shutdown signal by




CORS support
------------

Expand Down
39 changes: 39 additions & 0 deletions docs/web_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1511,6 +1511,45 @@ Utilities
.. seealso:: :ref:`aiohttp-web-file-upload`


.. function:: run_app(app, *, host='0.0.0.0', port=None, loop=None, \
shutdown_timeout=60.0, ssl_context=None)

An utility function for running an application, serving it until
keyboard interrupt and performing a
:ref:`aiohttp-web-graceful-shutdown`.

Suitable as handy tool for scaffolding aiohttp based projects.
Perhaps production config will use more sophisticated runner but it
good enough at least at very beginning stage.

:param app: :class:`Application` instance to run

:param str host: host for HTTP server, ``'0.0.0.0'`` by default

:param int port: port for HTTP server. By default is ``8080`` for
plain text HTTP and ``8443`` for HTTP via SSL
(when *ssl_context* parameter is specified).

:param int shutdown_timeout: a delay to wait for graceful server
shutdown before disconnecting all
open client sockets hard way.

A system with properly
:ref:`aiohttp-web-graceful-shutdown`
implemented never waits for this
timeout but closes a server in a few
milliseconds.

:param ssl_context: :class:`ssl.SSLContext` for HTTPS server,
``None`` for HTTP connection.

:param loop: :ref:`event loop<asyncio-event-loop>` used
for processing HTTP requests.

If param is ``None`` :func:`asyncio.get_event_loop`
used for getting default event loop.


Constants
---------

Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def loop(request):
yield loop

if not loop._closed:
loop.stop()
loop.call_soon(loop.stop)
loop.run_forever()
loop.close()
gc.collect()
Expand Down
85 changes: 85 additions & 0 deletions tests/test_run_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import asyncio
import pytest
import ssl

from unittest import mock
from aiohttp import web


def test_run_app_http(loop):
loop = mock.Mock(spec=asyncio.AbstractEventLoop, wrap=loop)
loop.call_later(0.01, loop.stop)
app = mock.Mock(wrap=web.Application(loop=loop))

web.run_app(app, loop=loop)

app.make_handler.assert_called_with()

loop.close.assert_called_with()
app.finish.assert_called_with()
app.shutdown.assert_called_with()

loop.create_server.assert_called_with(mock.ANY, '0.0.0.0', 8080, ssl=None)


def test_run_app_https(loop):
loop = mock.Mock(spec=asyncio.AbstractEventLoop, wrap=loop)
loop.call_later(0.01, loop.stop)

app = mock.Mock(wrap=web.Application(loop=loop))

ssl_context = ssl.create_default_context()

web.run_app(app, ssl_context=ssl_context, loop=loop)

app.make_handler.assert_called_with()

loop.close.assert_called_with()
app.finish.assert_called_with()
app.shutdown.assert_called_with()

loop.create_server.assert_called_with(mock.ANY, '0.0.0.0', 8443,
ssl=ssl_context)


def test_run_app_nondefault_host_port(loop, unused_port):
port = unused_port()
host = 'localhost'

loop = mock.Mock(spec=asyncio.AbstractEventLoop, wrap=loop)
loop.call_later(0.01, loop.stop)

app = mock.Mock(wrap=web.Application(loop=loop))

web.run_app(app, host=host, port=port, loop=loop)

loop.create_server.assert_called_with(mock.ANY, host, port, ssl=None)


def test_run_app_default_eventloop(loop):
# don't perform any assert checks just make sure the run_app was successful
# mocking a default loop produces a failure on Python 3.4
# with PYTHONASYNCIODEBUG enabled

asyncio.set_event_loop(loop)
loop.call_later(0.01, loop.stop)

web.run_app(web.Application())


def test_run_app_exit_with_exception(loop):
loop = mock.Mock(spec=asyncio.AbstractEventLoop, wrap=loop)

loop.run_forever.return_value = None # to disable wrapping
loop.run_forever.side_effect = exc = RuntimeError()

app = mock.Mock(wrap=web.Application(loop=loop))

with pytest.raises(RuntimeError) as ctx:
web.run_app(app, loop=loop)

assert ctx.value is exc

assert not loop.close.called
app.finish.assert_called_with()
app.shutdown.assert_called_with()