init commit
commit
0660cc061f
|
@ -0,0 +1,22 @@
|
|||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cargo build --release --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
path
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "httpmq-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
axum = "0.4"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version="0.3", features = ["env-filter"] }
|
||||
tower = { version = "0.4", features = ["util", "timeout", "load-shed", "limit"] }
|
||||
tower-http = { version = "0.2.0", features = ["add-extension", "auth", "compression-full", "trace"] }
|
||||
rocksdb = { version = "*", features = ["multi-threaded-cf"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
clap = {version = "*"}
|
||||
once_cell = {version = "*" }
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
|
@ -0,0 +1,58 @@
|
|||
httpmq-rs
|
||||
===
|
||||
[![Rust](https://github.com/hnlq715/httpmq-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/hnlq715/httpmq-rs/actions/workflows/rust.yml)
|
||||
|
||||
Benchmark
|
||||
---
|
||||
|
||||
Test machine(Hackintosh):
|
||||
|
||||
```text
|
||||
'c.
|
||||
,xNMM. -----------------------
|
||||
.OMMMMo OS: macOS 11.6.1 20G224 x86_64
|
||||
OMMM0, Host: Hackintosh (SMBIOS: iMac20,1)
|
||||
.;loddo:' loolloddol;. Kernel: 20.6.0
|
||||
cKMMMMMMMMMMNWMMMMMMMMMM0: Uptime: 13 hours, 16 mins
|
||||
.KMMMMMMMMMMMMMMMMMMMMMMMWd. Packages: 45 (brew)
|
||||
XMMMMMMMMMMMMMMMMMMMMMMMX. Shell: zsh 5.8
|
||||
;MMMMMMMMMMMMMMMMMMMMMMMM: Resolution: 1920x1080@2x
|
||||
:MMMMMMMMMMMMMMMMMMMMMMMM: DE: Aqua
|
||||
.MMMMMMMMMMMMMMMMMMMMMMMMX. WM: Quartz Compositor
|
||||
kMMMMMMMMMMMMMMMMMMMMMMMMWd. WM Theme: Blue (Dark)
|
||||
.XMMMMMMMMMMMMMMMMMMMMMMMMMMk Terminal: vscode
|
||||
.XMMMMMMMMMMMMMMMMMMMMMMMMK. CPU: Intel i5-10600K (12) @ 4.10GHz
|
||||
kMMMMMMMMMMMMMMMMMMMMMMd GPU: Radeon Pro W5500X
|
||||
;KMMMMMMMWXXWMMMMMMMk. Memory: 17549MiB / 32768MiB
|
||||
.cooc,. .,coo:.
|
||||
```
|
||||
|
||||
PUT
|
||||
|
||||
```bash
|
||||
wrk -c 10 -t 2 -d 10s "http://127.0.0.1:1218/?name=xoyo&opt=put&data=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
Running 10s test @ http://127.0.0.1:1218/?name=xoyo&opt=put&data=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
2 threads and 10 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 185.78us 163.52us 9.86ms 99.45%
|
||||
Req/Sec 26.82k 1.09k 29.17k 64.85%
|
||||
539029 requests in 10.10s, 76.08MB read
|
||||
Requests/sec: 53370.23
|
||||
Transfer/sec: 7.53MB
|
||||
|
||||
|
||||
```
|
||||
|
||||
GET
|
||||
|
||||
```bash
|
||||
wrk -c 10 -t 2 -d 10s "http://127.0.0.1:1218/?name=xoyo&opt=get"
|
||||
Running 10s test @ http://127.0.0.1:1218/?name=xoyo&opt=get
|
||||
2 threads and 10 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 155.09us 407.65us 12.22ms 99.30%
|
||||
Req/Sec 36.70k 3.08k 40.48k 77.72%
|
||||
737643 requests in 10.10s, 456.55MB read
|
||||
Requests/sec: 73034.96
|
||||
Transfer/sec: 45.20MB
|
||||
```
|
|
@ -0,0 +1,332 @@
|
|||
use axum::{
|
||||
error_handling::HandleErrorLayer, extract::Extension, extract::Query, http::StatusCode,
|
||||
response::IntoResponse, routing::get, Router,
|
||||
};
|
||||
use clap::{App, Arg};
|
||||
use once_cell::sync::OnceCell;
|
||||
use rocksdb::DB;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
net::SocketAddr,
|
||||
str,
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
use tower::{BoxError, ServiceBuilder};
|
||||
use tower_http::add_extension::AddExtensionLayer;
|
||||
use tracing::debug;
|
||||
|
||||
static DEFAULT_MAX_QUEUE_CELL: OnceCell<i32> = OnceCell::new();
|
||||
|
||||
// httpmq read metadata api
|
||||
// retrieve from leveldb
|
||||
// name.maxqueue - maxqueue
|
||||
// name.putpos - putpos
|
||||
// name.getpos - getpos
|
||||
fn httpmq_read_metadata(db: &rocksdb::DB, name: &String) -> Option<Vec<i32>> {
|
||||
let mut result: Vec<_> = db
|
||||
.multi_get(vec![
|
||||
name.to_string() + ".maxqueue",
|
||||
name.to_string() + ".putpos",
|
||||
name.to_string() + ".getpos",
|
||||
])
|
||||
.iter()
|
||||
.map(|x| match x {
|
||||
Ok(Some(xx)) => str::from_utf8(xx).unwrap().parse::<i32>().unwrap(),
|
||||
_ => 0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug!("result {:?}", result);
|
||||
if result[0] == 0 {
|
||||
result[0] = *DEFAULT_MAX_QUEUE_CELL.get().unwrap();
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn httpmq_now_getpos(db: &rocksdb::DB, name: &String) -> Option<i32> {
|
||||
let metadata = httpmq_read_metadata(db, name);
|
||||
let maxqueue = metadata.as_ref()?[0];
|
||||
let putpos = metadata.as_ref()?[1];
|
||||
let mut getpos = metadata.as_ref()?[2];
|
||||
|
||||
if getpos == 0 && putpos > 0 {
|
||||
getpos = 1 // first get operation, set getpos 1
|
||||
} else if getpos < putpos {
|
||||
getpos += 1 // 1nd lap, increase getpos
|
||||
} else if getpos > putpos && getpos < maxqueue {
|
||||
getpos += 1 // 2nd lap
|
||||
} else if getpos > putpos && getpos == maxqueue {
|
||||
getpos = 1 // 2nd first operation, set getpos 1
|
||||
} else {
|
||||
return Some(0); // all data in queue has been get
|
||||
}
|
||||
|
||||
debug!("getpos {} {:?}", getpos, metadata);
|
||||
|
||||
db.put(name.to_string() + ".getpos", getpos.to_string())
|
||||
.ok()?;
|
||||
Some(getpos)
|
||||
}
|
||||
|
||||
fn httpmq_now_putpos(db: &rocksdb::DB, name: &String) -> Option<i32> {
|
||||
let metadata = httpmq_read_metadata(db, name);
|
||||
let maxqueue = metadata.as_ref()?[0];
|
||||
let mut putpos = metadata.as_ref()?[1];
|
||||
let getpos = metadata.as_ref()?[2];
|
||||
|
||||
let newpos;
|
||||
|
||||
putpos += 1; // increase put queue pos
|
||||
if putpos == getpos {
|
||||
// queue is full
|
||||
return Some(0); // return 0 to reject put operation
|
||||
} else if getpos <= 1 && putpos > maxqueue {
|
||||
// get operation less than 1
|
||||
return Some(0); // and queue is full, just reject it
|
||||
} else if putpos > maxqueue {
|
||||
// 2nd lap
|
||||
newpos = 1 // reset putpos as 1 and write to leveldb
|
||||
} else {
|
||||
// 1nd lap, convert int to string and write to leveldb
|
||||
newpos = putpos;
|
||||
}
|
||||
|
||||
debug!("newpos {} {:?}", newpos, metadata);
|
||||
|
||||
db.put(name.to_string() + ".putpos", newpos.to_string())
|
||||
.unwrap();
|
||||
|
||||
Some(newpos)
|
||||
}
|
||||
|
||||
type SharedState = Arc<RwLock<State>>;
|
||||
|
||||
struct State {
|
||||
database: rocksdb::DB,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new() -> State {
|
||||
let db = DB::open_default("path").unwrap();
|
||||
State { database: db }
|
||||
}
|
||||
}
|
||||
|
||||
async fn kv_get(
|
||||
Query(args): Query<KVSet>,
|
||||
Extension(state): Extension<SharedState>,
|
||||
) -> Result<String, StatusCode> {
|
||||
let db = &state.read().unwrap().database;
|
||||
let getpos = httpmq_now_getpos(&db, &args.name).unwrap_or_default();
|
||||
|
||||
debug!("{} {:?}", getpos, args);
|
||||
|
||||
if getpos == 0 {
|
||||
Ok(String::from("HTTPMQ_GET_END"))
|
||||
} else {
|
||||
let queue_name = args.name.to_string() + &getpos.to_string();
|
||||
let val = match db.get(queue_name) {
|
||||
Ok(Some(obj)) => String::from_utf8(obj.clone()).unwrap_or(String::from("")),
|
||||
Ok(None) => String::from("HTTPMQ_GET_NONE"),
|
||||
Err(_) => String::from("HTTPMQ_GET_ERROR"),
|
||||
};
|
||||
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct KVSet {
|
||||
opt: String,
|
||||
name: String,
|
||||
data: Option<String>,
|
||||
// pos: Option<i32>,
|
||||
num: Option<i32>,
|
||||
}
|
||||
|
||||
async fn kv_maxqueue(
|
||||
Query(args): Query<KVSet>,
|
||||
Extension(state): Extension<SharedState>,
|
||||
) -> Result<String, StatusCode> {
|
||||
let num = args.num.unwrap_or(0);
|
||||
if num > 0 && num <= *DEFAULT_MAX_QUEUE_CELL.get().unwrap() {
|
||||
let db = &state.read().unwrap().database;
|
||||
db.put(args.name.to_string() + ".maxqueue", num.to_string())
|
||||
.unwrap();
|
||||
Ok(String::from("HTTPMQ_MAXQUEUE_OK"))
|
||||
} else {
|
||||
Ok(String::from("HTTPMQ_MAXQUEUE_CANCLE"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn kv_set(
|
||||
Query(args): Query<KVSet>,
|
||||
Extension(state): Extension<SharedState>,
|
||||
) -> Result<String, StatusCode> {
|
||||
let db = &state.read().unwrap().database;
|
||||
|
||||
let putpos = httpmq_now_putpos(&db, &args.name).unwrap_or_default();
|
||||
|
||||
debug!("{} {:?}", putpos, args);
|
||||
|
||||
if putpos > 0 {
|
||||
let queue_name = args.name.to_string() + &putpos.to_string();
|
||||
let data = args.data.unwrap_or("".to_string());
|
||||
if data.len() > 0 {
|
||||
db.put(queue_name, data).unwrap();
|
||||
return Ok(String::from("HTTPMQ_PUT_OK"));
|
||||
}
|
||||
Ok(String::from("HTTPMQ_PUT_NO_DATA"))
|
||||
} else {
|
||||
Ok(String::from("HTTPMQ_PUT_END"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn kv_status(
|
||||
Query(args): Query<KVSet>,
|
||||
Extension(state): Extension<SharedState>,
|
||||
) -> Result<String, StatusCode> {
|
||||
let db = &state.read().unwrap().database;
|
||||
let metadata = httpmq_read_metadata(db, &args.name).unwrap_or(vec![0, 0, 0]);
|
||||
let maxqueue = metadata[0];
|
||||
let putpos = metadata[1];
|
||||
let getpos = metadata[2];
|
||||
|
||||
let mut ungetnum = 0;
|
||||
let mut put_times = "";
|
||||
let mut get_times = "";
|
||||
if putpos >= getpos {
|
||||
ungetnum = (putpos - getpos).abs();
|
||||
put_times = "1st lap";
|
||||
get_times = "1st lap";
|
||||
} else if putpos < getpos {
|
||||
ungetnum = (maxqueue + putpos - getpos).abs();
|
||||
put_times = "2st lap";
|
||||
get_times = "1st lap";
|
||||
}
|
||||
|
||||
let buf = format!(
|
||||
"HTTP Simple Queue Service
|
||||
------------------------------
|
||||
Queue Name: {}
|
||||
Maximum number of queues: {}
|
||||
Put position of queue ({}): {}
|
||||
Get position of queue ({}): {}
|
||||
Number of unread queue: {}
|
||||
",
|
||||
args.name.to_string(),
|
||||
maxqueue,
|
||||
put_times,
|
||||
putpos,
|
||||
get_times,
|
||||
getpos,
|
||||
ungetnum
|
||||
);
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
async fn kv_reset(
|
||||
Query(args): Query<KVSet>,
|
||||
Extension(state): Extension<SharedState>,
|
||||
) -> Result<String, StatusCode> {
|
||||
let db = &state.read().unwrap().database;
|
||||
db.put(
|
||||
args.name.to_string() + ".maxqueue",
|
||||
DEFAULT_MAX_QUEUE_CELL.get().unwrap().to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
db.put(args.name.to_string() + ".putpos", "0").unwrap();
|
||||
db.put(args.name.to_string() + ".getpos", "0").unwrap();
|
||||
|
||||
Ok(String::from("HTTPMQ_RESET_OK"))
|
||||
}
|
||||
|
||||
async fn process(
|
||||
Query(args): Query<KVSet>,
|
||||
Extension(state): Extension<SharedState>,
|
||||
) -> Result<String, StatusCode> {
|
||||
let res = match &args.opt[..] {
|
||||
"get" => kv_get(Query(args), Extension(state)).await,
|
||||
"put" => kv_set(Query(args), Extension(state)).await,
|
||||
"status" => kv_status(Query(args), Extension(state)).await,
|
||||
"reset" => kv_reset(Query(args), Extension(state)).await,
|
||||
"maxqueue" => kv_maxqueue(Query(args), Extension(state)).await,
|
||||
_ => Ok(String::from("invalid opt")),
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
async fn handle_error(error: BoxError) -> impl IntoResponse {
|
||||
if error.is::<tower::timeout::error::Elapsed>() {
|
||||
return (StatusCode::REQUEST_TIMEOUT, Cow::from("request timed out"));
|
||||
}
|
||||
|
||||
if error.is::<tower::load_shed::error::Overloaded>() {
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Cow::from("service is overloaded, try again later"),
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Cow::from(format!("Unhandled internal error: {}", error)),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Set the RUST_LOG, if it hasn't been explicitly defined
|
||||
if std::env::var_os("RUST_LOG").is_none() {
|
||||
std::env::set_var("RUST_LOG", "httpmq-rs=debug,tower_http=debug")
|
||||
}
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let state = SharedState::new(RwLock::new(State::new()));
|
||||
|
||||
let matches = App::new("httpmq-rs")
|
||||
.bin_name("httpmq-rs")
|
||||
.arg(
|
||||
Arg::new("maxqueue")
|
||||
.long("maxqueue")
|
||||
.default_value("100000000"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
DEFAULT_MAX_QUEUE_CELL
|
||||
.set(
|
||||
matches
|
||||
.value_of("maxqueue")
|
||||
.unwrap()
|
||||
.parse::<i32>()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Build our application by composing routes
|
||||
let app = Router::new()
|
||||
.route("/", get(process))
|
||||
// Add middleware to all routes
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
// Handle errors from middleware
|
||||
.layer(HandleErrorLayer::new(handle_error))
|
||||
.load_shed()
|
||||
.concurrency_limit(1024)
|
||||
.timeout(Duration::from_secs(10))
|
||||
// .layer(TraceLayer::new_for_http())
|
||||
.layer(AddExtensionLayer::new(state))
|
||||
.into_inner(),
|
||||
);
|
||||
|
||||
// Run our app with hyper
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 1218));
|
||||
tracing::debug!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
#[test]
|
||||
fn test_add() {
|
||||
// using common code.
|
||||
}
|
Loading…
Reference in New Issue