Skip to content

Commit

Permalink
Add Quart (#4490)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
K900 authored and NateBrady23 committed Feb 28, 2019
1 parent 519a5de commit e62a8f5
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 0 deletions.
31 changes: 31 additions & 0 deletions frameworks/Python/quart/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 109 additions & 0 deletions frameworks/Python/quart/app.py
Original file line number Diff line number Diff line change
@@ -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"


@app.before_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,
)


@app.route("/json")
def json():
return jsonify(message="Hello, World!")


@app.route("/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


@app.route("/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


@app.route("/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)


@app.route("/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]
)


@app.route("/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 changes: 53 additions & 0 deletions frameworks/Python/quart/benchmark_config.json
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions frameworks/Python/quart/gunicorn_conf.py
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions frameworks/Python/quart/hypercorn_conf.py
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions frameworks/Python/quart/quart-uvicorn.dockerfile
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions frameworks/Python/quart/quart.dockerfile
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions frameworks/Python/quart/requirements-uvicorn.txt
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions frameworks/Python/quart/requirements.txt
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions frameworks/Python/quart/templates/fortunes.html
Original file line number Diff line number Diff line change
@@ -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>

0 comments on commit e62a8f5

Please sign in to comment.