diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
index 28f7fa3..6735026 100644
--- a/.github/workflows/benchmarks.yml
+++ b/.github/workflows/benchmarks.yml
@@ -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:
diff --git a/Dockerfile b/Dockerfile
index b088bd8..f43d42d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/frameworks/aiohttp_nginx/app.py b/frameworks/aiohttp_nginx/app.py
new file mode 100644
index 0000000..24445da
--- /dev/null
+++ b/frameworks/aiohttp_nginx/app.py
@@ -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 = "HTML OK"
+ 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)
diff --git a/frameworks/aiohttp_nginx/requirements.txt b/frameworks/aiohttp_nginx/requirements.txt
new file mode 100644
index 0000000..8f9530d
--- /dev/null
+++ b/frameworks/aiohttp_nginx/requirements.txt
@@ -0,0 +1 @@
+aiohttp == 3.8.1
diff --git a/frameworks/aiohttp_nginx/start.sh b/frameworks/aiohttp_nginx/start.sh
new file mode 100755
index 0000000..a54cd4f
--- /dev/null
+++ b/frameworks/aiohttp_nginx/start.sh
@@ -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 < /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
diff --git a/frameworks/aiohttp_nginx/test_aiohttp.py b/frameworks/aiohttp_nginx/test_aiohttp.py
new file mode 100644
index 0000000..43fe6ef
--- /dev/null
+++ b/frameworks/aiohttp_nginx/test_aiohttp.py
@@ -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 == 'HTML OK'
+
+
+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