diff --git a/frameworks/rocket/Cargo.toml b/frameworks/rocket/Cargo.toml new file mode 100644 index 0000000..40dd608 --- /dev/null +++ b/frameworks/rocket/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "httparena-rocket" +version = "0.1.0" +edition = "2021" + +[dependencies] +rocket = { version = "0.5", features = ["json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +num_cpus = "1" +rusqlite = { version = "0.31", features = ["bundled"] } +flate2 = "1" +tokio = { version = "1", features = ["full"] } + +[profile.release] +opt-level = 3 +codegen-units = 1 +lto = "thin" +panic = "abort" diff --git a/frameworks/rocket/Dockerfile b/frameworks/rocket/Dockerfile new file mode 100644 index 0000000..30bf1d3 --- /dev/null +++ b/frameworks/rocket/Dockerfile @@ -0,0 +1,11 @@ +FROM rust:1.88 AS build +WORKDIR /app +COPY Cargo.toml . +RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src/ target/release/httparena-rocket* target/release/deps/httparena_rocket* +COPY src ./src +RUN RUSTFLAGS="-C target-cpu=native" cargo build --release + +FROM debian:bookworm-slim +COPY --from=build /app/target/release/httparena-rocket /server +EXPOSE 8080 +CMD ["/server"] diff --git a/frameworks/rocket/meta.json b/frameworks/rocket/meta.json new file mode 100644 index 0000000..f955e77 --- /dev/null +++ b/frameworks/rocket/meta.json @@ -0,0 +1,21 @@ +{ + "display_name": "rocket", + "language": "Rust", + "type": "framework", + "engine": "rocket", + "description": "Rocket 0.5 on Tokio with Rustls for HTTP/2, compiled with -O3 and thin LTO.", + "repo": "https://github.com/SergioBenitez/Rocket", + "enabled": true, + "tests": [ + "baseline", + "noisy", + "pipelined", + "limited-conn", + "json", + "upload", + "compression", + "mixed", + "baseline-h2", + "static-h2" + ] +} diff --git a/frameworks/rocket/src/main.rs b/frameworks/rocket/src/main.rs new file mode 100644 index 0000000..6e89837 --- /dev/null +++ b/frameworks/rocket/src/main.rs @@ -0,0 +1,501 @@ +use flate2::write::GzEncoder; +use flate2::Compression; +use rocket::data::{Data, ToByteUnit}; +use rocket::http::{ContentType, Header, Status}; +use rocket::request::{self, FromRequest, Outcome, Request}; +use rocket::response::{self, Responder, Response}; +use rocket::{get, post, routes, State}; +use rusqlite::Connection; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::io::{Cursor, Write}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +// ─── Request guard to extract raw query string ─── + +struct RawQuery(Option); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for RawQuery { + type Error = (); + + async fn from_request(req: &'r Request<'_>) -> Outcome { + Outcome::Success(RawQuery( + req.uri().query().map(|q| q.as_str().to_string()), + )) + } +} + +// ─── Custom responder that always sets Server header ─── + +struct ServerResponse { + status: Status, + content_type: ContentType, + body: Vec, + extra_headers: Vec<(&'static str, String)>, +} + +impl ServerResponse { + fn text(body: String) -> Self { + Self { + status: Status::Ok, + content_type: ContentType::Plain, + body: body.into_bytes(), + extra_headers: vec![], + } + } + + fn json_bytes(body: Vec) -> Self { + Self { + status: Status::Ok, + content_type: ContentType::JSON, + body, + extra_headers: vec![], + } + } + + fn with_header(mut self, name: &'static str, value: String) -> Self { + self.extra_headers.push((name, value)); + self + } + + fn not_found() -> Self { + Self { + status: Status::NotFound, + content_type: ContentType::Plain, + body: b"Not Found".to_vec(), + extra_headers: vec![], + } + } + + fn error(msg: &str) -> Self { + Self { + status: Status::InternalServerError, + content_type: ContentType::Plain, + body: msg.as_bytes().to_vec(), + extra_headers: vec![], + } + } +} + +impl<'r> Responder<'r, 'static> for ServerResponse { + fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'static> { + let mut builder = Response::build(); + builder + .status(self.status) + .header(self.content_type) + .header(Header::new("Server", "rocket")) + .sized_body(self.body.len(), Cursor::new(self.body)); + for (name, value) in self.extra_headers { + builder.header(Header::new(name, value)); + } + builder.ok() + } +} + +// ─── Data types ─── + +#[derive(Deserialize, Clone)] +struct Rating { + score: f64, + count: i64, +} + +#[derive(Deserialize, Clone)] +struct DatasetItem { + id: i64, + name: String, + category: String, + price: f64, + quantity: i64, + active: bool, + tags: Vec, + rating: Rating, +} + +#[derive(Serialize)] +struct RatingOut { + score: f64, + count: i64, +} + +#[derive(Serialize)] +struct ProcessedItem { + id: i64, + name: String, + category: String, + price: f64, + quantity: i64, + active: bool, + tags: Vec, + rating: RatingOut, + total: f64, +} + +#[derive(Serialize)] +struct JsonResponse { + items: Vec, + count: usize, +} + +// ─── App state ─── + +struct StaticFile { + data: Vec, + content_type: String, +} + +struct AppState { + dataset: Vec, + json_large_cache: Vec, + gzip_large_cache: Vec, + static_files: HashMap, + db_pool: Vec>, + db_counter: AtomicUsize, +} + +fn process_items(dataset: &[DatasetItem]) -> Vec { + dataset + .iter() + .map(|d| ProcessedItem { + id: d.id, + name: d.name.clone(), + category: d.category.clone(), + price: d.price, + quantity: d.quantity, + active: d.active, + tags: d.tags.clone(), + rating: RatingOut { + score: d.rating.score, + count: d.rating.count, + }, + total: (d.price * d.quantity as f64 * 100.0).round() / 100.0, + }) + .collect() +} + +fn build_json_cache(dataset: &[DatasetItem]) -> Vec { + let items = process_items(dataset); + let resp = JsonResponse { + count: items.len(), + items, + }; + serde_json::to_vec(&resp).unwrap_or_default() +} + +fn gzip_compress(data: &[u8]) -> Vec { + let mut encoder = GzEncoder::new(Vec::new(), Compression::fast()); + encoder.write_all(data).unwrap(); + encoder.finish().unwrap() +} + +fn load_dataset() -> Vec { + let path = std::env::var("DATASET_PATH").unwrap_or_else(|_| "/data/dataset.json".to_string()); + match std::fs::read_to_string(&path) { + Ok(data) => serde_json::from_str(&data).unwrap_or_default(), + Err(_) => Vec::new(), + } +} + +fn load_static_files() -> HashMap { + let mime_types: HashMap<&str, &str> = [ + (".css", "text/css"), + (".js", "application/javascript"), + (".html", "text/html"), + (".woff2", "font/woff2"), + (".svg", "image/svg+xml"), + (".webp", "image/webp"), + (".json", "application/json"), + ] + .into(); + let mut files = HashMap::new(); + if let Ok(entries) = std::fs::read_dir("/data/static") { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if let Ok(data) = std::fs::read(entry.path()) { + let ext = name.rfind('.').map(|i| &name[i..]).unwrap_or(""); + let ct = mime_types.get(ext).unwrap_or(&"application/octet-stream"); + files.insert( + name, + StaticFile { + data, + content_type: ct.to_string(), + }, + ); + } + } + } + files +} + +fn open_db_pool(count: usize) -> Vec> { + let db_path = "/data/benchmark.db"; + if !std::path::Path::new(db_path).exists() { + return Vec::new(); + } + (0..count) + .filter_map(|_| { + let conn = Connection::open_with_flags( + db_path, + rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, + ) + .ok()?; + conn.execute_batch("PRAGMA mmap_size=268435456").ok(); + Some(Mutex::new(conn)) + }) + .collect() +} + +fn parse_query_sum(query: &str) -> i64 { + let mut sum: i64 = 0; + for pair in query.split('&') { + if let Some(val) = pair.split('=').nth(1) { + if let Ok(n) = val.parse::() { + sum += n; + } + } + } + sum +} + +fn parse_query_param(query: &str, name: &str) -> Option { + for pair in query.split('&') { + if let Some(v) = pair.strip_prefix(name).and_then(|s| s.strip_prefix('=')) { + if let Ok(n) = v.parse() { + return Some(n); + } + } + } + None +} + +// ─── Routes ─── + +#[get("/pipeline")] +fn pipeline() -> ServerResponse { + ServerResponse::text("ok".to_string()) +} + +#[get("/baseline11")] +fn baseline11_get(raw: RawQuery) -> ServerResponse { + let sum = raw.0.as_deref().map(parse_query_sum).unwrap_or(0); + ServerResponse::text(sum.to_string()) +} + +#[post("/baseline11", data = "")] +async fn baseline11_post(raw: RawQuery, body: Data<'_>) -> ServerResponse { + let mut sum = raw.0.as_deref().map(parse_query_sum).unwrap_or(0); + if let Ok(bytes) = body.open(25.mebibytes()).into_bytes().await { + if let Ok(s) = std::str::from_utf8(&bytes) { + if let Ok(n) = s.trim().parse::() { + sum += n; + } + } + } + ServerResponse::text(sum.to_string()) +} + +#[get("/baseline2")] +fn baseline2(raw: RawQuery) -> ServerResponse { + let sum = raw.0.as_deref().map(parse_query_sum).unwrap_or(0); + ServerResponse::text(sum.to_string()) +} + +#[get("/json")] +fn json_endpoint(state: &State>) -> ServerResponse { + if state.dataset.is_empty() { + return ServerResponse::error("No dataset"); + } + let items = process_items(&state.dataset); + let resp = JsonResponse { + count: items.len(), + items, + }; + ServerResponse::json_bytes(serde_json::to_vec(&resp).unwrap_or_default()) +} + +#[get("/compression")] +fn compression_endpoint(state: &State>) -> ServerResponse { + ServerResponse { + status: Status::Ok, + content_type: ContentType::JSON, + body: state.gzip_large_cache.clone(), + extra_headers: vec![], + } + .with_header("Content-Encoding", "gzip".to_string()) +} + +#[get("/db")] +fn db_endpoint(state: &State>, raw: RawQuery) -> ServerResponse { + let query = raw.0.as_deref().unwrap_or(""); + let min = parse_query_param(query, "min").unwrap_or(10.0); + let max = parse_query_param(query, "max").unwrap_or(50.0); + + if state.db_pool.is_empty() { + return ServerResponse::error("Database not available"); + } + + let idx = state.db_counter.fetch_add(1, Ordering::Relaxed) % state.db_pool.len(); + let conn = state.db_pool[idx].lock().unwrap(); + let mut stmt = conn + .prepare_cached( + "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ?1 AND ?2 LIMIT 50", + ) + .unwrap(); + let rows = stmt.query_map(rusqlite::params![min, max], |row| { + Ok(serde_json::json!({ + "id": row.get::<_, i64>(0)?, + "name": row.get::<_, String>(1)?, + "category": row.get::<_, String>(2)?, + "price": row.get::<_, f64>(3)?, + "quantity": row.get::<_, i64>(4)?, + "active": row.get::<_, i64>(5)? == 1, + "tags": serde_json::from_str::(&row.get::<_, String>(6)?).unwrap_or_default(), + "rating": serde_json::json!({ + "score": row.get::<_, f64>(7)?, + "count": row.get::<_, i64>(8)? + }) + })) + }); + let items: Vec = match rows { + Ok(mapped) => mapped.filter_map(|r| r.ok()).collect(), + Err(_) => Vec::new(), + }; + let result = serde_json::json!({"items": items, "count": items.len()}); + ServerResponse::json_bytes(serde_json::to_vec(&result).unwrap_or_default()) +} + +#[post("/upload", data = "")] +async fn upload(body: Data<'_>) -> ServerResponse { + match body.open(25.mebibytes()).into_bytes().await { + Ok(bytes) => ServerResponse::text(bytes.len().to_string()), + Err(_) => ServerResponse::error("Failed to read body"), + } +} + +#[get("/static/")] +fn static_file(state: &State>, filename: &str) -> ServerResponse { + if let Some(sf) = state.static_files.get(filename) { + ServerResponse { + status: Status::Ok, + content_type: ContentType::parse_flexible(&sf.content_type) + .unwrap_or(ContentType::Binary), + body: sf.data.clone(), + extra_headers: vec![], + } + } else { + ServerResponse::not_found() + } +} + +// ─── Catchers ─── + +#[rocket::catch(404)] +fn not_found(_req: &Request<'_>) -> ServerResponse { + ServerResponse::not_found() +} + +#[rocket::catch(405)] +fn method_not_allowed(_req: &Request<'_>) -> ServerResponse { + ServerResponse { + status: Status::MethodNotAllowed, + content_type: ContentType::Plain, + body: b"Method Not Allowed".to_vec(), + extra_headers: vec![], + } +} + +// ─── Build Rocket instance ─── + +fn build_rocket( + state: Arc, + figment: rocket::figment::Figment, +) -> rocket::Rocket { + rocket::custom(figment) + .manage(state) + .mount( + "/", + routes![ + pipeline, + baseline11_get, + baseline11_post, + baseline2, + json_endpoint, + compression_endpoint, + db_endpoint, + upload, + static_file, + ], + ) + .register("/", rocket::catchers![not_found, method_not_allowed]) +} + +// ─── Main ─── + +#[rocket::main] +async fn main() -> Result<(), Box> { + let dataset = load_dataset(); + let large_dataset: Vec = + match std::fs::read_to_string("/data/dataset-large.json") { + Ok(data) => serde_json::from_str(&data).unwrap_or_default(), + Err(_) => Vec::new(), + }; + let json_large_cache = build_json_cache(&large_dataset); + let gzip_large_cache = gzip_compress(&json_large_cache); + + let workers = num_cpus::get(); + let state = Arc::new(AppState { + dataset, + json_large_cache, + gzip_large_cache, + static_files: load_static_files(), + db_pool: open_db_pool(workers), + db_counter: AtomicUsize::new(0), + }); + + let cert_path = std::env::var("TLS_CERT").unwrap_or_else(|_| "/certs/server.crt".to_string()); + let key_path = std::env::var("TLS_KEY").unwrap_or_else(|_| "/certs/server.key".to_string()); + let has_tls = + std::path::Path::new(&cert_path).exists() && std::path::Path::new(&key_path).exists(); + + // HTTP server on port 8080 + let http_figment = rocket::Config::figment() + .merge(("address", "0.0.0.0")) + .merge(("port", 8080u16)) + .merge(("workers", workers)) + .merge(("log_level", "off")) + .merge(("limits.data-form", "25 MiB")) + .merge(("limits.bytes", "25 MiB")); + + if has_tls { + // TLS server on port 8443 + let tls_figment = rocket::Config::figment() + .merge(("address", "0.0.0.0")) + .merge(("port", 8443u16)) + .merge(("workers", workers)) + .merge(("log_level", "off")) + .merge(("limits.data-form", "25 MiB")) + .merge(("limits.bytes", "25 MiB")) + .merge(("tls.certs", &cert_path)) + .merge(("tls.key", &key_path)); + + let tls_rocket = build_rocket(state.clone(), tls_figment); + let http_rocket = build_rocket(state.clone(), http_figment); + + // Launch both concurrently + let tls_handle = tokio::spawn(async move { + let _ = tls_rocket.launch().await; + }); + let http_handle = tokio::spawn(async move { + let _ = http_rocket.launch().await; + }); + + let _ = tokio::try_join!(tls_handle, http_handle); + } else { + let http_rocket = build_rocket(state.clone(), http_figment); + let _ = http_rocket.launch().await; + } + + Ok(()) +}