Skip to content

Commit c7d9137

Browse files
authored
Merge pull request #12 from dimacurrentai/redb
`redb`.
2 parents ec3acc7 + c7fb4af commit c7d9137

File tree

6 files changed

+347
-1
lines changed

6 files changed

+347
-1
lines changed

step02_httpserver/code/src/jsontemplate.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
// NOTE(dkorolev): Support both top-level `_links` and inner-level `._links`, see the example.
6666
if (value && value._links) {
6767
return value._links;
68-
} else if (links !== undefined && i in links) {
68+
} else if (typeof links === 'Object' && i in links) {
6969
return links[i];
7070
} else {
7171
return undefined;

step07_redb/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.db

step07_redb/code/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "httpserver"
3+
edition = "2021"
4+
5+
[dependencies]
6+
axum = "0.7"
7+
hyper = { version = "1", features = ["server", "http1"] }
8+
redb = "1.4"
9+
serde = { version = "1", features = ["derive"] }
10+
serde_json = "1"
11+
tokio = { version = "1", features = ["full"] }
12+
tower = "0.4"
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
8+
<!-- Can be changed with `._title`. -->
9+
<title>JSON</title>
10+
11+
<!-- Can be changed with `._favicon`, orange 1x1 by default. -->
12+
<link href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4v5QBAARLAaU/Dnq2AAAAAElFTkSuQmCC" rel="icon" type="image/x-icon" />
13+
14+
<style>
15+
body {
16+
font-family: Arial, sans-serif;
17+
background-color: #282c34;
18+
color: white;
19+
padding: 1px;
20+
}
21+
pre {
22+
white-space: pre;
23+
overflow-x: auto;
24+
background: #1e1e1e;
25+
padding: 15px;
26+
border-radius: 5px;
27+
overflow-x: auto;
28+
}
29+
.string {
30+
color: #ce9178;
31+
}
32+
.number {
33+
color: #b5cea8;
34+
}
35+
.boolean {
36+
color: #569cd6;
37+
}
38+
.null {
39+
color: #569cd6;
40+
}
41+
.key {
42+
color: #9cdcfe;
43+
}
44+
a {
45+
color: #9cdcfe;
46+
}
47+
a:hover {
48+
text-decoration: underline;
49+
}
50+
</style>
51+
</head>
52+
53+
<body>
54+
<script>
55+
const escapeHtml = (str) => {
56+
return str
57+
.replace(/&/g, "&amp;")
58+
.replace(/</g, "&lt;")
59+
.replace(/>/g, "&gt;")
60+
.replace(/"/g, "&quot;")
61+
.replace(/'/g, "&#039;");
62+
};
63+
64+
const innerLinks = (value, links, i) => {
65+
// NOTE(dkorolev): Support both top-level `_links` and inner-level `._links`, see the example.
66+
if (value && value._links) {
67+
return value._links;
68+
} else if (typeof links === 'Object' && i in links) {
69+
return links[i];
70+
} else {
71+
return undefined;
72+
}
73+
};
74+
75+
const createJson = (value, links, indent) => {
76+
if (Array.isArray(value)) {
77+
let s = `[\n`;
78+
for (let i = 0; i < value.length; ++i) {
79+
s += `${indent} ${createJson(value[i], innerLinks(value, links, i), indent + ' ')}${i + 1 == value.length ? '' : ','}\n`;
80+
}
81+
s += `${indent}]`;
82+
return s;
83+
} else if (value === null) {
84+
return `<span class="null">null</span>`;
85+
} else {
86+
const type = typeof value;
87+
if (type === 'string') {
88+
return `<span class="string">"${escapeHtml(value)}"</span>`;
89+
} else if (type === 'boolean') {
90+
return `<span class="boolean">${type ? 'true' : 'false'}</span>`;
91+
} else if (type === 'number') {
92+
return `<span class="number">${value}</span>`;
93+
} else if (type === 'object') {
94+
let s = `{\n`;
95+
let k = Object.keys(value).filter(e => e != '_links');
96+
for (let i = 0; i < k.length; ++i) {
97+
if (links && k[i] in links && typeof links[k[i]] === 'string') {
98+
s += `${indent} <a href='${links[k[i]]}'><span class="key">${escapeHtml(k[i])}</span></a>: ${createJson(value[k[i]], innerLinks(value[k[i]], links, k[i]), indent + ' ')}${i + 1 == k.length ? '' : ','}\n`;
99+
} else {
100+
s += `${indent} <span class="key">${escapeHtml(k[i])}</span>: ${createJson(value[k[i]], innerLinks(value[k[i]], links, k[i]), indent + ' ')}${i + 1 == k.length ? '' : ','}\n`;
101+
}
102+
}
103+
s += `${indent}}`;
104+
return s;
105+
}
106+
}
107+
};
108+
const data = {};
109+
</script>
110+
<pre id="json-output"></pre>
111+
<script>
112+
let favico = null;
113+
if ('_title' in data && typeof data._title === 'string') {
114+
document.title = data._title;
115+
delete data._title;
116+
}
117+
if ('_favico' in data && typeof data._favico === 'string') {
118+
favico = data._favico;
119+
delete data._favico;
120+
}
121+
document.getElementById("json-output").innerHTML = createJson(data, '_links' in data ? data._links : null, '');
122+
if (favico) {
123+
document.querySelector("link[rel~='icon']").href = favico;
124+
}
125+
</script>
126+
</body>
127+
128+
</html>

step07_redb/code/src/main.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
use axum::body::Body;
2+
use axum::middleware::{from_fn, Next};
3+
use axum::response::{IntoResponse, Response};
4+
use axum::{routing::get, serve, Router};
5+
use hyper::header::ACCEPT;
6+
use hyper::header::CONTENT_TYPE;
7+
use hyper::Request;
8+
use hyper::StatusCode;
9+
use redb::{Database, ReadableTable, TableDefinition};
10+
use serde::{Deserialize, Serialize};
11+
use std::fs;
12+
use std::net::SocketAddr;
13+
use std::path::Path;
14+
use std::sync::Arc;
15+
use tokio::signal::unix::{signal, SignalKind};
16+
use tokio::{net::TcpListener, sync::mpsc};
17+
use tower::ServiceBuilder;
18+
19+
static GLOBALS: TableDefinition<u64, u64> = TableDefinition::new("globals");
20+
21+
#[derive(Clone)]
22+
struct BrowserFriendlyJson {
23+
data: String,
24+
}
25+
26+
struct JsonHtmlTemplate<'a>(&'a str, &'a str);
27+
28+
impl IntoResponse for BrowserFriendlyJson {
29+
fn into_response(self) -> Response {
30+
let mut response = StatusCode::NOT_IMPLEMENTED.into_response();
31+
response.extensions_mut().insert(self);
32+
response
33+
}
34+
}
35+
36+
const fn find_split_position(bytes: &[u8]) -> usize {
37+
let mut i = 0;
38+
while i < bytes.len() && (bytes[i] != b'{' || bytes[i + 1] != b'}') {
39+
i += 1;
40+
}
41+
i
42+
// TODO(dkorolev): Panic if did not find `{}` or if found more than one `{}`.
43+
// NOTE(dkorolev): Why not create the split `str` slice at compile time, huh?
44+
}
45+
46+
static JSON_TEMPLATE_HTML: &[u8] = include_bytes!("jsontemplate.html");
47+
static JSON_TEMPLATE_HTML_SPLIT_IDX: usize = find_split_position(&JSON_TEMPLATE_HTML);
48+
49+
#[derive(Serialize, Deserialize, Debug)]
50+
#[serde(tag = "type")]
51+
enum JSONResponse {
52+
Point { x: i32, y: i32 },
53+
Message { text: String },
54+
Counters { counter_runs: u64, counter_requests: u64 },
55+
}
56+
57+
fn create_response<S: Into<String>>(content_type: &str, body: S) -> Response<Body> {
58+
Response::builder().status(StatusCode::OK).header(CONTENT_TYPE, content_type).body(Body::from(body.into())).unwrap()
59+
}
60+
61+
async fn browser_json_renderer(request: Request<Body>, next: Next, tmpl: Arc<JsonHtmlTemplate<'_>>) -> Response {
62+
// TODO(dkorolev): Can this be more Rusty?
63+
let mut accept_html = false;
64+
request.headers().get(&ACCEPT).map(|value| {
65+
let s = std::str::from_utf8(value.as_ref()).unwrap();
66+
s.split(',').for_each(|value| {
67+
if value == "text/html" || value == "html" {
68+
accept_html = true;
69+
}
70+
})
71+
});
72+
73+
// NOTE(dkorolev): I could not put the above logic to inside after `if let`, although, clearly it should be there.
74+
let mut response = next.run(request).await;
75+
if let Some(my_data) = response.extensions_mut().remove::<BrowserFriendlyJson>() {
76+
if accept_html {
77+
return create_response("text/html", format!("{}{}{}", tmpl.0, my_data.data, tmpl.1));
78+
} else {
79+
return create_response("application/json", my_data.data);
80+
}
81+
}
82+
83+
response
84+
}
85+
86+
#[tokio::main]
87+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
88+
fs::create_dir_all(&Path::new("./.db"))?;
89+
let redb = Database::create("./.db/demo.redb")?;
90+
run_main(&redb, inc_counter(&redb).await?).await;
91+
Ok(())
92+
}
93+
94+
async fn inc_counter(redb: &Database) -> Result<u64, Box<dyn std::error::Error>> {
95+
let mut counter_runs: u64 = 0;
96+
let txn = redb.begin_write()?;
97+
{
98+
let mut table = txn.open_table(GLOBALS)?;
99+
if let Some(value) = table.get(&1)? {
100+
counter_runs = value.value();
101+
}
102+
counter_runs += 1;
103+
println!("Run counter in the DB: {}", counter_runs);
104+
table.insert(&1, &counter_runs)?;
105+
}
106+
txn.commit()?;
107+
Ok(counter_runs)
108+
}
109+
110+
async fn run_main(_redb: &Database, counter_runs: u64) {
111+
// NOTE(dkorolev): Can this be done at compile time?
112+
let html_template = Arc::new(JsonHtmlTemplate(
113+
std::str::from_utf8(&JSON_TEMPLATE_HTML[0..JSON_TEMPLATE_HTML_SPLIT_IDX]).expect("NON-UTF8 TEMPLATE"),
114+
std::str::from_utf8(&JSON_TEMPLATE_HTML[(JSON_TEMPLATE_HTML_SPLIT_IDX + 2)..]).expect("NON-UTF8 TEMPLATE"),
115+
));
116+
117+
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
118+
119+
let counter_runs = Arc::new(counter_runs);
120+
121+
let app = Router::new()
122+
.route("/healthz", get(|| async { "OK\n" }))
123+
.route("/", get(|| async { "hello this is a rust http server\n" }))
124+
.route(
125+
"/quit",
126+
get({
127+
let shutdown_tx = shutdown_tx.clone();
128+
|| async move {
129+
let _ = shutdown_tx.send(()).await;
130+
"yes i am shutting down\n"
131+
}
132+
}),
133+
)
134+
.route(
135+
"/json",
136+
get({
137+
let counter_runs = Box::new(*counter_runs);
138+
let cnt_requests = Box::new(42); // NOT IMPLEMENTED YET
139+
|| async move {
140+
let response = JSONResponse::Counters { counter_runs: *counter_runs, counter_requests: *cnt_requests };
141+
BrowserFriendlyJson { data: serde_json::to_string(&response).unwrap() }
142+
}
143+
}),
144+
)
145+
.layer(ServiceBuilder::new().layer(from_fn({
146+
// TODO(dkorolev): Can I just move the `html_template` into `browser_json_renderer`?
147+
let html_template = Arc::clone(&html_template);
148+
move |req, next| browser_json_renderer(req, next, Arc::clone(&html_template))
149+
})));
150+
151+
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
152+
let listener = TcpListener::bind(addr).await.unwrap();
153+
154+
println!("rust http server ready on {}", addr);
155+
156+
let server = serve(listener, app);
157+
158+
let mut term_signal = signal(SignalKind::terminate()).expect("failed to register SIGTERM handler");
159+
let mut int_signal = signal(SignalKind::interrupt()).expect("failed to register SIGINT handler");
160+
161+
tokio::select! {
162+
_ = server.with_graceful_shutdown(async move { shutdown_rx.recv().await; }) => { println! ("done"); }
163+
_ = tokio::signal::ctrl_c() => { println!("terminating due to Ctrl+C"); }
164+
_ = term_signal.recv() => { println!("terminating due to SIGTERM"); }
165+
_ = int_signal.recv() => { println!("terminating due to SIGINT"); }
166+
}
167+
168+
println!("rust http server down");
169+
}

step07_redb/run.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
docker build -f ../Dockerfile.template . -t demo
6+
7+
mkdir -p .db
8+
9+
N=3
10+
for i in $(seq 1 $N) ; do
11+
echo "Run $i of $N."
12+
13+
docker run --rm -v ./.db/:/.db/ --network=bridge -p 3000:3000 -t demo &
14+
PID=$!
15+
16+
while true ; do
17+
R="$(curl -s localhost:3000/healthz || echo NOPE)"
18+
if [ "$R" = "OK" ] ; then
19+
echo "server healthy"
20+
break
21+
fi
22+
sleep 0.5
23+
echo "server not yet healthy"
24+
done
25+
26+
curl -s localhost:3000
27+
28+
curl -s localhost:3000/json
29+
echo
30+
31+
curl -s localhost:3000/quit
32+
wait $PID
33+
echo
34+
done
35+
36+
echo "All runs done."

0 commit comments

Comments
 (0)