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
3,248 changes: 3,248 additions & 0 deletions frameworks/veil-api/Cargo.lock

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions frameworks/veil-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "httparena-veil-api"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
veil = { package = "veil-proxy", version = "=0.10.1", default-features = false, features = ["full-container", "api"] }
prost = "0.13"
futures = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[build-dependencies]
veil-build = { package = "veil-proxy-build", version = "=0.10.1" }

[profile.release]
lto = "fat"
codegen-units = 1

[workspace]
12 changes: 12 additions & 0 deletions frameworks/veil-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM rust:1.98-bookworm AS build
RUN apt-get update -y && apt-get install -y --no-install-recommends cmake nasm libclang-dev protobuf-compiler \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN cargo build --release --locked

FROM debian:bookworm-slim
COPY --from=build /app/target/release/httparena-veil-api /app/server
COPY veil.toml /app/veil.toml
EXPOSE 8080 8081 8082 8443/tcp 8443/udp
CMD ["/app/server"]
34 changes: 34 additions & 0 deletions frameworks/veil-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# veil-api (Rust)

An application written with the API library of [veil](https://github.com/aofusa/veil-proxy) (the
`veil-proxy` crate with the `api` feature). veil serves HTTP/1.1, HTTP/2, HTTP/3 and WebSocket on its own async
runtime (no tokio, no hyper); this entry uses the `full-container` feature set (epoll readiness reactor).

## Endpoints

| Endpoint | Implementation |
|---|---|
| `GET`/`POST /baseline11`, `GET /baseline2`, `GET /pipeline` | `#[veil::route]` handlers; `a` and `b` are bound from the query, the POST body is `req.body()` |
| `GET /json/{count}?m=` | `#[veil::route]` with a path parameter and an optional query parameter; the response is `Response::json` (serde_json, per request) from the dataset read at startup |
| `GET /delay/{ms}` | `async` handler awaiting `veil::runtime::time::sleep` |
| `POST /echo` | returns the request body (`Response::bytes`) |
| `GET /ws` | `WebSocketUpgrade::on_upgrade`, echoing text and binary messages |
| `benchmark.BenchmarkService` (gRPC) | the trait generated from `proto/benchmark.proto` by `veil-build` (prost), registered with `add_benchmark_service` |
| `/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's production guide for API applications
(`docs/guide/library-production.md`): JSON application log, no access log, OpenAPI off, response compression
on (`[api] compression`: br or gzip by `Accept-Encoding`, for JSON and text of 256 bytes or more), and the static
caches following the disk (`watch`). The four HttpArena listeners are `[[listener]]` entries, served by **one
process**. Worker threads default to the CPUs the container may use.

## Completeness

- routing: `#[veil::route(path = "/json/{count}", method = "GET")]` matches method and path with parameters.
- middleware: tower `Layer`s around the app or a route prefix (`App::layer`, `App::layer_on`, `App::wrap_on`), able to run before and
after the handler and to answer early.
- request: path, query and headers are bound by the macro; the body is available buffered (`req.body()`,
`Json<T>`, forms, multipart) or as a stream (`streaming = true`).
- response: `Response::json`, `Response::bytes`, streams and files, with status and headers in one value.
4 changes: 4 additions & 0 deletions frameworks/veil-api/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fn main() -> std::io::Result<()> {
println!("cargo:rerun-if-changed=proto/benchmark.proto");
veil_build::compile_protos(&["proto/benchmark.proto"], &["proto"])
}
42 changes: 42 additions & 0 deletions frameworks/veil-api/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"display_name": "veil-api",
"language": "Rust",
"type": "emerging",
"mode": "standard",
"completeness": {
"routing": true,
"middleware": true,
"request": true,
"response": true
},
"engine": "veil",
"description": "veil 0.10 API library (crates.io veil-proxy, full-container: epoll readiness reactor, no tokio or hyper). #[veil::route] handlers with path/query extraction, serde_json responses, compression through [api] compression, WebSocket through WebSocketUpgrade, gRPC services generated by veil-build (prost), /static/* through App::serve_dir. One process with four listeners from the configuration file; HTTP/3 via quiche.",
"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-api/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;
}
191 changes: 191 additions & 0 deletions frameworks/veil-api/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
//! HttpArena entry for the veil API library (Rust).
//!
//! Every endpoint is a `#[veil::route]` handler or a gRPC service generated by `veil-build`; `/static/*` is
//! `App::serve_dir`. Listeners, TLS, compression and logging come from `veil.toml` (one process, four listeners).
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
use veil::api::grpc::{BoxStream, Request as GrpcRequest, Response as GrpcResponse, Status, Streaming};
use veil::api::ws::{Message, WebSocket, WebSocketUpgrade};
use veil::api::ServeOptions;
use veil::prelude::*;

pub mod benchmark {
veil::include_proto!("benchmark");
}
use benchmark::benchmark_service_server::{add_benchmark_service, BenchmarkService};
use benchmark::{StreamRequest, SumReply, SumRequest};

#[cfg(unix)]
#[global_allocator]
static GLOBAL: veil::veil_alloc::VeilAlloc = veil::veil_alloc::VeilAlloc;

#[derive(Deserialize, Serialize, Clone)]
struct Rating {
score: i64,
count: i64,
}

#[derive(Deserialize, Serialize, Clone)]
struct Item {
id: i64,
name: String,
category: String,
price: i64,
quantity: i64,
active: bool,
tags: Vec<String>,
rating: Rating,
}

#[derive(Serialize)]
struct PricedItem<'a> {
#[serde(flatten)]
item: &'a Item,
total: i64,
}

#[derive(Serialize)]
struct Items<'a> {
items: Vec<PricedItem<'a>>,
count: usize,
}

static DATASET: OnceLock<Vec<Item>> = OnceLock::new();

fn dataset() -> &'static [Item] {
DATASET.get().map(Vec::as_slice).unwrap_or(&[])
}

fn body_int(req: &Request) -> i64 {
std::str::from_utf8(req.body())
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0)
}

#[veil::route(path = "/baseline11", method = "GET")]
async fn baseline11_get(a: i64, b: i64) -> String {
(a + b).to_string()
}

#[veil::route(path = "/baseline11", method = "POST")]
async fn baseline11_post(req: &Request, a: i64, b: i64) -> String {
(a + b + body_int(req)).to_string()
}

#[veil::route(path = "/baseline2", method = "GET")]
async fn baseline2(a: i64, b: i64) -> String {
(a + b).to_string()
}

#[veil::route(path = "/pipeline", method = "GET")]
async fn pipeline() -> &'static str {
"ok"
}

#[veil::route(path = "/json/{count}", method = "GET")]
async fn json(count: usize, m: Option<i64>) -> Response {
let m = m.unwrap_or(1);
let items: Vec<PricedItem> = dataset()
.iter()
.take(count)
.map(|item| PricedItem {
item,
total: item.price * item.quantity * m,
})
.collect();
Response::json(&Items {
count: items.len(),
items,
})
}

#[veil::route(path = "/delay/{ms}", method = "GET")]
async fn delay(ms: u64) -> String {
veil::runtime::time::sleep(std::time::Duration::from_millis(ms.min(60_000))).await;
ms.to_string()
}

#[veil::route(path = "/echo", method = "POST")]
async fn echo(req: &Request) -> Response {
Response::bytes(req.body().clone())
}

#[veil::route(path = "/ws", method = "GET")]
async fn ws(upgrade: WebSocketUpgrade) -> Response {
upgrade.on_upgrade(|mut ws: WebSocket| async move {
while let Some(Ok(msg)) = ws.recv().await {
match msg {
Message::Text(_) | Message::Binary(_) => {
if ws.send(msg).await.is_err() {
break;
}
}
Message::Close(_) => break,
_ => {}
}
}
})
}

struct Benchmark;

impl BenchmarkService for Benchmark {
async fn get_sum(&self, req: GrpcRequest<SumRequest>) -> Result<GrpcResponse<SumReply>, Status> {
let r = req.into_inner();
Ok(GrpcResponse::new(SumReply { result: r.a + r.b }))
}

async fn stream_sum(
&self,
req: GrpcRequest<StreamRequest>,
) -> Result<GrpcResponse<BoxStream<SumReply>>, Status> {
let r = req.into_inner();
let result = r.a + r.b;
let n = r.count.clamp(0, 1_000_000);
let s = futures::stream::iter((0..n).map(move |_| Ok(SumReply { result })));
Ok(GrpcResponse::new(Box::pin(s)))
}

async fn collect_sum(
&self,
req: GrpcRequest<Streaming<SumRequest>>,
) -> Result<GrpcResponse<SumReply>, Status> {
let mut s = req.into_inner();
let mut total = 0i32;
while let Some(r) = s.message().await? {
total = total.wrapping_add(r.a + r.b);
}
Ok(GrpcResponse::new(SumReply { result: total }))
}

async fn echo_sum(
&self,
req: GrpcRequest<Streaming<SumRequest>>,
) -> Result<GrpcResponse<BoxStream<SumReply>>, Status> {
let s = req
.into_inner()
.map(|r| r.map(|r| SumReply { result: r.a + r.b }));
Ok(GrpcResponse::new(Box::pin(s)))
}
}

#[veil::main]
async fn main() -> Result<(), veil::api::Error> {
let raw = std::fs::read("/data/dataset.json").unwrap_or_else(|_| b"[]".to_vec());
let _ = DATASET.set(serde_json::from_slice(&raw).unwrap_or_default());

let config = Config::from_file(std::env::var("VEIL_CONFIG").unwrap_or_else(|_| "/app/veil.toml".into()))?;
let app = App::new()
.config(config)
.serve_dir(
"/static",
"/data/static",
ServeOptions {
compression: true,
..ServeOptions::default()
},
);
add_benchmark_service(app, Benchmark).run()
}
50 changes: 50 additions & 0 deletions frameworks/veil-api/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