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

ASGI refactoring attempt #1475

Merged
merged 19 commits into from
Jun 20, 2019
Merged
Show file tree
Hide file tree
Changes from 16 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
63 changes: 52 additions & 11 deletions docs/sanic/deploying.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Deploying

Deploying Sanic is made simple by the inbuilt webserver. After defining an
instance of `sanic.Sanic`, we can call the `run` method with the following
Deploying Sanic is very simple using one of three options: the inbuilt webserver,
an [ASGI webserver](https://asgi.readthedocs.io/en/latest/implementations.html), or `gunicorn`.
It is also very common to place Sanic behind a reverse proxy, like `nginx`.

## Running via Sanic webserver

After defining an instance of `sanic.Sanic`, we can call the `run` method with the following
keyword arguments:

- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
Expand All @@ -17,7 +22,13 @@ keyword arguments:
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
- `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server).

## Workers
```python
app.run(host='0.0.0.0', port=1337, access_log=False)
```

In the above example, we decided to turn off the access log in order to increase performance.

### Workers

By default, Sanic listens in the main process using only one CPU core. To crank
up the juice, just specify the number of workers in the `run` arguments.
Expand All @@ -29,9 +40,9 @@ app.run(host='0.0.0.0', port=1337, workers=4)
Sanic will automatically spin up multiple processes and route traffic between
them. We recommend as many workers as you have available cores.

## Running via command
### Running via command

If you like using command line arguments, you can launch a Sanic server by
If you like using command line arguments, you can launch a Sanic webserver by
executing the module. For example, if you initialized Sanic as `app` in a file
named `server.py`, you could run the server like so:

Expand All @@ -46,6 +57,33 @@ if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
```

## Running via ASGI

Sanic is also ASGI-compliant. This means you can use your preferred ASGI webserver
to run Sanic. The three main implementations of ASGI are
[Daphne](http://github.com/django/daphne), [Uvicorn](https://www.uvicorn.org/),
and [Hypercorn](https://pgjones.gitlab.io/hypercorn/index.html).

Follow their documentation for the proper way to run them, but it should look
something like:

```
daphne myapp:app
uvicorn myapp:app
hypercorn myapp:app
```

A couple things to note when using ASGI:

1. When using the Sanic webserver, websockets will run using the [`websockets`](https://websockets.readthedocs.io/) package. In ASGI mode, there is no need for this package since websockets are managed in the ASGI server.
1. The ASGI [lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) supports
only two server events: startup and shutdown. Sanic has four: before startup, after startup,
before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and shutdown events will
run consecutively and not actually around the server process beginning and ending (since that
is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and
`before_server_stop`.
1. ASGI mode is still in "beta" as of Sanic v19.6.

## Running via Gunicorn

[Gunicorn](http://gunicorn.org/) ‘Green Unicorn’ is a WSGI HTTP Server for UNIX.
Expand All @@ -64,7 +102,9 @@ of the memory leak.

See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.

## Running behind a reverse proxy
## Other deployment considerations

### Running behind a reverse proxy

Sanic can be used with a reverse proxy (e.g. nginx). There's a simple example of nginx configuration:

Expand All @@ -84,7 +124,7 @@ server {

If you want to get real client ip, you should configure `X-Real-IP` and `X-Forwarded-For` HTTP headers and set `app.config.PROXIES_COUNT` to `1`; see the configuration page for more information.

## Disable debug logging
### Disable debug logging for performance

To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.

Expand All @@ -104,9 +144,10 @@ Or you can rewrite app config directly
app.config.ACCESS_LOG = False
```

## Asynchronous support
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
However be advised that this method does not support using multiple processes, and is not the preferred way
### Asynchronous support and sharing the loop

This is suitable if you *need* to share the Sanic process with other applications, in particular the `loop`.
However, be advised that this method does not support using multiple processes, and is not the preferred way
to run the app in general.

Here is an incomplete example (please see `run_async.py` in examples for something more practical):
Expand All @@ -116,4 +157,4 @@ server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
loop.run_forever()
```
```
88 changes: 88 additions & 0 deletions examples/run_asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""
1. Create a simple Sanic app
0. Run with an ASGI server:
$ uvicorn run_asgi:app
or
$ hypercorn run_asgi:app
"""

from pathlib import Path
from sanic import Sanic, response


app = Sanic(__name__)


@app.route("/text")
def handler_text(request):
return response.text("Hello")


@app.route("/json")
def handler_json(request):
return response.json({"foo": "bar"})


@app.websocket("/ws")
async def handler_ws(request, ws):
name = "<someone>"
while True:
data = f"Hello {name}"
await ws.send(data)
name = await ws.recv()

if not name:
break


@app.route("/file")
async def handler_file(request):
return await response.file(Path("../") / "setup.py")


@app.route("/file_stream")
async def handler_file_stream(request):
return await response.file_stream(
Path("../") / "setup.py", chunk_size=1024
)


@app.route("/stream", stream=True)
async def handler_stream(request):
while True:
body = await request.stream.read()
if body is None:
break
body = body.decode("utf-8").replace("1", "A")
# await response.write(body)
return response.stream(body)


@app.listener("before_server_start")
async def listener_before_server_start(*args, **kwargs):
print("before_server_start")


@app.listener("after_server_start")
async def listener_after_server_start(*args, **kwargs):
print("after_server_start")


@app.listener("before_server_stop")
async def listener_before_server_stop(*args, **kwargs):
print("before_server_stop")


@app.listener("after_server_stop")
async def listener_after_server_stop(*args, **kwargs):
print("after_server_stop")


@app.middleware("request")
async def print_on_request(request):
print("print_on_request")


@app.middleware("response")
async def print_on_response(request, response):
print("print_on_response")
62 changes: 47 additions & 15 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from urllib.parse import urlencode, urlunparse

from sanic import reloader_helpers
from sanic.asgi import ASGIApp
from sanic.blueprint_group import BlueprintGroup
from sanic.config import BASE_LOGO, Config
from sanic.constants import HTTP_METHODS
Expand All @@ -25,7 +26,7 @@
from sanic.router import Router
from sanic.server import HttpProtocol, Signal, serve, serve_multiple
from sanic.static import register as static_register
from sanic.testing import SanicTestClient
from sanic.testing import SanicASGITestClient, SanicTestClient
from sanic.views import CompositionView
from sanic.websocket import ConnectionClosed, WebSocketProtocol

Expand Down Expand Up @@ -53,6 +54,7 @@ def __init__(
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)

self.name = name
self.asgi = False
self.router = router or Router()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
Expand Down Expand Up @@ -80,7 +82,7 @@ def loop(self):

Only supported when using the `app.run` method.
"""
if not self.is_running:
if not self.is_running and self.asgi is False:
raise SanicException(
"Loop can only be retrieved after the app has started "
"running. Not supported with `create_server` function"
Expand Down Expand Up @@ -469,13 +471,23 @@ async def websocket_handler(request, *args, **kwargs):
getattr(handler, "__blueprintname__", "")
+ handler.__name__
)
try:
protocol = request.transport.get_protocol()
except AttributeError:
# On Python3.5 the Transport classes in asyncio do not
# have a get_protocol() method as in uvloop
protocol = request.transport._protocol
ws = await protocol.websocket_handshake(request, subprotocols)

pass

if self.asgi:
ws = request.transport.get_websocket_connection()
else:
try:
protocol = request.transport.get_protocol()
except AttributeError:
# On Python3.5 the Transport classes in asyncio do not
# have a get_protocol() method as in uvloop
protocol = request.transport._protocol
protocol.app = self

ws = await protocol.websocket_handshake(
request, subprotocols
)

# schedule the application handler
# its future is kept in self.websocket_tasks in case it
Expand Down Expand Up @@ -983,8 +995,16 @@ async def handle_request(self, request, write_callback, stream_callback):
raise CancelledError()

# pass the response to the correct callback
if isinstance(response, StreamingHTTPResponse):
await stream_callback(response)
if write_callback is None or isinstance(
response, StreamingHTTPResponse
):
if stream_callback:
await stream_callback(response)
else:
# Should only end here IF it is an ASGI websocket.
# TODO:
# - Add exception handling
pass
else:
write_callback(response)

Expand All @@ -996,6 +1016,10 @@ async def handle_request(self, request, write_callback, stream_callback):
def test_client(self):
return SanicTestClient(self)

@property
def asgi_client(self):
return SanicASGITestClient(self)

# -------------------------------------------------------------------- #
# Execution
# -------------------------------------------------------------------- #
Expand Down Expand Up @@ -1122,10 +1146,6 @@ def stop(self):
"""This kills the Sanic"""
get_event_loop().stop()

def __call__(self):
"""gunicorn compatibility"""
return self

async def create_server(
self,
host: Optional[str] = None,
Expand Down Expand Up @@ -1367,3 +1387,15 @@ def _helper(
def _build_endpoint_name(self, *parts):
parts = [self.name, *parts]
return ".".join(parts)

# -------------------------------------------------------------------- #
# ASGI
# -------------------------------------------------------------------- #

async def __call__(self, scope, receive, send):
"""To be ASGI compliant, our instance must be a callable that accepts
three arguments: scope, receive, send. See the ASGI reference for more
details: https://asgi.readthedocs.io/en/latest/"""
self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send)
await asgi_app()
Loading