ソースを参照

chore(Rust): Add Viz (#7488)

Fangdun Tsai 2 年 前
コミット
595d746dab

+ 2 - 0
frameworks/Rust/viz/.gitignore

@@ -0,0 +1,2 @@
+.cargo
+target

+ 47 - 0
frameworks/Rust/viz/Cargo.toml

@@ -0,0 +1,47 @@
+[package]
+name = "viz"
+version = "0.1.0"
+edition = "2021"
+authors = ["Fangdun Tsai <[email protected]>"]
+
+[[bin]]
+name = "viz"
+path = "src/main.rs"
+
+[[bin]]
+name = "viz-pg"
+path = "src/main_pg.rs"
+required-features = ["tokio-postgres", "yarte"]
+
+[[bin]]
+name = "viz-sqlx"
+path = "src/main_sqlx.rs"
+required-features = ["sqlx", "markup", "v_htmlescape"]
+
+[dependencies]
+viz = "0.4.3"
+hyper = "0.14"
+atoi = "2.0.0"
+serde = { version = "1.0", features = ["derive"] }
+tokio = { version = "1.21", features = ["macros", "rt-multi-thread"] }
+nanorand = "0.7"
+thiserror = "1.0"
+futures-util = "0.3.24"
+stretto = { version = "0.7", features = ["async"] }
+
+tokio-postgres = { version = "0.7.7", optional = true }
+sqlx = { version = "0.6.2", features = [
+  "postgres",
+  "macros",
+  "runtime-tokio-native-tls",
+], optional = true }
+# diesel = { version = "1.4.8", features = ["postgres"], optional = true }
+
+yarte = { version = "0.15", features = ["bytes-buf", "json"], optional = true }
+markup = { version = "0.13.1", optional = true }
+v_htmlescape = { version = "0.15.7", optional = true }
+
+[profile.release]
+lto = true
+opt-level = 3
+codegen-units = 1

+ 5 - 0
frameworks/Rust/viz/README.md

@@ -0,0 +1,5 @@
+# [viz](https://github.com/viz-rs) web framework
+
+## Description
+
+Fast, robust, flexible, lightweight web framework for Rust.

+ 70 - 0
frameworks/Rust/viz/benchmark_config.json

@@ -0,0 +1,70 @@
+{
+  "framework": "viz",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Platform",
+        "database": "none",
+        "framework": "Viz",
+        "language": "Rust",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "Rust",
+        "webserver": "Hyper",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Viz",
+        "notes": "",
+        "versus": "None"
+      },
+      "pg": {
+        "db_url": "/db",
+        "fortune_url": "/fortunes",
+        "query_url": "/queries?q=",
+        "update_url": "/updates?q=",
+        "cached_query_url": "/cached_queries?q=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Fullstack",
+        "database": "postgres",
+        "framework": "Viz",
+        "language": "Rust",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "Rust",
+        "webserver": "Hyper",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Viz [Postgresql]",
+        "notes": "",
+        "versus": "None"
+      },
+      "sqlx": {
+        "db_url": "/db",
+        "fortune_url": "/fortunes",
+        "query_url": "/queries?q=",
+        "update_url": "/updates?q=",
+        "cached_query_url": "/cached_queries?q=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Fullstack",
+        "database": "postgres",
+        "framework": "Viz",
+        "language": "Rust",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "Rust",
+        "webserver": "Hyper",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Viz [Postgresql - SQLx]",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 45 - 0
frameworks/Rust/viz/config.toml

@@ -0,0 +1,45 @@
+[framework]
+name = "viz"
+
+[main]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "viz"
+versus = ""
+
+[pg]
+urls.db = "/db"
+urls.query = "/queries?q="
+urls.update = "/updates?q="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Fullstack"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "viz"
+versus = ""
+
+[sqlx]
+urls.db = "/db"
+urls.query = "/queries?q="
+urls.update = "/updates?q="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Fullstack"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "viz"
+versus = ""

+ 2 - 0
frameworks/Rust/viz/rustfmt.toml

@@ -0,0 +1,2 @@
+max_width = 89
+reorder_imports = true

+ 199 - 0
frameworks/Rust/viz/src/db_pg.rs

@@ -0,0 +1,199 @@
+use std::fmt::Write;
+use std::io;
+
+use futures_util::{stream::FuturesUnordered, TryFutureExt, TryStreamExt};
+use nanorand::{Rng, WyRand};
+use tokio_postgres::{connect, types::ToSql, Client, NoTls, Statement};
+use viz::{Error, IntoResponse, Response, StatusCode};
+
+use crate::models::{Fortune, World};
+
+/// Postgres Error
+#[derive(Debug, thiserror::Error)]
+pub enum PgError {
+    #[error("connect to database was failed")]
+    Connect,
+    #[error(transparent)]
+    Io(#[from] io::Error),
+    #[error(transparent)]
+    Pg(#[from] tokio_postgres::Error),
+}
+
+impl From<PgError> for Error {
+    fn from(e: PgError) -> Self {
+        Error::Responder(e.into_response())
+    }
+}
+
+impl IntoResponse for PgError {
+    fn into_response(self) -> Response {
+        (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
+    }
+}
+
+/// Postgres interface
+pub struct PgConnection {
+    rng: WyRand,
+    client: Client,
+    world: Statement,
+    fortune: Statement,
+    updates: Vec<Statement>,
+}
+
+impl PgConnection {
+    pub async fn connect(db_url: &str) -> PgConnection {
+        let (client, conn) = connect(db_url, NoTls)
+            .await
+            .expect("can not connect to postgresql");
+
+        // Spawn connection
+        tokio::spawn(async move {
+            if let Err(error) = conn.await {
+                eprintln!("Connection error: {}", error);
+            }
+        });
+
+        let fortune = client.prepare("SELECT * FROM fortune").await.unwrap();
+        let mut updates = Vec::new();
+
+        for num in 1..=500u16 {
+            let mut pl = 1;
+            let mut q = String::new();
+
+            q.push_str("UPDATE world SET randomnumber = CASE id ");
+
+            for _ in 1..=num {
+                let _ = write!(q, "when ${} then ${} ", pl, pl + 1);
+                pl += 2;
+            }
+
+            q.push_str("ELSE randomnumber END WHERE id IN (");
+
+            for _ in 1..=num {
+                let _ = write!(q, "${},", pl);
+                pl += 1;
+            }
+
+            q.pop();
+            q.push(')');
+
+            updates.push(client.prepare(&q).await.unwrap());
+        }
+
+        let world = client
+            .prepare("SELECT * FROM world WHERE id = $1")
+            .await
+            .unwrap();
+
+        PgConnection {
+            rng: WyRand::new(),
+            world,
+            client,
+            fortune,
+            updates,
+        }
+    }
+}
+
+impl PgConnection {
+    async fn query_one_world(&self, id: i32) -> Result<World, PgError> {
+        self.client
+            .query_one(&self.world, &[&id])
+            .await
+            .map(|row| World {
+                id: row.get(0),
+                randomnumber: row.get(1),
+            })
+            .map_err(PgError::Pg)
+    }
+
+    pub async fn get_world(&self) -> Result<World, PgError> {
+        let random_id = (self.rng.clone().generate::<u32>() % 10_000 + 1) as i32;
+        self.query_one_world(random_id).await
+    }
+
+    pub async fn get_worlds(&self, num: u16) -> Result<Vec<World>, PgError> {
+        let mut rng = self.rng.clone();
+        (0..num)
+            .map(|_| {
+                let id = (rng.generate::<u32>() % 10_000 + 1) as i32;
+                self.query_one_world(id)
+            })
+            .collect::<FuturesUnordered<_>>()
+            .try_collect()
+            .await
+    }
+
+    pub async fn get_worlds_by_limit(&self, limit: i64) -> Result<Vec<World>, PgError> {
+        self.client
+            .query("SELECT * FROM world LIMIT $1", &[&limit])
+            .await
+            .map(|rows| {
+                rows.iter()
+                    .map(|row| World {
+                        id: row.get(0),
+                        randomnumber: row.get(1),
+                    })
+                    .collect()
+            })
+            .map_err(PgError::Pg)
+    }
+
+    pub async fn update(&self, num: u16) -> Result<Vec<World>, PgError> {
+        let mut rng = self.rng.clone();
+
+        let worlds: Vec<World> = (0..num)
+            .map(|_| {
+                let id = (rng.generate::<u32>() % 10_000 + 1) as i32;
+                let rid = (rng.generate::<u32>() % 10_000 + 1) as i32;
+                self.query_one_world(id).map_ok(move |mut world| {
+                    world.randomnumber = rid;
+                    world
+                })
+            })
+            .collect::<FuturesUnordered<_>>()
+            .try_collect()
+            .await?;
+
+        let mut params: Vec<&(dyn ToSql + Sync)> = Vec::with_capacity(num as usize * 3);
+
+        for w in &worlds {
+            params.push(&w.id);
+            params.push(&w.randomnumber);
+        }
+
+        for w in &worlds {
+            params.push(&w.id);
+        }
+
+        let st = self.updates[(num as usize) - 1].clone();
+
+        self.client.query(&st, &params[..]).await?;
+
+        Ok(worlds)
+    }
+
+    pub async fn tell_fortune(&self) -> Result<Vec<Fortune>, PgError> {
+        let mut items = Vec::with_capacity(32);
+
+        items.push(Fortune {
+            id: 0,
+            message: "Additional fortune added at request time.".to_string(),
+        });
+
+        self.client
+            .query(&self.fortune, &[])
+            .await?
+            .iter()
+            .for_each(|row| {
+                items.push(Fortune {
+                    id: row.get(0),
+                    message: row.get(1),
+                })
+            });
+
+        items.sort_by(|it, next| it.message.cmp(&next.message));
+
+        Ok(items)
+    }
+}

+ 59 - 0
frameworks/Rust/viz/src/db_sqlx.rs

@@ -0,0 +1,59 @@
+use sqlx::Pool;
+
+use viz::{
+    async_trait, Error, FromRequest, IntoResponse, Request, RequestExt, Response,
+    StatusCode,
+};
+
+use crate::utils::get_query_param;
+
+pub use sqlx::{
+    pool::PoolConnection,
+    postgres::{PgArguments, PgPoolOptions},
+    Postgres,
+};
+
+pub struct DatabaseConnection(pub PoolConnection<Postgres>);
+
+#[async_trait]
+impl FromRequest for DatabaseConnection {
+    type Error = PgError;
+
+    async fn extract(req: &mut Request) -> Result<Self, Self::Error> {
+        req.state::<Pool<Postgres>>()
+            .ok_or(PgError(sqlx::Error::Io(std::io::Error::from(
+                std::io::ErrorKind::NotConnected,
+            ))))?
+            .acquire()
+            .await
+            .map(Self)
+            .map_err(PgError)
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error(transparent)]
+pub struct PgError(#[from] pub sqlx::Error);
+
+impl From<PgError> for Error {
+    fn from(e: PgError) -> Self {
+        Error::Responder(e.into_response())
+    }
+}
+
+impl IntoResponse for PgError {
+    fn into_response(self) -> Response {
+        (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
+    }
+}
+
+pub struct Counter(pub u16);
+
+#[async_trait]
+impl FromRequest for Counter {
+    type Error = Error;
+
+    async fn extract(req: &mut Request) -> Result<Self, Self::Error> {
+        Ok(Counter(get_query_param(req.query_string())))
+    }
+}

+ 38 - 0
frameworks/Rust/viz/src/main.rs

@@ -0,0 +1,38 @@
+use serde::Serialize;
+use viz::{
+    header::SERVER, Error, Request, Response, ResponseExt, Result, Router, ServiceMaker,
+};
+
+mod server;
+mod utils;
+
+#[derive(Serialize)]
+struct Message {
+    message: &'static str,
+}
+
+async fn plaintext(_: Request) -> Result<Response> {
+    let mut res = Response::text("Hello, World!");
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+async fn json(_: Request) -> Result<Response> {
+    let mut res = Response::json(Message {
+        message: "Hello, World!",
+    })?;
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let app = Router::new()
+        .get("/plaintext", plaintext)
+        .get("/json", json);
+
+    server::builder()
+        .serve(ServiceMaker::from(app))
+        .await
+        .map_err(Error::normal)
+}

+ 114 - 0
frameworks/Rust/viz/src/main_pg.rs

@@ -0,0 +1,114 @@
+use std::convert::identity;
+use std::sync::Arc;
+
+use nanorand::{Rng, WyRand};
+use stretto::AsyncCache;
+use viz::{
+    header::SERVER, types::State, Error, HandlerExt, Request, RequestExt, Response,
+    ResponseExt, Result, Router, ServiceMaker,
+};
+use yarte::ywrite_html;
+
+mod db_pg;
+mod models;
+mod server;
+mod utils;
+
+use db_pg::{PgConnection, PgError};
+
+async fn db(req: Request) -> Result<Response> {
+    let db = req.state::<Arc<PgConnection>>().ok_or(PgError::Connect)?;
+
+    let world = db.get_world().await?;
+
+    let mut res = Response::json(world)?;
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+async fn fortunes(req: Request) -> Result<Response> {
+    let db = req.state::<Arc<PgConnection>>().ok_or(PgError::Connect)?;
+
+    let fortunes = db.tell_fortune().await?;
+
+    let mut buf = String::with_capacity(2048);
+    ywrite_html!(buf, "{{> fortune }}");
+
+    let mut res = Response::html(buf);
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+async fn queries(req: Request) -> Result<Response> {
+    let db = req.state::<Arc<PgConnection>>().ok_or(PgError::Connect)?;
+
+    let count = utils::get_query_param(req.query_string());
+    let worlds = db.get_worlds(count).await?;
+
+    let mut res = Response::json(worlds)?;
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+async fn cached_queries(req: Request) -> Result<Response> {
+    let cahced = req
+        .state::<AsyncCache<i32, models::World>>()
+        .ok_or(PgError::Connect)?;
+
+    let count = utils::get_query_param(req.query_string());
+    let mut rng = WyRand::new();
+
+    let worlds = (0..count)
+        .map(|_| {
+            let id = (rng.generate::<u32>() % 10_000 + 1) as i32;
+            cahced.get(&id).map(|v| v.read())
+        })
+        .filter_map(identity)
+        .collect::<Vec<_>>();
+
+    let mut res = Response::json(worlds)?;
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+async fn updates(req: Request) -> Result<Response> {
+    let db = req.state::<Arc<PgConnection>>().ok_or(PgError::Connect)?;
+
+    let count = utils::get_query_param(req.query_string());
+    let worlds = db.update(count).await?;
+
+    let mut res = Response::json(worlds)?;
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    const DB_URL: &str =
+        "postgres://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world";
+
+    let pg_conn = Arc::new(PgConnection::connect(DB_URL).await);
+
+    let cached = AsyncCache::new(10_000, 1e6 as i64, tokio::spawn).unwrap();
+
+    {
+        let worlds = pg_conn.get_worlds_by_limit(10_000).await?;
+        for w in worlds {
+            cached.insert(w.id, w, 1).await;
+        }
+        cached.wait().await.expect("cache insert failed");
+    }
+
+    let app = Router::new()
+        .get("/db", db)
+        .get("/fortunes", fortunes)
+        .get("/queries", queries)
+        .get("/updates", updates)
+        .with(State::new(pg_conn))
+        .get("/cached_queries", cached_queries.with(State::new(cached)));
+
+    server::builder()
+        .serve(ServiceMaker::from(app))
+        .await
+        .map_err(Error::normal)
+}

+ 218 - 0
frameworks/Rust/viz/src/main_sqlx.rs

@@ -0,0 +1,218 @@
+use std::convert::identity;
+
+use nanorand::{Rng, WyRand};
+use sqlx::Arguments;
+use stretto::AsyncCache;
+use viz::{
+    get, header::SERVER, types::State, BytesMut, Error, IntoHandler, Response,
+    ResponseExt, Result, Router, ServiceMaker,
+};
+
+mod db_sqlx;
+mod models_sqlx;
+mod server;
+mod utils;
+
+use db_sqlx::{Counter, DatabaseConnection, PgArguments, PgError, PgPoolOptions};
+use models_sqlx::{Fortune, World};
+
+async fn db(
+    State(mut rng): State<WyRand>,
+    DatabaseConnection(mut conn): DatabaseConnection,
+) -> Result<Response> {
+    let random_id = (rng.generate::<u32>() % 10_000 + 1) as i32;
+    let mut args = PgArguments::default();
+    args.add(random_id);
+
+    let world: World =
+        sqlx::query_as_with("SELECT id, randomnumber FROM World WHERE id = $1", args)
+            .fetch_one(&mut conn)
+            .await
+            .map_err(PgError)?;
+
+    let mut res = Response::json(world)?;
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+async fn fortunes(DatabaseConnection(mut conn): DatabaseConnection) -> Result<Response> {
+    let mut items: Vec<Fortune> = sqlx::query_as("SELECT * FROM Fortune")
+        .fetch_all(&mut conn)
+        .await
+        .map_err(PgError)?;
+
+    items.push(Fortune {
+        id: 0,
+        message: "Additional fortune added at request time.".to_string(),
+    });
+
+    items.sort_by(|it, next| it.message.cmp(&next.message));
+
+    let mut buf = BytesMut::with_capacity(2048);
+    buf.extend(FortunesTemplate { items }.to_string().as_bytes());
+
+    let mut res = Response::html(buf.freeze());
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+async fn queries(
+    Counter(count): Counter,
+    State(mut rng): State<WyRand>,
+    DatabaseConnection(mut conn): DatabaseConnection,
+) -> Result<Response> {
+    let mut worlds = Vec::with_capacity(count as usize);
+
+    for _ in 0..count {
+        let id = (rng.generate::<u32>() % 10_000 + 1) as i32;
+
+        let mut args = PgArguments::default();
+        args.add(id);
+
+        let world = sqlx::query_as_with::<_, World, _>(
+            "SELECT id, randomnumber FROM World WHERE id = $1",
+            args,
+        )
+        .fetch_one(&mut conn)
+        .await
+        .map_err(PgError)?;
+
+        worlds.push(world);
+    }
+
+    let mut res = Response::json(worlds)?;
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+async fn cached_queries(
+    Counter(count): Counter,
+    State(mut rng): State<WyRand>,
+    State(cached): State<AsyncCache<i32, World>>,
+) -> Result<Response> {
+    let worlds = (0..count)
+        .map(|_| {
+            let id = (rng.generate::<u32>() % 10_000 + 1) as i32;
+            cached.get(&id).map(|v| v.read())
+        })
+        .filter_map(identity)
+        .collect::<Vec<_>>();
+
+    let mut res = Response::json(worlds)?;
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+async fn updates(
+    Counter(count): Counter,
+    State(mut rng): State<WyRand>,
+    DatabaseConnection(mut conn): DatabaseConnection,
+) -> Result<Response> {
+    let mut worlds = Vec::with_capacity(count as usize);
+
+    for _ in 0..count {
+        let id = (rng.generate::<u32>() % 10_000 + 1) as i32;
+
+        let mut args = PgArguments::default();
+        args.add(id);
+
+        let world = sqlx::query_as_with::<_, World, _>(
+            "SELECT id, randomnumber FROM World WHERE id = $1",
+            args,
+        )
+        .fetch_one(&mut conn)
+        .await
+        .map_err(PgError)?;
+
+        worlds.push(world);
+    }
+
+    for w in &mut worlds {
+        let randomnumber = (rng.generate::<u32>() % 10_000 + 1) as i32;
+        let mut args = PgArguments::default();
+        args.add(randomnumber);
+        args.add(w.id);
+        w.randomnumber = randomnumber;
+
+        sqlx::query_with("UPDATE World SET randomNumber = $1 WHERE id = $2", args)
+            .execute(&mut conn)
+            .await
+            .map_err(PgError)?;
+    }
+
+    let mut res = Response::json(worlds)?;
+    res.headers_mut().insert(SERVER, utils::HDR_SERVER);
+    Ok(res)
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    const DB_URL: &str =
+        "postgres://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world";
+
+    let pool = PgPoolOptions::new()
+        .max_connections(56)
+        .min_connections(56)
+        .connect(DB_URL)
+        .await
+        .map_err(PgError)?;
+
+    let rng = WyRand::new();
+
+    let cached = AsyncCache::new(10_000, 1e6 as i64, tokio::spawn).unwrap();
+
+    {
+        let mut conn = pool.acquire().await.map_err(PgError)?;
+        let mut args = PgArguments::default();
+        args.add(10_000);
+        let worlds: Vec<World> =
+            sqlx::query_as_with("SELECT id, randomnumber FROM World LIMIT $1", args)
+                .fetch_all(&mut conn)
+                .await
+                .map_err(PgError)?;
+
+        for w in worlds {
+            cached.insert(w.id, w, 1).await;
+        }
+        cached.wait().await.expect("cache insert failed");
+    }
+
+    let app = Router::new()
+        .route("/db", get(db.into_handler()))
+        .route("/fortunes", get(fortunes.into_handler()))
+        .route("/queries", get(queries.into_handler()))
+        .route("/updates", get(updates.into_handler()))
+        .with(State::new(pool))
+        .route(
+            "/cached_queries",
+            get(cached_queries.into_handler()).with(State::new(cached)),
+        )
+        .with(State::new(rng));
+
+    server::builder()
+        .serve(ServiceMaker::from(app))
+        .await
+        .map_err(Error::normal)
+}
+
+markup::define! {
+    FortunesTemplate(items: Vec<Fortune>) {
+        {markup::doctype()}
+        html {
+            head {
+                title { "Fortunes" }
+            }
+            body {
+                table {
+                    tr { th { "id" } th { "message" } }
+                    @for item in items {
+                        tr {
+                            td { {item.id} }
+                            td { {markup::raw(v_htmlescape::escape(&item.message).to_string())} }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 13 - 0
frameworks/Rust/viz/src/models.rs

@@ -0,0 +1,13 @@
+use serde::Serialize;
+
+#[derive(Clone, Copy, Serialize, Debug, yarte::Serialize)]
+pub struct World {
+    pub id: i32,
+    pub randomnumber: i32,
+}
+
+#[derive(Serialize, Debug)]
+pub struct Fortune {
+    pub id: i32,
+    pub message: String,
+}

+ 14 - 0
frameworks/Rust/viz/src/models_sqlx.rs

@@ -0,0 +1,14 @@
+use serde::{Deserialize, Serialize};
+use sqlx::FromRow;
+
+#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, FromRow)]
+pub struct World {
+    pub id: i32,
+    pub randomnumber: i32,
+}
+
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromRow)]
+pub struct Fortune {
+    pub id: i32,
+    pub message: String,
+}

+ 37 - 0
frameworks/Rust/viz/src/server.rs

@@ -0,0 +1,37 @@
+use std::io;
+use std::net::{Ipv4Addr, SocketAddr};
+
+use hyper::server::conn::AddrIncoming;
+use tokio::net::{TcpListener, TcpSocket};
+
+pub fn builder() -> hyper::server::Builder<AddrIncoming> {
+    let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 8080));
+    let listener = reuse_listener(addr).expect("couldn't bind to addr");
+    let incoming = AddrIncoming::from_listener(listener).unwrap();
+
+    println!("Started viz server at 8080");
+
+    viz::Server::builder(incoming)
+        .tcp_keepalive(None)
+        .tcp_nodelay(true)
+        .http1_only(true)
+        .http1_pipeline_flush(true)
+}
+
+fn reuse_listener(addr: SocketAddr) -> io::Result<TcpListener> {
+    let socket = match addr {
+        SocketAddr::V4(_) => TcpSocket::new_v4()?,
+        SocketAddr::V6(_) => TcpSocket::new_v6()?,
+    };
+
+    #[cfg(unix)]
+    {
+        if let Err(e) = socket.set_reuseport(true) {
+            eprintln!("error setting SO_REUSEPORT: {}", e);
+        }
+    }
+
+    socket.set_reuseaddr(true)?;
+    socket.bind(addr)?;
+    socket.listen(1024)
+}

+ 17 - 0
frameworks/Rust/viz/src/utils.rs

@@ -0,0 +1,17 @@
+use std::cmp;
+
+use atoi::FromRadix10;
+use viz::header::HeaderValue;
+
+pub const HDR_SERVER: HeaderValue = HeaderValue::from_static("VIZ");
+
+#[allow(dead_code)]
+pub fn get_query_param(query: Option<&str>) -> u16 {
+    let query = query.unwrap_or("");
+    let q = if let Some(pos) = query.find('q') {
+        u16::from_radix_10(query.split_at(pos + 2).1.as_ref()).0
+    } else {
+        1
+    };
+    cmp::min(500, cmp::max(1, q))
+}

+ 5 - 0
frameworks/Rust/viz/templates/fortune.hbs

@@ -0,0 +1,5 @@
+<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>
+      {{~# each fortunes ~}}
+      <tr><td>{{id}}</td><td>{{message}}</td></tr>
+      {{~/each ~}}
+</table></body></html>

+ 12 - 0
frameworks/Rust/viz/templates/fortune.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+  <head><title>Fortunes</title></head>
+  <body>
+    <table>
+      <tr><th>id</th><th>message</th></tr>
+      {% for item in items %}
+      <tr><td>{{item.id}}</td><td>{{item.message}}</td></tr>
+      {% endfor %}
+    </table>
+  </body>
+</html>

+ 13 - 0
frameworks/Rust/viz/viz-pg.dockerfile

@@ -0,0 +1,13 @@
+FROM rust:1.64.0
+
+RUN apt-get update -yqq && apt-get install -yqq cmake g++
+
+ADD ./ /viz
+WORKDIR /viz
+
+RUN cargo clean
+RUN RUSTFLAGS="-C target-cpu=native" cargo build --release --bin viz-pg --features="tokio-postgres,yarte"
+
+EXPOSE 8080
+
+CMD ./target/release/viz-pg

+ 13 - 0
frameworks/Rust/viz/viz-sqlx.dockerfile

@@ -0,0 +1,13 @@
+FROM rust:1.64.0
+
+RUN apt-get update -yqq && apt-get install -yqq cmake g++
+
+ADD ./ /viz
+WORKDIR /viz
+
+RUN cargo clean
+RUN RUSTFLAGS="-C target-cpu=native" cargo build --release --bin viz-sqlx --features="sqlx,markup,v_htmlescape"
+
+EXPOSE 8080
+
+CMD ./target/release/viz-sqlx

+ 13 - 0
frameworks/Rust/viz/viz.dockerfile

@@ -0,0 +1,13 @@
+FROM rust:1.64.0
+
+RUN apt-get update -yqq && apt-get install -yqq cmake g++
+
+ADD ./ /viz
+WORKDIR /viz
+
+RUN cargo clean
+RUN RUSTFLAGS="-C target-cpu=native" cargo build --release --bin viz
+
+EXPOSE 8080
+
+CMD ./target/release/viz