Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 55 additions & 123 deletions frameworks/Python/blacksheep/app.py
Original file line number Diff line number Diff line change
@@ -1,65 +1,44 @@
import multiprocessing
import os
import ujson
import asyncpg
import multiprocessing
import random
import asyncio
from operator import itemgetter
import blacksheep as bs
import jinja2
from email.utils import formatdate

try:
from ujson import dumps as jsonify
except:
from json import dumps as jsonify


_is_travis = os.environ.get('TRAVIS') == 'true'

_is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")

_cpu_count = multiprocessing.cpu_count()
if _is_travis:
_cpu_count = 2


#from blacksheep.settings.json import json_settings
#json_settings.use(dumps=jsonify)

DBDRV = "postgres"
DBHOST = "tfb-database"
DBUSER = "benchmarkdbuser"
DBPSWD = "benchmarkdbpass"
import msgspec
from pathlib import Path

READ_ROW_SQL = 'SELECT "id", "randomnumber" 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."]
MAX_POOL_SIZE = 1000 // multiprocessing.cpu_count()
MIN_POOL_SIZE = max(int(MAX_POOL_SIZE / 2), 1)

db_pool = None
key = itemgetter(1)

g_response_server = None
g_response_add_date = False

try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except Exception:
...

async def setup_db(app):
global db_pool
db_pool = await asyncpg.create_pool(
user=os.getenv('PGUSER', DBUSER),
password=os.getenv('PGPASS', DBPSWD),
user=os.getenv('PGUSER', "benchmarkdbuser"),
password=os.getenv('PGPASS', "benchmarkdbpass"),
database='hello_world',
host=DBHOST,
host="tfb-database",
port=5432,
min_size=MIN_POOL_SIZE,
max_size=MAX_POOL_SIZE,
max_size=MAX_POOL_SIZE,
)


def load_fortunes_template():
path = os.path.join('templates', 'fortune.html')
with open(path, 'r') as template_file:
template_text = template_file.read()
return jinja2.Template(template_text)
with Path("templates/fortune.html").open("r") as f:
return jinja2.Template(f.read())


fortune_template = load_fortunes_template()
Expand All @@ -82,139 +61,92 @@ def get_num_queries(request):
return 500
return query_count

ENCODER = msgspec.json.Encoder()
DECODER = msgspec.json.Decoder()
JSON_CONTENT_TYPE = b"application/json"
def jsonify(
data,
status=200,
headers=None,
):
"""
Returns a response with application/json content,
and given status (default HTTP 200 OK).
"""
return bs.Response(
status=status,
headers=headers,
content=bs.Content(content_type=JSON_CONTENT_TYPE, data=ENCODER.encode(data)),
)

# ------------------------------------------------------------------------------------------

async def bs_middleware(request, handler):
global g_response_server, g_response_add_date
response = await handler(request)
if g_response_server:
response.headers[b'Server'] = g_response_server
if g_response_add_date:
response.headers[b'Date'] = formatdate(timeval=None, localtime=False, usegmt=True)
return response
class Result(msgspec.Struct):
id: int
randomNumber: int

# ------------------------------------------------------------------------------------------

@app.route('/json')
@bs.get('/json')
async def json_test(request):
return bs.json( {'message': 'Hello, world!'} )

return jsonify( {'message': 'Hello, world!'} )

@app.route('/db')
@bs.get('/db')
async def single_db_query_test(request):
row_id = random.randint(1, 10000)

async with db_pool.acquire() as db_conn:
number = await db_conn.fetchval(READ_ROW_SQL, row_id)

world = {'id': row_id, 'randomNumber': number}
return bs.json(world)
return jsonify(Result(id=row_id, randomNumber=number))
# return ({'id': row_id, 'randomNumber': number})


@app.route('/queries')
@bs.get('/queries')
async def multiple_db_queries_test(request):
num_queries = get_num_queries(request)
row_ids = random.sample(range(1, 10000), num_queries)
worlds = [ ]
worlds = []

async with db_pool.acquire() as db_conn:
statement = await db_conn.prepare(READ_ROW_SQL)
for row_id in row_ids:
number = await statement.fetchval(row_id)
worlds.append( {"id": row_id, "randomNumber": number} )
# worlds.append( {"id": row_id, "randomNumber": number} )
worlds.append(Result(id=row_id, randomNumber=number))

return bs.json(worlds)
return jsonify(worlds)


@app.route('/fortunes')
@bs.get('/fortunes')
async def fortunes_test(request):
async with db_pool.acquire() as db_conn:
fortunes = await db_conn.fetch("SELECT * FROM Fortune")

fortunes.append(ADDITIONAL_ROW)
fortunes.sort(key = lambda row: row[1])
fortunes.sort(key = key)
data = fortune_template.render(fortunes=fortunes)
return bs.html(data)


@app.route('/updates')
@bs.get('/updates')
async def db_updates_test(request):
num_queries = get_num_queries(request)
ids = sorted(random.sample(range(1, 10000 + 1), num_queries))
numbers = sorted(random.sample(range(1, 10000), num_queries))
updates = list(zip(ids, numbers))

worlds = [ {"id": row_id, "randomNumber": number} for row_id, number in updates ]

# worlds = [ {"id": row_id, "randomNumber": number} for row_id, number in updates ]
worlds = [Result(id=row_id, randomNumber=number) for row_id, number in updates]
async with db_pool.acquire() as db_conn:
statement = await db_conn.prepare(READ_ROW_SQL)
for row_id, _ in updates:
await statement.fetchval(row_id)
await db_conn.executemany(WRITE_ROW_SQL, updates)

return bs.json(worlds)
return jsonify(worlds)


@app.route('/plaintext')
@bs.get('/plaintext')
async def plaintext_test(request):
return bs.Response(200, content=bs.Content(b"text/plain", b'Hello, World!'))
#return bs.text('Hello, World!')


# -----------------------------------------------------------------------------------

if __name__ == "__main__":
import optparse
import logging
import re

parser = optparse.OptionParser("usage: %prog [options]", add_help_option=False)
parser.add_option("-h", "--host", dest="host", default='0.0.0.0', type="string")
parser.add_option("-p", "--port", dest="port", default=8080, type="int")
parser.add_option("-s", "--server", dest="server", default="uvicorn", type="string")
parser.add_option("-w", "--workers", dest="workers", default=0, type="int")
parser.add_option("-k", "--keepalive", dest="keepalive", default=60, type="int")
parser.add_option("-v", "--verbose", dest="verbose", default=0, type="int")
(opt, args) = parser.parse_args()

workers = _cpu_count
if workers > 0:
workers = opt.workers

if _is_travis:
workers = 2

def run_app():
global g_response_server, g_response_add_date

if opt.gateway == "uvicorn":
import uvicorn
log_level = logging.ERROR
uvicorn.run(app, host=opt.host, port=opt.port, workers=1, loop="uvloop", log_level=log_level, access_log=False)

if opt.server == 'fastwsgi':
import fastwsgi
from blacksheep.utils.aio import get_running_loop
g_response_server = b'FastWSGI'
app.middlewares.append(bs_middleware)
loop = get_running_loop()
loop.run_until_complete(app.start())
fastwsgi.run(app, host=opt.host, port=opt.port, loglevel=opt.verbose)

if opt.server == 'socketify':
import socketify
msg = "Listening on http://0.0.0.0:{port} now\n".format(port=opt.port)
socketify.WSGI(app).listen(opt.port, lambda config: logging.info(msg)).run()

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 :)

23 changes: 23 additions & 0 deletions frameworks/Python/blacksheep/benchmark_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,29 @@
"display_name": "blacksheep",
"versus": "None",
"notes": ""
},
"nginx-unit": {
"json_url": "/json",
"fortune_url": "/fortunes",
"plaintext_url": "/plaintext",
"db_url": "/db",
"query_url": "/queries?queries=",
"update_url": "/updates?queries=",
"port": 8080,
"approach": "Realistic",
"classification": "Micro",
"framework": "blacksheep",
"language": "Python",
"flavor": "Python3",
"platform": "ASGI",
"webserver": "nginx-unit",
"os": "Linux",
"orm": "Raw",
"database_os": "Linux",
"database": "Postgres",
"display_name": "blacksheep-nginx-unit",
"versus": "None",
"notes": ""
}
}]
}
17 changes: 17 additions & 0 deletions frameworks/Python/blacksheep/blacksheep-nginx-unit.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM unit:python3.13-slim

WORKDIR /blacksheep

COPY ./ /blacksheep

RUN pip3 install -U pip
RUN pip3 install Cython==3.0.12
RUN pip3 install -r /blacksheep/requirements.txt
RUN pip3 install -r /blacksheep/requirements-uvicorn.txt

RUN chmod +x start-unit.sh

ENV PGSSLMODE=disable

EXPOSE 8080
CMD ["./start-unit.sh"]
4 changes: 2 additions & 2 deletions frameworks/Python/blacksheep/blacksheep.dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
FROM python:3.11-bullseye
FROM python:3.13

WORKDIR /blacksheep

COPY ./ /blacksheep

RUN pip3 install -U pip
RUN pip3 install cython==0.29.34
RUN pip3 install Cython==3.0.12
RUN pip3 install -r /blacksheep/requirements.txt
RUN pip3 install -r /blacksheep/requirements-gunicorn.txt
RUN pip3 install -r /blacksheep/requirements-uvicorn.txt
Expand Down
18 changes: 18 additions & 0 deletions frameworks/Python/blacksheep/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,21 @@ orm = "Raw"
platform = "ASGI"
webserver = "uvicorn"
versus = "None"


[nginx-unit]
urls.plaintext = "/plaintext"
urls.json = "/json"
urls.db = "/db"
urls.query = "/queries?queries="
urls.update = "/updates?queries="
urls.fortune = "/fortunes"
approach = "Realistic"
classification = "Platform"
database = "Postgres"
database_os = "Linux"
os = "Linux"
orm = "Raw"
platform = "ASGI"
webserver = "nginx-unit"
versus = "None"
2 changes: 1 addition & 1 deletion frameworks/Python/blacksheep/requirements-gunicorn.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
gunicorn==22.0.0
gunicorn==23.0.0
2 changes: 1 addition & 1 deletion frameworks/Python/blacksheep/requirements-hypercorn.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
hypercorn==0.14.3
hypercorn==0.17.3
6 changes: 3 additions & 3 deletions frameworks/Python/blacksheep/requirements-uvicorn.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
uvloop==0.17.0
uvicorn==0.21.1
httptools==0.5.0
uvloop==0.21.0
uvicorn==0.34.1
httptools==0.6.4
6 changes: 3 additions & 3 deletions frameworks/Python/blacksheep/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
asyncpg==0.27.0
asyncpg==0.30.0
Jinja2==3.1.6
blacksheep==1.2.13
ujson==5.7.0
blacksheep==2.1.0
msgspec==0.19.0
18 changes: 18 additions & 0 deletions frameworks/Python/blacksheep/start-unit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash

NPROC=$(nproc)
sed "s/{{NPROC}}/$NPROC/" unit-config.template.json > nginx-unit-config.json

unitd --no-daemon --control unix:/var/run/control.unit.sock &

# wait UNIT started
sleep 1

# PUT configure
curl -X PUT \
--data-binary @nginx-unit-config.json \
--unix-socket /var/run/control.unit.sock \
http://localhost/config

# Then keep the container alive
wait
Loading
Loading