Skip to content
Open
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
8 changes: 8 additions & 0 deletions frameworks/veil-python/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p gen && python -m grpc_tools.protoc -I proto --python_out=gen --grpc_python_out=gen proto/benchmark.proto
EXPOSE 8080 8081 8082 8443/tcp 8443/udp
CMD ["python3", "app.py"]
30 changes: 30 additions & 0 deletions frameworks/veil-python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# veil-python

An application written with [veil-api](https://pypi.org/project/veil-api/), the Python bindings of the
[veil](https://github.com/aofusa/veil-proxy) server. The HTTP/1.1, HTTP/2, HTTP/3 and WebSocket server is veil's
(Rust); Python runs the handlers. The Linux wheels use veil's epoll readiness reactor.

## Endpoints

| Endpoint | Implementation |
|---|---|
| `GET`/`POST /baseline11`, `GET /baseline2`, `GET /pipeline` | `@app.get` / `@app.post` with `a: int, b: int` bound from the query; `offload=False` runs these small, non-blocking handlers on the network worker (documented in veil-api's production notes) |
| `GET /json/{count}?m=` | returns a `dict`, serialized to JSON by veil on every request |
| `GET /delay/{ms}` | `async def` with `await asyncio.sleep`, on veil's persistent asyncio event loop |
| `POST /echo` | returns the request body |
| `GET /ws` | `@app.websocket`, echoing text and binary messages |
| `benchmark.BenchmarkService` (gRPC) | the servicer generated by `grpcio-tools`, added to `veil.grpc.Server(app)` with the generated `add_BenchmarkServiceServicer_to_server` |
| `/static/*` | `app.serve_dir` (veil's File backend: br/gzip compressed on the fly on HTTP/1.1, HTTP/2 and HTTP/3, uncompressed responses with sendfile on HTTP/1.1, caches that follow the disk) |

## Configuration

`veil.toml` holds the settings of veil-api's production guide: JSON application log, no access log, OpenAPI off,
response compression on (`[api] compression`), static caches following the disk. The four HttpArena listeners are
`[[listener]]` entries in **one process**.

## Completeness

- routing, request, response: decorators with path parameters; query, header and body binding (buffered, or a
stream with `stream_body=True`); `dict` / `str` / `bytes` / `veil.Response` / streaming and file responses.
- middleware: declared `false`. veil-api has dependencies (`Depends`, run before the handler and able to answer
early) and native CORS, but no general before-and-after middleware around native routes.
108 changes: 108 additions & 0 deletions frameworks/veil-python/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""HttpArena entry for veil-api (Python).

Handlers are ``@app.get`` / ``@app.post`` functions; ``async def`` handlers run on veil's persistent event loop,
``/ws`` is ``@app.websocket``, gRPC uses the grpcio-generated servicer on ``veil.grpc.Server``, and ``/static`` is
``app.serve_dir``. Listeners, TLS, compression and logging come from ``veil.toml`` (one process, four listeners).
"""

import asyncio
import json
import os
import sys

import veil
import veil.grpc

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "gen"))

import benchmark_pb2 as pb # noqa: E402
import benchmark_pb2_grpc # noqa: E402

try:
with open("/data/dataset.json") as f:
DATASET = json.load(f)
except OSError:
DATASET = []

app = veil.App(title="httparena")


@app.get("/baseline11", offload=False)
def baseline11_get(a: int, b: int):
return str(a + b)


@app.post("/baseline11", offload=False)
def baseline11_post(request: veil.Request, a: int, b: int):
try:
n = int(request.body.strip() or 0)
except ValueError:
n = 0
return str(a + b + n)


@app.get("/baseline2", offload=False)
def baseline2(a: int, b: int):
return str(a + b)


@app.get("/pipeline", offload=False)
def pipeline():
return "ok"


@app.get("/json/{count}")
def json_items(count: int, m: int = 1):
items = [dict(item, total=item["price"] * item["quantity"] * m) for item in DATASET[:count]]
return {"items": items, "count": len(items)}


@app.get("/delay/{ms}")
async def delay(ms: int):
await asyncio.sleep(min(ms, 60000) / 1000)
return str(ms)


@app.post("/echo")
def echo(request: veil.Request):
return veil.Response(request.body, media_type="application/octet-stream")


@app.websocket("/ws")
async def ws(websocket: veil.WebSocket):
await websocket.accept()
while True:
m = await websocket.receive()
if m["type"] == "websocket.disconnect":
return
if m.get("text") is not None:
await websocket.send_text(m["text"])
else:
await websocket.send_bytes(m["bytes"])


class Benchmark(benchmark_pb2_grpc.BenchmarkServiceServicer):
def GetSum(self, request, context):
return pb.SumReply(result=request.a + request.b)

def StreamSum(self, request, context):
reply = pb.SumReply(result=request.a + request.b)
for _ in range(max(0, min(request.count, 1000000))):
yield reply

def CollectSum(self, request_iterator, context):
return pb.SumReply(result=sum(r.a + r.b for r in request_iterator))

def EchoSum(self, request_iterator, context):
for r in request_iterator:
yield pb.SumReply(result=r.a + r.b)


app.serve_dir("/static", "/data/static", compression={"enabled": True})

server = veil.grpc.Server(app)
benchmark_pb2_grpc.add_BenchmarkServiceServicer_to_server(Benchmark(), server)

if __name__ == "__main__":
app.run(config_path=os.environ.get("VEIL_CONFIG", os.path.join(HERE, "veil.toml")))
41 changes: 41 additions & 0 deletions frameworks/veil-python/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"display_name": "veil-python",
"language": "Python",
"type": "emerging",
"mode": "standard",
"completeness": {
"middleware": false
},
"engine": "veil",
"description": "veil-api 0.10 from PyPI (Python bindings of the veil server; Linux wheels use the epoll readiness reactor). @app.get/@app.post handlers with path/query binding, dict responses serialized to JSON, async def handlers on veil's persistent asyncio event loop, @app.websocket for /ws, gRPC through the grpcio-generated servicer on veil.grpc.Server, /static/* through app.serve_dir, compression through [api] compression. One process with four listeners from the configuration file.",
"repo": "https://github.com/aofusa/veil-proxy",
"website": "https://github.com/aofusa/veil-proxy",
"enabled": true,
"tests": [
"baseline",
"pipelined",
"limited-conn",
"async",
"latency-1m",
"latency-10k",
"latency-500k-8cpu",
"json-comp",
"json-tls",
"8gbit",
"static-tls",
"baseline-h2",
"static-h2",
"baseline-h2c",
"json-h2c",
"baseline-h3",
"static-h3",
"unary-grpc",
"unary-grpc-tls",
"echo-ws",
"echo-ws-pipeline",
"echo-ws-limited"
],
"maintainers": [
"aofusa"
]
}
37 changes: 37 additions & 0 deletions frameworks/veil-python/proto/benchmark.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Canonical benchmark.proto used by ghz for gRPC streaming tests.
// Each gRPC framework must implement the RPCs it subscribes to in its own
// service; the shapes must match this file exactly.

syntax = "proto3";

package benchmark;

service BenchmarkService {
// Unary — 1 request, 1 reply. Used by unary-grpc (h2load).
rpc GetSum (SumRequest) returns (SumReply);

// Server streaming — 1 request, server emits `count` replies. Used by
// stream-grpc / stream-grpc-tls (ghz).
rpc StreamSum (StreamRequest) returns (stream SumReply);

// Client streaming — N requests streamed in, 1 reply with the total.
rpc CollectSum (stream SumRequest) returns (SumReply);

// Bidirectional streaming — echo: 1 reply per request over a persistent stream.
rpc EchoSum (stream SumRequest) returns (stream SumReply);
}

message SumRequest {
int32 a = 1;
int32 b = 2;
}

message StreamRequest {
int32 a = 1;
int32 b = 2;
int32 count = 3;
}

message SumReply {
int32 result = 1;
}
3 changes: 3 additions & 0 deletions frameworks/veil-python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
veil-api==0.10.1
grpcio==1.84.1
grpcio-tools==1.84.1
50 changes: 50 additions & 0 deletions frameworks/veil-python/veil.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# veil API library settings for HttpArena: the recommended production settings
# (docs/guide/library-production.md), with the four HttpArena listeners in one process.

[[listener]]
name = "plain" # HTTP/1.1, h2c by prior knowledge (gRPC), WebSocket
listen = "0.0.0.0:8080"
tls = false

[[listener]]
name = "h1tls" # TLS, HTTP/1.1 only
listen = "0.0.0.0:8081"
tls = true
http2 = false

[[listener]]
name = "h2c" # cleartext HTTP/2
listen = "0.0.0.0:8082"
tls = false

[[listener]]
name = "https" # TLS h2 + HTTP/1.1, and HTTP/3 on UDP 8443
listen = "0.0.0.0:8443"
tls = true
http2 = true
http3 = true

[server]
graceful_shutdown_timeout_secs = 5

[tls]
cert_path = "/certs/server.crt"
key_path = "/certs/server.key"

[logging]
level = "info"
format = "json"

[access_log]
enabled = false

[api]
openapi_enabled = false
compression = true
max_body_size = 1048576

[performance]
open_file_cache_watch = true # static files follow the disk

[static_file_cache]
watch = true
Loading