├── _config.yml
├── src
├── segment
│ ├── mod.rs
│ ├── hint.rs
│ └── data.rs
├── util
│ ├── mod.rs
│ ├── misc.rs
│ └── io.rs
├── lib.rs
├── config.rs
├── error.rs
├── bin
│ ├── tinkv-server.rs
│ └── tinkv.rs
├── server.rs
├── store.rs
└── resp.rs
├── .gitignore
├── .travis.yml
├── LICENSE
├── Cargo.toml
├── examples
├── hello.rs
└── basic.rs
├── tests
└── store.rs
├── benches
└── store_benchmark.rs
└── README.md
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-architect
--------------------------------------------------------------------------------
/src/segment/mod.rs:
--------------------------------------------------------------------------------
1 | mod data;
2 | mod hint;
3 |
4 | pub(crate) use data::{DataFile, Entry as DataEntry};
5 | pub(crate) use hint::HintFile;
6 |
--------------------------------------------------------------------------------
/src/util/mod.rs:
--------------------------------------------------------------------------------
1 | pub use io::{BufReaderWithOffset, BufWriterWithOffset, ByteLineReader, FileWithBufWriter};
2 | pub use misc::*;
3 |
4 | mod io;
5 | pub mod misc;
6 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! A simple key-value storage.
2 | pub mod config;
3 | mod error;
4 | mod resp;
5 | mod segment;
6 | mod server;
7 | mod store;
8 | pub mod util;
9 |
10 | pub use error::{Result, TinkvError};
11 | pub use server::Server;
12 | pub use store::{OpenOptions, Store};
13 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | pub const REMOVE_TOMESTONE: &[u8] = b"%TINKV_REMOVE_TOMESTOME%";
2 | pub const DATA_FILE_SUFFIX: &str = ".tinkv.data";
3 | pub const HINT_FILE_SUFFIX: &str = ".tinkv.hint";
4 | pub const DEFAULT_MAX_DATA_FILE_SIZE: u64 = 1024 * 1024 * 10; // 10MB
5 | pub const DEFAULT_MAX_KEY_SIZE: u64 = 64;
6 | pub const DEFAULT_MAX_VALUE_SIZE: u64 = 65536;
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
7 | Cargo.lock
8 |
9 | # These are backup files generated by rustfmt
10 | **/*.rs.bk
11 |
12 | .vscode
13 |
14 | .tinkv
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: rust
2 | cache:
3 | directories:
4 | - target/
5 |
6 | script:
7 | - rustup component add clippy && cargo clippy
8 | - cargo test -- --nocapture
9 |
10 | after_success: |
11 | cargo doc \
12 | && echo '' > target/doc/index.html && \
13 | sudo pip install ghp-import && \
14 | ghp-import -n target/doc && \
15 | git push -qf https://${TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
16 |
17 | notifications:
18 | email: false
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 iFaceless
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/util/misc.rs:
--------------------------------------------------------------------------------
1 | //! Define some helper functions.
2 | use std::path::Path;
3 | use std::time::{SystemTime, UNIX_EPOCH};
4 |
5 | pub fn current_timestamp() -> u128 {
6 | SystemTime::now()
7 | .duration_since(UNIX_EPOCH)
8 | .expect("time went backwards")
9 | .as_nanos()
10 | }
11 |
12 | pub fn checksum(data: &[u8]) -> u32 {
13 | crc::crc32::checksum_ieee(data)
14 | }
15 |
16 | pub fn parse_file_id(path: &Path) -> Option {
17 | path.file_name()?
18 | .to_str()?
19 | .split('.')
20 | .next()?
21 | .parse::()
22 | .ok()
23 | }
24 |
25 | pub fn to_utf8_string(value: &[u8]) -> String {
26 | String::from_utf8_lossy(value).to_string()
27 | }
28 |
29 | #[cfg(test)]
30 | mod tests {
31 | use super::*;
32 |
33 | #[test]
34 | fn test_parse_file_id() {
35 | let r = parse_file_id(Path::new("path/to/12345.tinkv.data"));
36 | assert_eq!(r, Some(12345 as u64));
37 |
38 | let r = parse_file_id(Path::new("path/to/.tinkv.data"));
39 | assert_eq!(r, None);
40 |
41 | let r = parse_file_id(Path::new("path/to"));
42 | assert_eq!(r, None);
43 | }
44 |
45 | #[test]
46 | fn test_to_utf8_str() {
47 | assert_eq!(to_utf8_string(b"hello, world"), "hello, world".to_owned());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [[example]]
2 | name = 'basic'
3 | path = 'examples/basic.rs'
4 |
5 | [[example]]
6 | name = 'hello'
7 | path = 'examples/hello.rs'
8 |
9 | [[bench]]
10 | name = 'store_benchmark'
11 | harness = false
12 |
13 | [package]
14 | name = 'tinkv'
15 | version = '0.10.0'
16 | authors = ['0xE8551CCB ']
17 | edition = '2018'
18 | description = 'A fast and simple key-value storage engine.'
19 | keywords = [
20 | 'database',
21 | 'key-value',
22 | 'storage',
23 | ]
24 | categories = ['database-implementations']
25 | license = 'MIT'
26 | readme = 'README.md'
27 | homepage = 'https://github.com/iFaceless/tinkv'
28 |
29 | [dependencies]
30 | clap = '2.33.1'
31 | structopt = '0.3.14'
32 | thiserror = '1.0.19'
33 | anyhow = '1.0.31'
34 | crc = '1.8.1'
35 | glob = '0.3.0'
36 | log = '0.4.8'
37 | pretty_env_logger = '0.4.0'
38 | impls = '1.0.3'
39 | bincode = '1.2.1'
40 | clap-verbosity-flag = '0.3.1'
41 | bytefmt = '0.1.7'
42 | lazy_static = '1.4.0'
43 | os_info = '2.0.6'
44 | sys-info = '0.7.0'
45 |
46 | [dependencies.serde]
47 | version = '1.0.111'
48 | features = ['derive']
49 |
50 | [dev-dependencies]
51 | assert_cmd = '0.11.0'
52 | predicates = '1.0.0'
53 | tempfile = '3.1.0'
54 | walkdir = '2.3.1'
55 | criterion = '0.3.2'
56 | sled = "0.32.0"
57 |
58 | [dev-dependencies.rand]
59 | version = '0.7'
60 | features = [
61 | 'std',
62 | 'small_rng',
63 | ]
64 |
--------------------------------------------------------------------------------
/examples/hello.rs:
--------------------------------------------------------------------------------
1 | use pretty_env_logger;
2 | use std::time;
3 | use tinkv::{self};
4 |
5 | fn main() -> tinkv::Result<()> {
6 | pretty_env_logger::init_timed();
7 | let mut store = tinkv::OpenOptions::new()
8 | .max_data_file_size(1024 * 100)
9 | .open("/usr/local/var/tinkv")?;
10 |
11 | let begin = time::Instant::now();
12 |
13 | const TOTAL_KEYS: usize = 1000;
14 | for i in 0..TOTAL_KEYS {
15 | let k = format!("hello_{}", i);
16 | let v = format!("world_{}", i);
17 | store.set(k.as_bytes(), v.as_bytes())?;
18 | store.set(k.as_bytes(), format!("{}_new", v).as_bytes())?;
19 | }
20 |
21 | let duration = time::Instant::now().duration_since(begin);
22 | let speed = (TOTAL_KEYS * 2) as f32 / duration.as_secs_f32();
23 | println!(
24 | "{} keys written in {} secs, {} keys/s",
25 | TOTAL_KEYS * 2,
26 | duration.as_secs_f32(),
27 | speed
28 | );
29 |
30 | let stats = store.stats();
31 | println!("{:?}", stats);
32 |
33 | store.compact()?;
34 |
35 | let mut index = 100;
36 | store.for_each(&mut |k, v| {
37 | index += 1;
38 |
39 | println!(
40 | "key={}, value={}",
41 | String::from_utf8_lossy(&k),
42 | String::from_utf8_lossy(&v)
43 | );
44 |
45 | if index > 5 {
46 | Ok(false)
47 | } else {
48 | Ok(true)
49 | }
50 | })?;
51 |
52 | let v = store.get("hello_1".as_bytes())?.unwrap_or_default();
53 | println!("{}", String::from_utf8_lossy(&v));
54 |
55 | let stats = store.stats();
56 | println!("{:?}", stats);
57 |
58 | Ok(())
59 | }
60 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use std::io;
2 | use std::num::ParseIntError;
3 | use std::path::PathBuf;
4 | use thiserror::Error;
5 |
6 | /// The result of any operation.
7 | pub type Result = ::std::result::Result;
8 |
9 | /// The kind of error that could be produced during tinkv operation.
10 | #[derive(Error, Debug)]
11 | pub enum TinkvError {
12 | #[error(transparent)]
13 | ParseInt(#[from] ParseIntError),
14 | #[error("parse resp value failed")]
15 | ParseRespValue,
16 | #[error(transparent)]
17 | Io(#[from] io::Error),
18 | #[error(transparent)]
19 | Glob(#[from] glob::GlobError),
20 | #[error(transparent)]
21 | Pattern(#[from] glob::PatternError),
22 | #[error(transparent)]
23 | Codec(#[from] Box),
24 | /// Custom error definitions.
25 | #[error("crc check failed, data entry (key='{}', file_id={}, offset={}) was corrupted", String::from_utf8_lossy(.key), .file_id, .offset)]
26 | DataEntryCorrupted {
27 | file_id: u64,
28 | key: Vec,
29 | offset: u64,
30 | },
31 | #[error("key '{}' not found", String::from_utf8_lossy(.0))]
32 | KeyNotFound(Vec),
33 | #[error("file '{}' is not writeable", .0.display())]
34 | FileNotWriteable(PathBuf),
35 | #[error("key is too large")]
36 | KeyIsTooLarge,
37 | #[error("value is too large")]
38 | ValueIsTooLarge,
39 | #[error("{}", .0)]
40 | Custom(String),
41 | #[error(transparent)]
42 | Other(#[from] anyhow::Error),
43 | #[error("{} {}", .name, .msg)]
44 | RespCommon { name: String, msg: String },
45 | #[error("wrong number of arguments for '{}' command", .0)]
46 | RespWrongNumOfArgs(String),
47 | }
48 |
49 | impl TinkvError {
50 | pub fn new_resp_common(name: &str, msg: &str) -> Self {
51 | Self::RespCommon {
52 | name: name.to_owned(),
53 | msg: msg.to_owned(),
54 | }
55 | }
56 |
57 | pub fn resp_wrong_num_of_args(name: &str) -> Self {
58 | Self::RespWrongNumOfArgs(name.to_owned())
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/basic.rs:
--------------------------------------------------------------------------------
1 | use pretty_env_logger;
2 | use std::time;
3 | use tinkv::{self, Store};
4 |
5 | fn main() -> tinkv::Result<()> {
6 | pretty_env_logger::init_timed();
7 | let mut store = Store::open(".tinkv")?;
8 |
9 | let begin = time::Instant::now();
10 |
11 | const TOTAL_KEYS: usize = 100000;
12 | for i in 0..TOTAL_KEYS {
13 | let k = format!("key_{}", i);
14 | let v = format!(
15 | "value_{}_{}_hello_world_this_is_a_bad_day",
16 | i,
17 | tinkv::util::current_timestamp()
18 | );
19 | store.set(k.as_bytes(), v.as_bytes())?;
20 | store.set(k.as_bytes(), v.as_bytes())?;
21 | }
22 |
23 | let duration = time::Instant::now().duration_since(begin);
24 | let speed = (TOTAL_KEYS * 2) as f32 / duration.as_secs_f32();
25 | println!(
26 | "{} keys written in {} secs, {} keys/s",
27 | TOTAL_KEYS * 2,
28 | duration.as_secs_f32(),
29 | speed
30 | );
31 |
32 | println!("initial: {:?}", store.stats());
33 |
34 | let v = store.get("key_1".as_bytes())?.unwrap_or_default();
35 | println!("key_1 => {:?}", String::from_utf8_lossy(&v));
36 |
37 | store.set("hello".as_bytes(), "tinkv".as_bytes())?;
38 | println!("after set 1: {:?}", store.stats());
39 |
40 | store.set("hello".as_bytes(), "tinkv 2".as_bytes())?;
41 | println!("after set 2: {:?}", store.stats());
42 |
43 | store.set("hello 2".as_bytes(), "tinkv".as_bytes())?;
44 | println!("after set 3: {:?}", store.stats());
45 |
46 | let value = store.get("hello".as_bytes())?;
47 | assert_eq!(value, Some("tinkv 2".as_bytes().to_vec()));
48 |
49 | store.remove("hello".as_bytes())?;
50 | println!("after remove: {:?}", store.stats());
51 |
52 | let value_not_found = store.get("hello".as_bytes())?;
53 | assert_eq!(value_not_found, None);
54 |
55 | store.compact()?;
56 | println!("after compaction: {:?}", store.stats());
57 |
58 | let v = store.get("key_1".as_bytes())?.unwrap();
59 | println!("key_1 => {:?}", String::from_utf8_lossy(&v));
60 |
61 | Ok(())
62 | }
63 |
--------------------------------------------------------------------------------
/src/bin/tinkv-server.rs:
--------------------------------------------------------------------------------
1 | //! TinKV server is a redis-compatible storage server.
2 | use clap_verbosity_flag::Verbosity;
3 | use std::error::Error;
4 | use std::net::SocketAddr;
5 |
6 | use log::debug;
7 | use structopt::StructOpt;
8 | use tinkv::{config, OpenOptions, Server};
9 |
10 | const DEFAULT_DATASTORE_PATH: &str = "/usr/local/var/tinkv";
11 | const DEFAULT_LISTENING_ADDR: &str = "127.0.0.1:7379";
12 |
13 | #[derive(Debug, StructOpt)]
14 | #[structopt(
15 | rename_all = "kebab-case",
16 | name = "tinkv-server",
17 | version = env!("CARGO_PKG_VERSION"),
18 | author = env!("CARGO_PKG_AUTHORS"),
19 | about = "TiKV is a redis-compatible key/value storage server.",
20 | )]
21 | struct Opt {
22 | #[structopt(flatten)]
23 | verbose: Verbosity,
24 | /// Set listening address.
25 | #[structopt(
26 | short = "a",
27 | long,
28 | value_name = "IP:PORT",
29 | default_value = DEFAULT_LISTENING_ADDR,
30 | parse(try_from_str),
31 | )]
32 | addr: SocketAddr,
33 | /// Set max key size (in bytes).
34 | #[structopt(long, value_name = "KEY-SIZE")]
35 | max_key_size: Option,
36 | /// Set max value size (in bytes).
37 | #[structopt(long, value_name = "VALUE-SIZE")]
38 | max_value_size: Option,
39 | /// Set max file size (in bytes).
40 | #[structopt(long, value_name = "FILE-SIZE")]
41 | max_data_file_size: Option,
42 | /// Sync all pending writes to disk after each writing operation (default to false).
43 | #[structopt(long, value_name = "SYNC")]
44 | sync: bool,
45 | }
46 | fn main() -> Result<(), Box> {
47 | let opt = Opt::from_args();
48 | if let Some(level) = opt.verbose.log_level() {
49 | std::env::set_var("RUST_LOG", format!("{}", level));
50 | }
51 | pretty_env_logger::init_timed();
52 |
53 | debug!("get tinkv server config from command line: {:?}", &opt);
54 | let store = OpenOptions::new()
55 | .max_key_size(
56 | opt.max_key_size
57 | .unwrap_or_else(|| config::DEFAULT_MAX_KEY_SIZE),
58 | )
59 | .max_value_size(
60 | opt.max_value_size
61 | .unwrap_or_else(|| config::DEFAULT_MAX_VALUE_SIZE),
62 | )
63 | .max_data_file_size(
64 | opt.max_data_file_size
65 | .unwrap_or_else(|| config::DEFAULT_MAX_DATA_FILE_SIZE),
66 | )
67 | .sync(opt.sync)
68 | .open(DEFAULT_DATASTORE_PATH)?;
69 |
70 | Server::new(store).run(opt.addr)?;
71 |
72 | Ok(())
73 | }
74 |
--------------------------------------------------------------------------------
/tests/store.rs:
--------------------------------------------------------------------------------
1 | use tempfile::TempDir;
2 | use tinkv::{self, Result, Store};
3 |
4 | #[test]
5 | fn get_stored_value() -> Result<()> {
6 | let tmpdir = TempDir::new().expect("unable to create tmp dir");
7 | let mut store = Store::open(&tmpdir.path())?;
8 |
9 | store.set(b"version", b"1.0")?;
10 | store.set(b"name", b"tinkv")?;
11 |
12 | assert_eq!(store.get(b"version")?, Some(b"1.0".to_vec()));
13 | assert_eq!(store.get(b"name")?, Some(b"tinkv".to_vec()));
14 | assert_eq!(store.len(), 2);
15 |
16 | store.close()?;
17 |
18 | // open again, check persisted data.
19 | let mut store = Store::open(&tmpdir.path())?;
20 | assert_eq!(store.get(b"version")?, Some(b"1.0".to_vec()));
21 | assert_eq!(store.get(b"name")?, Some(b"tinkv".to_vec()));
22 | assert_eq!(store.len(), 2);
23 |
24 | Ok(())
25 | }
26 |
27 | #[test]
28 | fn overwrite_value() -> Result<()> {
29 | let tmpdir = TempDir::new().expect("unable to create tmp dir");
30 | let mut store = Store::open(&tmpdir.path())?;
31 |
32 | store.set(b"version", b"1.0")?;
33 | assert_eq!(store.get(b"version")?, Some(b"1.0".to_vec()));
34 |
35 | store.set(b"version", b"2.0")?;
36 | assert_eq!(store.get(b"version")?, Some(b"2.0".to_vec()));
37 |
38 | store.close()?;
39 |
40 | // open again and check data
41 | let mut store = Store::open(&tmpdir.path())?;
42 | assert_eq!(store.get(b"version")?, Some(b"2.0".to_vec()));
43 |
44 | Ok(())
45 | }
46 |
47 | #[test]
48 | fn get_non_existent_key() -> Result<()> {
49 | let tmpdir = TempDir::new().expect("unable to create tmp dir");
50 | let mut store = Store::open(&tmpdir.path())?;
51 |
52 | store.set(b"version", b"1.0")?;
53 | assert_eq!(store.get(b"version_foo")?, None);
54 | store.close()?;
55 |
56 | let mut store = Store::open(&tmpdir.path())?;
57 | assert_eq!(store.get(b"version_foo")?, None);
58 |
59 | Ok(())
60 | }
61 |
62 | #[test]
63 | fn remove_key() -> Result<()> {
64 | let tmpdir = TempDir::new().expect("unable to create tmp dir");
65 | let mut store = Store::open(&tmpdir.path())?;
66 |
67 | store.set(b"version", b"1.0")?;
68 | assert!(store.remove(b"version").is_ok());
69 | assert_eq!(store.get(b"version")?, None);
70 |
71 | Ok(())
72 | }
73 |
74 | #[test]
75 | fn remove_non_existent_key() -> Result<()> {
76 | let tmpdir = TempDir::new().expect("unable to create tmp dir");
77 | let mut store = Store::open(&tmpdir.path())?;
78 |
79 | assert!(store.remove(b"version").is_err());
80 |
81 | Ok(())
82 | }
83 |
84 | #[test]
85 | fn compaction() -> Result<()> {
86 | let tmpdir = TempDir::new().expect("unable to create tmp dir");
87 | let mut store = Store::open(&tmpdir.path())?;
88 |
89 | for it in 0..100 {
90 | for id in 0..1000 {
91 | let k = format!("key_{}", id);
92 | let v = format!("value_{}", it);
93 | store.set(k.as_bytes(), v.as_bytes())?;
94 | }
95 |
96 | let stats = store.stats();
97 | if stats.total_stale_entries <= 10000 {
98 | continue;
99 | }
100 |
101 | // trigger compaction
102 | store.compact()?;
103 |
104 | let stats = store.stats();
105 | assert_eq!(stats.size_of_stale_entries, 0);
106 | assert_eq!(stats.total_stale_entries, 0);
107 | assert_eq!(stats.total_active_entries, 1000);
108 |
109 | // close and reopen, chack persisted data
110 | store.close()?;
111 |
112 | store = Store::open(&tmpdir.path())?;
113 |
114 | let stats = store.stats();
115 | assert_eq!(stats.size_of_stale_entries, 0);
116 | assert_eq!(stats.total_stale_entries, 0);
117 | assert_eq!(stats.total_active_entries, 1000);
118 |
119 | for id in 0..1000 {
120 | let k = format!("key_{}", id);
121 | assert_eq!(
122 | store.get(k.as_bytes())?,
123 | Some(format!("value_{}", it).as_bytes().to_vec())
124 | );
125 | }
126 | }
127 |
128 | Ok(())
129 | }
130 |
--------------------------------------------------------------------------------
/benches/store_benchmark.rs:
--------------------------------------------------------------------------------
1 | use criterion::{criterion_group, criterion_main, BatchSize, Criterion, ParameterizedBenchmark};
2 | use rand::prelude::*;
3 |
4 | use sled::{Db, Tree};
5 | use std::iter;
6 | use std::path::Path;
7 | use tempfile::TempDir;
8 | use tinkv::{self, Result, Store, TinkvError};
9 |
10 | #[derive(Clone)]
11 | pub struct SledStore(Db);
12 |
13 | impl SledStore {
14 | fn open>(path: P) -> Self {
15 | let tree = sled::open(path).expect("failed to open db");
16 | SledStore(tree)
17 | }
18 |
19 | fn set(&mut self, key: String, value: String) -> Result<()> {
20 | let tree: &Tree = &self.0;
21 | tree.insert(key, value.into_bytes())
22 | .map(|_| ())
23 | .map_err(|e| TinkvError::Custom(format!("{}", e)))?;
24 | tree.flush()
25 | .map_err(|e| TinkvError::Custom(format!("{}", e)))?;
26 | Ok(())
27 | }
28 |
29 | fn get(&mut self, key: String) -> Result