فهرست منبع

Python aiohttp (#2529)

* starting work on python/aiohttp

* adding more test endpoints

* completing default tests

* raw SQL endpoints

* improve performance of raw updates for aiohttp

* use prepared statements for repeated queries

* setting port to 8080 to match others

* adding aiohttp to travis

* fix response with 'foo' queries and escaping
Samuel Colvin 8 سال پیش
والد
کامیت
95a16408f3

+ 1 - 0
.travis.yml

@@ -146,6 +146,7 @@ env:
     - "TESTDIR=PHP/zend"
     - "TESTDIR=PHP/zend1"
     - "TESTDIR=PHP/phreeze"
+    - "TESTDIR=Python/aiohttp"
     - "TESTDIR=Python/api_hour"
     - "TESTDIR=Python/bottle"
     - "TESTDIR=Python/cherrypy"

+ 76 - 0
frameworks/Python/aiohttp/README.md

@@ -0,0 +1,76 @@
+# [aiohttp](http://aiohttp.readthedocs.io/) Benchmark Test
+
+The information below is specific to aiohttp. For further guidance, 
+review the [documentation](http://frameworkbenchmarks.readthedocs.org/en/latest/). 
+Also note that there is additional information that's provided in 
+the [Python README](../).
+
+This is the Python aiohttp portion of a [benchmarking tests suite](../../) 
+comparing a variety of frameworks.
+
+All test implementations are located within ([./app](app)).
+
+## Description
+
+aiohttp with [aiopg + sqlalchemy](http://aiopg.readthedocs.io/en/stable/sa.html) and 
+separately [asyncpg](https://magicstack.github.io/asyncpg/current/) for database access.
+ 
+[uvloop](https://github.com/MagicStack/uvloop) is used for a more performant event loop.
+
+### Database
+
+PostgreSQL
+
+### Server
+
+gunicorn+uvloop on CPython
+
+## Test URLs
+
+### Test 1: JSON Encoding 
+
+    http://localhost:8080/json
+
+### Test 2: Single Row Query
+
+With ORM:
+
+    http://localhost:8080/db
+
+Without ORM (raw):
+
+    http://localhost:8080/raw/db
+
+### Test 3: Multi Row Query 
+
+With ORM:
+
+    http://localhost:8080/queries?queries=20
+
+Without ORM (raw):
+
+    http://localhost:8080/raw/queries?queries=20
+
+### Test 4: Fortunes (Template rendering)
+
+With ORM:
+
+    http://localhost:8080/fortunes
+
+Without ORM (raw):
+
+    http://localhost:8080/raw/fortunes
+
+### Test 5: Update Query
+
+With ORM:
+
+    http://localhost:8080/updates?queries=20
+
+Without ORM (raw):
+
+    http://localhost:8080/raw/updates?queries=20
+
+### Test6: Plaintext
+
+    http://localhost:8080/plaintext

+ 0 - 0
frameworks/Python/aiohttp/app/__init__.py


+ 6 - 0
frameworks/Python/aiohttp/app/gunicorn.py

@@ -0,0 +1,6 @@
+import asyncio
+
+from .main import create_app
+
+loop = asyncio.get_event_loop()
+app = create_app(loop)

+ 80 - 0
frameworks/Python/aiohttp/app/main.py

@@ -0,0 +1,80 @@
+import os
+from pathlib import Path
+
+import aiohttp_jinja2
+import aiopg.sa
+import asyncpg
+import jinja2
+from aiohttp import web
+from sqlalchemy.engine.url import URL
+
+from .views import (
+    json,
+    single_database_query_orm,
+    multiple_database_queries_orm,
+    fortunes,
+    updates,
+    plaintext,
+
+    single_database_query_raw,
+    multiple_database_queries_raw,
+    fortunes_raw,
+    updates_raw,
+)
+
+THIS_DIR = Path(__file__).parent
+
+
+def pg_dsn() -> str:
+    """
+    :return: DSN url suitable for sqlalchemy and aiopg.
+    """
+    return str(URL(
+        database='hello_world',
+        password=os.getenv('PGPASS', 'benchmarkdbpass'),
+        host=os.getenv('DBHOST', 'localhost'),
+        port='5432',
+        username=os.getenv('PGUSER', 'benchmarkdbuser'),
+        drivername='postgres',
+    ))
+
+
+async def startup(app: web.Application):
+    dsn = pg_dsn()
+    app.update(
+        aiopg_engine=await aiopg.sa.create_engine(dsn=dsn, minsize=10, maxsize=20, loop=app.loop),
+        asyncpg_pool=await asyncpg.create_pool(dsn=dsn, min_size=10, max_size=20, loop=app.loop),
+    )
+
+
+async def cleanup(app: web.Application):
+    app['aiopg_engine'].close()
+    await app['aiopg_engine'].wait_closed()
+    await app['asyncpg_pool'].close()
+
+
+def setup_routes(app):
+    app.router.add_get('/json', json)
+    app.router.add_get('/db', single_database_query_orm)
+    app.router.add_get('/queries', multiple_database_queries_orm)
+    app.router.add_get('/fortunes', fortunes)
+    app.router.add_get('/updates', updates)
+    app.router.add_get('/plaintext', plaintext)
+
+    app.router.add_get('/raw/db', single_database_query_raw)
+    app.router.add_get('/raw/queries', multiple_database_queries_raw)
+    app.router.add_get('/raw/fortunes', fortunes_raw)
+    app.router.add_get('/raw/updates', updates_raw)
+
+
+def create_app(loop):
+    app = web.Application(loop=loop)
+
+    jinja2_loader = jinja2.FileSystemLoader(str(THIS_DIR / 'templates'))
+    aiohttp_jinja2.setup(app, loader=jinja2_loader)
+
+    app.on_startup.append(startup)
+    app.on_cleanup.append(cleanup)
+
+    setup_routes(app)
+    return app

+ 20 - 0
frameworks/Python/aiohttp/app/models.py

@@ -0,0 +1,20 @@
+from sqlalchemy import Column, Integer, String
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class World(Base):
+    __tablename__ = 'world'
+    id = Column(Integer, primary_key=True)
+    randomnumber = Column(Integer)
+
+sa_worlds = World.__table__
+
+
+class Fortune(Base):
+    __tablename__ = 'fortune'
+    id = Column(Integer, primary_key=True)
+    message = Column(String)
+
+sa_fortunes = Fortune.__table__

+ 20 - 0
frameworks/Python/aiohttp/app/templates/fortune.jinja

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Fortunes</title>
+</head>
+<body>
+<table>
+<tr>
+<th>id</th>
+<th>message</th>
+</tr>
+{% for fortune in fortunes %}
+<tr>
+<td>{{ fortune.id }}</td>
+<td>{{ fortune.message|escape }}</td>
+</tr>
+{% endfor %}
+</table>
+</body>
+</html>

+ 179 - 0
frameworks/Python/aiohttp/app/views.py

@@ -0,0 +1,179 @@
+from operator import attrgetter, itemgetter
+from random import randint
+
+from aiohttp_jinja2 import template
+from aiohttp.web import Response
+import ujson
+
+from sqlalchemy import select
+
+from .models import sa_fortunes, sa_worlds, Fortune
+
+
+def json_response(data):
+    body = ujson.dumps(data)
+    return Response(body=body.encode(), content_type='application/json')
+
+
+def get_num_queries(request):
+    try:
+        num_queries = int(request.GET.get('queries', 1))
+    except ValueError:
+        return 1
+    if num_queries < 1:
+        return 1
+    if num_queries > 500:
+        return 500
+    return num_queries
+
+
+async def json(request):
+    """
+    Test 1
+    """
+    return json_response({'message': 'Hello, World!'})
+
+
+async def single_database_query_orm(request):
+    """
+    Test 2 ORM
+    """
+    id_ = randint(1, 10000)
+    async with request.app['aiopg_engine'].acquire() as conn:
+        cur = await conn.execute(select([sa_worlds.c.randomnumber]).where(sa_worlds.c.id == id_))
+        r = await cur.first()
+    return json_response({'id': id_, 'randomNumber': r[0]})
+
+
+async def single_database_query_raw(request):
+    """
+    Test 2 RAW
+    """
+    id_ = randint(1, 10000)
+
+    async with request.app['asyncpg_pool'].acquire() as conn:
+        r = await conn.fetchval('SELECT randomnumber FROM world WHERE id = $1', id_)
+    return json_response({'id': id_, 'randomNumber': r})
+
+
+async def multiple_database_queries_orm(request):
+    """
+    Test 3 ORM
+    """
+    num_queries = get_num_queries(request)
+
+    ids = [randint(1, 10000) for _ in range(num_queries)]
+    ids.sort()
+
+    result = []
+    async with request.app['aiopg_engine'].acquire() as conn:
+        for id_ in ids:
+            cur = await conn.execute(select([sa_worlds.c.randomnumber]).where(sa_worlds.c.id == id_))
+            r = await cur.first()
+            result.append({'id': id_, 'randomNumber': r[0]})
+    return json_response(result)
+
+
+async def multiple_database_queries_raw(request):
+    """
+    Test 3 RAW
+    """
+    num_queries = get_num_queries(request)
+
+    ids = [randint(1, 10000) for _ in range(num_queries)]
+    ids.sort()
+
+    result = []
+    async with request.app['asyncpg_pool'].acquire() as conn:
+        stmt = await conn.prepare('SELECT randomnumber FROM world WHERE id = $1')
+        for id_ in ids:
+            result.append({
+                'id': id_,
+                'randomNumber': await stmt.fetchval(id_),
+            })
+    return json_response(result)
+
+
+@template('fortune.jinja')
+async def fortunes(request):
+    """
+    Test 4 ORM
+    """
+    async with request.app['aiopg_engine'].acquire() as conn:
+        cur = await conn.execute(select([sa_fortunes.c.id, sa_fortunes.c.message]))
+        fortunes = list(await cur.fetchall())
+    fortunes.append(Fortune(id=0, message='Additional fortune added at request time.'))
+    fortunes.sort(key=attrgetter('message'))
+    return {'fortunes': fortunes}
+
+
+@template('fortune.jinja')
+async def fortunes_raw(request):
+    """
+    Test 4 RAW
+    """
+    async with request.app['asyncpg_pool'].acquire() as conn:
+        fortunes = await conn.fetch('SELECT * FROM Fortune')
+    fortunes.append(dict(id=0, message='Additional fortune added at request time.'))
+    fortunes.sort(key=itemgetter('message'))
+    return {'fortunes': fortunes}
+
+
+async def updates(request):
+    """
+    Test 5 ORM
+    """
+    num_queries = get_num_queries(request)
+    result = []
+
+    ids = [randint(1, 10000) for _ in range(num_queries)]
+    ids.sort()
+
+    async with request.app['aiopg_engine'].acquire() as conn:
+        for id_ in ids:
+            cur = await conn.execute(
+                select([sa_worlds.c.randomnumber])
+                .where(sa_worlds.c.id == id_)
+            )
+            r = await cur.first()
+            await conn.execute(
+                sa_worlds.update()
+                .where(sa_worlds.c.id == id_)
+                .values(randomnumber=randint(1, 10000))
+            )
+            result.append({'id': id_, 'randomNumber': r.randomnumber})
+
+    return json_response(result)
+
+
+async def updates_raw(request):
+    """
+    Test 5 RAW
+    """
+    num_queries = get_num_queries(request)
+
+    ids = [randint(1, 10000) for _ in range(num_queries)]
+    ids.sort()
+
+    result = []
+    updates = []
+    async with request.app['asyncpg_pool'].acquire() as conn:
+        stmt = await conn.prepare('SELECT randomnumber FROM world WHERE id = $1')
+
+        for id_ in ids:
+            result.append({
+                'id': id_,
+                'randomNumber': await stmt.fetchval(id_)
+            })
+
+            updates.append((randint(1, 10000), id_))
+        await conn.executemany('UPDATE world SET randomnumber=$1 WHERE id=$2', updates)
+
+    return json_response(result)
+
+
+async def plaintext(request):
+    """
+    Test 6
+    """
+    return Response(body=b'Hello, World!', content_type='text/plain')

+ 50 - 0
frameworks/Python/aiohttp/benchmark_config.json

@@ -0,0 +1,50 @@
+{
+  "framework": "aiohttp",
+  "tests": [{
+    "default": {
+      "setup_file": "setup",
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "Postgres",
+      "framework": "aiohttp",
+      "language": "Python",
+      "flavor": "Python3",
+      "orm": "Full",
+      "platform": "asyncio",
+      "webserver": "gunicorn",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "aiohttp",
+      "notes": "uses aiopg with sqlalchemy for database access"
+    },
+    "pg-raw": {
+      "setup_file": "setup",
+      "db_url": "/raw/db",
+      "query_url": "/raw/queries?queries=",
+      "fortune_url": "/raw/fortunes",
+      "update_url": "/raw/updates?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "Postgres",
+      "framework": "aiohttp",
+      "language": "Python",
+      "flavor": "Python3",
+      "orm": "Raw",
+      "platform": "asyncio",
+      "webserver": "gunicorn",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "aiohttp-pg-raw",
+      "notes": "uses asyncpg for database access",
+      "versus": "default"
+    }
+  }]
+}

+ 14 - 0
frameworks/Python/aiohttp/gunicorn_conf.py

@@ -0,0 +1,14 @@
+import multiprocessing
+import os
+
+if os.environ.get('TRAVIS') == 'true':
+    workers = 2
+else:
+    workers = multiprocessing.cpu_count()
+
+bind = '0.0.0.0:8080'
+keepalive = 120
+errorlog = '-'
+pidfile = 'gunicorn.pid'
+
+worker_class = 'aiohttp.worker.GunicornUVLoopWebWorker'

+ 10 - 0
frameworks/Python/aiohttp/requirements.txt

@@ -0,0 +1,10 @@
+aiohttp==1.2.0
+aiohttp-jinja2==0.13.0
+aiopg==0.13.0
+asyncpg==0.8.4
+cchardet==1.1.2
+gunicorn==19.6.0
+psycopg2==2.6.2
+SQLAlchemy==1.1.5
+ujson==1.35
+uvloop==0.7.2

+ 7 - 0
frameworks/Python/aiohttp/setup.sh

@@ -0,0 +1,7 @@
+#!/bin/bash
+
+fw_depends postgresql python3
+
+pip3 install --install-option="--prefix=${PY3_ROOT}" -r $TROOT/requirements.txt
+
+gunicorn app.gunicorn:app -c gunicorn_conf.py &