Explorar o código

Add Quart (#4490)

* Add Quart

* Use asyncpg connection pooling and don't use prepared statements

Turns out asyncpg explodes when you try to do concurrent requests
on a single connection, so use a connection pool instead

* quart: steal all the good ideas from starlette

* use kwargs for pool configuration instead of DSN
* use prepared statements, executemany, fetch as necessary

* quart: steal more good ideas from starlette

* run hypercorn with python config file
* enable uvloop

* quart: add alternate configuration with uvicorn
K900 %!s(int64=6) %!d(string=hai) anos
pai
achega
e62a8f5826

+ 31 - 0
frameworks/Python/quart/README.md

@@ -0,0 +1,31 @@
+# [Quart](https://gitlab.com/pgjones/quart) Benchmarking Test
+
+This benchmark uses Quart with the default Hypercorn server, and asyncpg for database connectivity
+(because there is still no good asyncio ORM, sadly).
+
+All code is contained in [app.py](app.py), and should be fairly self-documenting.
+
+## Test URLs
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/query?queries=
+
+### UPDATE
+
+http://localhost:8080/update?queries=
+
+### FORTUNES
+
+http://localhost:8080/fortunes

+ 109 - 0
frameworks/Python/quart/app.py

@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+import random
+import os
+
+import asyncpg
+from quart import Quart, jsonify, make_response, request, render_template
+
+app = Quart(__name__)
+
+GET_WORLD = "select randomnumber from world where id = $1"
+UPDATE_WORLD = "update world set randomNumber = $2 where id = $1"
+
+
[email protected]_first_request
+async def connect_to_db():
+    app.db = await asyncpg.create_pool(
+        user=os.getenv("PGUSER", "benchmarkdbuser"),
+        password=os.getenv("PGPASS", "benchmarkdbpass"),
+        database="hello_world",
+        host="tfb-database",
+        port=5432,
+    )
+
+
[email protected]("/json")
+def json():
+    return jsonify(message="Hello, World!")
+
+
[email protected]("/plaintext")
+async def plaintext():
+    response = await make_response(b"Hello, World!")
+    # Quart assumes string responses are 'text/html', so make a custom one
+    response.mimetype = "text/plain"
+    return response
+
+
[email protected]("/db")
+async def db():
+    async with app.db.acquire() as conn:
+        key = random.randint(1, 10000)
+        number = await conn.fetchval(GET_WORLD, key)
+        return jsonify({"id": key, "randomNumber": number})
+
+
+def get_query_count(args):
+    qc = args.get("queries")
+
+    if qc is None:
+        return 1
+
+    try:
+        qc = int(qc)
+    except ValueError:
+        return 1
+
+    qc = max(qc, 1)
+    qc = min(qc, 500)
+    return qc
+
+
[email protected]("/queries")
+async def queries():
+    queries = get_query_count(request.args)
+
+    worlds = []
+    async with app.db.acquire() as conn:
+        pst = await conn.prepare(GET_WORLD)
+        for _ in range(queries):
+            key = random.randint(1, 10000)
+            number = await pst.fetchval(key)
+            worlds.append({"id": key, "randomNumber": number})
+
+    return jsonify(worlds)
+
+
[email protected]("/updates")
+async def updates():
+    queries = get_query_count(request.args)
+
+    new_worlds = []
+    async with app.db.acquire() as conn, conn.transaction():
+        pst = await conn.prepare(GET_WORLD)
+
+        for _ in range(queries):
+            key = random.randint(1, 10000)
+            old_number = await pst.fetchval(key)
+            new_number = random.randint(1, 10000)
+            new_worlds.append((key, new_number))
+
+        await conn.executemany(UPDATE_WORLD, new_worlds)
+
+    return jsonify(
+        [{"id": key, "randomNumber": new_number} for key, new_number in new_worlds]
+    )
+
+
[email protected]("/fortunes")
+async def fortunes():
+    async with app.db.acquire() as conn:
+        rows = await conn.fetch("select * from fortune")
+    rows.append((0, "Additional fortune added at request time."))
+    rows.sort(key=lambda row: row[1])
+
+    return await render_template("fortunes.html", fortunes=rows)
+
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", port=8080)

+ 53 - 0
frameworks/Python/quart/benchmark_config.json

@@ -0,0 +1,53 @@
+{
+  "framework": "quart",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "Quart",
+        "language": "Python",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "Hypercorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Quart",
+        "notes": "",
+        "versus": "None"
+      },
+      "uvicorn": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "Quart",
+        "language": "Python",
+        "flavor": "Uvicorn",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "Uvicorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Quart",
+        "notes": "",
+        "versus": "uvicorn"
+      }
+    }
+  ]
+}

+ 13 - 0
frameworks/Python/quart/gunicorn_conf.py

@@ -0,0 +1,13 @@
+import multiprocessing
+import os
+
+_is_travis = os.environ.get('TRAVIS') == 'true'
+
+workers = multiprocessing.cpu_count()
+if _is_travis:
+    workers = 2
+
+bind = "0.0.0.0:8080"
+keepalive = 120
+errorlog = '-'
+loglevel = 'error'

+ 12 - 0
frameworks/Python/quart/hypercorn_conf.py

@@ -0,0 +1,12 @@
+import multiprocessing
+import os
+
+_is_travis = os.environ.get('TRAVIS') == 'true'
+
+workers = multiprocessing.cpu_count()
+if _is_travis:
+    workers = 2
+
+bind = ["0.0.0.0:8080"]
+keep_alive_timeout = 120
+worker_class = "uvloop"

+ 11 - 0
frameworks/Python/quart/quart-uvicorn.dockerfile

@@ -0,0 +1,11 @@
+FROM python:3.7-stretch
+
+ADD ./ /quart
+
+WORKDIR /quart
+
+RUN pip3 install -r /quart/requirements.txt
+RUN pip3 install -r /quart/requirements-uvicorn.txt
+
+CMD gunicorn app:app -k uvicorn.workers.UvicornWorker -c gunicorn_conf.py
+

+ 9 - 0
frameworks/Python/quart/quart.dockerfile

@@ -0,0 +1,9 @@
+FROM python:3.7-stretch
+
+ADD ./ /quart
+
+WORKDIR /quart
+
+RUN pip3 install -r /quart/requirements.txt
+
+CMD hypercorn app:app --config=python:hypercorn_conf.py

+ 7 - 0
frameworks/Python/quart/requirements-uvicorn.txt

@@ -0,0 +1,7 @@
+Click==7.0
+gunicorn==19.9.0
+h11==0.8.1
+httptools==0.0.13
+uvicorn==0.4.6
+uvloop==0.12.1
+websockets==7.0

+ 19 - 0
frameworks/Python/quart/requirements.txt

@@ -0,0 +1,19 @@
+aiofiles==0.4.0
+asyncpg==0.18.3
+blinker==1.4
+Click==7.0
+h11==0.8.1
+h2==3.1.0
+hpack==3.0.0
+Hypercorn==0.5.3
+hyperframe==5.2.0
+itsdangerous==1.1.0
+Jinja2==2.10
+MarkupSafe==1.1.1
+multidict==4.5.2
+pytoml==0.1.20
+Quart==0.8.1
+sortedcontainers==2.1.0
+typing-extensions==3.7.2
+uvloop==0.12.1
+wsproto==0.13.0

+ 12 - 0
frameworks/Python/quart/templates/fortunes.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+<tr><th>id</th><th>message</th></tr>
+{% for row in fortunes %}
+<tr><td>{{ row[0] }}</td><td>{{ row[1] }}</td></tr>
+{% endfor %}
+</table>
+</body>
+</html>