|
| 1 | +use actix_web::http::header::{ContentType, HeaderValue, SERVER}; |
| 2 | +use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; |
| 3 | +use rustls::ServerConfig; |
| 4 | +use serde::{Deserialize, Serialize}; |
| 5 | +use std::collections::HashMap; |
| 6 | +use std::io; |
| 7 | +use std::sync::Arc; |
| 8 | + |
| 9 | +static SERVER_HDR: HeaderValue = HeaderValue::from_static("actix"); |
| 10 | + |
| 11 | +#[derive(Deserialize, Clone)] |
| 12 | +struct Rating { |
| 13 | + score: f64, |
| 14 | + count: i64, |
| 15 | +} |
| 16 | + |
| 17 | +#[derive(Deserialize, Clone)] |
| 18 | +struct DatasetItem { |
| 19 | + id: i64, |
| 20 | + name: String, |
| 21 | + category: String, |
| 22 | + price: f64, |
| 23 | + quantity: i64, |
| 24 | + active: bool, |
| 25 | + tags: Vec<String>, |
| 26 | + rating: Rating, |
| 27 | +} |
| 28 | + |
| 29 | +#[derive(Serialize)] |
| 30 | +struct RatingOut { |
| 31 | + score: f64, |
| 32 | + count: i64, |
| 33 | +} |
| 34 | + |
| 35 | +#[derive(Serialize)] |
| 36 | +struct ProcessedItem { |
| 37 | + id: i64, |
| 38 | + name: String, |
| 39 | + category: String, |
| 40 | + price: f64, |
| 41 | + quantity: i64, |
| 42 | + active: bool, |
| 43 | + tags: Vec<String>, |
| 44 | + rating: RatingOut, |
| 45 | + total: f64, |
| 46 | +} |
| 47 | + |
| 48 | +#[derive(Serialize)] |
| 49 | +struct JsonResponse { |
| 50 | + items: Vec<ProcessedItem>, |
| 51 | + count: usize, |
| 52 | +} |
| 53 | + |
| 54 | +struct StaticFile { |
| 55 | + data: Vec<u8>, |
| 56 | + content_type: String, |
| 57 | +} |
| 58 | + |
| 59 | +struct AppState { |
| 60 | + dataset: Vec<DatasetItem>, |
| 61 | + json_cache: Vec<u8>, |
| 62 | + static_files: HashMap<String, StaticFile>, |
| 63 | +} |
| 64 | + |
| 65 | +fn load_dataset() -> Vec<DatasetItem> { |
| 66 | + let path = std::env::var("DATASET_PATH").unwrap_or_else(|_| "/data/dataset.json".to_string()); |
| 67 | + match std::fs::read_to_string(&path) { |
| 68 | + Ok(data) => serde_json::from_str(&data).unwrap_or_default(), |
| 69 | + Err(_) => Vec::new(), |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +fn build_json_cache(dataset: &[DatasetItem]) -> Vec<u8> { |
| 74 | + let items: Vec<ProcessedItem> = dataset |
| 75 | + .iter() |
| 76 | + .map(|d| ProcessedItem { |
| 77 | + id: d.id, |
| 78 | + name: d.name.clone(), |
| 79 | + category: d.category.clone(), |
| 80 | + price: d.price, |
| 81 | + quantity: d.quantity, |
| 82 | + active: d.active, |
| 83 | + tags: d.tags.clone(), |
| 84 | + rating: RatingOut { |
| 85 | + score: d.rating.score, |
| 86 | + count: d.rating.count, |
| 87 | + }, |
| 88 | + total: (d.price * d.quantity as f64 * 100.0).round() / 100.0, |
| 89 | + }) |
| 90 | + .collect(); |
| 91 | + let resp = JsonResponse { |
| 92 | + count: items.len(), |
| 93 | + items, |
| 94 | + }; |
| 95 | + serde_json::to_vec(&resp).unwrap_or_default() |
| 96 | +} |
| 97 | + |
| 98 | +fn load_static_files() -> HashMap<String, StaticFile> { |
| 99 | + let mime_types: HashMap<&str, &str> = [ |
| 100 | + (".css", "text/css"), |
| 101 | + (".js", "application/javascript"), |
| 102 | + (".html", "text/html"), |
| 103 | + (".woff2", "font/woff2"), |
| 104 | + (".svg", "image/svg+xml"), |
| 105 | + (".webp", "image/webp"), |
| 106 | + (".json", "application/json"), |
| 107 | + ] |
| 108 | + .into(); |
| 109 | + let mut files = HashMap::new(); |
| 110 | + if let Ok(entries) = std::fs::read_dir("/data/static") { |
| 111 | + for entry in entries.flatten() { |
| 112 | + let name = entry.file_name().to_string_lossy().to_string(); |
| 113 | + if let Ok(data) = std::fs::read(entry.path()) { |
| 114 | + let ext = name.rfind('.').map(|i| &name[i..]).unwrap_or(""); |
| 115 | + let ct = mime_types.get(ext).unwrap_or(&"application/octet-stream"); |
| 116 | + files.insert( |
| 117 | + name, |
| 118 | + StaticFile { |
| 119 | + data, |
| 120 | + content_type: ct.to_string(), |
| 121 | + }, |
| 122 | + ); |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + files |
| 127 | +} |
| 128 | + |
| 129 | +fn parse_query_sum(query: &str) -> i64 { |
| 130 | + let mut sum: i64 = 0; |
| 131 | + for pair in query.split('&') { |
| 132 | + if let Some(val) = pair.split('=').nth(1) { |
| 133 | + if let Ok(n) = val.parse::<i64>() { |
| 134 | + sum += n; |
| 135 | + } |
| 136 | + } |
| 137 | + } |
| 138 | + sum |
| 139 | +} |
| 140 | + |
| 141 | +async fn pipeline() -> HttpResponse { |
| 142 | + HttpResponse::Ok() |
| 143 | + .insert_header((SERVER, SERVER_HDR.clone())) |
| 144 | + .content_type(ContentType::plaintext()) |
| 145 | + .body("ok") |
| 146 | +} |
| 147 | + |
| 148 | +async fn baseline11_get(req: HttpRequest) -> HttpResponse { |
| 149 | + let sum = req |
| 150 | + .uri() |
| 151 | + .query() |
| 152 | + .map(parse_query_sum) |
| 153 | + .unwrap_or(0); |
| 154 | + HttpResponse::Ok() |
| 155 | + .insert_header((SERVER, SERVER_HDR.clone())) |
| 156 | + .content_type(ContentType::plaintext()) |
| 157 | + .body(sum.to_string()) |
| 158 | +} |
| 159 | + |
| 160 | +async fn baseline11_post(req: HttpRequest, body: web::Bytes) -> HttpResponse { |
| 161 | + let mut sum = req |
| 162 | + .uri() |
| 163 | + .query() |
| 164 | + .map(parse_query_sum) |
| 165 | + .unwrap_or(0); |
| 166 | + if let Ok(s) = std::str::from_utf8(&body) { |
| 167 | + if let Ok(n) = s.trim().parse::<i64>() { |
| 168 | + sum += n; |
| 169 | + } |
| 170 | + } |
| 171 | + HttpResponse::Ok() |
| 172 | + .insert_header((SERVER, SERVER_HDR.clone())) |
| 173 | + .content_type(ContentType::plaintext()) |
| 174 | + .body(sum.to_string()) |
| 175 | +} |
| 176 | + |
| 177 | +async fn baseline2(req: HttpRequest) -> HttpResponse { |
| 178 | + let sum = req |
| 179 | + .uri() |
| 180 | + .query() |
| 181 | + .map(parse_query_sum) |
| 182 | + .unwrap_or(0); |
| 183 | + HttpResponse::Ok() |
| 184 | + .insert_header((SERVER, SERVER_HDR.clone())) |
| 185 | + .content_type(ContentType::plaintext()) |
| 186 | + .body(sum.to_string()) |
| 187 | +} |
| 188 | + |
| 189 | +async fn json_endpoint(state: web::Data<Arc<AppState>>) -> HttpResponse { |
| 190 | + HttpResponse::Ok() |
| 191 | + .insert_header((SERVER, SERVER_HDR.clone())) |
| 192 | + .content_type(ContentType::json()) |
| 193 | + .body(state.json_cache.clone()) |
| 194 | +} |
| 195 | + |
| 196 | +async fn static_file( |
| 197 | + state: web::Data<Arc<AppState>>, |
| 198 | + path: web::Path<String>, |
| 199 | +) -> HttpResponse { |
| 200 | + let filename = path.into_inner(); |
| 201 | + if let Some(sf) = state.static_files.get(&filename) { |
| 202 | + HttpResponse::Ok() |
| 203 | + .insert_header((SERVER, SERVER_HDR.clone())) |
| 204 | + .insert_header(("content-type", sf.content_type.as_str())) |
| 205 | + .body(sf.data.clone()) |
| 206 | + } else { |
| 207 | + HttpResponse::NotFound().finish() |
| 208 | + } |
| 209 | +} |
| 210 | + |
| 211 | +fn load_tls_config() -> Option<ServerConfig> { |
| 212 | + let cert_path = std::env::var("TLS_CERT").unwrap_or_else(|_| "/certs/server.crt".to_string()); |
| 213 | + let key_path = std::env::var("TLS_KEY").unwrap_or_else(|_| "/certs/server.key".to_string()); |
| 214 | + let cert_file = std::fs::File::open(&cert_path).ok()?; |
| 215 | + let key_file = std::fs::File::open(&key_path).ok()?; |
| 216 | + let certs: Vec<_> = rustls_pemfile::certs(&mut io::BufReader::new(cert_file)) |
| 217 | + .filter_map(|r| r.ok()) |
| 218 | + .collect(); |
| 219 | + let key = rustls_pemfile::private_key(&mut io::BufReader::new(key_file)).ok()??; |
| 220 | + let mut config = ServerConfig::builder() |
| 221 | + .with_no_client_auth() |
| 222 | + .with_single_cert(certs, key) |
| 223 | + .ok()?; |
| 224 | + config.alpn_protocols = vec![b"h2".to_vec()]; |
| 225 | + Some(config) |
| 226 | +} |
| 227 | + |
| 228 | +#[actix_web::main] |
| 229 | +async fn main() -> io::Result<()> { |
| 230 | + let dataset = load_dataset(); |
| 231 | + let json_cache = build_json_cache(&dataset); |
| 232 | + let state = Arc::new(AppState { |
| 233 | + dataset, |
| 234 | + json_cache, |
| 235 | + static_files: load_static_files(), |
| 236 | + }); |
| 237 | + |
| 238 | + let tls_config = load_tls_config(); |
| 239 | + let workers = num_cpus::get(); |
| 240 | + |
| 241 | + let mut server = HttpServer::new({ |
| 242 | + let state = state.clone(); |
| 243 | + move || { |
| 244 | + App::new() |
| 245 | + .app_data(web::Data::new(state.clone())) |
| 246 | + .route("/pipeline", web::get().to(pipeline)) |
| 247 | + .route("/baseline11", web::get().to(baseline11_get)) |
| 248 | + .route("/baseline11", web::post().to(baseline11_post)) |
| 249 | + .route("/baseline2", web::get().to(baseline2)) |
| 250 | + .route("/json", web::get().to(json_endpoint)) |
| 251 | + .route("/static/{filename}", web::get().to(static_file)) |
| 252 | + } |
| 253 | + }) |
| 254 | + .workers(workers) |
| 255 | + .backlog(4096) |
| 256 | + .bind("0.0.0.0:8080")?; |
| 257 | + |
| 258 | + if let Some(tls_cfg) = tls_config { |
| 259 | + server = server.bind_rustls_0_23("0.0.0.0:8443", tls_cfg)?; |
| 260 | + } |
| 261 | + |
| 262 | + server.run().await |
| 263 | +} |
0 commit comments