Browse Source

Add Falcon ASGI/Asyncio variant and make workers for Bjoern server (#7803)

* Remove redundant parameter parsing

* Remove redundant "num" parameter parsing

* Rename function names to make more sense

* Add asgi+tortoise version

* Fix 'Attribute "asgi" not found in module "asgi_tortoise"'

* Make bjoern some workers

* Add note

* Note on middleware
Decrease keepalive value to 60

* Update base image from python:3.8-buster to python:3.10-bullseye
and pypy:3.8-7.3 to pypy:3.9-bullseye

* Made gunicorn as process manager for uvicorn workers

* Downgrade python:3.10-bullseye to python:3.9-bullseye because
pony refuse to work in python 3.10

* Refactor for more native way of doing update in tortoise ORM

* Refine the testing by adding atomic decorator for single query and
multiple queries

* Add notes on README file
rename asgi_tortoise to app_asgi_tortoise
Ghulam Zakiy 2 years ago
parent
commit
65d423da03

+ 5 - 1
frameworks/Python/falcon/README.md

@@ -30,14 +30,18 @@ Native asyncio support.
 
 
 ### Server
 ### Server
 
 
-* Waitress on CPython3
+* Socketify.py on CPython3
+* Socketify.py on Pypy3
+* Gunicorn + Uvicorn workers with Orjson on CPython3 
 * Bjoern on Cpython3
 * Bjoern on Cpython3
 * Gunicorn + Meinheld on CPython3
 * Gunicorn + Meinheld on CPython3
 * Gunicorn + Meinheld with Orjson on CPython3
 * Gunicorn + Meinheld with Orjson on CPython3
 * Gunicorn Sync on PyPy3
 * Gunicorn Sync on PyPy3
+* Waitress on CPython3
 
 
 ### Database
 ### Database
 
 
+* Tortoise ORM [PostgresSQL] - (asynpg on CPython3)
 * Pony ORM [PostgreSQL] - (psycopg2-binary on CPython3, psycopg2cffi on PyPy3)
 * Pony ORM [PostgreSQL] - (psycopg2-binary on CPython3, psycopg2cffi on PyPy3)
 
 
 ## Test Paths & Sources
 ## Test Paths & Sources

+ 6 - 8
frameworks/Python/falcon/app.py

@@ -16,7 +16,7 @@ class JSONResource(object):
         response.media = {'message': "Hello, world!"}
         response.media = {'message': "Hello, world!"}
 
 
 
 
-class RandomWorld(object):
+class SingleQuery(object):
     @session(serializable=False)
     @session(serializable=False)
     def on_get(self, request, response):
     def on_get(self, request, response):
         wid = randint(1, 10000)
         wid = randint(1, 10000)
@@ -24,10 +24,9 @@ class RandomWorld(object):
         response.media = world.to_dict()
         response.media = world.to_dict()
 
 
 
 
-class RandomQueries(object):
+class MultipleQueries(object):
     @session(serializable=False)
     @session(serializable=False)
-    def on_get(self, request, response, **params):
-        num = params.get("num", "1")
+    def on_get(self, request, response, num):
         num = sanitize(num)
         num = sanitize(num)
         worlds = [World[ident].to_dict() for ident in generate_ids(num)]
         worlds = [World[ident].to_dict() for ident in generate_ids(num)]
         response.media = worlds
         response.media = worlds
@@ -35,8 +34,7 @@ class RandomQueries(object):
 
 
 class UpdateQueries(object):
 class UpdateQueries(object):
     @session(serializable=False)
     @session(serializable=False)
-    def on_get(self, request, response, **params):
-        num = params.get("num", "1")
+    def on_get(self, request, response, num):
         num = sanitize(num)
         num = sanitize(num)
         ids = generate_ids(num)
         ids = generate_ids(num)
         ids.sort()
         ids.sort()
@@ -69,8 +67,8 @@ class PlaintextResource(object):
 
 
 # register resources
 # register resources
 app.add_route("/json", JSONResource())
 app.add_route("/json", JSONResource())
-app.add_route("/db", RandomWorld())
-app.add_route("/queries/{num}", RandomQueries())
+app.add_route("/db", SingleQuery())
+app.add_route("/queries/{num}", MultipleQueries())
 app.add_route("/updates/{num}", UpdateQueries())
 app.add_route("/updates/{num}", UpdateQueries())
 app.add_route("/fortunes", Fortunes())
 app.add_route("/fortunes", Fortunes())
 app.add_route("/plaintext", PlaintextResource())
 app.add_route("/plaintext", PlaintextResource())

+ 109 - 0
frameworks/Python/falcon/app_asgi_tortoise.py

@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+import orjson
+import falcon.asgi
+from falcon import media
+from helpers import load_template, FortuneTuple, generate_ids, sanitize
+from operator import attrgetter
+from random import randint
+from tortoise import Tortoise
+from tortoise_models import World, Fortune
+from tortoise.transactions import in_transaction, atomic
+from tortoise.exceptions import OperationalError
+
+
+# Middleware
+class TortoiseInit:
+    async def process_startup(self, scope, event):
+        await Tortoise.init(
+            db_url="postgres://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world",
+            modules={"models": ["tortoise_models"]}
+        )
+        await Tortoise.generate_schemas(safe=True)
+
+    async def process_shutdown(self, scopre, event):
+        await Tortoise.close_connections()
+
+
+tortoise_init = TortoiseInit()
+
+
+# resource endpoints
+class JSONResource(object):
+    async def on_get(self, request, response):
+        response.media = {'message': "Hello, world!"}
+
+
+class SingleQuery(object):
+    # Note: There's much improvement when we decorate
+    # the query, even just for retreiving data
+    @atomic()
+    async def on_get(self, request, response):
+        resp = await World.get(id=randint(1, 10000))
+        response.media = resp.to_dict()
+
+
+class MultipleQueries(object):
+    # Note: Not much different between using atomic or
+    # in_transaction decorator here.
+    @atomic()
+    async def on_get(self, request, response, num):
+        num = sanitize(num)
+        worlds = []
+        for ids in generate_ids(num):
+            data = await World.get(id=ids)
+            worlds.append(data.to_dict())
+        response.media = worlds
+
+
+class UpdateQueries(object):
+    async def on_get(self, request, response, num):
+        try:
+            async with in_transaction():
+                num = sanitize(num)
+                items = await World.filter(id__in=generate_ids(num))
+                worlds = []
+                for ids in items:
+                    ids.randomNumber = randint(1, 10000)
+                    await ids.save()
+                    worlds.append({'id': ids.id, 'randomNumber': ids.randomNumber})
+                response.media = worlds
+        except OperationalError:
+            pass
+
+
+class Fortunes(object):
+    _template = load_template()
+
+    async def on_get(self, request, response):
+        fortunes = [FortuneTuple(id=f.id, message=f.message) for f in await Fortune.all()]
+        fortunes.append(FortuneTuple(id=0, message="Additional fortune added at request time."))
+        fortunes.sort(key=attrgetter("message"))
+        content = self._template.render(fortunes=fortunes)
+        response.content_type = falcon.MEDIA_HTML
+        response.text = content
+
+
+class PlaintextResource(object):
+    async def on_get(self, request, response):
+        response.content_type = falcon.MEDIA_TEXT
+        response.text = 'Hello, world!'
+
+
+asgi = falcon.asgi.App(middleware=[tortoise_init])
+
+# Change JSON handler to orjson
+JSONHandler = media.JSONHandler(dumps=orjson.dumps, loads=orjson.loads)
+extra_handlers = {
+    "application/json": JSONHandler,
+    "application/json; charset=UTF-8": JSONHandler
+}
+asgi.req_options.media_handlers.update(extra_handlers)
+asgi.resp_options.media_handlers.update(extra_handlers)
+
+# register resources
+asgi.add_route("/json", JSONResource())
+asgi.add_route("/db", SingleQuery())
+asgi.add_route("/queries/{num}", MultipleQueries())
+asgi.add_route("/updates/{num}", UpdateQueries())
+asgi.add_route("/fortunes", Fortunes())
+asgi.add_route("/plaintext", PlaintextResource())

+ 29 - 9
frameworks/Python/falcon/app_bjoern.py

@@ -27,7 +27,7 @@ class JSONResource(object):
         response.media = {'message': "Hello, world!"}
         response.media = {'message': "Hello, world!"}
 
 
 
 
-class RandomWorld(object):
+class SingleQuery(object):
     @session(serializable=False)
     @session(serializable=False)
     def on_get(self, request, response):
     def on_get(self, request, response):
         wid = randint(1, 10000)
         wid = randint(1, 10000)
@@ -37,10 +37,9 @@ class RandomWorld(object):
         response.media = world.to_dict()
         response.media = world.to_dict()
 
 
 
 
-class RandomQueries(object):
+class MultipleQueries(object):
     @session(serializable=False)
     @session(serializable=False)
-    def on_get(self, request, response, **params):
-        num = params.get("num", "1")
+    def on_get(self, request, response, num):
         num = sanitize(num)
         num = sanitize(num)
         worlds = [World[ident].to_dict() for ident in generate_ids(num)]
         worlds = [World[ident].to_dict() for ident in generate_ids(num)]
         response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
         response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
@@ -50,8 +49,7 @@ class RandomQueries(object):
 
 
 class UpdateQueries(object):
 class UpdateQueries(object):
     @session(serializable=False)
     @session(serializable=False)
-    def on_get(self, request, response, **params):
-        num = params.get("num", "1")
+    def on_get(self, request, response, num):
         num = sanitize(num)
         num = sanitize(num)
         ids = generate_ids(num)
         ids = generate_ids(num)
         ids.sort()
         ids.sort()
@@ -90,15 +88,37 @@ class PlaintextResource(object):
 
 
 # register resources
 # register resources
 app.add_route("/json", JSONResource())
 app.add_route("/json", JSONResource())
-app.add_route("/db", RandomWorld())
-app.add_route("/queries/{num}", RandomQueries())
+app.add_route("/db", SingleQuery())
+app.add_route("/queries/{num}", MultipleQueries())
 app.add_route("/updates/{num}", UpdateQueries())
 app.add_route("/updates/{num}", UpdateQueries())
 app.add_route("/fortunes", Fortunes())
 app.add_route("/fortunes", Fortunes())
 app.add_route("/plaintext", PlaintextResource())
 app.add_route("/plaintext", PlaintextResource())
 
 
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
+    import os
+    import multiprocessing
+
+    _is_travis = os.environ.get('TRAVIS') == 'true'
+
+    workers = int(multiprocessing.cpu_count())
+    if _is_travis:
+        workers = 2
+
     host = '0.0.0.0'
     host = '0.0.0.0'
     port = 8080
     port = 8080
 
 
-    bjoern.run(wsgi, host=host, port=port)
+    def run_app():
+        bjoern.run(wsgi, host=host, port=port, reuse_port=True)
+
+    def create_fork():
+        n = os.fork()
+        # n greater than 0 means parent process
+        if not n > 0:
+            run_app()
+
+    # fork limiting the cpu count - 1
+    for i in range(1, workers):
+        create_fork()
+
+    run_app()  # run app on the main process too :)

+ 2 - 0
frameworks/Python/falcon/app_waitress.py

@@ -6,6 +6,7 @@ import logging
 
 
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
+    # waitress is very verbose while running, so we disable it
     logging.basicConfig()
     logging.basicConfig()
     logging.getLogger().setLevel(logging.CRITICAL)
     logging.getLogger().setLevel(logging.CRITICAL)
     logging.disable(True)
     logging.disable(True)
@@ -15,6 +16,7 @@ if __name__ == "__main__":
         listen=bind,
         listen=bind,
         log_socket_errors=False,
         log_socket_errors=False,
         threads=workers,
         threads=workers,
+        asyncore_use_poll=True,
         expose_tracebacks=False,
         expose_tracebacks=False,
         connection_limit=128,
         connection_limit=128,
         channel_timeout=keepalive,
         channel_timeout=keepalive,

+ 23 - 0
frameworks/Python/falcon/benchmark_config.json

@@ -191,6 +191,29 @@
       "display_name": "Falcon [Pypy3]",
       "display_name": "Falcon [Pypy3]",
       "notes": "",
       "notes": "",
       "versus": "wsgi"
       "versus": "wsgi"
+    },
+    "asgi": {
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries/",
+      "update_url": "/updates/",
+      "fortune_url": "/fortunes",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "Postgres",
+      "framework": "Falcon",
+      "language": "Python",
+      "flavor": "Python3",
+      "orm": "Full",
+      "platform": "ASGI",
+      "webserver": "Uvicorn",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "Falcon [ASGI/Tortoise]",
+      "notes": "",
+      "versus": "uvicorn"
     }
     }
   }]
   }]
 }
 }

+ 23 - 0
frameworks/Python/falcon/config.toml

@@ -191,3 +191,26 @@ database.os = "Linux"
 display.name = "Falcon [Pypy3]"
 display.name = "Falcon [Pypy3]"
 notes = ""
 notes = ""
 versus = "wsgi"
 versus = "wsgi"
+
+[asgi]
+json.url = "/json"
+db.url = "/db"
+query.url = "/queries/"
+update.url = "/updates/"
+fortune.url = "/fortunes"
+plaintext.url = "/plaintext"
+port = 8080
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+framework = "Falcon"
+language = "Python"
+flavor = "Python3"
+orm = "Full"
+platform = "ASGI"
+webserver = "Uvicorn"
+os = "Linux"
+database.os = "Linux"
+display.name = "Falcon [ASGI/Tortoise]"
+notes = ""
+versus = "uvicorn"

+ 15 - 0
frameworks/Python/falcon/falcon-asgi.dockerfile

@@ -0,0 +1,15 @@
+FROM python:3.9-bullseye
+
+RUN apt-get update; apt-get install libpq-dev python3-dev -y
+WORKDIR /falcon
+COPY ./ /falcon
+RUN pip3 install -U pip; \
+    pip3 install cython==0.29.26; \
+    pip3 install -r /falcon/requirements.txt; \
+    pip3 install falcon==3.1.1 --no-binary :all:;
+
+ENV ASYNCIO=true
+
+EXPOSE 8080
+
+CMD ["gunicorn", "app_asgi_tortoise:asgi", "-c", "gunicorn_conf.py"]

+ 2 - 2
frameworks/Python/falcon/falcon-bjoern.dockerfile

@@ -1,8 +1,8 @@
-FROM python:3.8-buster
+FROM python:3.9-bullseye
 
 
 RUN apt-get update; apt-get install libpq-dev python3-dev libev-dev -y
 RUN apt-get update; apt-get install libpq-dev python3-dev libev-dev -y
-COPY ./ /falcon
 WORKDIR /falcon
 WORKDIR /falcon
+COPY ./ /falcon
 RUN pip3 install -U pip; pip3 install -r /falcon/requirements-bjoern.txt
 RUN pip3 install -U pip; pip3 install -r /falcon/requirements-bjoern.txt
 
 
 EXPOSE 8080
 EXPOSE 8080

+ 4 - 4
frameworks/Python/falcon/falcon-orjson.dockerfile

@@ -1,12 +1,12 @@
-FROM python:3.8-buster
+FROM python:3.9-bullseye
 
 
 RUN apt-get update; apt-get install libpq-dev python3-dev -y
 RUN apt-get update; apt-get install libpq-dev python3-dev -y
-COPY ./ /falcon
 WORKDIR /falcon
 WORKDIR /falcon
+COPY ./ /falcon
 RUN pip3 install -U pip; \
 RUN pip3 install -U pip; \
     pip3 install cython==0.29.26; \
     pip3 install cython==0.29.26; \
-    pip3 install falcon==3.0.1 --no-binary :all:; \
-    pip3 install -r /falcon/requirements.txt
+    pip3 install -r /falcon/requirements.txt; \
+    pip3 install falcon==3.1.1 --no-binary :all:;
 
 
 EXPOSE 8080
 EXPOSE 8080
 
 

+ 1 - 1
frameworks/Python/falcon/falcon-pypy3.dockerfile

@@ -1,4 +1,4 @@
-FROM pypy:3.8-7.3
+FROM pypy:3.9-bullseye
 
 
 RUN apt-get update; apt-get install libpq-dev python3-dev -y
 RUN apt-get update; apt-get install libpq-dev python3-dev -y
 COPY ./ /falcon
 COPY ./ /falcon

+ 2 - 2
frameworks/Python/falcon/falcon-waitress.dockerfile

@@ -1,8 +1,8 @@
-FROM python:3.8-buster
+FROM python:3.9-bullseye
 
 
 RUN apt-get update; apt-get install libpq-dev python3-dev -y
 RUN apt-get update; apt-get install libpq-dev python3-dev -y
-COPY ./ /falcon
 WORKDIR /falcon
 WORKDIR /falcon
+COPY ./ /falcon
 RUN pip3 install -U pip; pip3 install -r /falcon/requirements.txt
 RUN pip3 install -U pip; pip3 install -r /falcon/requirements.txt
 
 
 EXPOSE 8080
 EXPOSE 8080

+ 4 - 4
frameworks/Python/falcon/falcon.dockerfile

@@ -1,12 +1,12 @@
-FROM python:3.8-buster
+FROM python:3.9-bullseye
 
 
 RUN apt-get update; apt-get install libpq-dev python3-dev -y
 RUN apt-get update; apt-get install libpq-dev python3-dev -y
-COPY ./ /falcon
 WORKDIR /falcon
 WORKDIR /falcon
+COPY ./ /falcon
 RUN pip3 install -U pip; \
 RUN pip3 install -U pip; \
     pip3 install cython==0.29.26; \
     pip3 install cython==0.29.26; \
-    pip3 install falcon==3.0.1 --no-binary :all:; \
-    pip3 install -r /falcon/requirements.txt
+    pip3 install -r /falcon/requirements.txt; \
+    pip3 install falcon==3.1.1 --no-binary :all:;
 
 
 EXPOSE 8080
 EXPOSE 8080
 
 

+ 4 - 1
frameworks/Python/falcon/gunicorn_conf.py

@@ -5,18 +5,21 @@ import sys
 
 
 _is_pypy = hasattr(sys, 'pypy_version_info')
 _is_pypy = hasattr(sys, 'pypy_version_info')
 _is_travis = os.environ.get('TRAVIS') == 'true'
 _is_travis = os.environ.get('TRAVIS') == 'true'
+_is_asyncio = os.environ.get('ASYNCIO') == 'true'
 
 
 workers = int(multiprocessing.cpu_count() * 1.5)
 workers = int(multiprocessing.cpu_count() * 1.5)
 if _is_travis:
 if _is_travis:
     workers = 2
     workers = 2
 
 
 bind = "0.0.0.0:8080"
 bind = "0.0.0.0:8080"
-keepalive = 120
+keepalive = 60
 errorlog = '-'
 errorlog = '-'
 pidfile = 'gunicorn.pid'
 pidfile = 'gunicorn.pid'
 
 
 if _is_pypy:
 if _is_pypy:
     worker_class = "sync"
     worker_class = "sync"
+elif _is_asyncio:
+    worker_class = "uvicorn.workers.UvicornWorker"
 else:
 else:
     worker_class = "meinheld.gmeinheld.MeinheldWorker"
     worker_class = "meinheld.gmeinheld.MeinheldWorker"
 
 

+ 1 - 1
frameworks/Python/falcon/requirements-pypy.txt

@@ -1,5 +1,5 @@
 falcon==3.0.1
 falcon==3.0.1
 gunicorn==20.1.0
 gunicorn==20.1.0
 jinja2==3.0.3
 jinja2==3.0.3
-pony==0.7.14
+pony==0.7.16
 psycopg2cffi==2.9.0; implementation_name=='pypy'
 psycopg2cffi==2.9.0; implementation_name=='pypy'

+ 5 - 2
frameworks/Python/falcon/requirements.txt

@@ -1,9 +1,12 @@
+asyncpg==0.27.0
 Cython==0.29.26
 Cython==0.29.26
-falcon==3.0.1
+falcon==3.1.1
 gunicorn==20.1.0
 gunicorn==20.1.0
 jinja2==3.0.3
 jinja2==3.0.3
 meinheld==1.0.2
 meinheld==1.0.2
 orjson==3.6.5
 orjson==3.6.5
-pony==0.7.14
+pony==0.7.16
 psycopg2-binary==2.9.3; implementation_name=='cpython'
 psycopg2-binary==2.9.3; implementation_name=='cpython'
+tortoise-orm==0.19.2
+uvicorn==0.20.0
 waitress==2.1.2
 waitress==2.1.2

+ 25 - 0
frameworks/Python/falcon/tortoise_models.py

@@ -0,0 +1,25 @@
+from tortoise.models import Model
+from tortoise import fields
+
+
+class World(Model):
+    class Meta:
+        table = "world"
+
+    id = fields.IntField(pk=True)
+    randomNumber = fields.IntField(source_field="randomnumber")
+
+    def to_dict(self):
+        """Return object data in easily serializeable format"""
+        return {"id": self.id, "randomNumber": self.randomNumber}
+
+
+class Fortune(Model):
+    class Meta:
+        table = "fortune"
+
+    id = fields.IntField(pk=True)
+    message = fields.TextField()
+
+
+__models__ = [World, Fortune]