Skip to content

Commit 828d185

Browse files
committed
feat: add support for litestar
1 parent 9e4fadb commit 828d185

30 files changed

+1107
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Litestar Benchmarking Test
2+
3+
This is the Litestar portion of a [benchmarking tests suite](../../)
4+
comparing a variety of web development platforms.
5+
6+
The information below is specific to Litestar. For further guidance,
7+
review the [documentation](https://github.com/TechEmpower/FrameworkBenchmarks/wiki).
8+
Also note that there is additional information provided in
9+
the [Python README](../).
10+
11+
## Description
12+
13+
[**Litestar**](https://github.com/tiangolo/fastapi) is a modern, fast (high-performance), web framework for building APIs with Python 3.6+.
14+
15+
The key features are:
16+
17+
* **Fast**: Very high performance, on par with **NodeJS** and **Go**.
18+
19+
* **Fast to code**: Increase the speed to develop features by about 200% to 300% *.
20+
* **Less bugs**: Reduce about 40% of human (developer) induced errors. *
21+
* **Intuitive**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging.
22+
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
23+
* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Less bugs.
24+
* **Robust**: Get production-ready code. With automatic interactive documentation.
25+
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
26+
27+
<small>* estimation based on tests on an internal development team, building production applications.</small>
28+
29+
## Test Paths & Sources
30+
31+
All of the test implementations are located within a single file ([app.py](app.py)).
32+
33+
34+
## Resources
35+
36+
* [Litestar source code on GitHub](https://github.com/litestar-org/litestar)
37+
* [Litestar website - documentation](https://litestar.dev)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import os
2+
import multiprocessing
3+
import logging
4+
5+
import orjson
6+
from litestar import Litestar, get, MediaType
7+
from socketify import ASGI
8+
9+
10+
app = Litestar()
11+
12+
@get("/json")
13+
async def json_serialization():
14+
return orjson.dumps({"message": "Hello, world!"})
15+
16+
@get("/plaintext", media_type=MediaType.TEXT)
17+
async def plaintext():
18+
return b"Hello, world!"
19+
20+
21+
_is_travis = os.environ.get('TRAVIS') == 'true'
22+
23+
workers = int(multiprocessing.cpu_count())
24+
if _is_travis:
25+
workers = 2
26+
27+
def run_app():
28+
ASGI(app).listen(8080, lambda config: logging.info(f"Listening on port http://localhost:{config.port} now\n")).run()
29+
30+
31+
def create_fork():
32+
n = os.fork()
33+
# n greater than 0 means parent process
34+
if not n > 0:
35+
run_app()
36+
37+
38+
# fork limiting the cpu count - 1
39+
for i in range(1, workers):
40+
create_fork()
41+
42+
run_app() # run app on the main process too :)

frameworks/Python/litestar/app.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import multiprocessing
2+
from contextlib import asynccontextmanager
3+
from pathlib import Path
4+
5+
import asyncpg
6+
import os
7+
8+
import orjson
9+
from litestar import Litestar, Request, get, MediaType
10+
11+
from random import randint, sample
12+
13+
from litestar.contrib.jinja import JinjaTemplateEngine
14+
from litestar.response import Template
15+
from litestar.template import TemplateConfig
16+
17+
READ_ROW_SQL = 'SELECT "id", "randomnumber" FROM "world" WHERE id = $1'
18+
WRITE_ROW_SQL = 'UPDATE "world" SET "randomnumber"=$1 WHERE id=$2'
19+
ADDITIONAL_ROW = [0, "Additional fortune added at request time."]
20+
MAX_POOL_SIZE = 1000//multiprocessing.cpu_count()
21+
MIN_POOL_SIZE = max(int(MAX_POOL_SIZE / 2), 1)
22+
23+
24+
def get_num_queries(queries):
25+
try:
26+
query_count = int(queries)
27+
except (ValueError, TypeError):
28+
return 1
29+
30+
if query_count < 1:
31+
return 1
32+
if query_count > 500:
33+
return 500
34+
return query_count
35+
36+
37+
connection_pool = None
38+
39+
40+
41+
async def setup_database():
42+
return await asyncpg.create_pool(
43+
user=os.getenv("PGUSER", "benchmarkdbuser"),
44+
password=os.getenv("PGPASS", "benchmarkdbpass"),
45+
database="hello_world",
46+
host="tfb-database",
47+
port=5432,
48+
min_size=MIN_POOL_SIZE,
49+
max_size=MAX_POOL_SIZE,
50+
)
51+
52+
53+
@asynccontextmanager
54+
async def lifespan(app: Litestar):
55+
# Set up the database connection pool
56+
app.state.connection_pool = await setup_database()
57+
yield
58+
# Close the database connection pool
59+
await app.state.connection_pool.close()
60+
61+
62+
app = Litestar(lifespan=[lifespan], template_config=TemplateConfig(
63+
directory=Path("templates"),
64+
engine=JinjaTemplateEngine,
65+
),)
66+
67+
68+
@get("/json")
69+
async def json_serialization():
70+
return orjson.dumps({"message": "Hello, world!"})
71+
72+
73+
@get("/db")
74+
async def single_database_query():
75+
row_id = randint(1, 10000)
76+
async with app.state.connection_pool.acquire() as connection:
77+
number = await connection.fetchval(READ_ROW_SQL, row_id)
78+
79+
return orjson.dumps({"id": row_id, "randomNumber": number})
80+
81+
82+
@get("/queries")
83+
async def multiple_database_queries(queries = None):
84+
num_queries = get_num_queries(queries)
85+
row_ids = sample(range(1, 10000), num_queries)
86+
worlds = []
87+
88+
async with app.state.connection_pool.acquire() as connection:
89+
statement = await connection.prepare(READ_ROW_SQL)
90+
for row_id in row_ids:
91+
number = await statement.fetchval(row_id)
92+
worlds.append({"id": row_id, "randomNumber": number})
93+
94+
return orjson.dumps(worlds)
95+
96+
97+
@get("/fortunes")
98+
async def fortunes(request: Request):
99+
async with app.state.connection_pool.acquire() as connection:
100+
fortunes = await connection.fetch("SELECT * FROM Fortune")
101+
102+
fortunes.append(ADDITIONAL_ROW)
103+
fortunes.sort(key=lambda row: row[1])
104+
return Template("fortune.html", context={"fortunes": fortunes, "request": request})
105+
106+
107+
@get("/updates")
108+
async def database_updates(queries = None):
109+
num_queries = get_num_queries(queries)
110+
# To avoid deadlock
111+
ids = sorted(sample(range(1, 10000 + 1), num_queries))
112+
numbers = sorted(sample(range(1, 10000), num_queries))
113+
updates = list(zip(ids, numbers))
114+
115+
worlds = [
116+
{"id": row_id, "randomNumber": number} for row_id, number in updates
117+
]
118+
119+
async with app.state.connection_pool.acquire() as connection:
120+
statement = await connection.prepare(READ_ROW_SQL)
121+
for row_id, _ in updates:
122+
await statement.fetchval(row_id)
123+
await connection.executemany(WRITE_ROW_SQL, updates)
124+
125+
return orjson.dumps(worlds)
126+
127+
128+
@get("/plaintext", media_type=MediaType.TEXT)
129+
async def plaintext():
130+
return b"Hello, world!"
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import logging
2+
import multiprocessing
3+
import os
4+
from contextlib import asynccontextmanager
5+
from operator import attrgetter
6+
from pathlib import Path
7+
from random import randint, sample
8+
9+
import ujson5
10+
from litestar.contrib.jinja import JinjaTemplateEngine
11+
from litestar.response import Template
12+
from litestar.template import TemplateConfig
13+
from sqlalchemy import Column, Integer, String, select
14+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
15+
from sqlalchemy.ext.declarative import declarative_base
16+
from sqlalchemy.orm.attributes import flag_modified
17+
18+
from litestar import Litestar, Request, MediaType, get
19+
20+
logger = logging.getLogger(__name__)
21+
22+
Base = declarative_base()
23+
24+
25+
class World(Base):
26+
__tablename__ = "world"
27+
id = Column(Integer, primary_key=True)
28+
randomnumber = Column(Integer)
29+
30+
def __json__(self):
31+
return {"id": self.id, "randomnumber": self.randomnumber}
32+
33+
34+
sa_data = World.__table__
35+
36+
37+
class Fortune(Base):
38+
__tablename__ = "fortune"
39+
id = Column(Integer, primary_key=True)
40+
message = Column(String)
41+
42+
43+
sa_fortunes = Fortune.__table__
44+
45+
ADDITIONAL_FORTUNE = Fortune(
46+
id=0, message="Additional fortune added at request time."
47+
)
48+
MAX_POOL_SIZE = 1000//multiprocessing.cpu_count()
49+
50+
sort_fortunes_key = attrgetter("message")
51+
52+
template_path = os.path.join(
53+
os.path.dirname(os.path.realpath(__file__)), "templates"
54+
)
55+
56+
57+
async def setup_database():
58+
dsn = "postgresql+asyncpg://%s:%s@tfb-database:5432/hello_world" % (
59+
os.getenv("PGPASS", "benchmarkdbuser"),
60+
os.getenv("PGPASS", "benchmarkdbpass"),
61+
)
62+
63+
engine = create_async_engine(
64+
dsn,
65+
future=True,
66+
pool_size=MAX_POOL_SIZE,
67+
connect_args={
68+
"ssl": False # NEEDED FOR NGINX-UNIT OTHERWISE IT FAILS
69+
},
70+
)
71+
return async_sessionmaker(engine)
72+
73+
74+
@asynccontextmanager
75+
async def lifespan(app: Litestar):
76+
# Set up the database connection pool
77+
app.state.db_session = await setup_database()
78+
yield
79+
# Close the database connection pool
80+
await app.state.db_session().close()
81+
82+
83+
app = Litestar(
84+
template_config=TemplateConfig(
85+
directory=Path("templates"),
86+
engine=JinjaTemplateEngine,
87+
),
88+
lifespan=[lifespan],
89+
)
90+
91+
92+
def get_num_queries(queries):
93+
try:
94+
query_count = int(queries)
95+
except (ValueError, TypeError):
96+
return 1
97+
98+
if query_count < 1:
99+
return 1
100+
if query_count > 500:
101+
return 500
102+
return query_count
103+
104+
105+
@get("/json")
106+
async def json_serialization():
107+
return ujson5.dumps({"message": "Hello, world!"})
108+
109+
110+
@get("/db")
111+
async def single_database_query():
112+
id_ = randint(1, 10000)
113+
114+
async with app.state.db_session() as sess:
115+
result = await sess.get(World, id_)
116+
117+
return ujson5.dumps(result.__json__())
118+
119+
120+
@get("/queries")
121+
async def multiple_database_queries(queries=None):
122+
num_queries = get_num_queries(queries)
123+
data = []
124+
125+
async with app.state.db_session() as sess:
126+
for id_ in sample(range(1, 10001), num_queries):
127+
result = await sess.get(World, id_)
128+
data.append(result.__json__())
129+
130+
return ujson5.dumps(data)
131+
132+
133+
@get("/fortunes")
134+
async def fortunes(request: Request):
135+
async with app.state.db_session() as sess:
136+
ret = await sess.execute(select(Fortune.id, Fortune.message))
137+
data = ret.all()
138+
139+
data.append(ADDITIONAL_FORTUNE)
140+
data.sort(key=sort_fortunes_key)
141+
142+
return Template(
143+
"fortune.jinja", context={"request": request, "fortunes": data}
144+
)
145+
146+
147+
@get("/updates")
148+
async def database_updates(queries=None):
149+
num_queries = get_num_queries(queries)
150+
151+
ids = sorted(sample(range(1, 10000 + 1), num_queries))
152+
data = []
153+
async with app.state.db_session.begin() as sess:
154+
for id_ in ids:
155+
world = await sess.get(World, id_, populate_existing=True)
156+
world.randomnumber = randint(1, 10000)
157+
# force sqlalchemy to UPDATE entry even if the value has not changed
158+
# doesn't make sense in a real application, added only for pass `tfb verify`
159+
flag_modified(world, "randomnumber")
160+
data.append(world.__json__())
161+
162+
return ujson5.dumps(data)
163+
164+
165+
@get("/plaintext", media_type=MediaType.TEXT)
166+
async def plaintext():
167+
return b"Hello, world!"

0 commit comments

Comments
 (0)