Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replicated cache #16

Merged
merged 32 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d6d1301
docs: update readme
jdockerty Apr 10, 2024
75b5317
refactor: minor rejig in prep for replication crate
jdockerty Apr 10, 2024
f0e1643
chore: scaffold replication
jdockerty Apr 10, 2024
c279eef
docs: update lib
jdockerty Apr 10, 2024
b02ae01
chore: basic file structure/test
jdockerty Apr 10, 2024
f24f7ec
feat: use client/server wrappers (#17)
jdockerty Apr 11, 2024
c9516c4
chore: merge branch 'main' into feat/cache-replication
jdockerty Apr 11, 2024
97f6967
feat: generic impl of replication client
jdockerty Apr 13, 2024
595d5cc
docs: update timestamp docstring for versioning
jdockerty Apr 13, 2024
ec3fcf9
wip: replication
jdockerty Apr 13, 2024
9b4337a
refactor: move KvServer to server.rs
jdockerty Apr 13, 2024
b349192
feat: include timestamp in return value
jdockerty Apr 13, 2024
b12b4c7
feat: include timestamp in return value
jdockerty Apr 13, 2024
e26f9ca
wip: replication
jdockerty Apr 13, 2024
8db2414
Merge branch 'feat/cache-replication' of github.com:jdockerty/sqrl in…
jdockerty Apr 13, 2024
71f3d39
refactor: rename to StandaloneServer
jdockerty Apr 14, 2024
e5fb015
refactor: rename ReplicationClient to ReplicatedServer
jdockerty Apr 14, 2024
86fb678
wip: replication
jdockerty Apr 14, 2024
1e537b9
wip: replication
jdockerty Apr 14, 2024
0646b4d
wip: replication
jdockerty Apr 14, 2024
f72e075
wip: replication
jdockerty Apr 14, 2024
a444510
wip: replication
jdockerty Apr 14, 2024
6f9505d
wip: replication
jdockerty Apr 15, 2024
4b09eff
feat: replicate to followers
jdockerty Apr 15, 2024
f877c7f
fix: use standalone server for tests
jdockerty Apr 15, 2024
0cbe9e5
minor cleanup/rename
jdockerty Apr 15, 2024
a73b8f1
removal for replicated server
jdockerty Apr 15, 2024
8565b78
docstring for replication crate
jdockerty Apr 15, 2024
7b7f3e5
cargo clippy
jdockerty Apr 15, 2024
abc456c
basic test for replication mode cli
jdockerty Apr 15, 2024
b102616
scaffold cli for replication server
jdockerty Apr 15, 2024
c9e2181
feat: rearchitecture replication to leader/follower
jdockerty Apr 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ edition = "2021"
name = "sqrl_bench"
harness = false

[features]
default = ["replication"]
replication = []


[dev-dependencies]
assert_cmd = "0.11"
Expand Down Expand Up @@ -39,6 +43,7 @@ rayon = "1.9.0"
tokio = { version = "1.36.0", features = ["full"] }
prost = "0.12.4"
tonic = "0.11.0"
futures = "0.3.30"

[build-dependencies]
tonic-build = "0.11.0"
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# sqrl
# squirrel

_Pronounced squirrel, as in "to squirrel away"._
_As in "[to squirrel away][squirrel-away]"._

A distributed key-value store which uses a simple implementation of [bitcask](https://github.com/basho/bitcask/blob/develop/doc/bitcask-intro.pdf) as the underlying storage mechanism.
A (replicated) key-value store which uses a simple implementation of [bitcask](https://github.com/basho/bitcask/blob/develop/doc/bitcask-intro.pdf) as the underlying storage mechanism.

## How it works

This follows a simple structure and exposes a very common API surface: `set(k,v)`, `get(k)`, and `remove(k)`.

## Implementation
## Notes

It was initially based off my implementation of the PingCAP talent plan course for building a key-value store in Rust:
This was initially built through my implementation of the PingCAP talent plan course for building a key-value store in Rust:

- [Course](https://github.com/pingcap/talent-plan/tree/master/courses/rust#the-goal-of-this-course)
- [Lesson plan](https://github.com/pingcap/talent-plan/blob/master/courses/rust/docs/lesson-plan.md#pna-rust-lesson-plan)

And has since grown into my own toy project.

[squirrel-away]: https://dictionary.cambridge.org/dictionary/english/squirrel-away
2 changes: 2 additions & 0 deletions proto/actions.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ message Acknowledgement {
message SetRequest {
string key = 1;
string value = 2;
int64 timestamp = 3;
}

message GetRequest {
Expand All @@ -23,6 +24,7 @@ message GetRequest {

message GetResponse {
optional string value = 1;
int64 timestamp = 2;
}

message RemoveRequest {
Expand Down
16 changes: 16 additions & 0 deletions src/action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use clap::Subcommand;
use serde::{Deserialize, Serialize};

/// Actions that can be performed by the client.
#[derive(Debug, Subcommand, Serialize, Deserialize)]
pub enum Action {
/// Enter a key-value pair into the store.
Set { key: String, value: String },

/// Get a value from the store with the provided key.
Get { key: String },

/// Remove a value from the store with the provided key.
#[clap(name = "rm")]
Remove { key: String },
}
24 changes: 10 additions & 14 deletions src/bin/sqrl-client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use clap::Parser;
use sqrl::actions::action_client::ActionClient;
use sqrl::actions::{GetRequest, SetRequest};
use sqrl::client::Action;

use sqrl::action::Action;
use sqrl::client::{Client, RemoteNodeClient};

#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
Expand All @@ -16,26 +16,22 @@ struct App {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = App::parse();
let mut client = ActionClient::connect(format!("http://{}", cli.server)).await?;
let mut client = RemoteNodeClient::new(cli.server).await?;

match cli.subcmd {
Action::Set { key, value } => {
client
.set(tonic::Request::new(SetRequest { key, value }))
.await?;
client.set(key, value).await?;
}
Action::Get { key } => {
let response = client.get(tonic::Request::new(GetRequest { key })).await?;
match response.into_inner().value {
Some(v) => println!("{}", v),
let response = client.get(key).await?;
match response {
Some(v) => println!("{}", v.value.unwrap()),
None => println!("Key not found"),
}
}
Action::Remove { key } => {
let response = client
.remove(tonic::Request::new(sqrl::actions::RemoveRequest { key }))
.await?;
match response.into_inner().success {
let response = client.remove(key).await?;
match response.success {
true => {}
false => {
eprintln!("Key not found");
Expand Down
116 changes: 43 additions & 73 deletions src/bin/sqrl-server.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
use clap::Parser;
use sqrl::actions;
use sqrl::actions::action_server::Action as ActionSrv;
use sqrl::actions::action_server::ActionServer;
use futures::StreamExt;
use sqrl::client::RemoteNodeClient;
use sqrl::replication;
use sqrl::replication::ReplicatedServer;
use sqrl::KvStore;
use sqrl::KvsEngine;
use sqrl::StandaloneServer;
use sqrl::ENGINE_FILE;
use std::sync::Arc;
use std::{ffi::OsString, path::PathBuf};
use std::{fmt::Display, net::SocketAddr};
use tracing::info;
use tracing::{debug, warn};

mod proto {
tonic::include_proto!("actions");
}

#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
Expand All @@ -24,6 +28,18 @@ struct App {

#[arg(long, global = true, default_value = default_log_location())]
log_file: PathBuf,

#[cfg(feature = "replication")]
#[arg(long, default_value = "follower")]
replication_mode: replication::Mode,

#[cfg(feature = "replication")]
#[arg(
long,
requires_if(replication::Mode::Leader, "replication_mode"),
value_delimiter = ','
)]
followers: Vec<String>,
}

fn default_log_location() -> OsString {
Expand All @@ -48,76 +64,30 @@ impl Display for Engine {
}
}

#[derive(Clone)]
struct KvServer {
pub store: Arc<KvStore>,
}

impl KvServer {
pub fn new<P>(path: P) -> anyhow::Result<Self>
where
P: Into<std::path::PathBuf>,
{
let store = Arc::new(KvStore::open(path)?);
Ok(Self { store })
}
}

#[tonic::async_trait]
impl ActionSrv for KvServer {
async fn get(
&self,
req: tonic::Request<actions::GetRequest>,
) -> tonic::Result<tonic::Response<actions::GetResponse>, tonic::Status> {
let req = req.into_inner();
let value = self.store.get(req.key).await.unwrap();
Ok(tonic::Response::new(actions::GetResponse { value }))
}
async fn set(
&self,
req: tonic::Request<actions::SetRequest>,
) -> tonic::Result<tonic::Response<actions::Acknowledgement>, tonic::Status> {
let req = req.into_inner();
self.store.set(req.key, req.value).await.unwrap();
Ok(tonic::Response::new(actions::Acknowledgement {
success: true,
}))
}
async fn remove(
&self,
req: tonic::Request<actions::RemoveRequest>,
) -> tonic::Result<tonic::Response<actions::Acknowledgement>, tonic::Status> {
let req = req.into_inner();
match self.store.remove(req.key).await {
Ok(_) => Ok(tonic::Response::new(actions::Acknowledgement {
success: true,
})),
Err(_) => Ok(tonic::Response::new(actions::Acknowledgement {
success: false,
})),
}
}
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let app = App::parse();

// We must error if the previous storage engine was not 'sqrl' as it is incompatible.
KvStore::engine_is_sqrl(app.engine_name.to_string(), app.log_file.join(ENGINE_FILE))?;
let srv = KvServer::new(app.log_file)?;

info!(
"sqrl-server version: {}, engine: {}",
env!("CARGO_PKG_VERSION"),
app.engine_name
);

info!("Listening on {}", app.addr);
tonic::transport::Server::builder()
.add_service(ActionServer::new(srv))
.serve(app.addr)
.await?;

Ok(())
match app.replication_mode {
replication::Mode::Leader => {
assert_eq!(
app.followers.len(),
2,
"Only 2 followers are configurable at present"
);
let clients = futures::stream::iter(app.followers.iter())
.filter_map(|f| async move { RemoteNodeClient::new(f.to_string()).await.ok() })
.collect::<Vec<RemoteNodeClient>>()
.await;
debug!("Replicating to {} followers", clients.len());
if clients.len() > 3 {
warn!("Replicating to many followers can greatly impact write performance");
}
ReplicatedServer::new(clients.into(), app.log_file, app.addr)?
.run()
.await
}
replication::Mode::Follower => StandaloneServer::new(app.log_file, app.addr)?.run().await,
}
}
Loading
Loading