Selaa lähdekoodia

[Flask] Some additional clean-up and tuning (#6712)

* Some additional clean-up and tuning of the Flask-Raw code.

* Fixing the Pypy
Tim Armstrong 4 vuotta sitten
vanhempi
commit
8d34aff41c

+ 1 - 1
frameworks/Python/flask/README.md

@@ -13,7 +13,7 @@ All test implementations are located within a single file
 
 ## Description
 
-Flask + Flask-SQLAlchemy
+Flask + Flask-Pony
 
 ### Database
 

+ 30 - 19
frameworks/Python/flask/app.py

@@ -9,21 +9,33 @@ from pony import orm
 
 if sys.version_info[0] == 3:
     xrange = range
-_is_pypy = hasattr(sys, 'pypy_version_info')
+_is_pypy = hasattr(sys, "pypy_version_info")
 if _is_pypy:
     from psycopg2cffi import compat
+
     compat.register()
 
-DBDRIVER = 'postgres'
-DBHOST = 'tfb-database'
+DBDRIVER = "postgres"
+DBHOST = "tfb-database"
 
 # setup
 
 app = Flask(__name__)
-app.config['STORM_DATABASE_URI'] = "{DBDRIVER}://benchmarkdbuser:benchmarkdbpass@{DBHOST}:5432/hello_world".format(DBDRIVER=DBDRIVER, DBHOST=DBHOST)
-app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False
+app.config[
+    "STORM_DATABASE_URI"
+] = "{DBDRIVER}://benchmarkdbuser:benchmarkdbpass@{DBHOST}:5432/hello_world".format(
+    DBDRIVER=DBDRIVER, DBHOST=DBHOST
+)
+app.config["JSONIFY_PRETTYPRINT_REGULAR"] = False
 db = orm.Database()
-db.bind(DBDRIVER, host=DBHOST, port=5432, user="benchmarkdbuser", password="benchmarkdbpass", database="hello_world")
+db.bind(
+    DBDRIVER,
+    host=DBHOST,
+    port=5432,
+    user="benchmarkdbuser",
+    password="benchmarkdbpass",
+    database="hello_world",
+)
 
 
 class World(db.Entity):
@@ -33,10 +45,7 @@ class World(db.Entity):
 
     def to_dict(self):
         """Return object data in easily serializeable format"""
-        return {
-            'id'         : self.id,
-            'randomNumber': self.randomNumber
-        }
+        return {"id": self.id, "randomNumber": self.randomNumber}
 
 
 class Fortune(db.Entity):
@@ -69,14 +78,13 @@ def generate_ids(num_queries):
 
 @app.route("/json")
 def hello():
-    return jsonify(message='Hello, World!')
+    return jsonify(message="Hello, World!")
 
 
 @app.route("/query")
 def get_random_world():
     with orm.db_session(serializable=False):
-        worlds = [World[ident].to_dict()
-              for ident in generate_ids(get_num_queries())]
+        worlds = [World[ident].to_dict() for ident in generate_ids(get_num_queries())]
     return jsonify(worlds)
 
 
@@ -93,9 +101,11 @@ def get_fortunes():
     with orm.db_session(serializable=False):
         fortunes = list(orm.select(fortune for fortune in Fortune))
     tmp_fortune = namedtuple("Fortune", ["id", "message"])
-    fortunes.append(tmp_fortune(id=0, message="Additional fortune added at request time."))
-    fortunes.sort(key=attrgetter('message'))
-    return render_template('fortunes.html', fortunes=fortunes)
+    fortunes.append(
+        tmp_fortune(id=0, message="Additional fortune added at request time.")
+    )
+    fortunes.sort(key=attrgetter("message"))
+    return render_template("fortunes.html", fortunes=fortunes)
 
 
 @app.route("/updates")
@@ -113,16 +123,17 @@ def updates():
     return jsonify(worlds)
 
 
[email protected]('/plaintext')
[email protected]("/plaintext")
 def plaintext():
     """Test 6: Plaintext"""
-    response = make_response(b'Hello, World!')
-    response.content_type = 'text/plain'
+    response = make_response(b"Hello, World!")
+    response.content_type = "text/plain"
     return response
 
 
 try:
     import meinheld
+
     meinheld.server.set_access_logger(None)
     meinheld.set_keepalive(120)
 except ImportError:

+ 46 - 81
frameworks/Python/flask/app_raw.py

@@ -1,126 +1,91 @@
 #!/usr/bin/env python
-from collections import namedtuple
-from operator import attrgetter, itemgetter
-from random import randint
-import sys
+import multiprocessing
+from itertools import repeat
+from operator import itemgetter
 
-from flask import Flask, request, render_template, make_response, jsonify
+from flask import Flask, Response
 
+from helpers import *
 
-if sys.version_info[0] == 3:
-    xrange = range
-_is_pypy = hasattr(sys, 'pypy_version_info')
-if _is_pypy:
-    from psycopg2cffi.pool import ThreadedConnectionPool
-else:
-    from psycopg2.pool import ThreadedConnectionPool
 # setup
 
-app = Flask(__name__)
-
-pool = ThreadedConnectionPool(minconn=2, maxconn=2, database="hello_world", user="benchmarkdbuser", password="benchmarkdbpass", host="tfb-database", port=5432)
-
-READ_ROW_SQL = 'SELECT world."randomnumber", world."id" FROM "world" WHERE id = %(id)s'
-WRITE_ROW_SQL = 'UPDATE "world" SET "randomnumber"=%(randomNumber)s WHERE id=%(id)s'
 
-def get_num_queries():
-    try:
-        num_queries = request.args.get("queries", 1, type=int)
-    except ValueError:
-        num_queries = 1
-    if num_queries < 1:
-        return 1
-    if num_queries > 500:
-        return 500
-    return num_queries
 
-
-def generate_ids(num_queries):
-    ids = {randint(1, 10000) for _ in xrange(num_queries)}
-    while len(ids) < num_queries:
-        ids.add(randint(1, 10000))
-    return list(sorted(ids))
+app = Flask(__name__)
+(
+    POOL,
+    TEMPLATE,
+    READ_ROW_SQL,
+    PREPARED_READ_ROW_SQL,
+    WRITE_ROW_SQL,
+    ADDITIONAL_ROW,
+) = setup(multiprocessing.cpu_count() * 2.5)
 
 
 @app.route("/json")
 def hello():
-    return jsonify(message='Hello, World!')
+    return orjson.dumps({"message": "Hello, World!"}), {
+        "Content-Type": "application/json"
+    }
 
 
 @app.route("/query")
 def get_random_world():
-    db = pool.getconn()
+    db = POOL.getconn()
     cursor = db.cursor()
-    def query(ident):
-        cursor.execute(READ_ROW_SQL, {"id": ident})
-        result = cursor.fetchone()
-        return result
-    worlds = [{'id': result[0], 'randomNumber': result[1]} for result in map(query, generate_ids(get_num_queries()))]
-    pool.putconn(db)
-    return jsonify(worlds)
+    num_queries = get_num_queries()
+    results = map(query, zip(repeat(cursor, num_queries), generate_ids(num_queries)))
+    worlds = [{"id": result[0], "randomNumber": result[1]} for result in results]
+    POOL.putconn(db)
+    return orjson.dumps(worlds), {"Content-Type": "application/json"}
 
 
 @app.route("/db")
 def get_random_world_single():
-    db = pool.getconn()
+    db = POOL.getconn()
     cursor = db.cursor()
-    cursor.execute(READ_ROW_SQL, {"id": randint(1, 10000)})
+    cursor.execute("EXECUTE read_stmt(%s)", generate_ids(1))
     result = cursor.fetchone()
-    world = {'id': result[0], 'randomNumber': result[1]}
-    pool.putconn(db)
-    return jsonify(world)
-
-
-Fortune = namedtuple("Fortune", ["id", "message"])
+    world = {"id": result[0], "randomNumber": result[1]}
+    POOL.putconn(db)
+    return orjson.dumps(world), {"Content-Type": "application/json"}
 
 
 @app.route("/fortunes")
 def get_fortunes():
-    db = pool.getconn()
+    db = POOL.getconn()
     cursor = db.cursor()
-    cursor.execute('SELECT * FROM "Fortune"')
-    fortunes = [Fortune(id=row[0], message=row[1]) for row in cursor.fetchall()]
-    fortunes.append(Fortune(id=0, message="Additional fortune added at request time."))
-    fortunes.sort(key=attrgetter('message'))
-    pool.putconn(db)
-    return render_template('fortunes.html', fortunes=fortunes)
+    cursor.execute("EXECUTE fortune")
+    fortunes = list(cursor.fetchall())
+    fortunes.append(ADDITIONAL_ROW)
+    fortunes.sort(key=itemgetter(1))
+    POOL.putconn(db)
+    return Response(TEMPLATE.render(fortunes=fortunes))
 
 
 @app.route("/updates")
 def updates():
     """Test 5: Database Updates"""
-    db = pool.getconn()
+    db = POOL.getconn()
     cursor = db.cursor()
     num_queries = get_num_queries()
     ids = generate_ids(num_queries)
-    updates = generate_ids(num_queries)
-    def query(ident):
-        cursor.execute(READ_ROW_SQL, {"id": ident})
-        result = cursor.fetchone()
-        return result
-    list(map(query, generate_ids(get_num_queries())))
-    worlds = [{"id": ident, "randomNumber": update} for ident, update in zip(ids, updates)]
-    for world in worlds:
-        cursor.execute(WRITE_ROW_SQL, world)
+    update_values = generate_ids(num_queries)
+    list(map(query, zip(repeat(cursor, num_queries), generate_ids(num_queries))))
+    worlds = list(zip(ids, update_values))
+    execute_batch(cursor, "EXECUTE write_stmt(%s, %s)", worlds)
     db.commit()
-    pool.putconn(db)
-    return jsonify(worlds)
+    POOL.putconn(db)
+    return orjson.dumps(
+        [{"id": ident, "randomNumber": update} for ident, update in worlds]
+    ), {"Content-Type": "application/json"}
 
 
[email protected]('/plaintext')
[email protected]("/plaintext")
 def plaintext():
     """Test 6: Plaintext"""
-    response = make_response(b'Hello, World!')
-    response.content_type = 'text/plain'
-    return response
-
+    return b"Hello, World!", {"Content-Type": "text/plain"}
 
-try:
-    import meinheld
-    meinheld.server.set_access_logger(None)
-    meinheld.set_keepalive(120)
-except ImportError:
-    pass
 
 # entry point for debugging
 if __name__ == "__main__":

+ 1 - 1
frameworks/Python/flask/flask-raw.dockerfile

@@ -10,4 +10,4 @@ WORKDIR /flask
 
 EXPOSE 8080
 
-CMD gunicorn app_raw:app -c gunicorn_conf.py
+CMD gunicorn app_raw:app -c gunicorn_conf.py -k egg:meinheld#gunicorn_worker

+ 5 - 13
frameworks/Python/flask/gunicorn_conf.py

@@ -2,25 +2,17 @@ import multiprocessing
 import os
 import sys
 
-_is_pypy = hasattr(sys, 'pypy_version_info')
-_is_travis = os.environ.get('TRAVIS') == 'true'
+_is_pypy = hasattr(sys, "pypy_version_info")
+_is_travis = os.environ.get("TRAVIS") == "true"
 
-workers = multiprocessing.cpu_count()*3
+workers = int(multiprocessing.cpu_count() * 2.5)
 if _is_travis:
     workers = 2
 
 bind = "0.0.0.0:8080"
 keepalive = 120
-errorlog = '-'
-pidfile = 'gunicorn.pid'
+errorlog = "-"
+pidfile = "gunicorn.pid"
 
 if _is_pypy:
     worker_class = "sync"
-else:
-    worker_class = "meinheld.gmeinheld.MeinheldWorker"
-
-    def post_fork(server, worker):
-        # Disable access log.
-        # (Until https://github.com/mopemope/meinheld/pull/42 is released)
-        import meinheld.server
-        meinheld.server.set_access_logger(None)

+ 103 - 0
frameworks/Python/flask/helpers.py

@@ -0,0 +1,103 @@
+import os
+import sys
+from collections import namedtuple
+from random import randint
+
+import jinja2
+from flask import request
+
+
+if sys.version_info[0] == 3:
+    xrange = range
+
+_is_pypy = hasattr(sys, "pypy_version_info")
+if _is_pypy:
+    from psycopg2cffi.pool import ThreadedConnectionPool
+    from psycopg2cffi.extras import execute_batch
+    import ujson as orjson
+else:
+    from psycopg2.pool import ThreadedConnectionPool
+    from psycopg2.extras import execute_batch
+    import orjson
+    try:
+        import meinheld
+        import meinheld.patch
+
+        meinheld.server.set_access_logger(None)
+        meinheld.set_keepalive(30)
+        meinheld.patch.patch_all()
+    except ImportError:
+        pass
+
+
+def get_num_queries():
+    try:
+        num_queries = request.args.get("queries", 1, type=int)
+    except ValueError:
+        num_queries = 1
+    if num_queries < 1:
+        return 1
+    if num_queries > 500:
+        return 500
+    return num_queries
+
+
+def generate_ids(num_queries):
+    ids = {randint(1, 10000) for _ in xrange(num_queries)}
+    while len(ids) < num_queries:
+        ids.add(randint(1, 10000))
+    return list(sorted(ids))
+
+
+def load_fortunes_template():
+    path = os.path.join("templates", "fortune_raw.html")
+    with open(path, "r") as template_file:
+        template_text = template_file.read()
+        return jinja2.Template(template_text)
+
+
+def setup(threads):
+    pool = ThreadedConnectionPool(
+        minconn=int(threads / 4),
+        maxconn=int(threads / 4),
+        database="hello_world",
+        user="benchmarkdbuser",
+        password="benchmarkdbpass",
+        host="tfb-database",
+        port=5432,
+    )
+    template = load_fortunes_template()
+    read_row_sql = (
+        'SELECT world."randomnumber", world."id" FROM "world" WHERE id = %(id)s'
+    )
+    prepared_read_row_sql = (
+        'SELECT world."randomnumber", world."id" FROM "world" WHERE id = $1'
+    )
+    write_row_sql = 'UPDATE "world" SET "randomnumber"=$1 WHERE id=$2'
+    additional_row = (0, "Additional fortune added at request time.")
+    db = pool.getconn()
+    cursor = db.cursor()
+    cursor.execute("PREPARE read_stmt (int) AS " + prepared_read_row_sql)
+    cursor.execute("PREPARE write_stmt (int, int) AS " + write_row_sql)
+    cursor.execute('PREPARE fortune AS SELECT * FROM "Fortune"')
+    del cursor
+    pool.putconn(db)
+    del db
+    return (
+        pool,
+        template,
+        read_row_sql,
+        prepared_read_row_sql,
+        write_row_sql,
+        additional_row,
+    )
+
+
+def query(arg):
+    (cursor, ident) = arg
+    cursor.execute("EXECUTE read_stmt(%s)", [ident])
+    result = cursor.fetchone()
+    return result
+
+
+Fortune = namedtuple("Fortune", ["id", "message"])

+ 4 - 3
frameworks/Python/flask/requirements-pypy.txt

@@ -1,13 +1,14 @@
 click==8.0.1
 pony==0.7.14
 psycopg2cffi>=2.7
-Flask==1.1.2
+Flask==2.0.1
 tornado==6.1
-gevent>=1.4
+greenlet<0.5
 gunicorn==20.1.0
 itsdangerous==2.0.1
 Jinja2==3.0.1
 MarkupSafe==2.0.1
 meinheld==1.0.2
 uWSGI==2.0.19.1
-Werkzeug==2.0.1
+Werkzeug==2.0.1
+ujson==4.0.2

+ 4 - 2
frameworks/Python/flask/requirements.txt

@@ -3,8 +3,8 @@ pony==0.7.14
 psycopg2==2.9.1
 psycopg2-binary==2.9.1
 psycopg2-pool==1.1
-Flask==1.1.2
-greenlet==0.4.15
+Flask==2.0.1
+greenlet<0.5
 gunicorn==20.1.0
 itsdangerous==2.0.1
 Jinja2==3.0.1
@@ -12,3 +12,5 @@ MarkupSafe==2.0.1
 meinheld==1.0.2
 uWSGI==2.0.19.1
 Werkzeug==2.0.1
+ujson==4.0.2
+orjson==3.6.0

+ 10 - 0
frameworks/Python/flask/templates/fortune_raw.html

@@ -0,0 +1,10 @@
+<!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[0] }}</td><td>{{ fortune[1]|e }}</td></tr>
+{% endfor %}</table>
+</body>
+</html>