Skip to content

Commit bd1f115

Browse files
authored
Initial BenchKit backend implementation (#1018)
1 parent 2b5ba09 commit bd1f115

File tree

10 files changed

+836
-1
lines changed

10 files changed

+836
-1
lines changed

.dockerignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.*/
2+
build
3+
dist
4+
venv*/
5+
.coverage
6+
tests/
7+
testkit/
8+
testkitbackend/

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ repos:
3737
- id: mypy
3838
name: mypy static type check
3939
entry: mypy
40-
args: [ --show-error-codes, src, tests, testkitbackend ]
40+
args: [ --show-error-codes, src, tests, testkitbackend, benchkit ]
4141
'types_or': [ python, pyi ]
4242
language: system
4343
pass_filenames: false

benchkit/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM python:3.12
2+
3+
WORKDIR /driver
4+
5+
COPY . /driver
6+
7+
# Install dependencies
8+
RUN pip install -U pip && \
9+
pip install -Ur requirements-dev.txt
10+
11+
ENTRYPOINT ["python", "-m", "benchkit"]

benchkit/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from __future__ import annotations

benchkit/__main__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from __future__ import annotations
2+
3+
from sanic import Sanic
4+
from sanic.worker.loader import AppLoader
5+
6+
from .app import create_app
7+
from .env import env
8+
9+
10+
if __name__ == '__main__':
11+
loader = AppLoader(factory=create_app)
12+
app = loader.load()
13+
14+
# For local development:
15+
# app.prepare(port=env.backend_port, debug=True, workers=1, dev=True)
16+
17+
# For production:
18+
app.prepare(host="0.0.0.0", port=env.backend_port, workers=1)
19+
20+
Sanic.serve(primary=app, app_loader=loader)

benchkit/app.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from __future__ import annotations
2+
3+
from contextlib import contextmanager
4+
from multiprocessing import Semaphore
5+
6+
import typing_extensions as te
7+
from sanic import Sanic
8+
from sanic.config import Config
9+
from sanic.exceptions import (
10+
BadRequest,
11+
NotFound,
12+
)
13+
from sanic.request import Request
14+
from sanic.response import (
15+
empty,
16+
HTTPResponse,
17+
text,
18+
)
19+
20+
from .context import BenchKitContext
21+
from .env import env
22+
from .workloads import Workload
23+
24+
25+
T_App: te.TypeAlias = "Sanic[Config, BenchKitContext]"
26+
27+
28+
def create_app() -> T_App:
29+
app: T_App = Sanic("Python_BenchKit", ctx=BenchKitContext())
30+
31+
@app.main_process_start
32+
async def main_process_start(app: T_App) -> None:
33+
app.shared_ctx.running = Semaphore(1)
34+
35+
@app.before_server_start
36+
async def before_server_start(app: T_App) -> None:
37+
if env.driver_debug:
38+
from neo4j.debug import watch
39+
watch("neo4j")
40+
41+
running = app.shared_ctx.running
42+
acquired = running.acquire(block=False)
43+
if not acquired:
44+
raise RuntimeError(
45+
"The server does not support multiple worker processes"
46+
)
47+
48+
@app.after_server_stop
49+
async def after_server_stop(app: T_App) -> None:
50+
await app.ctx.shutdown()
51+
running = app.shared_ctx.running
52+
running.release()
53+
54+
@contextmanager
55+
def _loading_workload():
56+
try:
57+
yield
58+
except (ValueError, TypeError) as e:
59+
print(e)
60+
raise BadRequest(str(e))
61+
62+
def _get_workload(app: T_App, name: str) -> Workload:
63+
try:
64+
workload = app.ctx.workloads[name]
65+
except KeyError:
66+
raise NotFound(f"Workload {name} not found")
67+
return workload
68+
69+
@app.get("/ready")
70+
async def ready(_: Request) -> HTTPResponse:
71+
await app.ctx.get_db() # check that the database is available
72+
return empty()
73+
74+
@app.post("/workload")
75+
async def post_workload(request: Request) -> HTTPResponse:
76+
data = request.json
77+
with _loading_workload():
78+
name = app.ctx.workloads.store_workload(data)
79+
location = f"/workload/{name}"
80+
return text(f"created at {location}",
81+
status=204,
82+
headers={"location": location})
83+
84+
@app.put("/workload")
85+
async def put_workload(request: Request) -> HTTPResponse:
86+
data = request.json
87+
with _loading_workload():
88+
workload = app.ctx.workloads.parse_workload(data)
89+
driver = await app.ctx.get_db()
90+
await workload(driver)
91+
return empty()
92+
93+
@app.get("/workload/<name>")
94+
async def get_workload(_: Request, name: str) -> HTTPResponse:
95+
workload = _get_workload(app, name)
96+
driver = await app.ctx.get_db()
97+
await workload(driver)
98+
return empty()
99+
100+
@app.patch("/workload/<name>")
101+
async def patch_workload(request: Request, name: str) -> HTTPResponse:
102+
data = request.json
103+
workload = _get_workload(app, name)
104+
with _loading_workload():
105+
workload.patch(data)
106+
return empty()
107+
108+
@app.delete("/workload/<name>")
109+
async def delete_workload(_: Request, name: str) -> HTTPResponse:
110+
_get_workload(app, name)
111+
del app.ctx.workloads[name]
112+
return empty()
113+
114+
return app

benchkit/context.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
import typing as t
4+
5+
import neo4j
6+
from neo4j import (
7+
AsyncDriver,
8+
AsyncGraphDatabase,
9+
)
10+
11+
from .env import env
12+
from .workloads import Workloads
13+
14+
15+
__all__ = [
16+
"BenchKitContext",
17+
]
18+
19+
20+
class BenchKitContext:
21+
_db: t.Optional[AsyncDriver]
22+
workloads: Workloads
23+
24+
def __init__(self) -> None:
25+
self._db = None
26+
self.workloads = Workloads()
27+
28+
async def get_db(self) -> AsyncDriver:
29+
if self._db is None:
30+
url = f"{env.neo4j_scheme}://{env.neo4j_host}:{env.neo4j_port}"
31+
auth = (env.neo4j_user, env.neo4j_pass)
32+
self._db = AsyncGraphDatabase.driver(url, auth=auth)
33+
try:
34+
await self._db.verify_connectivity()
35+
except Exception:
36+
db = self._db
37+
self._db = None
38+
await db.close()
39+
raise
40+
return self._db
41+
42+
async def shutdown(self) -> None:
43+
if self._db is not None:
44+
await self._db.close()
45+
self._db = None

benchkit/env.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import typing as t
5+
6+
7+
__all__ = [
8+
"Env",
9+
"env",
10+
]
11+
12+
13+
class Env(t.NamedTuple):
14+
backend_port: int
15+
neo4j_host: str
16+
neo4j_port: int
17+
neo4j_scheme: str
18+
neo4j_user: str
19+
neo4j_pass: str
20+
driver_debug: bool
21+
22+
23+
env = Env(
24+
backend_port=int(os.environ.get("TEST_BACKEND_PORT", "9000")),
25+
neo4j_host=os.environ.get("TEST_NEO4J_HOST", "localhost"),
26+
neo4j_port=int(os.environ.get("TEST_NEO4J_PORT", "7687")),
27+
neo4j_scheme=os.environ.get("TEST_NEO4J_SCHEME", "neo4j"),
28+
neo4j_user=os.environ.get("TEST_NEO4J_USER", "neo4j"),
29+
neo4j_pass=os.environ.get("TEST_NEO4J_PASS", "password"),
30+
driver_debug=os.environ.get("TEST_DRIVER_DEBUG", "").lower() in (
31+
"y", "yes", "true", "1", "on"
32+
)
33+
)

0 commit comments

Comments
 (0)