Skip to content

Commit 1ba3957

Browse files
authored
Merge pull request #602 from MDA2AV/add-h2c-results
Add h2c results
2 parents e429d90 + 2e791a9 commit 1ba3957

48 files changed

Lines changed: 1315 additions & 425325 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/scheduled_tasks.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"sessionId":"dd6c0b8a-60e9-493e-9cb2-71c6abe1781c","pid":416323,"acquiredAt":1776866999477}

frameworks/actix-h2c/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "httparena-actix-h2c"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
actix-web = { version = "4.5" }
8+
serde = { version = "1", features = ["derive"] }
9+
serde_json = "1"
10+
num_cpus = "1"
11+
futures-util = "0.3"
12+
13+
[profile.release]
14+
opt-level = 3
15+
codegen-units = 1
16+
lto = "thin"
17+
panic = "abort"

frameworks/actix-h2c/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM rust:1.94 AS build
2+
WORKDIR /app
3+
COPY Cargo.toml .
4+
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src/ target/release/httparena-actix-h2c* target/release/deps/httparena_actix_h2c*
5+
COPY src ./src
6+
RUN RUSTFLAGS="-C target-cpu=native" cargo build --release
7+
8+
FROM debian:bookworm-slim
9+
COPY --from=build /app/target/release/httparena-actix-h2c /server
10+
EXPOSE 8082
11+
CMD ["/server"]

frameworks/actix-h2c/meta.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"display_name": "actix",
3+
"language": "Rust",
4+
"type": "production",
5+
"engine": "actix",
6+
"description": "Actix-web 4 h2c-only listener on port 8082 using bind_auto_h2c. A middleware rejects HTTP/1.1 requests so the port serves HTTP/2 cleartext prior-knowledge exclusively. Handlers share the same shape as the main actix entry (serde_json per request).",
7+
"repo": "https://github.com/actix/actix-web",
8+
"enabled": true,
9+
"tests": [
10+
"baseline-h2c",
11+
"json-h2c"
12+
],
13+
"maintainers": []
14+
}

frameworks/actix-h2c/src/main.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
fn cgroup_cpus() -> usize {
2+
std::fs::read_to_string("/sys/fs/cgroup/cpu.max")
3+
.ok()
4+
.and_then(|s| {
5+
let mut parts = s.trim().split(' ');
6+
let quota = parts.next()?;
7+
if quota == "max" { return None; }
8+
let period: usize = parts.next()?.parse().ok()?;
9+
let q: usize = quota.parse().ok()?;
10+
let cpus = q / period;
11+
if cpus >= 1 { Some(cpus) } else { None }
12+
})
13+
.unwrap_or_else(num_cpus::get)
14+
}
15+
16+
use actix_web::dev::{Service, ServiceRequest, ServiceResponse};
17+
use actix_web::http::header::{HeaderValue, SERVER};
18+
use actix_web::http::Version;
19+
use actix_web::{web, App, HttpResponse, HttpServer};
20+
use futures_util::future::{ready, Either};
21+
use serde::{Deserialize, Serialize};
22+
use std::io;
23+
24+
static SERVER_HDR: HeaderValue = HeaderValue::from_static("actix");
25+
26+
#[derive(Deserialize)]
27+
struct BaselineQuery {
28+
a: Option<i64>,
29+
b: Option<i64>,
30+
}
31+
32+
#[derive(Deserialize)]
33+
struct JsonQuery {
34+
m: Option<i64>,
35+
}
36+
37+
#[derive(Deserialize, Clone)]
38+
struct Rating {
39+
score: i64,
40+
count: i64,
41+
}
42+
43+
#[derive(Deserialize, Clone)]
44+
struct DatasetItem {
45+
id: i64,
46+
name: String,
47+
category: String,
48+
price: i64,
49+
quantity: i64,
50+
active: bool,
51+
tags: Vec<String>,
52+
rating: Rating,
53+
}
54+
55+
#[derive(Serialize, Clone)]
56+
struct RatingOut {
57+
score: i64,
58+
count: i64,
59+
}
60+
61+
#[derive(Serialize, Clone)]
62+
struct ProcessedItem {
63+
id: i64,
64+
name: String,
65+
category: String,
66+
price: i64,
67+
quantity: i64,
68+
active: bool,
69+
tags: Vec<String>,
70+
rating: RatingOut,
71+
total: i64,
72+
}
73+
74+
#[derive(Serialize)]
75+
struct JsonResponse {
76+
items: Vec<ProcessedItem>,
77+
count: usize,
78+
}
79+
80+
struct AppState {
81+
dataset: Vec<DatasetItem>,
82+
}
83+
84+
fn build_json_body(dataset: &[DatasetItem], count: usize, m: i64) -> Vec<u8> {
85+
let count = count.min(dataset.len());
86+
let items: Vec<ProcessedItem> = dataset[..count]
87+
.iter()
88+
.map(|d| ProcessedItem {
89+
id: d.id,
90+
name: d.name.clone(),
91+
category: d.category.clone(),
92+
price: d.price,
93+
quantity: d.quantity,
94+
active: d.active,
95+
tags: d.tags.clone(),
96+
rating: RatingOut {
97+
score: d.rating.score,
98+
count: d.rating.count,
99+
},
100+
total: d.price * d.quantity * m,
101+
})
102+
.collect();
103+
let resp = JsonResponse { count, items };
104+
serde_json::to_vec(&resp).unwrap_or_default()
105+
}
106+
107+
fn load_dataset() -> Vec<DatasetItem> {
108+
let path = std::env::var("DATASET_PATH").unwrap_or_else(|_| "/data/dataset.json".to_string());
109+
match std::fs::read_to_string(&path) {
110+
Ok(data) => serde_json::from_str(&data).unwrap_or_default(),
111+
Err(_) => Vec::new(),
112+
}
113+
}
114+
115+
async fn baseline2(query: web::Query<BaselineQuery>) -> HttpResponse {
116+
let sum = query.a.unwrap_or(0) + query.b.unwrap_or(0);
117+
HttpResponse::Ok()
118+
.insert_header((SERVER, SERVER_HDR.clone()))
119+
.content_type("text/plain")
120+
.body(sum.to_string())
121+
}
122+
123+
async fn json_endpoint(
124+
state: web::Data<AppState>,
125+
path: web::Path<usize>,
126+
query: web::Query<JsonQuery>,
127+
) -> HttpResponse {
128+
let count = path.into_inner().min(state.dataset.len());
129+
let m = query.m.unwrap_or(1);
130+
let body = build_json_body(&state.dataset, count, m);
131+
HttpResponse::Ok()
132+
.insert_header((SERVER, SERVER_HDR.clone()))
133+
.content_type("application/json")
134+
.body(body)
135+
}
136+
137+
#[actix_web::main]
138+
async fn main() -> io::Result<()> {
139+
let dataset = load_dataset();
140+
let state = web::Data::new(AppState { dataset });
141+
let workers = cgroup_cpus();
142+
143+
HttpServer::new(move || {
144+
App::new()
145+
.app_data(state.clone())
146+
// bind_auto_h2c accepts H1 and h2c on the same socket; this
147+
// middleware short-circuits H1 requests with 400 so the port
148+
// serves h2c exclusively from the app's perspective. The
149+
// validate.sh anti-cheat requires port 8082 to refuse HTTP/1.1.
150+
.wrap_fn(|req: ServiceRequest, srv| {
151+
if req.request().version() == Version::HTTP_11 {
152+
let (r, _pl) = req.into_parts();
153+
let resp = HttpResponse::BadRequest()
154+
.content_type("text/plain")
155+
.body("HTTP/2 cleartext prior-knowledge required");
156+
Either::Left(ready(Ok(ServiceResponse::new(r, resp))))
157+
} else {
158+
Either::Right(srv.call(req))
159+
}
160+
})
161+
.route("/baseline2", web::get().to(baseline2))
162+
.route("/json/{count}", web::get().to(json_endpoint))
163+
})
164+
.workers(workers)
165+
.bind_auto_h2c("0.0.0.0:8082")?
166+
.run()
167+
.await
168+
}

frameworks/h2o-h2c/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
cmake_minimum_required(VERSION 3.16)
2+
project(h2o_h2c C)
3+
4+
set(CMAKE_C_STANDARD 11)
5+
6+
add_executable(h2o-h2c-app src/main.c)
7+
8+
target_include_directories(h2o-h2c-app PRIVATE /usr/local/include)
9+
target_link_directories(h2o-h2c-app PRIVATE /usr/local/lib)
10+
target_link_libraries(h2o-h2c-app PRIVATE h2o-evloop ssl crypto cjson pthread m)

frameworks/h2o-h2c/Dockerfile

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
FROM buildpack-deps:bookworm AS build
2+
3+
RUN apt-get update && apt-get install -y --no-install-recommends \
4+
cmake clang libssl-dev zlib1g-dev libuv1-dev libbrotli-dev libcjson-dev
5+
6+
# Build h2o from source (same pin as the infrastructure h2o entry)
7+
ARG H2O_VERSION=ccea64b17ade832753db933658047ede9f31a380
8+
WORKDIR /tmp/h2o
9+
RUN curl -LSs "https://github.com/h2o/h2o/archive/${H2O_VERSION}.tar.gz" | \
10+
tar --strip-components=1 -xz && \
11+
cmake -B build \
12+
-DCMAKE_BUILD_TYPE=Release \
13+
-DCMAKE_C_COMPILER=clang \
14+
-DCMAKE_C_FLAGS="-flto=auto -march=native -mtune=native" \
15+
-DWITH_MRUBY=off \
16+
-S . && \
17+
cmake --build build -j && \
18+
cmake --install build
19+
20+
# Build app
21+
COPY CMakeLists.txt /app/
22+
COPY src /app/src/
23+
WORKDIR /app/build
24+
RUN cmake \
25+
-DCMAKE_BUILD_TYPE=Release \
26+
-DCMAKE_C_COMPILER=clang \
27+
-DCMAKE_C_FLAGS="-flto=auto -march=native -mtune=native" \
28+
-S .. && \
29+
cmake --build . -j
30+
31+
FROM debian:bookworm-slim
32+
RUN apt-get update && apt-get install -y --no-install-recommends \
33+
libssl3 libbrotli1 libcjson1 && \
34+
rm -rf /var/lib/apt/lists/*
35+
COPY --from=build /usr/local/lib/libh2o-evloop.so* /usr/local/lib/
36+
COPY --from=build /app/build/h2o-h2c-app /server
37+
RUN ldconfig
38+
EXPOSE 8082
39+
CMD ["/server"]

frameworks/h2o-h2c/meta.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"display_name": "h2o",
3+
"language": "C",
4+
"type": "infrastructure",
5+
"engine": "h2o",
6+
"description": "libh2o evloop with a dedicated h2c-only listener on port 8082. The accept callback calls h2o_http2_accept directly instead of h2o_accept, so the connection must begin with the HTTP/2 client preface — plain HTTP/1.1 clients are dropped at protocol negotiation. Handlers for /baseline2 (query sum) and /json/{count} (serialized via cJSON).",
7+
"repo": "https://github.com/h2o/h2o",
8+
"enabled": true,
9+
"tests": [
10+
"baseline-h2c",
11+
"json-h2c"
12+
],
13+
"maintainers": []
14+
}

0 commit comments

Comments
 (0)