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

Aiohttp with nginx #203

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ jobs:
name: aiohttp
network: data

- name: Benchmark AIOHTTP with Nginx
uses: ./.github/actions/benchmark
with:
name: aiohttp_nginx
network: data

- name: Benchmark Blacksheep
uses: ./.github/actions/benchmark
with:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM python:3.9-slim

RUN apt-get update && \
apt-get -y install --no-install-recommends build-essential
apt-get -y install --no-install-recommends build-essential nginx

ENV PIP_DISABLE_PIP_VERSION_CHECK=1

Expand Down
82 changes: 82 additions & 0 deletions frameworks/aiohttp_nginx/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import os
import argparse
import time
from uuid import uuid4

from aiohttp.web import (
RouteTableDef,
Application,
Response,
json_response,
HTTPBadRequest,
HTTPUnauthorized,
run_app
)

routes = RouteTableDef()


# first add ten more routes to load routing system
# ------------------------------------------------
async def req_ok(request):
return Response(text='ok')

for n in range(5):
routes.get(f"/route-{n}")(req_ok)
routes.get(f"/route-dyn-{n}/{{part}}")(req_ok)


# then prepare endpoints for the benchmark
# ----------------------------------------
@routes.get('/html')
async def html(request):
"""Return HTML content and a custom header."""
content = "<b>HTML OK</b>"
headers = {'x-time': f"{time.time()}"}
return Response(text=content, content_type="text/html", headers=headers)


@routes.post('/upload')
async def upload(request):
"""Load multipart data and store it as a file."""
if not request.headers['content-type'].startswith('multipart/form-data'):
raise HTTPBadRequest()

reader = await request.multipart()
data = await reader.next()
if data.name != 'file':
raise HTTPBadRequest()

with open(f"/tmp/{uuid4().hex}", 'wb') as target:
target.write(await data.read())

return Response(text=target.name, content_type="text/plain")


@routes.put(r'/api/users/{user:\d+}/records/{record:\d+}')
async def api(request):
"""Check headers for authorization, load JSON/query data and return as JSON."""
if not request.headers.get('authorization'):
raise HTTPUnauthorized()

return json_response({
'params': {
'user': int(request.match_info['user']),
'record': int(request.match_info['record']),
},
'query': dict(request.query),
'data': await request.json(),
})


app = Application()
app.add_routes(routes)


if __name__ == '__main__':
os.setuid(65534)
parser = argparse.ArgumentParser(description="aiohttp server example")
parser.add_argument('--path')
parser.add_argument('--port')
args = parser.parse_args()
run_app(app, path=args.path, port=args.port)
1 change: 1 addition & 0 deletions frameworks/aiohttp_nginx/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aiohttp == 3.8.1
54 changes: 54 additions & 0 deletions frameworks/aiohttp_nginx/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/sh

python app.py --path=/tmp/example_1.sock &
python app.py --path=/tmp/example_2.sock &
python app.py --path=/tmp/example_3.sock &
python app.py --path=/tmp/example_4.sock &

sleep 2

cat <<EOF > /tmp/nginx.conf
error_log /dev/stdout info;

events {
use epoll;
worker_connections 128;
}

http {
access_log /dev/stdout;
server {
listen 8080;
client_max_body_size 4G;
server_name localhost;

location / {
proxy_redirect off;
proxy_buffering off;
proxy_pass http://aiohttp;
}
}

upstream aiohttp {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response

# Unix domain servers
server unix:/tmp/example_1.sock fail_timeout=0;
server unix:/tmp/example_2.sock fail_timeout=0;
server unix:/tmp/example_3.sock fail_timeout=0;
server unix:/tmp/example_4.sock fail_timeout=0;

# Unix domain sockets are used in this example due to their high performance,
# but TCP/IP sockets could be used instead:
# server 127.0.0.1:8081 fail_timeout=0;
# server 127.0.0.1:8082 fail_timeout=0;
# server 127.0.0.1:8083 fail_timeout=0;
# server 127.0.0.1:8084 fail_timeout=0;
}
}
EOF



/usr/sbin/nginx -g 'daemon off;' -c /tmp/nginx.conf
78 changes: 78 additions & 0 deletions frameworks/aiohttp_nginx/test_aiohttp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import pathlib
import random
from importlib import import_module

import pytest
from aiohttp.test_utils import TestClient, TestServer


@pytest.fixture(scope='module')
def app():
return import_module('.aiohttp.app', package='frameworks').app


@pytest.fixture(scope='module')
async def aiohttp_client(app):
server = TestServer(app)
client = TestClient(server)
await client.start_server()
yield client
await client.close()


async def test_html(aiohttp_client, ts):
res = await aiohttp_client.get('/html')
assert res.status == 200
assert res.headers.get('content-type').startswith('text/html')
header = res.headers.get('x-time')
assert header
assert float(header) >= ts
text = await res.text()
assert text == '<b>HTML OK</b>'


async def test_upload(aiohttp_client):
res = await aiohttp_client.get("/upload")
assert res.status == 405

res = await aiohttp_client.post("/upload")
assert res.status == 400

res = await aiohttp_client.post("/upload", data={'file': open(__file__)})
assert res.status == 200
assert res.content_type.startswith('text/plain')
text = await res.text()
assert pathlib.Path(text).read_text('utf-8').startswith('import pathlib')


async def test_api(aiohttp_client):
rand = random.randint(10, 99)

url = f"/api/users/{rand}/records/{rand}?query=test"
res = await aiohttp_client.get(url)
assert res.status == 405

res = await aiohttp_client.put(url, json={'foo': 'bar'})
assert res.status == 401

res = await aiohttp_client.put(url, headers={'authorization': '1'}, json={'foo': 'bar'})
assert res.status == 200
assert res.headers.get('content-type') == 'application/json; charset=utf-8'
json = await res.json()
assert json['data']
assert json['data'] == {'foo': 'bar'}

assert json['query']
assert json['query']['query'] == 'test'

assert json['params'] == {'user': rand, 'record': rand}


async def test_routing(aiohttp_client):
rand = random.randint(10, 99)
for n in range(5):
res = await aiohttp_client.get(f"/route-{n}")
assert res.status == 200

res = await aiohttp_client.get(f"/route-dyn-{n}/{rand}")
assert res.status == 200