Bladeren bron

hyper: refactor, update Rust, Hyper, and deps (#9720)

* hyper: refactor, update Rust, Hyper, and deps

- Update Rust to 1.85.0
- Update hyper to 1.6.0
- Use socket2 instead of net2
- Bump other dependencies
- Remove hyper-db test. This is now provided as a route in the main test.
- Add a new test using tokio multi-thread runtime to compare with the
  single-thread runtime. This uses the same docker image, but passes a
  command line argument to the server to use the multi-thread runtime.
- Add a healthcheck /ping url to the dockerfile to ensure that the
  server is running before running the tests.
- Use deadpool-postgres for connection pooling.
- Use cached prepared statements for the queries.
- Factor each test into its own module.
- Simplify the previous approach that used closures into more a more
  straightforward approach that uses linear functions.
- Add single query and multiple query tests.

* hyper: add maintainers

* fix: don't pre-compute json content-length

* fix: add back socket.set_nodelay(true)

* fix: don't precompute plaintext content-length header
Josh McKinney 4 maanden geleden
bovenliggende
commit
2875eaf840

+ 1 - 0
frameworks/Rust/hyper/.dockerignore

@@ -0,0 +1 @@
+target/

File diff suppressed because it is too large
+ 557 - 215
frameworks/Rust/hyper/Cargo.lock


+ 36 - 19
frameworks/Rust/hyper/Cargo.toml

@@ -5,30 +5,47 @@ edition = "2018"
 authors = [
     "Steve Klabnik <[email protected]>",
     "Alexander Polyakov <[email protected]>",
-    "Sean McArthur <[email protected]>"
+    "Sean McArthur <[email protected]>",
 ]
 
-[[bin]]
-name = "hyper-techempower"
-path = "src/main.rs"
-
-[[bin]]
-name = "hyper-db-techempower"
-path = "src/main_db.rs"
-
 [dependencies]
+clap = { version = "4.5.32", features = ["derive", "wrap_help"] }
+deadpool-postgres = "0.14.1"
+fastrand = "2.3.0"
+http = "1.3.1"
+http-body-util = "0.1.3"
 # Disable default runtime, so that tokio single thread can be used instead.
-hyper = { version = "0.14", features = ["server", "http1"], default-features = false }
+hyper = { version = "1.6.0", features = [
+    "server",
+    "http1",
+], default-features = false }
+hyper-util = { version = "0.1.10", features = [
+    "http1",
+    "server",
+    "service",
+    "tokio",
+] }
 # Since no logs are allowed anyways, just compile-time turn them all off
-log = { version = "0.4", features = ["release_max_level_off"] }
-markup = "0.3.1"
-net2 = "0.2"
-num_cpus = "1.2"
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-tokio = { version = "1", features = ["rt", "net", "time"] }
-tokio-postgres = { version = "0.7", default-features = false, features = ["runtime"] }
-v_htmlescape = "0.10"
+log = { version = "0.4.26", features = ["release_max_level_off"] }
+markup = "0.15.0"
+num_cpus = "1.16.0"
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = "1.0.140"
+socket2 = { version = "0.5.8", features = ["all"] }
+strum = { version = "0.27.1", features = ["derive"] }
+thiserror = "2.0.12"
+tokio = { version = "1.44.1", features = [
+    "rt",
+    "rt-multi-thread",
+    "net",
+    "time",
+] }
+tokio-postgres = { version = "0.7.13", default-features = false, features = [
+    "runtime",
+] }
+tracing = "0.1.41"
+tracing-subscriber = "0.3.19"
+v_htmlescape = "0.15.8"
 
 [profile.release]
 opt-level = 3

+ 19 - 4
frameworks/Rust/hyper/benchmark_config.json

@@ -1,9 +1,18 @@
 {
   "framework": "hyper",
+  "maintainers": [
+    "polachok",
+    "seanmonstar",
+    "steveklabnik"
+  ],
   "tests": [{
     "default": {
-      "plaintext_url": "/plaintext",
+      "docker_cmd": "hyper-techempower --runtime current-thread",
       "json_url": "/json",
+      "plaintext_url": "/plaintext",
+      "db_url": "/db",
+      "query_url": "/queries?count=",
+      "fortune_url": "/fortunes",
       "port": 8080,
       "approach": "Realistic",
       "classification": "Micro",
@@ -15,10 +24,16 @@
       "webserver": "hyper",
       "os": "Linux",
       "database_os": "Linux",
-      "display_name": "hyper",
+      "display_name": "Hyper (current-thread runtime)",
       "notes": ""
     },
-    "db": {
+    "multi-thread": {
+      "dockerfile": "hyper.dockerfile",
+      "docker_cmd": "hyper-techempower --runtime multi-thread",
+      "json_url": "/json",
+      "plaintext_url": "/plaintext",
+      "db_url": "/db",
+      "query_url": "/queries?count=",
       "fortune_url": "/fortunes",
       "port": 8080,
       "approach": "Realistic",
@@ -31,7 +46,7 @@
       "webserver": "hyper",
       "os": "Linux",
       "database_os": "Linux",
-      "display_name": "hyper",
+      "display_name": "Hyper (multi-thread runtime)",
       "notes": ""
     }
   }]

+ 0 - 11
frameworks/Rust/hyper/hyper-db.dockerfile

@@ -1,11 +0,0 @@
-FROM rust:1.67
-
-ADD ./ /hyper
-WORKDIR /hyper
-
-RUN cargo clean
-RUN RUSTFLAGS="-C target-cpu=native" cargo build --release
-
-EXPOSE 8080
-
-CMD ["./target/release/hyper-db-techempower"]

+ 6 - 9
frameworks/Rust/hyper/hyper.dockerfile

@@ -1,11 +1,8 @@
-FROM rust:1.67
-
-ADD ./ /hyper
-WORKDIR /hyper
-
-RUN cargo clean
-RUN RUSTFLAGS="-C target-cpu=native" cargo build --release
+FROM rust:1.85 AS hyper
 
+WORKDIR /src
+COPY . .
+RUN RUSTFLAGS="-C target-cpu=native" cargo install --path . --locked
 EXPOSE 8080
-
-CMD ["./target/release/hyper-techempower"]
+CMD ["hyper-techempower"]
+HEALTHCHECK CMD curl --fail http://localhost:8080/ping || exit 1

+ 4 - 0
frameworks/Rust/hyper/rustfmt.toml

@@ -0,0 +1,4 @@
+# unstable_features
+comment_width = 100
+group_imports = "StdExternalCrate"
+imports_granularity = "Module"

+ 17 - 42
frameworks/Rust/hyper/src/db.rs

@@ -1,46 +1,21 @@
-use std::net::SocketAddr;
+use std::sync::LazyLock;
 
-use tokio::net::TcpStream;
-use tokio::runtime::Handle;
-use tokio_postgres::{Client, Config, NoTls, Statement};
+use deadpool_postgres::{Config, CreatePoolError, Pool};
+use tokio_postgres::NoTls;
+use tracing::info;
 
-pub struct Db {
-    client: Client,
-    fortune: Statement,
-}
-
-pub struct Fortune {
-    pub id: i32,
-    pub message: String,
-}
-
-pub async fn connect(
-    addr: SocketAddr,
-    config: Config,
-    handle: Handle,
-) -> Result<Db, Box<dyn std::error::Error>> {
-    let stream = TcpStream::connect(&addr).await?;
-    let (client, conn) = config.connect_raw(stream, NoTls).await?;
-    handle.spawn(conn);
-    let fortune = client.prepare("SELECT id, message FROM fortune").await?;
-    Ok(Db { client, fortune })
-}
-
-impl Db {
-    pub async fn tell_fortune(&self) -> Result<Vec<Fortune>, tokio_postgres::Error> {
-        let mut items = vec![Fortune {
-            id: 0,
-            message: "Additional fortune added at request time.".to_string(),
-        }];
+pub static POOL: LazyLock<Pool> =
+    LazyLock::new(|| connect().expect("failed to create database connection pool"));
 
-        let rows = self.client.query(&self.fortune, &[]).await?;
-        for row in rows {
-            items.push(Fortune {
-                id: row.get(0),
-                message: row.get(1),
-            });
-        }
-        items.sort_by(|it, next| it.message.cmp(&next.message));
-        Ok(items)
-    }
+fn connect() -> Result<Pool, CreatePoolError> {
+    info!("Creating database connection pool");
+    let config = Config {
+        host: Some("tfb-database".to_string()),
+        port: Some(5432),
+        dbname: Some("hello_world".to_string()),
+        user: Some("benchmarkdbuser".to_string()),
+        password: Some("benchmarkdbpass".to_string()),
+        ..Default::default()
+    };
+    config.create_pool(None, NoTls)
 }

+ 75 - 0
frameworks/Rust/hyper/src/fortunes.rs

@@ -0,0 +1,75 @@
+use std::convert::Infallible;
+
+use http::header::{CONTENT_LENGTH, CONTENT_TYPE, SERVER};
+use http::Response;
+use http_body_util::combinators::BoxBody;
+use http_body_util::{BodyExt, Full};
+use hyper::body::Bytes;
+use tokio_postgres::Row;
+
+use crate::db::POOL;
+use crate::{Error, SERVER_HEADER, TEXT_HTML};
+
+const QUERY: &str = "SELECT id, message FROM fortune";
+
+pub struct Fortune {
+    pub id: i32,
+    pub message: String,
+}
+
+impl From<&Row> for Fortune {
+    fn from(row: &Row) -> Self {
+        Self {
+            id: row.get(0),
+            message: row.get(1),
+        }
+    }
+}
+
+pub async fn get() -> crate::Result<Response<BoxBody<Bytes, Infallible>>> {
+    let fortunes = tell_fortune().await?;
+    let content = FortunesTemplate { fortunes }.to_string();
+    Response::builder()
+        .header(SERVER, SERVER_HEADER.clone())
+        .header(CONTENT_TYPE, TEXT_HTML.clone())
+        .header(CONTENT_LENGTH, content.len())
+        .body(Full::from(content).boxed())
+        .map_err(Error::from)
+}
+
+async fn tell_fortune() -> crate::Result<Vec<Fortune>> {
+    let db = POOL.get().await?;
+    let statement = db.prepare_cached(QUERY).await?;
+    let rows = db.query(&statement, &[]).await?;
+    let mut fortunes = rows.iter().map(Fortune::from).collect::<Vec<_>>();
+
+    fortunes.push(Fortune {
+        id: 0,
+        message: "Additional fortune added at request time.".to_string(),
+    });
+    fortunes.sort_by(|a, b| a.message.cmp(&b.message));
+
+    Ok(fortunes)
+}
+
+markup::define! {
+    FortunesTemplate(fortunes: Vec<Fortune>) {
+        {markup::doctype()}
+        html {
+            head {
+                title { "Fortunes" }
+            }
+            body {
+                table {
+                    tr { th { "id" } th { "message" } }
+                    @for item in fortunes {
+                        tr {
+                            td { {item.id} }
+                            td { {markup::raw(v_htmlescape::escape(&item.message))} }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 29 - 0
frameworks/Rust/hyper/src/json.rs

@@ -0,0 +1,29 @@
+use std::convert::Infallible;
+
+use http::header::{CONTENT_LENGTH, CONTENT_TYPE, SERVER};
+use http::Response;
+use http_body_util::combinators::BoxBody;
+use http_body_util::{BodyExt, Full};
+use hyper::body::Bytes;
+use serde::Serialize;
+
+use crate::{Error, Result, APPLICATION_JSON, SERVER_HEADER};
+
+#[derive(Serialize)]
+struct JsonResponse<'a> {
+    message: &'a str,
+}
+
+static CONTENT: JsonResponse = JsonResponse {
+    message: "Hello, world!",
+};
+
+pub fn get() -> Result<Response<BoxBody<Bytes, Infallible>>> {
+    let content = serde_json::to_vec(&CONTENT)?;
+    Response::builder()
+        .header(SERVER, SERVER_HEADER.clone())
+        .header(CONTENT_TYPE, APPLICATION_JSON.clone())
+        .header(CONTENT_LENGTH, content.len())
+        .body(Full::from(content).boxed())
+        .map_err(Error::from)
+}

+ 202 - 81
frameworks/Rust/hyper/src/main.rs

@@ -1,91 +1,212 @@
-use hyper::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE, SERVER};
+use std::convert::Infallible;
+use std::net::{Ipv4Addr, SocketAddr};
+use std::{io, thread};
+
+use clap::{Parser, ValueEnum};
+use http_body_util::combinators::BoxBody;
+use http_body_util::{BodyExt, Empty, Full};
+use hyper::body::{Bytes, Incoming};
+use hyper::header::{HeaderValue, SERVER};
+use hyper::server::conn::http1;
 use hyper::service::service_fn;
-use hyper::{Body, Response, StatusCode};
-use serde::Serialize;
+use hyper::{Request, Response, StatusCode};
+use hyper_util::rt::TokioIo;
+use socket2::{Domain, SockAddr, Socket};
+use strum::Display;
+use thiserror::Error;
+use tokio::net::TcpListener;
+use tokio::runtime;
+use tracing::{error, info};
+
+mod db;
+mod fortunes;
+mod json;
+mod multiple_queries;
+mod plaintext;
+mod single_query;
+
+static SERVER_HEADER: HeaderValue = HeaderValue::from_static("hyper");
+static APPLICATION_JSON: HeaderValue = HeaderValue::from_static("application/json");
+static TEXT_HTML: HeaderValue = HeaderValue::from_static("text/html; charset=utf-8");
+static TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain");
+
+type Result<T, E = Error> = std::result::Result<T, E>;
+
+#[derive(Debug, Error)]
+enum Error {
+    #[error("I/O error: {0}")]
+    Io(#[from] io::Error),
+    #[error("Hyper error: {0}")]
+    Hyper(#[from] hyper::Error),
+    #[error("Database error: {0}")]
+    TokioPostgres(#[from] tokio_postgres::Error),
+    #[error("Http error: {0}")]
+    Http(#[from] http::Error),
+    #[error("Database pool error: {0}")]
+    DbPool(#[from] deadpool_postgres::PoolError),
+    #[error("Serde error: {0}")]
+    Serde(#[from] serde_json::Error),
+}
+
+#[derive(Debug, Parser)]
+struct Args {
+    /// The runtime to use.
+    #[arg(short, long, default_value_t = Runtime::default())]
+    runtime: Runtime,
+
+    /// The number of threads to use.
+    ///
+    /// Defaults to the number of logical CPUs cores available on the system.
+    ///
+    /// - For the current thread runtime, this is the number of threads to spawn in addition to the
+    ///   main thread.
+    /// - For the multi-thread runtime, this is the number of worker threads to configure the
+    ///   runtime to use.
+    #[arg(short, long, default_value_t = num_cpus::get())]
+    threads: usize,
+}
+
+#[derive(Clone, Debug, Default, Display, ValueEnum)]
+#[strum(serialize_all = "kebab-case")]
+enum Runtime {
+    #[default]
+    CurrentThread,
+    MultiThread,
+}
+
+fn main() -> Result<()> {
+    // Note: this is only here to capture logs outside of the hot path code. Avoid logging messages
+    // in the hot path code.
+    tracing_subscriber::fmt().with_thread_ids(true).init();
 
-mod server;
+    let args = Args::parse();
+    match args.runtime {
+        Runtime::CurrentThread => run_current_thread(args.threads)?,
+        Runtime::MultiThread => run_multi_thread(args.threads)?,
+    }
 
-static HELLO_WORLD: &[u8] = b"Hello, world!";
+    Ok(())
+}
+
+/// Runs the server using multiple current thread runtimes.
+fn run_current_thread(threads: usize) -> Result<()> {
+    info!("Running with {} threads", threads);
+
+    // Spawn a new runtime on each thread.
+    for _ in 1..threads {
+        let runtime = runtime::Builder::new_current_thread()
+            .enable_all()
+            .build()?;
+        thread::spawn(|| run_server(runtime));
+    }
+    // Run the server on the main thread.
+    let runtime = runtime::Builder::new_current_thread()
+        .enable_all()
+        .build()?;
+    run_server(runtime)
+}
 
-#[derive(Serialize)]
-struct JsonResponse<'a> {
-    message: &'a str,
+/// Runs the server using a single multi-thread runtime.
+fn run_multi_thread(threads: usize) -> Result<()> {
+    let runtime = runtime::Builder::new_multi_thread()
+        .enable_all()
+        .worker_threads(threads)
+        .build()?;
+    run_server(runtime)
 }
 
-fn main() {
-    // It seems most of the other benchmarks create static header values
-    // for performance, so just play by the same rules here...
-    let plaintext_len = HeaderValue::from_static("13");
-    let plaintext_ct = HeaderValue::from_static("text/plain");
-    let json_len = HeaderValue::from_static("27");
-    let json_ct = HeaderValue::from_static("application/json");
-    let server_header = HeaderValue::from_static("hyper");
-
-    server::run(move |socket, http, handle| {
-        // This closure is run for each connection...
-
-        // The plaintext benchmarks use pipelined requests.
-        http.pipeline_flush(true);
-
-        // Gotta clone these to be able to move into the Service...
-        let plaintext_len = plaintext_len.clone();
-        let plaintext_ct = plaintext_ct.clone();
-        let json_len = json_len.clone();
-        let json_ct = json_ct.clone();
-        let server_header = server_header.clone();
-
-        // This is the `Service` that will handle the connection.
-        // `service_fn_ok` is a helper to convert a function that
-        // returns a Response into a `Service`.
-        let svc = service_fn(move |req| {
-            // Gotta clone these to be able to move into future...
-            let plaintext_len = plaintext_len.clone();
-            let plaintext_ct = plaintext_ct.clone();
-            let json_len = json_len.clone();
-            let json_ct = json_ct.clone();
-            let server_header = server_header.clone();
-
-            async move {
-                let (req, _body) = req.into_parts();
-                // For speed, reuse the allocated header map from the request,
-                // instead of allocating a new one. Because.
-                let mut headers = req.headers;
-                headers.clear();
-
-                let body = match req.uri.path() {
-                    // Apparently, other benchmarks don't check the method, so we
-                    // don't either. Yay?
-                    "/plaintext" => {
-                        headers.insert(CONTENT_LENGTH, plaintext_len.clone());
-                        headers.insert(CONTENT_TYPE, plaintext_ct.clone());
-                        Body::from(HELLO_WORLD)
-                    }
-                    "/json" => {
-                        let rep = JsonResponse {
-                            message: "Hello, world!",
-                        };
-                        let rep_body = serde_json::to_vec(&rep).unwrap();
-                        headers.insert(CONTENT_LENGTH, json_len.clone());
-                        headers.insert(CONTENT_TYPE, json_ct.clone());
-                        Body::from(rep_body)
-                    }
-                    _ => {
-                        let mut res = Response::new(Body::empty());
-                        *res.status_mut() = StatusCode::NOT_FOUND;
-                        *res.headers_mut() = headers;
-                        return Ok::<_, std::io::Error>(res);
-                    }
-                };
-
-                headers.insert(SERVER, server_header.clone());
-
-                let mut res = Response::new(body);
-                *res.headers_mut() = headers;
-                Ok(res)
+fn run_server(runtime: runtime::Runtime) -> Result<()> {
+    // It's important to use [`Runtime::block_on()`] here and not [`handle::block_on()`] as
+    // otherwise the runtime will not drive I/O operations. See the [`Handle::block_on`]
+    // documentation for more information.
+    runtime.block_on(serve(runtime.handle()))
+}
+
+async fn serve(handle: &runtime::Handle) -> Result<()> {
+    let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 8080));
+    let socket = create_socket(addr)?;
+
+    let listener = TcpListener::from_std(socket.into())?;
+    let addr = listener.local_addr()?;
+    info!("Listening on: {}", addr);
+
+    // spawn accept loop into a task so it is scheduled on the runtime with all the other tasks.
+    let accept_loop = accept_loop(handle.clone(), listener);
+    handle.spawn(accept_loop).await.unwrap()
+}
+
+/// Create a socket that allows reuse of the address and port.
+///
+/// This makes it possible for multiple instances of the server task to bind to the same address and
+/// port.
+fn create_socket(addr: SocketAddr) -> Result<Socket> {
+    let domain = match addr {
+        SocketAddr::V4(_) => Domain::IPV4,
+        SocketAddr::V6(_) => Domain::IPV6,
+    };
+    let addr = SockAddr::from(addr);
+    let socket = Socket::new(domain, socket2::Type::STREAM, None)?;
+    let backlog = 4096; // maximum number of pending connections
+    #[cfg(unix)]
+    socket.set_reuse_port(true)?;
+    socket.set_reuse_address(true)?;
+    socket.set_nodelay(true)?;
+    socket.set_nonblocking(true)?; // required for tokio
+    socket.bind(&addr)?;
+    socket.listen(backlog)?;
+
+    Ok(socket)
+}
+
+/// Accept loop that accepts incoming connections and spawns a new task to handle each connection.
+async fn accept_loop(handle: runtime::Handle, listener: TcpListener) -> Result<()> {
+    let mut http = http1::Builder::new();
+    http.pipeline_flush(true);
+
+    let service = service_fn(router);
+    loop {
+        let (stream, _) = listener.accept().await?;
+        let http = http.clone();
+        handle.spawn(async move {
+            let io = TokioIo::new(stream);
+            if let Err(_e) = http.serve_connection(io, service).await {
+                // ignore errors until https://github.com/hyperium/hyper/pull/3863/ is merged
+                // This PR will allow us to filter out shutdown errors which are expected.
+                // warn!("Connection error (this may be normal during shutdown): {e}");
             }
         });
+    }
+}
+
+/// Routes requests to the appropriate handler.
+async fn router(request: Request<Incoming>) -> Result<Response<BoxBody<Bytes, Infallible>>> {
+    // The method is always GET, so we don't check it.
+    match request.uri().path() {
+        "/ping" => ping(),
+        "/json" => json::get(),
+        "/db" => single_query::get().await,
+        "/queries" => multiple_queries::get(request.uri().query()).await,
+        "/fortunes" => fortunes::get().await,
+        "/plaintext" => plaintext::get(),
+        _ => not_found_error(),
+    }
+}
+
+/// A handler that returns a "pong" response.
+///
+/// This handler is used to verify that the server is running and can respond to requests. It is
+/// used by the docker health check command.
+fn ping() -> Result<Response<BoxBody<Bytes, Infallible>>> {
+    Response::builder()
+        .body(Full::from("pong").boxed())
+        .map_err(Error::from)
+}
 
-        // Spawn the `serve_connection` future into the runtime.
-        handle.spawn(http.serve_connection(socket, svc));
-    })
+/// A handler that returns a 404 response.
+fn not_found_error() -> Result<Response<BoxBody<Bytes, Infallible>>> {
+    Response::builder()
+        .status(StatusCode::NOT_FOUND)
+        .header(SERVER, SERVER_HEADER.clone())
+        .body(Empty::new().boxed())
+        .map_err(Error::from)
 }

+ 0 - 109
frameworks/Rust/hyper/src/main_db.rs

@@ -1,109 +0,0 @@
-use std::fmt::Write;
-use std::sync::Arc;
-
-use hyper::header::{HeaderValue, CONTENT_TYPE, SERVER};
-use hyper::service::service_fn;
-use hyper::{Body, Response};
-use std::net::ToSocketAddrs;
-
-mod db;
-mod server;
-
-fn main() {
-    //"postgres://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world";
-    let mut psql_config = tokio_postgres::Config::new();
-    psql_config
-        .user("benchmarkdbuser")
-        .password("benchmarkdbpass")
-        .dbname("hello_world");
-    let psql_addr = ("tfb-database", 5432)
-        .to_socket_addrs()
-        .expect("must be able to resolve database hostname")
-        .next()
-        .expect("database hostname must resolve to an address");
-
-    server::run(move |socket, http, handle| {
-        let http = http.clone();
-        let handle2 = handle.clone();
-
-        let html_ct = HeaderValue::from_static("text/html; charset=utf-8");
-        let server_header = HeaderValue::from_static("hyper");
-        let config = psql_config.clone();
-
-        // Before handling any requests, we should grab a DB connection.
-        let db_fut = async move {
-            let handle = handle2.clone();
-            let db_conn = db::connect(psql_addr, config, handle).await?;
-            let db_conn = Arc::new(db_conn);
-
-            let html_ct = html_ct.clone();
-            let server_header = server_header.clone();
-
-            // This is the `Service` that will handle the connection.
-            // `service_fn` is a helper to convert a function that
-            // returns a Future<Item=Response> into a `Service`.
-            let svc = service_fn(move |req| {
-                let html_ct = html_ct.clone();
-                let server_header = server_header.clone();
-                let db_conn = db_conn.clone();
-
-                async move {
-                    let (req, _body) = req.into_parts();
-                    // For speed, reuse the allocated header map from the request,
-                    // instead of allocating a new one. Because.
-                    let mut headers = req.headers;
-                    headers.clear();
-
-                    headers.insert(CONTENT_TYPE, html_ct.clone());
-                    headers.insert(SERVER, server_header.clone());
-
-                    match req.uri.path() {
-                        "/fortunes" => {
-                            let fortunes = db_conn.tell_fortune().await?;
-                            let mut buf = String::with_capacity(2048);
-                            let _ = write!(&mut buf, "{}", FortunesTemplate { fortunes });
-                            let mut res = Response::new(Body::from(buf));
-                            *res.headers_mut() = headers;
-                            Ok::<_, Box<dyn std::error::Error + Send + Sync>>(res)
-                        }
-                        _ => {
-                            let mut res = Response::new(Body::empty());
-                            *res.status_mut() = hyper::StatusCode::NOT_FOUND;
-                            *res.headers_mut() = headers;
-                            Ok(res)
-                        }
-                    }
-                }
-            });
-            // Spawn the `serve_connection` future into the runtime.
-            handle2.spawn(http.serve_connection(socket, svc));
-            Ok::<_, Box<dyn std::error::Error>>(())
-        };
-
-        handle.spawn(async move {
-            let _ = db_fut.await;
-        });
-    });
-}
-
-markup::define! {
-    FortunesTemplate(fortunes: Vec<db::Fortune>) {
-        {markup::doctype()}
-        html {
-            head {
-                title { "Fortunes" }
-            }
-            body {
-                table {
-                    tr { th { "id" } th { "message" } }
-                    @for item in {fortunes} {
-                        tr {
-                            td { {item.id} }
-                            td { {markup::raw(v_htmlescape::escape(&item.message))} }
-                        }
-                    }
-                }
-            }
-        }
-    }
-}

+ 59 - 0
frameworks/Rust/hyper/src/multiple_queries.rs

@@ -0,0 +1,59 @@
+use std::convert::Infallible;
+
+use http::header::{CONTENT_LENGTH, CONTENT_TYPE, SERVER};
+use http::Response;
+use http_body_util::combinators::BoxBody;
+use http_body_util::{BodyExt, Full};
+use hyper::body::Bytes;
+use serde::Serialize;
+use tokio_postgres::Row;
+
+use crate::db::POOL;
+use crate::{Error, Result, APPLICATION_JSON, SERVER_HEADER};
+
+const QUERY: &str = "SELECT id, randomnumber FROM world WHERE id = $1";
+
+#[derive(Debug, Serialize)]
+pub struct World {
+    id: i32,
+    randomnumber: i32,
+}
+
+impl From<Row> for World {
+    fn from(row: Row) -> Self {
+        World {
+            id: row.get(0),
+            randomnumber: row.get(1),
+        }
+    }
+}
+
+pub async fn get(query: Option<&str>) -> Result<Response<BoxBody<Bytes, Infallible>>> {
+    let count = query
+        .and_then(|query| query.strip_prefix("count="))
+        .and_then(|query| query.parse().ok())
+        .unwrap_or(1)
+        .clamp(1, 500);
+
+    let worlds = query_worlds(count).await?;
+    let json = serde_json::to_vec(&worlds)?;
+
+    Response::builder()
+        .header(SERVER, SERVER_HEADER.clone())
+        .header(CONTENT_TYPE, APPLICATION_JSON.clone())
+        .header(CONTENT_LENGTH, json.len())
+        .body(Full::from(json).boxed())
+        .map_err(Error::from)
+}
+
+async fn query_worlds(count: usize) -> Result<Vec<World>> {
+    let db = POOL.get().await?;
+    let statement = db.prepare_cached(QUERY).await?;
+    let mut worlds = Vec::with_capacity(count);
+    for _ in 0..count {
+        let id = fastrand::i32(1..10_000);
+        let row = db.query_one(&statement, &[&id]).await?;
+        worlds.push(World::from(row));
+    }
+    Ok(worlds)
+}

+ 20 - 0
frameworks/Rust/hyper/src/plaintext.rs

@@ -0,0 +1,20 @@
+use std::convert::Infallible;
+
+use http::header::{CONTENT_LENGTH, CONTENT_TYPE, SERVER};
+use http::Response;
+use http_body_util::combinators::BoxBody;
+use http_body_util::{BodyExt, Full};
+use hyper::body::Bytes;
+
+use crate::{Error, Result, SERVER_HEADER, TEXT_PLAIN};
+
+static CONTENT: &[u8] = b"Hello, world!";
+
+pub fn get() -> Result<Response<BoxBody<Bytes, Infallible>>> {
+    Response::builder()
+        .header(SERVER, SERVER_HEADER.clone())
+        .header(CONTENT_TYPE, TEXT_PLAIN.clone())
+        .header(CONTENT_LENGTH, CONTENT.len())
+        .body(Full::from(CONTENT).boxed())
+        .map_err(Error::from)
+}

+ 0 - 79
frameworks/Rust/hyper/src/server.rs

@@ -1,79 +0,0 @@
-use std::io;
-use std::net::SocketAddr;
-use std::thread;
-
-use hyper::server::conn::Http;
-use tokio::net::{TcpListener, TcpStream};
-use tokio::runtime::{Builder as RuntimeBuilder, Handle};
-
-pub(crate) fn run<F>(per_connection: F)
-where
-    F: Fn(TcpStream, &mut Http, Handle) + Clone + Send + 'static,
-{
-    // Spawn a thread for each available core, minus one, since we'll
-    // reuse the main thread as a server thread as well.
-    for _ in 1..num_cpus::get() {
-        let per_connection = per_connection.clone();
-        thread::spawn(move || {
-            server_thread(per_connection);
-        });
-    }
-    server_thread(per_connection);
-}
-
-fn server_thread<F>(per_connection: F)
-where
-    F: Fn(TcpStream, &mut Http, Handle) + Send + 'static,
-{
-    let mut http = Http::new();
-    http.http1_only(true);
-
-    // Our event loop...
-    let core = RuntimeBuilder::new_current_thread()
-        .enable_all()
-        .build()
-        .expect("runtime");
-    let handle = core.handle();
-
-    // Bind to 0.0.0.0:8080
-    let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
-
-    // For every accepted connection, spawn an HTTP task
-    let server = async move {
-        let tcp = reuse_listener(&addr).expect("couldn't bind to addr");
-        loop {
-            match tcp.accept().await {
-                Ok((sock, _)) => {
-                    let _ = sock.set_nodelay(true);
-                    per_connection(sock, &mut http, handle.clone());
-                }
-                Err(e) => {
-                    log::warn!("accept error: {}", e)
-                }
-            }
-        }
-    };
-
-    core.block_on(server);
-}
-
-fn reuse_listener(addr: &SocketAddr) -> io::Result<TcpListener> {
-    let builder = match *addr {
-        SocketAddr::V4(_) => net2::TcpBuilder::new_v4()?,
-        SocketAddr::V6(_) => net2::TcpBuilder::new_v6()?,
-    };
-
-    #[cfg(unix)]
-    {
-        use net2::unix::UnixTcpBuilderExt;
-        if let Err(e) = builder.reuse_port(true) {
-            eprintln!("error setting SO_REUSEPORT: {}", e);
-        }
-    }
-
-    builder.reuse_address(true)?;
-    builder.bind(addr)?;
-    let listener = builder.listen(1024)?;
-    listener.set_nonblocking(true)?;
-    TcpListener::from_std(listener)
-}

+ 50 - 0
frameworks/Rust/hyper/src/single_query.rs

@@ -0,0 +1,50 @@
+use std::convert::Infallible;
+
+use http::header::{CONTENT_LENGTH, CONTENT_TYPE, SERVER};
+use http::Response;
+use http_body_util::combinators::BoxBody;
+use http_body_util::{BodyExt, Full};
+use hyper::body::Bytes;
+use serde::Serialize;
+use tokio_postgres::Row;
+
+use crate::db::POOL;
+use crate::{Error, Result, APPLICATION_JSON, SERVER_HEADER};
+
+static QUERY: &str = "SELECT id, randomnumber FROM world WHERE id = $1";
+
+#[derive(Debug, Serialize)]
+pub struct World {
+    id: i32,
+    randomnumber: i32,
+}
+
+impl From<Row> for World {
+    fn from(row: Row) -> Self {
+        World {
+            id: row.get(0),
+            randomnumber: row.get(1),
+        }
+    }
+}
+
+pub async fn get() -> Result<Response<BoxBody<Bytes, Infallible>>> {
+    let id = fastrand::i32(1..10_000);
+    let world = query_world(id).await?;
+    let json = serde_json::to_vec(&world)?;
+
+    Response::builder()
+        .header(SERVER, SERVER_HEADER.clone())
+        .header(CONTENT_TYPE, APPLICATION_JSON.clone())
+        .header(CONTENT_LENGTH, json.len())
+        .body(Full::from(json).boxed())
+        .map_err(Error::from)
+}
+
+async fn query_world(id: i32) -> Result<World, Error> {
+    let db = POOL.get().await?;
+    let statement = db.prepare_cached(QUERY).await?;
+    let row = db.query_one(&statement, &[&id]).await?;
+    let world = World::from(row);
+    Ok(world)
+}

Some files were not shown because too many files changed in this diff