Skip to content

Commit 6629f7c

Browse files
authored
[Python] Upgrade Blacksheep framework (#9828)
* feat: upgrade * add: nginx-unit * remove: cur_loop * feat: use Struct * change config.toml * fix: blacksheep-nginx-unit-build
1 parent 2696c98 commit 6629f7c

11 files changed

+160
-133
lines changed
Lines changed: 55 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,44 @@
1+
import multiprocessing
12
import os
2-
import ujson
33
import asyncpg
4-
import multiprocessing
54
import random
5+
import asyncio
6+
from operator import itemgetter
67
import blacksheep as bs
78
import jinja2
8-
from email.utils import formatdate
9-
10-
try:
11-
from ujson import dumps as jsonify
12-
except:
13-
from json import dumps as jsonify
14-
15-
16-
_is_travis = os.environ.get('TRAVIS') == 'true'
17-
18-
_is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
19-
20-
_cpu_count = multiprocessing.cpu_count()
21-
if _is_travis:
22-
_cpu_count = 2
23-
24-
25-
#from blacksheep.settings.json import json_settings
26-
#json_settings.use(dumps=jsonify)
27-
28-
DBDRV = "postgres"
29-
DBHOST = "tfb-database"
30-
DBUSER = "benchmarkdbuser"
31-
DBPSWD = "benchmarkdbpass"
9+
import msgspec
10+
from pathlib import Path
3211

3312
READ_ROW_SQL = 'SELECT "id", "randomnumber" FROM "world" WHERE id = $1'
3413
WRITE_ROW_SQL = 'UPDATE "world" SET "randomnumber"=$1 WHERE id=$2'
3514
ADDITIONAL_ROW = [0, "Additional fortune added at request time."]
3615
MAX_POOL_SIZE = 1000 // multiprocessing.cpu_count()
3716
MIN_POOL_SIZE = max(int(MAX_POOL_SIZE / 2), 1)
38-
3917
db_pool = None
18+
key = itemgetter(1)
4019

41-
g_response_server = None
42-
g_response_add_date = False
43-
20+
try:
21+
import uvloop
22+
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
23+
except Exception:
24+
...
4425

4526
async def setup_db(app):
4627
global db_pool
4728
db_pool = await asyncpg.create_pool(
48-
user=os.getenv('PGUSER', DBUSER),
49-
password=os.getenv('PGPASS', DBPSWD),
29+
user=os.getenv('PGUSER', "benchmarkdbuser"),
30+
password=os.getenv('PGPASS', "benchmarkdbpass"),
5031
database='hello_world',
51-
host=DBHOST,
32+
host="tfb-database",
5233
port=5432,
5334
min_size=MIN_POOL_SIZE,
54-
max_size=MAX_POOL_SIZE,
35+
max_size=MAX_POOL_SIZE,
5536
)
5637

5738

5839
def load_fortunes_template():
59-
path = os.path.join('templates', 'fortune.html')
60-
with open(path, 'r') as template_file:
61-
template_text = template_file.read()
62-
return jinja2.Template(template_text)
40+
with Path("templates/fortune.html").open("r") as f:
41+
return jinja2.Template(f.read())
6342

6443

6544
fortune_template = load_fortunes_template()
@@ -82,139 +61,92 @@ def get_num_queries(request):
8261
return 500
8362
return query_count
8463

64+
ENCODER = msgspec.json.Encoder()
65+
DECODER = msgspec.json.Decoder()
66+
JSON_CONTENT_TYPE = b"application/json"
67+
def jsonify(
68+
data,
69+
status=200,
70+
headers=None,
71+
):
72+
"""
73+
Returns a response with application/json content,
74+
and given status (default HTTP 200 OK).
75+
"""
76+
return bs.Response(
77+
status=status,
78+
headers=headers,
79+
content=bs.Content(content_type=JSON_CONTENT_TYPE, data=ENCODER.encode(data)),
80+
)
8581

86-
# ------------------------------------------------------------------------------------------
87-
88-
async def bs_middleware(request, handler):
89-
global g_response_server, g_response_add_date
90-
response = await handler(request)
91-
if g_response_server:
92-
response.headers[b'Server'] = g_response_server
93-
if g_response_add_date:
94-
response.headers[b'Date'] = formatdate(timeval=None, localtime=False, usegmt=True)
95-
return response
82+
class Result(msgspec.Struct):
83+
id: int
84+
randomNumber: int
9685

86+
# ------------------------------------------------------------------------------------------
9787

98-
@app.route('/json')
88+
@bs.get('/json')
9989
async def json_test(request):
100-
return bs.json( {'message': 'Hello, world!'} )
101-
90+
return jsonify( {'message': 'Hello, world!'} )
10291

103-
@app.route('/db')
92+
@bs.get('/db')
10493
async def single_db_query_test(request):
10594
row_id = random.randint(1, 10000)
10695

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

110-
world = {'id': row_id, 'randomNumber': number}
111-
return bs.json(world)
99+
return jsonify(Result(id=row_id, randomNumber=number))
100+
# return ({'id': row_id, 'randomNumber': number})
112101

113102

114-
@app.route('/queries')
103+
@bs.get('/queries')
115104
async def multiple_db_queries_test(request):
116105
num_queries = get_num_queries(request)
117106
row_ids = random.sample(range(1, 10000), num_queries)
118-
worlds = [ ]
107+
worlds = []
119108

120109
async with db_pool.acquire() as db_conn:
121110
statement = await db_conn.prepare(READ_ROW_SQL)
122111
for row_id in row_ids:
123112
number = await statement.fetchval(row_id)
124-
worlds.append( {"id": row_id, "randomNumber": number} )
113+
# worlds.append( {"id": row_id, "randomNumber": number} )
114+
worlds.append(Result(id=row_id, randomNumber=number))
125115

126-
return bs.json(worlds)
116+
return jsonify(worlds)
127117

128118

129-
@app.route('/fortunes')
119+
@bs.get('/fortunes')
130120
async def fortunes_test(request):
131121
async with db_pool.acquire() as db_conn:
132122
fortunes = await db_conn.fetch("SELECT * FROM Fortune")
133123

134124
fortunes.append(ADDITIONAL_ROW)
135-
fortunes.sort(key = lambda row: row[1])
125+
fortunes.sort(key = key)
136126
data = fortune_template.render(fortunes=fortunes)
137127
return bs.html(data)
138128

139129

140-
@app.route('/updates')
130+
@bs.get('/updates')
141131
async def db_updates_test(request):
142132
num_queries = get_num_queries(request)
143133
ids = sorted(random.sample(range(1, 10000 + 1), num_queries))
144134
numbers = sorted(random.sample(range(1, 10000), num_queries))
145135
updates = list(zip(ids, numbers))
146136

147-
worlds = [ {"id": row_id, "randomNumber": number} for row_id, number in updates ]
148-
137+
# worlds = [ {"id": row_id, "randomNumber": number} for row_id, number in updates ]
138+
worlds = [Result(id=row_id, randomNumber=number) for row_id, number in updates]
149139
async with db_pool.acquire() as db_conn:
150140
statement = await db_conn.prepare(READ_ROW_SQL)
151141
for row_id, _ in updates:
152142
await statement.fetchval(row_id)
153143
await db_conn.executemany(WRITE_ROW_SQL, updates)
154144

155-
return bs.json(worlds)
145+
return jsonify(worlds)
156146

157147

158-
@app.route('/plaintext')
148+
@bs.get('/plaintext')
159149
async def plaintext_test(request):
160150
return bs.Response(200, content=bs.Content(b"text/plain", b'Hello, World!'))
161151
#return bs.text('Hello, World!')
162152

163-
164-
# -----------------------------------------------------------------------------------
165-
166-
if __name__ == "__main__":
167-
import optparse
168-
import logging
169-
import re
170-
171-
parser = optparse.OptionParser("usage: %prog [options]", add_help_option=False)
172-
parser.add_option("-h", "--host", dest="host", default='0.0.0.0', type="string")
173-
parser.add_option("-p", "--port", dest="port", default=8080, type="int")
174-
parser.add_option("-s", "--server", dest="server", default="uvicorn", type="string")
175-
parser.add_option("-w", "--workers", dest="workers", default=0, type="int")
176-
parser.add_option("-k", "--keepalive", dest="keepalive", default=60, type="int")
177-
parser.add_option("-v", "--verbose", dest="verbose", default=0, type="int")
178-
(opt, args) = parser.parse_args()
179-
180-
workers = _cpu_count
181-
if workers > 0:
182-
workers = opt.workers
183-
184-
if _is_travis:
185-
workers = 2
186-
187-
def run_app():
188-
global g_response_server, g_response_add_date
189-
190-
if opt.gateway == "uvicorn":
191-
import uvicorn
192-
log_level = logging.ERROR
193-
uvicorn.run(app, host=opt.host, port=opt.port, workers=1, loop="uvloop", log_level=log_level, access_log=False)
194-
195-
if opt.server == 'fastwsgi':
196-
import fastwsgi
197-
from blacksheep.utils.aio import get_running_loop
198-
g_response_server = b'FastWSGI'
199-
app.middlewares.append(bs_middleware)
200-
loop = get_running_loop()
201-
loop.run_until_complete(app.start())
202-
fastwsgi.run(app, host=opt.host, port=opt.port, loglevel=opt.verbose)
203-
204-
if opt.server == 'socketify':
205-
import socketify
206-
msg = "Listening on http://0.0.0.0:{port} now\n".format(port=opt.port)
207-
socketify.WSGI(app).listen(opt.port, lambda config: logging.info(msg)).run()
208-
209-
def create_fork():
210-
n = os.fork()
211-
# n greater than 0 means parent process
212-
if not n > 0:
213-
run_app()
214-
215-
# fork limiting the cpu count - 1
216-
for i in range(1, workers):
217-
create_fork()
218-
219-
run_app() # run app on the main process too :)
220-

frameworks/Python/blacksheep/benchmark_config.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,29 @@
2323
"display_name": "blacksheep",
2424
"versus": "None",
2525
"notes": ""
26+
},
27+
"nginx-unit": {
28+
"json_url": "/json",
29+
"fortune_url": "/fortunes",
30+
"plaintext_url": "/plaintext",
31+
"db_url": "/db",
32+
"query_url": "/queries?queries=",
33+
"update_url": "/updates?queries=",
34+
"port": 8080,
35+
"approach": "Realistic",
36+
"classification": "Micro",
37+
"framework": "blacksheep",
38+
"language": "Python",
39+
"flavor": "Python3",
40+
"platform": "ASGI",
41+
"webserver": "nginx-unit",
42+
"os": "Linux",
43+
"orm": "Raw",
44+
"database_os": "Linux",
45+
"database": "Postgres",
46+
"display_name": "blacksheep-nginx-unit",
47+
"versus": "None",
48+
"notes": ""
2649
}
2750
}]
2851
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM unit:python3.13-slim
2+
3+
WORKDIR /blacksheep
4+
5+
COPY ./ /blacksheep
6+
7+
RUN pip3 install -U pip
8+
RUN pip3 install Cython==3.0.12
9+
RUN pip3 install -r /blacksheep/requirements.txt
10+
RUN pip3 install -r /blacksheep/requirements-uvicorn.txt
11+
12+
RUN chmod +x start-unit.sh
13+
14+
ENV PGSSLMODE=disable
15+
16+
EXPOSE 8080
17+
CMD ["./start-unit.sh"]

frameworks/Python/blacksheep/blacksheep.dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
FROM python:3.11-bullseye
1+
FROM python:3.13
22

33
WORKDIR /blacksheep
44

55
COPY ./ /blacksheep
66

77
RUN pip3 install -U pip
8-
RUN pip3 install cython==0.29.34
8+
RUN pip3 install Cython==3.0.12
99
RUN pip3 install -r /blacksheep/requirements.txt
1010
RUN pip3 install -r /blacksheep/requirements-gunicorn.txt
1111
RUN pip3 install -r /blacksheep/requirements-uvicorn.txt

frameworks/Python/blacksheep/config.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,21 @@ orm = "Raw"
1717
platform = "ASGI"
1818
webserver = "uvicorn"
1919
versus = "None"
20+
21+
22+
[nginx-unit]
23+
urls.plaintext = "/plaintext"
24+
urls.json = "/json"
25+
urls.db = "/db"
26+
urls.query = "/queries?queries="
27+
urls.update = "/updates?queries="
28+
urls.fortune = "/fortunes"
29+
approach = "Realistic"
30+
classification = "Platform"
31+
database = "Postgres"
32+
database_os = "Linux"
33+
os = "Linux"
34+
orm = "Raw"
35+
platform = "ASGI"
36+
webserver = "nginx-unit"
37+
versus = "None"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
gunicorn==22.0.0
1+
gunicorn==23.0.0
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
hypercorn==0.14.3
1+
hypercorn==0.17.3
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
uvloop==0.17.0
2-
uvicorn==0.21.1
3-
httptools==0.5.0
1+
uvloop==0.21.0
2+
uvicorn==0.34.1
3+
httptools==0.6.4
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
asyncpg==0.27.0
1+
asyncpg==0.30.0
22
Jinja2==3.1.6
3-
blacksheep==1.2.13
4-
ujson==5.7.0
3+
blacksheep==2.1.0
4+
msgspec==0.19.0
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
3+
NPROC=$(nproc)
4+
sed "s/{{NPROC}}/$NPROC/" unit-config.template.json > nginx-unit-config.json
5+
6+
unitd --no-daemon --control unix:/var/run/control.unit.sock &
7+
8+
# wait UNIT started
9+
sleep 1
10+
11+
# PUT configure
12+
curl -X PUT \
13+
--data-binary @nginx-unit-config.json \
14+
--unix-socket /var/run/control.unit.sock \
15+
http://localhost/config
16+
17+
# Then keep the container alive
18+
wait

0 commit comments

Comments
 (0)