Browse Source

add xitca-web (#6690)

* add xitca-web test

* pin to github rev 065b21b
fakeshadow 4 years ago
parent
commit
ebc304dd48

+ 31 - 0
frameworks/Rust/xitca-web/Cargo.toml

@@ -0,0 +1,31 @@
+[package]
+name = "xitca-web"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+xitca-http = { git = "https://github.com/fakeshadow/xitca-web.git", rev = "065b21b" }
+xitca-web = { git = "https://github.com/fakeshadow/xitca-web.git", rev = "065b21b" }
+
+ahash = { version = "0.7.4", features = ["compile-time-rng"] }
+atoi = "0.4.0"
+bytes = "1"
+futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
+mimalloc = { version = "0.1.25", default-features = false }
+rand = { version = "0.8", default-features = false, features = ["min_const_gen", "small_rng"] }
+sailfish = "0.3.3"
+serde = "1"
+simd-json = "0.4.6"
+tokio = { version = "1.7", features = ["macros", "rt"] }
+tokio-postgres = "0.7.2"
+
+[profile.release]
+lto = true
+opt-level = 3
+codegen-units = 1
+panic = "abort"
+
+[patch.crates-io]
+xitca-http = { git = "https://github.com/fakeshadow/xitca-web.git", rev = "065b21b" }
+xitca-server = { git = "https://github.com/fakeshadow/xitca-web.git", rev = "065b21b" }
+xitca-service = { git = "https://github.com/fakeshadow/xitca-web.git", rev = "065b21b" }

+ 35 - 0
frameworks/Rust/xitca-web/README.md

@@ -0,0 +1,35 @@
+# An experimental http library.
+
+## Description.
+
+An alternative http library inspired by actix and hyper. Implementation is rewritten with similar style and types.
+
+## Database
+
+PostgreSQL.
+
+## Test URLs
+
+### Test 1: JSON Encoding
+
+    http://localhost:8080/json
+
+### Test 2: Single Row Query
+
+    http://localhost:8080/db
+
+### Test 3: Multi Row Query
+
+    http://localhost:8080/queries?q={count}
+
+### Test 4: Fortunes (Template rendering)
+
+    http://localhost:8080/fortune
+
+### Test 5: Update Query
+
+    http://localhost:8080/updates?q={count}
+
+### Test 6: Plaintext
+
+    http://localhost:8080/plaintext

+ 29 - 0
frameworks/Rust/xitca-web/benchmark_config.json

@@ -0,0 +1,29 @@
+{
+  "framework": "xitca-web",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "fortune_url": "/fortunes",
+        "query_url": "/queries?q=",
+        "update_url": "/updates?q=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Platform",
+        "database": "Postgres",
+        "framework": "xitca-web",
+        "language": "Rust",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "xitca-server",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "xitca-web",
+        "notes": "",
+        "versus": ""
+      }
+    }
+  ]
+}

+ 19 - 0
frameworks/Rust/xitca-web/config.toml

@@ -0,0 +1,19 @@
+[framework]
+name = "xitca-web"
+
+[main]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/query?q="
+urls.update = "/update?q="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Platform"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "xitca-server"
+versus = ""

+ 146 - 0
frameworks/Rust/xitca-web/src/db.rs

@@ -0,0 +1,146 @@
+use std::{cell::RefCell, error::Error, fmt::Write};
+
+use ahash::AHashMap;
+use futures_util::stream::{FuturesUnordered, TryStreamExt};
+use rand::{rngs::SmallRng, Rng, SeedableRng};
+use tokio::pin;
+use tokio_postgres::{types::ToSql, NoTls, Statement};
+
+use super::ser::{Fortune, Fortunes, World};
+
+pub struct Client {
+    client: tokio_postgres::Client,
+    rng: RefCell<SmallRng>,
+    fortune: Statement,
+    world: Statement,
+    updates: AHashMap<u16, Statement>,
+}
+
+pub async fn create(config: &str) -> Client {
+    let (client, conn) = tokio_postgres::connect(config, NoTls).await.unwrap();
+
+    tokio::task::spawn_local(async move {
+        let _ = conn.await;
+    });
+
+    let fortune = client.prepare("SELECT * FROM fortune").await.unwrap();
+
+    let mut updates = AHashMap::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!(&mut q, "when ${} then ${} ", pl, pl + 1);
+            pl += 2;
+        }
+        q.push_str("ELSE randomnumber END WHERE id IN (");
+        for _ in 1..=num {
+            let _ = write!(&mut q, "${},", pl);
+            pl += 1;
+        }
+        q.pop();
+        q.push(')');
+
+        let st = client.prepare(&q).await.unwrap();
+        updates.insert(num, st);
+    }
+    let world = client
+        .prepare("SELECT * FROM world WHERE id=$1")
+        .await
+        .unwrap();
+
+    Client {
+        client,
+        rng: RefCell::new(SmallRng::from_entropy()),
+        fortune,
+        world,
+        updates,
+    }
+}
+
+type DbResult<T> = Result<T, Box<dyn Error>>;
+
+impl Client {
+    pub async fn get_world(&self) -> DbResult<World> {
+        let random_id = (self.rng.borrow_mut().gen::<u32>() % 10_000 + 1) as i32;
+        let row = self.client.query_one(&self.world, &[&random_id]).await?;
+
+        Ok(World::new(row.get(0), row.get(1)))
+    }
+
+    pub async fn get_worlds(&self, num: u16) -> DbResult<Vec<World>> {
+        let worlds = {
+            let mut rng = self.rng.borrow_mut();
+            (0..num)
+                .map(|_| {
+                    let w_id = (rng.gen::<u32>() % 10_000 + 1) as i32;
+                    async move {
+                        let row = self.client.query_one(&self.world, &[&w_id]).await?;
+                        Ok(World::new(row.get(0), row.get(1)))
+                    }
+                })
+                .collect::<FuturesUnordered<_>>()
+        };
+
+        worlds.try_collect().await
+    }
+
+    pub async fn update(&self, num: u16) -> DbResult<Vec<World>> {
+        let worlds = {
+            let mut rng = self.rng.borrow_mut();
+
+            (0..num)
+                .map(|_| {
+                    let id = (rng.gen::<u32>() % 10_000 + 1) as i32;
+                    let w_id = (rng.gen::<u32>() % 10_000 + 1) as i32;
+                    async move {
+                        let row = self.client.query_one(&self.world, &[&w_id]).await?;
+                        let mut world = World::new(row.get(0), row.get(1));
+                        world.randomnumber = id;
+                        Ok::<_, Box<dyn Error>>(world)
+                    }
+                })
+                .collect::<FuturesUnordered<_>>()
+        };
+
+        let worlds = worlds.try_collect::<Vec<_>>().await?;
+
+        let mut params = Vec::<&(dyn ToSql + Sync)>::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.get(&num).unwrap();
+
+        let _ = self.client.query(st, params.as_slice()).await?;
+
+        Ok(worlds)
+    }
+
+    pub async fn tell_fortune(&self) -> DbResult<Fortunes> {
+        let mut items = Vec::with_capacity(32);
+
+        items.push(Fortune::new(0, "Additional fortune added at request time."));
+
+        let stream = self
+            .client
+            .query_raw::<_, _, &[i32; 0]>(&self.fortune, &[])
+            .await?;
+
+        pin!(stream);
+
+        while let Some(row) = stream.try_next().await? {
+            items.push(Fortune::new(row.get(0), row.get::<_, String>(1)));
+        }
+
+        items.sort_by(|it, next| it.message.cmp(&next.message));
+
+        Ok(Fortunes::new(items))
+    }
+}

+ 196 - 0
frameworks/Rust/xitca-web/src/main.rs

@@ -0,0 +1,196 @@
+#[global_allocator]
+static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
+
+mod db;
+mod ser;
+
+use std::{cell::RefCell, cmp, convert::Infallible, error::Error, io};
+
+use bytes::{Bytes, BytesMut};
+use serde::Serialize;
+use xitca_http::http::{
+    header::{HeaderValue, CONTENT_TYPE, SERVER},
+    Method, StatusCode,
+};
+use xitca_web::{
+    dev::fn_service,
+    request::WebRequest,
+    response::{WebResponse, WebResponseBuilder},
+    App, HttpServer,
+};
+
+use self::db::Client;
+use self::ser::{Message, Writer};
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> io::Result<()> {
+    let config = "postgres://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world";
+
+    HttpServer::new(move || {
+        App::with_async_state(move || AppState::init(config)).service(fn_service(handle))
+    })
+    .force_flat_buf()
+    .max_request_headers::<8>()
+    .bind("0.0.0.0:8080")?
+    .run()
+    .await
+}
+
+type HandleResult = Result<WebResponse, Infallible>;
+
+async fn handle(mut req: WebRequest<'_, AppState>) -> HandleResult {
+    let inner = req.request_mut();
+
+    match (inner.method(), inner.uri().path()) {
+        (&Method::GET, "/plaintext") => plain_text(req),
+        (&Method::GET, "/json") => json(req),
+        (&Method::GET, "/db") => db(req).await,
+        (&Method::GET, "/fortunes") => fortunes(req).await,
+        (&Method::GET, "/queries") => queries(req).await,
+        (&Method::GET, "/updates") => updates(req).await,
+        _ => not_found(),
+    }
+}
+
+fn plain_text(req: WebRequest<'_, AppState>) -> HandleResult {
+    let mut res = req.into_response(Bytes::from_static(b"Hello, World!"));
+
+    res.headers_mut()
+        .append(SERVER, HeaderValue::from_static("TFB"));
+    res.headers_mut()
+        .append(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
+
+    Ok(res)
+}
+
+#[inline(always)]
+fn json(req: WebRequest<'_, AppState>) -> HandleResult {
+    json_response(req, &Message::new())
+}
+
+async fn db(req: WebRequest<'_, AppState>) -> HandleResult {
+    match req.state().client().get_world().await {
+        Ok(ref world) => json_response(req, world),
+        Err(_) => internal(),
+    }
+}
+
+async fn fortunes(req: WebRequest<'_, AppState>) -> HandleResult {
+    match _fortunes(req.state().client()).await {
+        Ok(body) => {
+            let mut res = req.into_response(body);
+
+            res.headers_mut()
+                .append(SERVER, HeaderValue::from_static("TFB"));
+            res.headers_mut().append(
+                CONTENT_TYPE,
+                HeaderValue::from_static("text/html; charset=utf-8"),
+            );
+
+            Ok(res)
+        }
+        Err(_) => internal(),
+    }
+}
+
+async fn queries(mut req: WebRequest<'_, AppState>) -> HandleResult {
+    let num = req.request_mut().uri().query().parse_query();
+
+    match req.state().client().get_worlds(num).await {
+        Ok(worlds) => json_response(req, worlds.as_slice()),
+        Err(_) => internal(),
+    }
+}
+
+async fn updates(mut req: WebRequest<'_, AppState>) -> HandleResult {
+    let num = req.request_mut().uri().query().parse_query();
+
+    match req.state().client().update(num).await {
+        Ok(worlds) => json_response(req, worlds.as_slice()),
+        Err(_) => internal(),
+    }
+}
+
+trait QueryParse {
+    fn parse_query(self) -> u16;
+}
+
+impl QueryParse for Option<&str> {
+    fn parse_query(self) -> u16 {
+        let num = self
+            .and_then(|this| {
+                use atoi::FromRadix10;
+                this.find('q')
+                    .map(|pos| u16::from_radix_10(this.split_at(pos + 2).1.as_ref()).0)
+            })
+            .unwrap_or(1);
+
+        cmp::min(500, cmp::max(1, num))
+    }
+}
+
+#[inline]
+async fn _fortunes(client: &Client) -> Result<Bytes, Box<dyn Error>> {
+    use sailfish::TemplateOnce;
+    let fortunes = client.tell_fortune().await?.render_once()?;
+    Ok(fortunes.into())
+}
+
+#[inline]
+fn json_response<S>(req: WebRequest<'_, AppState>, value: &S) -> HandleResult
+where
+    S: ?Sized + Serialize,
+{
+    let mut writer = req.state().writer();
+    simd_json::to_writer(&mut writer, value).unwrap();
+    let body = writer.take();
+
+    let mut res = req.into_response(body);
+    res.headers_mut()
+        .append(SERVER, HeaderValue::from_static("TFB"));
+    res.headers_mut()
+        .append(CONTENT_TYPE, HeaderValue::from_static("application/json"));
+
+    Ok(res)
+}
+
+struct AppState {
+    // postgres client
+    client: Client,
+    // a re-usable buffer for write response data.
+    write_buf: RefCell<BytesMut>,
+}
+
+impl AppState {
+    async fn init(config: &str) -> Self {
+        let client = db::create(config).await;
+        let write_buf = RefCell::new(BytesMut::new());
+
+        Self { client, write_buf }
+    }
+
+    #[inline]
+    fn writer(&self) -> Writer<'_> {
+        Writer(self.write_buf.borrow_mut())
+    }
+
+    #[inline]
+    fn client(&self) -> &Client {
+        &self.client
+    }
+}
+
+macro_rules! error {
+    ($error: ident, $code: path) => {
+        fn $error() -> HandleResult {
+            Ok(WebResponseBuilder::new()
+                .status($code)
+                .header(SERVER, HeaderValue::from_static("TFB"))
+                .body(Bytes::new().into())
+                .unwrap())
+        }
+    };
+}
+
+error!(not_found, StatusCode::NOT_FOUND);
+error!(internal, StatusCode::INTERNAL_SERVER_ERROR);

+ 84 - 0
frameworks/Rust/xitca-web/src/ser.rs

@@ -0,0 +1,84 @@
+use std::{borrow::Cow, cell::RefMut, io};
+
+use bytes::{Bytes, BytesMut};
+
+use sailfish::TemplateOnce;
+use serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Serialize)]
+pub struct Message {
+    message: &'static str,
+}
+
+impl Message {
+    #[inline]
+    pub fn new() -> Self {
+        Self {
+            message: "Hello, World!",
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+#[derive(Serialize, Debug)]
+pub struct World {
+    pub id: i32,
+    pub randomnumber: i32,
+}
+
+impl World {
+    #[inline]
+    pub fn new(id: i32, randomnumber: i32) -> Self {
+        Self { id, randomnumber }
+    }
+}
+
+pub struct Fortune {
+    pub id: i32,
+    pub message: Cow<'static, str>,
+}
+
+impl Fortune {
+    #[inline]
+    pub fn new(id: i32, message: impl Into<Cow<'static, str>>) -> Self {
+        Self {
+            id,
+            message: message.into(),
+        }
+    }
+}
+
+#[derive(TemplateOnce)]
+#[template(path = "fortune.stpl", rm_whitespace = true)]
+pub struct Fortunes {
+    items: Vec<Fortune>,
+}
+
+impl Fortunes {
+    #[inline]
+    pub fn new(items: Vec<Fortune>) -> Self {
+        Self { items }
+    }
+}
+
+pub struct Writer<'a>(pub RefMut<'a, BytesMut>);
+
+impl Writer<'_> {
+    #[inline]
+    pub fn take(mut self) -> Bytes {
+        self.0.split().freeze()
+    }
+}
+
+impl io::Write for &mut Writer<'_> {
+    #[inline]
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.0.extend_from_slice(buf);
+        Ok(buf.len())
+    }
+
+    #[inline]
+    fn flush(&mut self) -> io::Result<()> {
+        Ok(())
+    }
+}

+ 10 - 0
frameworks/Rust/xitca-web/templates/fortune.stpl

@@ -0,0 +1,10 @@
+<!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><% } %>
+    </table>
+  </body>
+</html>

+ 14 - 0
frameworks/Rust/xitca-web/xitca-web.dockerfile

@@ -0,0 +1,14 @@
+FROM rustlang/rust:nightly-slim
+
+RUN apt-get update -yqq && apt-get install -yqq cmake g++
+
+ADD ./ /xitca-web
+WORKDIR /xitca-web
+
+RUN cargo clean
+RUN RUSTFLAGS="-C target-cpu=native" cargo build --release
+
+EXPOSE 8080
+
+CMD ./target/release/xitca-web
+