Skip to content

Commit

Permalink
Merge pull request #11 from sebastianconcept/minor-review
Browse files Browse the repository at this point in the history
Minor review
  • Loading branch information
sebastianconcept authored Dec 20, 2023
2 parents afd1889 + f5d2456 commit b5cc51a
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 41 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ Cargo.lock
# Added by cargo

/target

output.txt
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "memoizer"
version = "0.1.0"
version = "0.1.2"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand All @@ -15,3 +15,4 @@ rand = "0.8.5"
twox-hash = "1.6.3"
clap = "3.2.14"
tokio = { version = "1.20.1", features = ["full"] }
benchmarking = "0.4.12"
121 changes: 119 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,129 @@ Minimalist thread-safe key-value store shared over TCP sockets.

Development version:

./build.sh
cargo build

Release version:

./release.sh
cargo build --release

## Running the service (release build)

./target/release/memoizer -b localhost -p 9091

## Running the inner benchmark

./target/release/memoizer --bind 0.0.0.0 --port 9091 --bench 10000 --payload '{"id":123,"name":"Sample JSON","description":"This is a sample JSON object with approximately 1024 bytes of data. It''s used for demonstration purposes.","tags":["json","sample","data"],"details":{"created_at":"2023-04-01T12:00:00","updated_at":"2023-04-01T14:30:00","status":"active"},"values":[1,2,3,4,5,6,7,8,9,10],"settings":{"enabled":true,"threshold":50,"options":["option1","option2","option3"]},"comments":[{"user":"user1","text":"This is a comment."},{"user":"user2","text":"Another comment here."}]}'


On this hardware [1] it renders:

```
Starting the benchmarking...
Benchmarking warmed up and ready to go.
Measuring Rust HashMap 10000 inserts...
Measuring Rust HashMap 10000 inserts...
Measuring Rust HashMap 10000 inserts...
Measuring Rust HashMap 10000 inserts...
Measuring Rust HashMap 10000 inserts...
Measuring Rust HashMap 10000 inserts...
Measuring Rust HashMap 10000 inserts...
Measuring Rust HashMap 10000 inserts...
Measuring Rust HashMap 10000 inserts...
Measuring Rust HashMap 10000 inserts...
It took 14.473349ms to perform 10000 insertions
Measuring Rust HashMap 10000 reads...
Measuring Rust HashMap 10000 reads...
Measuring Rust HashMap 10000 reads...
Measuring Rust HashMap 10000 reads...
Measuring Rust HashMap 10000 reads...
Measuring Rust HashMap 10000 reads...
Measuring Rust HashMap 10000 reads...
Measuring Rust HashMap 10000 reads...
Measuring Rust HashMap 10000 reads...
Measuring Rust HashMap 10000 reads...
It took 10.014888ms to perform 10000 reads
```

## Comparing with Redis
From [Pharo](https://pharo.org/), if you have [RediStick](https://github.com/mumez/RediStick) and [ABBench](https://github.com/emdonahue/ABBench) installed in the image, you can benchmark this memoizer service and compare performance with Redis. Here is a snippet with the numbers provided with this hardware [1]:

```Smalltalk
RsRedisConnectionPool primaryUrl: 'sync://localhost:6379'.
"Create a Redis client."
redis := RsRedisProxy of: #client1.
"Payload to use as part of the value."
sample1 := '\{"id": "{1}","name":"Sample JSON","description":"This is a sample JSON object with approximately 1024 bytes of data. It''s used for demonstration purposes.","tags":["json","sample","data"],"details":\{"created_at":"2023-04-01T12:00:00","updated_at":"2023-04-01T14:30:00","status":"active"},"values":[1,2,3,4,5,6,7,8,9,10],"settings":\{"enabled":true,"threshold":50,"options":["option1","option2","option3"]\},"comments":[\{"user":"user1","text":"This is a comment."\},\{"user":"user2","text":"Another comment here."\}]\}'.
"Cook some data to later iterate and add it to the server."
keys := ((1 to: 10000) collect:[ :e | UUID new asString36 ]) shuffled.
values := (1 to: 10000) collect:[ :e | sample1 format: { UUID new asString36 } ].
source := Dictionary newFromKeys: keys andValues: values.
"Closure used to connect a memoizer client."
client := [ socket := Socket newTCP.
hostAddress := NetNameResolver addressFromString: '127.0.0.1'.
socket connectTo: hostAddress port: 9091.
stream := SocketStream on: socket ].
"Closure used to reset the memoizer client."
reset := [ stream ifNotNil:[ stream close ] ].
"Get command using the given key."
get := [ :key | (stream nextPutAll: ('\{"s":"get","p": \{"k": "{1}","v": null \}\}' format: {key asString}); crlf; flush; nextLineLf) trim ].
"Set command using the given key/value."
set := [ :key :value | | content |
content := ('\{"s":"set", "p": \{"k": "{1}","v": {2}\}\}' format: {key asString. value asString}) trim.
stream nextPutAll: content; crlf; flush; nextLineLf ].
"Size command."
size := [ (stream nextPutAll: ('{"s":"size", "p": {}}'); crlf; flush; nextLineLf) trim ].
"Reset the client (useful if previously open)."
reset value.
"Connect to the memoizer server."
client value.
"Sanity check adding 1 value."
set value: 'answer' value: '42'.
record := source associations first.
"Sanity check adding sampled value."
set value: record key value: record value.
"Command to check memoizer's current size."
size value.
"Setting values in memoizer server"
Time millisecondsToRun: [ source keysAndValuesDo: [ :k :v | set value: k value: v ] ]. "710"
"Getting values from the memoizer server"
Time millisecondsToRun: [ keys collect: [ :k | get value: k ] ].
"582"
"Setting values in a local Redis"
Time millisecondsToRun: [ source keysAndValuesDo: [ :k :v | redis at: k put: v ] ].
"1364"
"Getting values from a local Redis"
Time millisecondsToRun: [ keys collect: [ :k | redis at: k ] ].
"1330"
"Comparing repeated same write"
ABBench bench: [ ABBench
a: [ redis at: keys anyOne put: values anyOne ]
b: [ set value: keys anyOne value: values anyOne ] ].
"B is 91.89% FASTER than A"
"Comparing repeated same read"
ABBench bench: [ ABBench
a: [ redis at: keys anyOne ]
b: [ get value: keys anyOne ] ].
"B is 125.85% FASTER than A"
```

[1] An Intel based MacBook Pro, 2,5 GHz Quad-Core Intel Core i7
2 changes: 0 additions & 2 deletions build.sh

This file was deleted.

2 changes: 0 additions & 2 deletions mod.rs

This file was deleted.

2 changes: 0 additions & 2 deletions release.sh

This file was deleted.

51 changes: 49 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
extern crate clap;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use clap::{value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command};

pub fn get_socket_address() -> String {
let args = get_arguments();
Expand All @@ -14,14 +14,23 @@ pub fn get_socket_address() -> String {
format!("{}:{}", bind, port)
}

pub fn get_bench_and_payload() -> (Option<usize>, Option<String>) {
let args = get_arguments();
let times = args.get_one("bench").copied();
let payload = args.get_one("payload").cloned();
(times, payload)
}

fn get_arguments<'a>() -> ArgMatches {
Command::new("Memoizer")
.version("1.0")
.version("0.1.2")
.author("Sebastian Sastre <[email protected]>")
.about("Minimalist thread-safe key-value store shared over TCP sockets.")
.arg(
Arg::with_name("bind")
.value_name("IP_ADDRESS")
.short('b')
.long("bind")
.multiple(false)
.action(ArgAction::Append)
.value_parser(value_parser!(String))
Expand All @@ -31,13 +40,51 @@ fn get_arguments<'a>() -> ArgMatches {
)
.arg(
Arg::with_name("port")
.value_name("PORT")
.short('p')
.long("port")
.multiple(false)
.action(ArgAction::Append)
.value_parser(value_parser!(String))
.help("Defines the port to use.")
.required(true)
.takes_value(true),
)
.arg(
Arg::with_name("bench")
.value_name("NUM_TIMES")
.short('e')
.long("bench")
.multiple(false)
.action(ArgAction::Append)
.value_parser(value_parser!(usize))
.help("Runs a benchmark a number of times with an optional custom payload")
.required(false)
.takes_value(true),
)
.arg(
Arg::with_name("payload")
.value_name("CUSTOM_PAYLOAD")
.short('d')
.long("payload")
.multiple(false)
.action(ArgAction::Append)
.value_parser(value_parser!(String))
.help("The custom payload to use in a benchmark")
.required(false)
.takes_value(true),
)
.group(
ArgGroup::with_name("bench_options")
.args(&["bench", "payload"])
.multiple(true)
.required(false), // Either times or string is required
)
.group(
ArgGroup::with_name("connection_options")
.args(&["bind", "port"])
.multiple(true)
.required(false), // bind and port are required unless bench is selected
)
.get_matches()
}
19 changes: 9 additions & 10 deletions src/listener.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


use std::error::Error;

pub(crate) use serde::{Deserialize, Serialize};
Expand All @@ -11,15 +9,15 @@ use crate::storage::{get, reset, set, size};

// A MemoizerMessage is used to receive a command (selector) and an argument (its payload)
// so the service can perform one of actions supported by its `route` method.
#[derive(Serialize, Deserialize, Debug)]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct MemoizerMessage {
s: String, // selector
p: Value, // payload
}

// Performs the corresponding action for the given
// MemoizerMessage and returns the corresponding answer
pub fn route(message: MemoizerMessage) -> String {
pub fn route(message: &MemoizerMessage) -> String {
match message.s.as_str() {
"get" => {
let key = message.p["k"].to_string();
Expand All @@ -44,18 +42,18 @@ pub fn route(message: MemoizerMessage) -> String {
format!("{}", size)
}
_ => {
println!("Received and unsupported value");
println!("Received an unsupported value {:?}", message);
format!("nok: {:?}", message)
}
}
}

// Handler that responds to the MemoizerMessage on the given line and stream.
fn on_line_received(message: serde_json::Result<MemoizerMessage>) -> String {
fn on_line_received(message: &serde_json::Result<MemoizerMessage>) -> String {
match message {
Ok(m) => route(m),
Err(err) => {
println!("Received and unsupported value");
println!("Received an unsupported value {:?}", message);
let error_message = format!("{:?}", err);
error_message
}
Expand All @@ -69,11 +67,12 @@ pub async fn on_socket_accept(mut stream: TcpStream) -> Result<(), Box<dyn Error
let mut lines = BufReader::new(reader).lines();
while let Some(line) = lines.next_line().await? {
let message: serde_json::Result<MemoizerMessage> = serde_json::from_str(&line);
let response = on_line_received(message);
let response = on_line_received(&message);
let paylaod = format!("{}\n\r", response);
writer.write_all(paylaod.as_bytes())
writer
.write_all(paylaod.as_bytes())
.await
.expect("Failed to write to the socket");
};
}
Ok(())
}
11 changes: 9 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@ pub mod config;
mod listener;
pub mod storage;

use storage::benchmark;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let (times, payload) = get_bench_and_payload();
if let Some(t) = times {
benchmark(t, payload);
}

let socket_address = get_socket_address();
let listener = TcpListener::bind(&socket_address).await?;

println!("Memoizer listening on {}", socket_address);
loop {
let (socket, _) = listener.accept().await?;
let _thread = tokio::spawn(async move {
on_socket_accept(socket)
.await
.expect("Failed to process incoming socket connection");
.expect("Failed to process incoming socket connection")
});
}
}
Loading

0 comments on commit b5cc51a

Please sign in to comment.