├── .gitignore
├── test_data
├── test.jpg
├── test.pdf
├── test.png
└── test.html
├── src
├── lib.rs
├── bin
│ └── segment.rs
├── config.rs
├── server.rs
├── db.rs
├── command
│ ├── mod.rs
│ └── test.rs
├── connection.rs
└── frame.rs
├── Cargo.toml
├── .github
└── workflows
│ └── build.yml
├── segment.conf
├── LICENSE
├── docs
└── protocol.v1.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 | .DS_Store
--------------------------------------------------------------------------------
/test_data/test.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thetinygoat/segment/HEAD/test_data/test.jpg
--------------------------------------------------------------------------------
/test_data/test.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thetinygoat/segment/HEAD/test_data/test.pdf
--------------------------------------------------------------------------------
/test_data/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thetinygoat/segment/HEAD/test_data/test.png
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | mod command;
2 | pub mod config;
3 | mod connection;
4 | mod db;
5 | mod frame;
6 | pub mod server;
7 |
--------------------------------------------------------------------------------
/test_data/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 | This is a test html doc for segment binary safety test
11 |
12 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "segment"
3 | version = "0.0.1"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | tokio = { version = "1", features = ["rt-multi-thread", "time", "macros", "sync", "signal", "net", "io-util"] }
10 | anyhow = "1.0.66"
11 | thiserror = "1.0.37"
12 | clap = { version = "4.0.18", features = ["derive"] }
13 | tokio-util = "0.7.4"
14 | crossbeam = "0.8.2"
15 | bytes = "1.2.1"
16 | tracing = "0.1.37"
17 | tracing-subscriber = "0.3.16"
18 | atoi = "2.0.0"
19 | parking_lot = "0.12.1"
20 | tokio-test = "0.4.2"
21 | async-recursion = "1.0.0"
22 | sysinfo = "0.26.8"
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [ "dev" ]
6 | pull_request:
7 | branches: [ "dev" ]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 |
12 | jobs:
13 | build-linux:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Build
20 | run: cargo build --verbose
21 | - name: Run tests
22 | run: cargo test --verbose
23 |
24 | build-macos:
25 |
26 | runs-on: macos-12
27 |
28 | steps:
29 | - uses: actions/checkout@v3
30 | - name: Build
31 | run: cargo build --verbose
32 | - name: Run tests
33 | run: cargo test --verbose
34 |
--------------------------------------------------------------------------------
/segment.conf:
--------------------------------------------------------------------------------
1 | port=1698
2 |
3 | # specifies the max memory that is availabe to the server. Once the server reaches
4 | # this memory limit the server will start evicting keys according to the max memory
5 | # policy configured for that keyspace. Only two units of memory are supported which are *mb* and *gb*.
6 | # If you want the server to not have any memory limit set this as 0 (0mb or 0gb).
7 | # Examples:
8 | # max_memory=200gb
9 | # max_memory=100mb
10 | # max_memory=0mb
11 | max_memory=512mb
12 |
13 | # connection buffer size is the size of the connection buffer in *bytes* which is used to read
14 | # data from the socket. You can tune acording to size of data that you expect. A larger buffer size will
15 | # use more memory. Only change this if you know what you are doing
16 | connection_buffer_size=4096
17 |
18 | # bind tells the segment server which interface to listen on
19 | bind=127.0.0.1
20 |
--------------------------------------------------------------------------------
/src/bin/segment.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use clap::Parser;
3 | use segment::config::ServerConfig;
4 | use segment::server;
5 | use tokio::net::TcpListener;
6 | use tracing::Level;
7 |
8 | #[derive(Debug, Parser)]
9 | struct Args {
10 | /// path to segment config file
11 | #[arg(long, default_value = "segment.conf")]
12 | config: String,
13 |
14 | /// start the server in debug mode
15 | #[arg(long)]
16 | debug: bool,
17 | }
18 |
19 | #[tokio::main]
20 | async fn main() -> Result<()> {
21 | let args = Args::parse();
22 | let mut log_level = Level::INFO;
23 | if args.debug {
24 | log_level = Level::DEBUG;
25 | }
26 | let subscriber = tracing_subscriber::fmt().with_max_level(log_level).finish();
27 | tracing::subscriber::set_global_default(subscriber)?;
28 | let cfg = ServerConfig::load_from_disk(&args.config)?;
29 | let ln = TcpListener::bind(format!("{}:{}", cfg.bind(), cfg.port())).await?;
30 | server::start(ln, cfg).await?;
31 | Ok(())
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Sachin Saini
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.
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io::{self, BufRead, BufReader};
3 | use std::net::{AddrParseError, IpAddr, Ipv4Addr};
4 | use std::num::ParseIntError;
5 | use std::str::FromStr;
6 | use thiserror::Error;
7 |
8 | const PORT_LABEL: &str = "port";
9 | const MAX_MEMORY_LABEL: &str = "max_memory";
10 | const CONNECTION_BUFFER_SIZE_LABEL: &str = "connection_buffer_size";
11 | const BIND_LABEL: &str = "bind";
12 |
13 | #[derive(Debug)]
14 | pub struct ServerConfig {
15 | port: u16,
16 | max_memory: u64,
17 | connection_buffer_size: usize,
18 | bind: IpAddr,
19 | }
20 |
21 | #[derive(Debug, Error)]
22 | pub enum ServerConfigError {
23 | #[error(transparent)]
24 | FileRead(#[from] io::Error),
25 |
26 | #[error("invalid config file format at '{0}'")]
27 | InvalidFormat(String),
28 |
29 | #[error("unknown directive '{0}' at '{1}'")]
30 | UnknownDirective(String, String),
31 |
32 | #[error(transparent)]
33 | ParseIntError(#[from] ParseIntError),
34 |
35 | #[error(transparent)]
36 | AddrParseError(#[from] AddrParseError),
37 | }
38 |
39 | impl ServerConfig {
40 | pub fn load_from_disk(path: &str) -> Result {
41 | let reader = BufReader::new(File::open(path)?);
42 | Self::parse(reader)
43 | }
44 |
45 | fn parse(reader: BufReader) -> Result {
46 | let mut config = ServerConfig {
47 | port: 1698,
48 | max_memory: 0,
49 | connection_buffer_size: 4096,
50 | bind: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
51 | };
52 | for maybe_line in reader.lines() {
53 | let line = &maybe_line?;
54 | if line.trim().starts_with('#') || line.trim().is_empty() {
55 | continue;
56 | }
57 |
58 | let tokens: Vec<&str> = line.split('=').map(|token| token.trim()).collect();
59 |
60 | if tokens.len() < 2 || tokens.len() > 2 {
61 | return Err(ServerConfigError::InvalidFormat(line.clone()));
62 | }
63 |
64 | match tokens[0] {
65 | PORT_LABEL => {
66 | let port = tokens[1].parse::()?;
67 | config.port = port;
68 | }
69 | MAX_MEMORY_LABEL => {
70 | if tokens[1].len() < 3 {
71 | return Err(ServerConfigError::InvalidFormat(line.clone()));
72 | }
73 | let unit = &tokens[1][tokens[1].len() - 2..];
74 | let memory = tokens[1][..tokens[1].len() - 2].parse::()?;
75 | match unit {
76 | "mb" => config.max_memory = memory * 1024 * 1024,
77 | "gb" => config.max_memory = memory * 1024 * 1024 * 1024,
78 | _ => {
79 | return Err(ServerConfigError::InvalidFormat(line.clone()));
80 | }
81 | }
82 | }
83 | CONNECTION_BUFFER_SIZE_LABEL => {
84 | let connection_buffer_size = tokens[1].parse::()?;
85 | config.connection_buffer_size = connection_buffer_size;
86 | }
87 | BIND_LABEL => {
88 | let bind = IpAddr::from_str(tokens[1])?;
89 | config.bind = bind
90 | }
91 | _ => {
92 | return Err(ServerConfigError::UnknownDirective(
93 | tokens[0].to_string(),
94 | line.clone(),
95 | ))
96 | }
97 | }
98 | }
99 |
100 | Ok(config)
101 | }
102 |
103 | pub fn port(&self) -> u16 {
104 | self.port
105 | }
106 |
107 | pub fn max_memory(&self) -> u64 {
108 | self.max_memory
109 | }
110 |
111 | pub fn connection_buffer_size(&self) -> usize {
112 | self.connection_buffer_size
113 | }
114 |
115 | pub fn bind(&self) -> String {
116 | self.bind.to_string()
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/docs/protocol.v1.md:
--------------------------------------------------------------------------------
1 | # Segment Wire Protocol
2 |
3 | This document is the specification for the Segment wire protocol.
4 |
5 | ## Introduction
6 |
7 | Segment has a text based TCP protocol that is used for client-server architecture. It is inspired by [RESP](https://redis.io/docs/reference/protocol-spec/) and [Memcached](https://github.com/memcached/memcached/blob/master/doc/protocol.txt). The protocol aims to be _human readable_, _fast to parse_ and _simple to implement_.
8 |
9 | ## Request-Response Model
10 |
11 | The client connects to the Segment server on port 1698 by default. The client sends a command which is made up of arguments and flags, on receiving a request the server sends back a reply. This is a traditional client-server architecture.
12 |
13 | ## Protocol Specification
14 |
15 | ### Data Types
16 |
17 | The protocol supports 8 data types:
18 |
19 | - [String](#strings)
20 | - [Integer](#integers)
21 | - [Double](#doubles)
22 | - [Boolean](#booleans)
23 | - [Null](#null)
24 | - [Error](#errors)
25 | - [Array](#arrays)
26 | - [Map](#maps)
27 |
28 | The first byte determines the data type.
29 |
30 | - For **Strings** the first byte is `$`
31 | - For **Integers** the first byte is `%`
32 | - For **Doubles** the first byte is `.`
33 | - For **Booleans** the first byte is `^`
34 | - For **Null** the first byte is `-`
35 | - For **Errors** the first byte is `!`
36 | - For **Arrays** the first byte is `*`
37 | - For **Maps** the first byte is `#`
38 |
39 | The protocol uses CRLF (`\r\n`) as the delimiter.
40 |
41 | #### Strings
42 |
43 | Strings are encoded as follows: A `$` character followed by the length of the string followed by CRLF. After encoding the length, the actual data is appended followed by a CRLF.
44 |
45 | ```
46 | $11\r\nhello world\r\n
47 | ```
48 |
49 | Since the strings are prefixed with their lengths we don't need to search for any delimiter to mark the end of the string. This makes it fast to parse and it also makes the strings **binary safe**.
50 |
51 | #### Integers
52 |
53 | Integers are encoded as follows: A `%` character followed by the integer that we want to encode followed by CRLF.
54 |
55 | ```
56 | %100\r\n
57 | ```
58 |
59 | #### Doubles
60 |
61 | Doubles are similar to integers, they are encoded as follows: A `.` character followed by the double that we want to encode followed by CRLF.
62 |
63 | ```
64 | .26.3\r\n
65 | ```
66 |
67 | #### Booleans
68 |
69 | Booleans are encoded as follows: A `^` character followed by a `0` for false and a `1` for true followed by CRLF.
70 |
71 | ```
72 | ^0\r\n // false
73 |
74 | ^1\r\n // true
75 | ```
76 |
77 | #### Null
78 |
79 | Nulls are encoded as follows: A `-` character followed by CRLF.
80 |
81 | ```
82 | -\r\n
83 | ```
84 |
85 | #### Errors
86 |
87 | Errors are similar to strings and are encoded as follows: A `!` character followed by the length of the error data followed by CRLF. After encoding the length, the data is appended followed by CRLF
88 |
89 | ```
90 | !5\r\nerror\r\n
91 | ```
92 |
93 | #### Arrays
94 |
95 | Array is a container type, it can contain all the other data types. An array is encoded as follows: A `*` character followed by the number of items in the array followed By CRLF. After encoding the length of the array we can just encode any type into it. Arrays can contain different data types at once.
96 |
97 | ```
98 | // empty array
99 | *0\r\n
100 |
101 | // array containing one integer
102 | *1\r\n%100\r\n
103 |
104 | // array containing an integer and a string
105 | *2\r\n%100\r\n$5\r\nhello\r\n
106 | ```
107 |
108 | #### Maps
109 |
110 | A map is a hash map, it's similar to an array and is encoded as follows: A `#` character followed by the number of items in the map followed by CRLF. Please note that a key-value pair is considered as a single unit/item. After encoding the number of items we can encode any type as key and value. Even though a key can be of any type, segment will only send keys as strings.
111 |
112 | ```
113 | // empty map
114 | #0\r\n
115 |
116 | // map containing one key-value pair, with key being hello and value being world
117 | #1\r\n$5\r\nhello\r\n$5\r\nworld\r\n
118 | ```
119 |
120 | ## Sending Commands to a Segment Server
121 |
122 | Now that you are familiar with the wire protocol, you can use it to write a client to interact with a Segment server.
123 |
124 | - A client can send the command to a Segment server as an _Array of Strings_ only. Using any other data type to send the command will result in an error.
125 | - The server can respond with any of the above data type.
126 |
127 | For example, the create command will be encoded as follows:
128 |
129 | ```shell
130 | *2\r\n$6\r\nCREATE\r\n$3\r\nfoo\r\n
131 | ```
132 |
--------------------------------------------------------------------------------
/src/server.rs:
--------------------------------------------------------------------------------
1 | use crate::command;
2 | use crate::config::ServerConfig;
3 | use crate::connection::Connection;
4 | use crate::db::Db;
5 | use anyhow::Result;
6 | use crossbeam::sync::WaitGroup;
7 | use std::sync::Arc;
8 | use std::time::Duration;
9 | use sysinfo::{Pid, ProcessExt, System, SystemExt};
10 | use tokio::net::{TcpListener, TcpStream};
11 | use tokio::signal;
12 | use tokio::sync::broadcast;
13 | use tracing::{debug, error, info};
14 |
15 | struct Server {
16 | ln: TcpListener,
17 | cfg: ServerConfig,
18 | wg: WaitGroup,
19 | db: Arc,
20 | done_tx: broadcast::Sender<()>,
21 | evict_tx: broadcast::Sender<()>,
22 | }
23 |
24 | struct ConnectionHandler {
25 | connection: Connection,
26 | done: broadcast::Receiver<()>,
27 | db: Arc,
28 | }
29 |
30 | pub async fn start(ln: TcpListener, cfg: ServerConfig) -> Result<()> {
31 | let srv = Server::new(ln, cfg);
32 | srv.start().await
33 | }
34 |
35 | impl Server {
36 | pub fn new(ln: TcpListener, cfg: ServerConfig) -> Self {
37 | let wg = WaitGroup::new();
38 | let (done_tx, _) = broadcast::channel(1);
39 | let (evict_tx, _) = broadcast::channel(1);
40 | let db = Db::new(done_tx.subscribe(), wg.clone(), evict_tx.subscribe());
41 | Server {
42 | ln,
43 | cfg,
44 | wg,
45 | done_tx,
46 | db: Arc::new(db),
47 | evict_tx,
48 | }
49 | }
50 |
51 | pub async fn start(self) -> Result<()> {
52 | info!(
53 | "server started on port {}:{}",
54 | self.cfg.bind(),
55 | self.cfg.port()
56 | );
57 | let monitor_wg = self.wg.clone();
58 | let mut monitor_done_rx = self.done_tx.subscribe();
59 | let monitor_evict_tx = self.evict_tx.clone();
60 | let server_max_memory = self.cfg.max_memory();
61 | // FIXME: move this to a separate fn
62 | tokio::spawn(async move {
63 | let pid = std::process::id() as i32;
64 | let mut monitor = System::new();
65 | monitor.refresh_process(Pid::from(pid));
66 | loop {
67 | tokio::select! {
68 | _ = monitor_done_rx.recv() => {
69 | debug!("stopping system monitor, shutdown signal received");
70 | break;
71 | }
72 | _ = tokio::time::sleep(Duration::from_millis(1000)) => {
73 | monitor.refresh_process(Pid::from(pid));
74 | if let Some(process) = monitor.process(Pid::from(pid)) {
75 | let memory = process.memory();
76 | if memory >= server_max_memory && server_max_memory > 0 {
77 | debug!("broadcasting evict event, server max memory (bytes) = {}, current memory usage (bytes) = {}", server_max_memory, memory);
78 | if let Err(err) = monitor_evict_tx.send(()) {
79 | error!("no listeners available for max memory eviction event, error = {:?}", err);
80 | }
81 | }
82 | }else {
83 | error!("no process found with pid {}, max memory evictors will not work", pid);
84 | break;
85 | }
86 | }
87 | }
88 | }
89 | drop(monitor_wg)
90 | });
91 | loop {
92 | tokio::select! {
93 | maybe_connection = self.ln.accept() => {
94 | let (stream, _) = maybe_connection?;
95 | let mut handler = ConnectionHandler::new(self.done_tx.subscribe(), stream, self.cfg.connection_buffer_size(), self.db.clone());
96 | let wg = self.wg.clone();
97 | tokio::spawn(async move {
98 | if let Err(e) = handler.handle().await {
99 | error!("{}", e)
100 | }
101 | drop(wg);
102 | });
103 | }
104 | _ = signal::ctrl_c() => {
105 | info!("shutdown signal received");
106 | drop(self.ln);
107 | drop(self.done_tx);
108 | break;
109 | }
110 | }
111 | }
112 | drop(self.db);
113 | self.wg.wait();
114 | info!("shutdown complete, bye bye :)");
115 | Ok(())
116 | }
117 | }
118 |
119 | impl ConnectionHandler {
120 | pub fn new(
121 | done: broadcast::Receiver<()>,
122 | stream: TcpStream,
123 | connection_buf_size: usize,
124 | db: Arc,
125 | ) -> Self {
126 | let connection = Connection::new(stream, connection_buf_size);
127 | ConnectionHandler {
128 | connection,
129 | done,
130 | db,
131 | }
132 | }
133 |
134 | pub async fn handle(&mut self) -> Result<()> {
135 | debug!("new connection started");
136 | loop {
137 | let maybe_frame = tokio::select! {
138 | _ = self.done.recv() => {
139 | break;
140 | }
141 | res = self.connection.read_frame() => res?,
142 | };
143 |
144 | let frame = match maybe_frame {
145 | Some(frame) => frame,
146 | None => return Ok(()),
147 | };
148 |
149 | let maybe_cmd = match command::parse(frame) {
150 | Ok(cmd) => Some(cmd),
151 | Err(e) => {
152 | self.connection.write_error(e).await?;
153 | None
154 | }
155 | };
156 |
157 | let cmd = match maybe_cmd {
158 | Some(cmd) => cmd,
159 | None => continue,
160 | };
161 |
162 | let maybe_result = match self.db.execute(cmd).await {
163 | Ok(frame) => Some(frame),
164 | Err(e) => {
165 | self.connection.write_error(e).await?;
166 | None
167 | }
168 | };
169 |
170 | match maybe_result {
171 | Some(frame) => self.connection.write_frame(&frame).await?,
172 |
173 | None => continue,
174 | }
175 | }
176 | Ok(())
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Segment
2 |
3 |
4 |
5 | 
6 |
7 | Segment is a _simple_ and _fast_ in-memory key-value database written in Rust.
8 |
9 | ### Features
10 |
11 | - Simple to use and understand.
12 | - Keys can be separated into multiple dynamic keyspaces.
13 | - Keyspace level configuration.
14 |
15 | ### Why Segment?
16 |
17 | Segment's goal to is to provide a simpler and more intuitive in-memory key-value solution. It has certain features that other solutions don't. Let's go over them one by one.
18 |
19 | #### Keyspaces
20 |
21 | Segment has a concept of keyspaces. You can think of keyspaces like tables in a relational database, except that keyspaces don't have any schema. When the segment server starts there are no keyspaces and you can create as many as you like.
22 |
23 | #### Evictors
24 |
25 | Separating keys into keyspaces comes with a benefit, we can now have keyspace level configurations. Evictors are one such configuration.
26 | There are two types of evicros in Segment, **expiring evictors** and **max memory evictors**.
27 |
28 | ##### Expiring Evictors
29 |
30 | The expiring evictor is responsible for evicting expired keys which runs for every keyspaces.
31 |
32 | ##### Max Memory Evictors
33 |
34 | The second type of evictor is max memory evictor, which is responsible for evicting keys when the server reaches the max memory specified in `segment.conf`.
35 | Currently there are 3 max memory evictors:
36 |
37 | - Nop - Stands for no-operation which doesn't evict any keys.
38 | - Random - Evicts keys in a random order.
39 | - LRU - Evicts keys in a LRU fashion.
40 |
41 | There are plans to include even more evictors out of the box in future.
42 |
43 | Max memory evitors can configured at a keyspace level, which means that you can have a keyspace that does not evict at all while some keyspaces evict.
44 | This is powerful becuase now you don't have to spin up a separate server just because you want to have a separate eviction policy.
45 |
46 | #### Multithreaded
47 |
48 | Segment is multithreaded, which means it uses locks which can be a deal breaker for some use cases. But It works for most use cases and that's what segment is aiming for.
49 |
50 | #### Ease of Use
51 |
52 | Segment aims to be easy to use and intuitive. One way we are aiming to solve this is by having only one way of doing things. There is only one command to insert data and one way to get it back, this helps reduce the stuff that a developer needs to remember.
53 |
54 | One more thing that we are doing is using simple commands, for example let's take a look at the command to insert some data in a keyspace.
55 |
56 | ```shell
57 | SET my_keyspace my_key my_value IF NOT EXISTS EXPIRE AFTER 60000
58 | ```
59 |
60 | This commnd tells the segment server to insert the key `my_key` with value `my_value` into the keyspace `my_keyspace` if the key does not exist already and expire the key after 1 minute (60000ms).
61 |
62 | The command reads like english and the intent is clear
63 |
64 | A similar command in redis would look like this.
65 |
66 | ```
67 | SET my_key my_value NX EX 60
68 | ```
69 |
70 | If you are not familiar with redis you will not understand what is happeining here. and if you want to have your ttl in milliseconds there is another flag for that and I don't even remember what it is called and that's the point, to reduce the dev effort.
71 |
72 | ### Installation
73 |
74 | Segment is built using Rust, so you will need rust and it's toolchain installed on your system. To install rust you can follow the steps [here](https://rustup.rs/).
75 |
76 | After installing you can follow the steps below to build and run segment from source.
77 |
78 | 1. Clone this repository.
79 | 2. `cd /path/to/cloned/repo`
80 | 3. `cargo build --release`
81 | 4. The final binary can be found in `./target/release`
82 |
83 | ### Running the sever
84 |
85 | After building you will find the `segment` binary in the `./target/release` directory.
86 |
87 | Segment requires `segment.conf` file to start. `segment.conf` is the config file that contains several server configurations.
88 |
89 | If you have the `segment.conf` file in the same directory as the segment binary you can just run the binary however, if the config file is in some separate directory you can start the Segment server using the command below
90 |
91 | ```shell
92 | segment --config=/path/to/segment.conf
93 | ```
94 |
95 | If the server is started successfully you will see a log similar to this in your terminal.
96 |
97 | ```shell
98 | 2022-10-29T07:23:05.308471Z INFO segment::server: server started on port 1698
99 | ```
100 |
101 | ### Client Libraries
102 |
103 | - [Node.js](https://github.com/segment-dev/segment-node)
104 | - [Rust (Alpha)](https://github.com/segment-dev/segment-rs)
105 |
106 | ### List of Commands
107 |
108 | #### `CREATE`
109 |
110 | ##### Description
111 |
112 | Used to create a new keyspace. By defualt it doesn't take any arguments except the name of the keyspace, but you can specify the evictor you want to use for the keyspace.
113 |
114 | ##### Essential Arguments
115 |
116 | - `` - Name of the keyspace that you want to create.
117 |
118 | ##### Optional Arguments
119 |
120 | - `EVICTOR` - Indicates the evictor that you want to use for the keyspace. Possible values include `NOP`, `RANDOM` and `LRU`.
121 |
122 | ##### Optional Flags
123 |
124 | - `IF NOT EXISTS` - If a keyspace already exists and you try to create it again the server will throw an error, but if you don't want an error you can pass this flag with the create command.
125 |
126 | ##### Return Type
127 |
128 | The return type can be a boolean or an error.
129 |
130 | ##### Examples
131 |
132 | ```shell
133 | CREATE my_keyspace
134 | ```
135 |
136 | ```shell
137 | CREATE my_keyspace EVICTOR LRU
138 | ```
139 |
140 | ```shell
141 | CREATE my_keyspace EVICTOR LRU IF NOT EXISTS
142 | ```
143 |
144 | #### `DROP`
145 |
146 | ##### Description
147 |
148 | Used to drop a keyspace.
149 |
150 | ##### Essential Arguments
151 |
152 | - `` - Name of the keyspace that you want to drop.
153 |
154 | ##### Optional Flags
155 |
156 | - `IF EXISTS` - If a keyspace doesn't already exists and you try to drop it the server will throw an error, but if you don't want an error you can pass this flag with the drop command.
157 |
158 | ##### Return Type
159 |
160 | The return type can be a boolean or an error.
161 |
162 | ##### Examples
163 |
164 | ```shell
165 | DROP my_keyspace
166 | ```
167 |
168 | ```shell
169 | DROP my_keyspace IF EXISTS
170 | ```
171 |
172 | #### `SET`
173 |
174 | ##### Description
175 |
176 | Used to insert a value in the keyspace.
177 |
178 | ##### Essential Arguments
179 |
180 | - `` - Name of the keyspace that you want to create.
181 | - `` - Key that you want to insert.
182 | - `` - Value for the key.
183 |
184 | ##### Optional Arguments
185 |
186 | - `EXPIRE AFTER` - Expiry time of the key in milliseconds after which it will expire.
187 | - `EXPIRE AT` - Unix timestamp after which the key will expire.
188 |
189 | ##### Optional Flags
190 |
191 | - `IF NOT EXISTS` - If you want to set a key only if it does not already exists.
192 | - `IF EXISTS` - If you want to set a key only if it already exists.
193 |
194 | ##### Return Type
195 |
196 | The return type can be a boolean or an error.
197 |
198 | ##### Examples
199 |
200 | ```shell
201 | SET my_keyspace my_key my_value
202 | ```
203 |
204 | ```shell
205 | SET my_keyspace my_key my_value IF NOT EXISTS
206 | ```
207 |
208 | ```shell
209 | SET my_keyspace my_key my_value IF EXISTS
210 | ```
211 |
212 | ```shell
213 | SET my_keyspace my_key my_value EXPIRE AFTER 60000
214 | ```
215 |
216 | ```shell
217 | SET my_keyspace my_key my_value EXPIRE AT 1667041052
218 | ```
219 |
220 | #### `GET`
221 |
222 | ##### Description
223 |
224 | Used to get a key from the keyspace.
225 |
226 | ##### Essential Arguments
227 |
228 | - `` - Name of the keyspace that you want to get the key from.
229 | - `` - key that you want to get.
230 |
231 | ##### Return Type
232 |
233 | The return type can be a string, null, or error.
234 |
235 | ##### Examples
236 |
237 | ```shell
238 | GET my_keyspace my_key
239 | ```
240 |
241 | #### `DEL`
242 |
243 | ##### Description
244 |
245 | Used to delete a key from the keyspace.
246 |
247 | ##### Essential Arguments
248 |
249 | - `` - Name of the keyspace that you want to create.
250 | - `` - Name of the keyspace that you want to create.
251 |
252 | ##### Return Type
253 |
254 | The return type can be a boolean or error.
255 |
256 | ##### Examples
257 |
258 | ```shell
259 | DEL my_keyspace my_key
260 | ```
261 |
262 | #### `COUNT`
263 |
264 | ##### Description
265 |
266 | Returns the number of keys in a keyspace.
267 |
268 | ##### Essential Arguments
269 |
270 | - `` - Name of the keyspace.
271 |
272 | ##### Return Type
273 |
274 | The return type can be an integer or error.
275 |
276 | ##### Examples
277 |
278 | ```shell
279 | COUNT my_keyspace
280 | ```
281 |
282 | #### `TTL`
283 |
284 | ##### Description
285 |
286 | Returns the remaining TTL of the key in milliseconds.
287 |
288 | ##### Essential Arguments
289 |
290 | - `` - Name of the keyspace.
291 | - `` - Name of the key.
292 |
293 | ##### Return Type
294 |
295 | The return type can be an integer or null (if the key doesn't have an expiry or is already expired) or an error.
296 |
297 | ##### Examples
298 |
299 | ```shell
300 | TTL my_keyspace my_key
301 | ```
302 |
303 | #### `PING`
304 |
305 | ##### Description
306 |
307 | Used to ping the server.
308 |
309 | ##### Return Type
310 |
311 | The return type is the string `pong`.
312 |
313 | ##### Examples
314 |
315 | ```shell
316 | PING
317 | ```
318 |
319 | #### `KEYSPACES`
320 |
321 | ##### Description
322 |
323 | Returns the list of keyspaces.
324 |
325 | ##### Return Type
326 |
327 | The return type is an array of strings.
328 |
329 | ##### Examples
330 |
331 | ```shell
332 | KEYSPACES
333 | ```
334 |
--------------------------------------------------------------------------------
/src/db.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | command::{Command, Count, Create, Del, Drop, Get, Set, Ttl},
3 | connection::ConnectionError,
4 | frame::Frame,
5 | };
6 | use bytes::Bytes;
7 | use crossbeam::sync::WaitGroup;
8 | use parking_lot::{Mutex, RwLock};
9 | use std::{
10 | collections::HashMap,
11 | str::{self, Utf8Error},
12 | time::Duration,
13 | };
14 | use std::{
15 | sync::Arc,
16 | time::{Instant, SystemTime, SystemTimeError, UNIX_EPOCH},
17 | };
18 | use thiserror::Error;
19 | use tokio::sync::broadcast;
20 | use tokio::time;
21 | use tracing::{debug, error};
22 |
23 | static EXPIRING_EVICTOR_SAMPLE_SIZE: u8 = 5;
24 | static MAX_MEMORY_EVICTOR_SAMPLE_SIZE: u8 = 3;
25 |
26 | #[derive(Debug)]
27 | pub struct Value {
28 | data: Bytes,
29 | last_accessed: Instant,
30 | expire_at: Option,
31 | }
32 |
33 | #[derive(Debug, Clone, Copy, PartialEq)]
34 | pub enum Evictor {
35 | Nop,
36 | Random,
37 | Lru,
38 | }
39 |
40 | #[derive(Debug)]
41 | pub struct Keyspace {
42 | store: Arc>>,
43 | expiring: Arc>>,
44 | evictor: Evictor,
45 | wg: WaitGroup,
46 | done: broadcast::Receiver<()>,
47 | drop: broadcast::Sender<()>,
48 | evict: broadcast::Receiver<()>,
49 | }
50 |
51 | #[derive(Debug)]
52 | pub struct Db {
53 | keyspaces: RwLock>,
54 | done: broadcast::Receiver<()>,
55 | wg: WaitGroup,
56 | evict: broadcast::Receiver<()>,
57 | }
58 |
59 | #[derive(Debug, Error)]
60 | pub enum ExecuteCommandError {
61 | #[error(transparent)]
62 | ConnectionError(#[from] ConnectionError),
63 |
64 | #[error("keyspace '{0}' already exists")]
65 | KeyspaceExists(String),
66 |
67 | #[error("keyspace '{0}' does not exist")]
68 | KeyspaceDoesNotExist(String),
69 |
70 | #[error(transparent)]
71 | Utf8Error(#[from] Utf8Error),
72 |
73 | #[error(transparent)]
74 | SystemTimeError(#[from] SystemTimeError),
75 | }
76 |
77 | impl Db {
78 | pub fn new(
79 | done: broadcast::Receiver<()>,
80 | wg: WaitGroup,
81 | evict: broadcast::Receiver<()>,
82 | ) -> Self {
83 | Db {
84 | keyspaces: RwLock::new(HashMap::new()),
85 | done,
86 | wg,
87 | evict,
88 | }
89 | }
90 |
91 | pub async fn execute(&self, command: Command) -> Result {
92 | match command {
93 | Command::Create(cmd) => self.exec_create(&cmd).await,
94 | Command::Drop(cmd) => self.exec_drop(&cmd),
95 | Command::Keyspaces => self.exec_keyspaces(),
96 | Command::Set(cmd) => self.exec_set(&cmd),
97 | Command::Ping => Ok(Frame::String(Bytes::from_static(b"PONG"))),
98 | Command::Get(cmd) => self.exec_get(&cmd),
99 | Command::Del(cmd) => self.exec_del(&cmd),
100 | Command::Count(cmd) => self.exec_count(&cmd),
101 | Command::Ttl(cmd) => self.exec_ttl(&cmd),
102 | }
103 | }
104 |
105 | async fn exec_create(&self, cmd: &Create) -> Result {
106 | let mut handle = self.keyspaces.write();
107 | if handle.contains_key(&cmd.keyspace()) {
108 | if cmd.if_not_exists() {
109 | return Ok(Frame::Boolean(false));
110 | } else {
111 | return Err(ExecuteCommandError::KeyspaceExists(
112 | str::from_utf8(&cmd.keyspace()[..])?.to_string(),
113 | ));
114 | }
115 | }
116 |
117 | let ks = Keyspace::new(
118 | self.done.resubscribe(),
119 | self.wg.clone(),
120 | cmd.evictor(),
121 | self.evict.resubscribe(),
122 | );
123 |
124 | ks.start_expiring_evictor();
125 | ks.start_max_memory_evictor();
126 |
127 | handle.insert(cmd.keyspace(), ks);
128 |
129 | Ok(Frame::Boolean(true))
130 | }
131 |
132 | fn exec_drop(&self, cmd: &Drop) -> Result {
133 | let mut handle = self.keyspaces.write();
134 | if !handle.contains_key(&cmd.keyspace()) {
135 | if cmd.if_exists() {
136 | return Ok(Frame::Boolean(false));
137 | } else {
138 | return Err(ExecuteCommandError::KeyspaceDoesNotExist(
139 | str::from_utf8(&cmd.keyspace()[..])?.to_string(),
140 | ));
141 | }
142 | }
143 | handle.remove(&cmd.keyspace());
144 | Ok(Frame::Boolean(true))
145 | }
146 |
147 | fn exec_keyspaces(&self) -> Result {
148 | let handle = self.keyspaces.read();
149 | let mut keyspaces = Vec::with_capacity(handle.keys().count());
150 | for key in handle.keys() {
151 | if let Some(keyspace) = handle.get(key) {
152 | let mut map = Vec::with_capacity(2);
153 | let name = Frame::String(key.clone());
154 | let evictor = Frame::String(Bytes::copy_from_slice(keyspace.evictor().as_bytes()));
155 | map.push(Frame::String(Bytes::from_static(b"name")));
156 | map.push(name);
157 | map.push(Frame::String(Bytes::from_static(b"evictor")));
158 | map.push(evictor);
159 | keyspaces.push(Frame::Map(map))
160 | } else {
161 | continue;
162 | }
163 | }
164 | Ok(Frame::Array(keyspaces))
165 | }
166 |
167 | fn exec_set(&self, cmd: &Set) -> Result {
168 | let handle = self.keyspaces.read();
169 | let keyspace = handle.get(&cmd.keyspace());
170 | if let Some(ks) = keyspace {
171 | if cmd.if_exists() || cmd.if_not_exists() {
172 | if cmd.if_exists() {
173 | return ks.set_if_exists(cmd.key(), cmd.value(), cmd.expire_at());
174 | } else {
175 | return ks.set_if_not_exists(cmd.key(), cmd.value(), cmd.expire_at());
176 | }
177 | } else {
178 | return ks.set(cmd.key(), cmd.value(), cmd.expire_at());
179 | }
180 | }
181 |
182 | Err(ExecuteCommandError::KeyspaceDoesNotExist(
183 | str::from_utf8(&cmd.keyspace()[..])?.to_string(),
184 | ))
185 | }
186 |
187 | fn exec_get(&self, cmd: &Get) -> Result {
188 | let handle = self.keyspaces.read();
189 | let keyspace = handle.get(&cmd.keyspace());
190 | if let Some(ks) = keyspace {
191 | return ks.get(cmd.key());
192 | }
193 |
194 | Err(ExecuteCommandError::KeyspaceDoesNotExist(
195 | str::from_utf8(&cmd.keyspace()[..])?.to_string(),
196 | ))
197 | }
198 |
199 | fn exec_del(&self, cmd: &Del) -> Result {
200 | let handle = self.keyspaces.read();
201 | let keyspace = handle.get(&cmd.keyspace());
202 | if let Some(ks) = keyspace {
203 | return ks.del(cmd.key());
204 | }
205 |
206 | Err(ExecuteCommandError::KeyspaceDoesNotExist(
207 | str::from_utf8(&cmd.keyspace()[..])?.to_string(),
208 | ))
209 | }
210 |
211 | fn exec_count(&self, cmd: &Count) -> Result {
212 | let handle = self.keyspaces.read();
213 | let keyspace = handle.get(&cmd.keyspace());
214 | if let Some(ks) = keyspace {
215 | return ks.count();
216 | }
217 |
218 | Err(ExecuteCommandError::KeyspaceDoesNotExist(
219 | str::from_utf8(&cmd.keyspace()[..])?.to_string(),
220 | ))
221 | }
222 |
223 | fn exec_ttl(&self, cmd: &Ttl) -> Result {
224 | let handle = self.keyspaces.read();
225 | let keyspace = handle.get(&cmd.keyspace());
226 | if let Some(ks) = keyspace {
227 | return ks.ttl(cmd.key());
228 | }
229 |
230 | Err(ExecuteCommandError::KeyspaceDoesNotExist(
231 | str::from_utf8(&cmd.keyspace()[..])?.to_string(),
232 | ))
233 | }
234 | }
235 |
236 | impl Keyspace {
237 | pub fn new(
238 | done: broadcast::Receiver<()>,
239 | wg: WaitGroup,
240 | evictor: Evictor,
241 | evict: broadcast::Receiver<()>,
242 | ) -> Self {
243 | let (drop_tx, _) = broadcast::channel(1);
244 | Keyspace {
245 | store: Arc::new(Mutex::new(HashMap::new())),
246 | expiring: Arc::new(Mutex::new(HashMap::new())),
247 | evictor,
248 | done,
249 | wg,
250 | drop: drop_tx,
251 | evict,
252 | }
253 | }
254 | pub fn set_if_not_exists(
255 | &self,
256 | key: Bytes,
257 | value: Bytes,
258 | expire_at: Option,
259 | ) -> Result {
260 | let handle = self.store.lock();
261 | let val = handle.get(&key);
262 | if val.is_some() {
263 | return Ok(Frame::Boolean(false));
264 | }
265 | drop(handle);
266 | self.set(key, value, expire_at)
267 | }
268 |
269 | pub fn set_if_exists(
270 | &self,
271 | key: Bytes,
272 | value: Bytes,
273 | expire_at: Option,
274 | ) -> Result {
275 | let handle = self.store.lock();
276 | let val = handle.get(&key);
277 | if val.is_none() {
278 | return Ok(Frame::Boolean(false));
279 | }
280 | drop(handle);
281 | self.set(key, value, expire_at)
282 | }
283 |
284 | pub fn set(
285 | &self,
286 | key: Bytes,
287 | value: Bytes,
288 | expire_at: Option,
289 | ) -> Result {
290 | let mut handle = self.store.lock();
291 | let value = Value::new(value, expire_at);
292 | handle.insert(key.clone(), value);
293 | if let Some(expiry) = expire_at {
294 | let mut expring_handle = self.expiring.lock();
295 | expring_handle.insert(key, expiry);
296 | }
297 | Ok(Frame::Boolean(true))
298 | }
299 |
300 | pub fn get(&self, key: Bytes) -> Result {
301 | let mut handle = self.store.lock();
302 | if let Some(val) = handle.get_mut(&key) {
303 | val.touch();
304 | if let Some(expiry) = val.expire_at() {
305 | let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
306 | if expiry < current_time {
307 | handle.remove(&key);
308 | return Ok(Frame::Null);
309 | }
310 | }
311 | return Ok(Frame::String(val.data()));
312 | }
313 | Ok(Frame::Null)
314 | }
315 |
316 | pub fn del(&self, key: Bytes) -> Result {
317 | let mut handle = self.store.lock();
318 | let result = handle.remove(&key);
319 | Ok(Frame::Boolean(result.is_some()))
320 | }
321 |
322 | pub fn count(&self) -> Result {
323 | let handle = self.store.lock();
324 | let count = handle.iter().count();
325 | Ok(Frame::Integer(count as i64))
326 | }
327 |
328 | pub fn ttl(&self, key: Bytes) -> Result {
329 | let mut handle = self.store.lock();
330 | if let Some(val) = handle.get_mut(&key) {
331 | val.touch();
332 | if let Some(expiry) = val.expire_at() {
333 | let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
334 | if expiry <= current_time {
335 | handle.remove(&key);
336 | return Ok(Frame::Null);
337 | } else {
338 | return Ok(Frame::Integer(((expiry - current_time) * 1000) as i64));
339 | }
340 | }
341 | return Ok(Frame::Null);
342 | }
343 | Ok(Frame::Null)
344 | }
345 | fn start_expiring_evictor(&self) {
346 | let mut done = self.done.resubscribe();
347 | let wg = self.wg.clone();
348 | let expiring = self.expiring.clone();
349 | let store = self.store.clone();
350 | let mut drop_rx = self.drop.subscribe();
351 | tokio::spawn(async move {
352 | debug!("expiring evictor started");
353 | loop {
354 | tokio::select! {
355 | _ = done.recv() => {
356 | drop(wg);
357 | debug!("shutting down expiring evictor, shutdown signal received");
358 | break;
359 | }
360 | _ = drop_rx.recv() => {
361 | drop(wg);
362 | debug!("shutting down expiring evictor, keyspace is dropped");
363 | break;
364 | }
365 | _ = time::sleep(Duration::from_millis(500)) => {
366 | let mut expring_handle = expiring.lock();
367 | let mut store_handle = store.lock();
368 | let mut expired_keys = Vec::with_capacity(5);
369 |
370 | for (idx, (key, expiry)) in expring_handle.iter().enumerate() {
371 | if idx >= EXPIRING_EVICTOR_SAMPLE_SIZE as usize {
372 | break;
373 | }
374 |
375 | let current_time = match SystemTime::now().duration_since(UNIX_EPOCH) {
376 | Ok(time) => time.as_secs(),
377 | Err(e) => {
378 | error!("{}", e);
379 | break;
380 | }
381 | };
382 | if *expiry <= current_time {
383 | expired_keys.push(key.clone());
384 | store_handle.remove(key);
385 | }
386 | }
387 |
388 | for key in expired_keys {
389 | expring_handle.remove(&key);
390 | }
391 | }
392 | }
393 | }
394 | });
395 | }
396 |
397 | fn start_max_memory_evictor(&self) {
398 | if self.evictor == Evictor::Nop {
399 | return;
400 | }
401 | let mut done = self.done.resubscribe();
402 | let mut drop_rx = self.drop.subscribe();
403 | let mut evict_rx = self.evict.resubscribe();
404 | let wg = self.wg.clone();
405 | let store = self.store.clone();
406 | let evictor = self.evictor;
407 | tokio::spawn(async move {
408 | debug!("max memory evictor started");
409 | loop {
410 | tokio::select! {
411 | _ = done.recv() => {
412 | drop(wg);
413 | debug!("shutting down max memory evictor, shutdown signal received");
414 | break;
415 | }
416 | _ = drop_rx.recv() => {
417 | drop(wg);
418 | debug!("shutting down max memory evictor, keyspace is dropped");
419 | break;
420 | }
421 | _ = evict_rx.recv() => {
422 | match evictor {
423 | Evictor::Lru => {
424 | let mut handle = store.lock();
425 | let mut lru = Instant::now();
426 | let mut to_evict: Option = None;
427 | for (idx, (key, value)) in handle.iter().enumerate() {
428 | if idx >= MAX_MEMORY_EVICTOR_SAMPLE_SIZE as usize {
429 | break;
430 | }
431 |
432 | let last_accessed = value.last_accessed();
433 |
434 | if last_accessed < lru {
435 | lru = last_accessed;
436 | to_evict = Some(key.clone());
437 | }
438 | }
439 |
440 | if let Some(key) = to_evict {
441 | debug!("key '{:?}' evicted using lru policy", key);
442 | handle.remove(&key);
443 | }
444 | },
445 | Evictor::Random => {
446 | let mut handle = store.lock();
447 | let mut to_evict: Option = None;
448 | for (idx, key) in handle.keys().enumerate() {
449 | if idx >= MAX_MEMORY_EVICTOR_SAMPLE_SIZE as usize {
450 | break;
451 | }
452 | to_evict = Some(key.clone());
453 | }
454 |
455 | if let Some(key) = to_evict {
456 | debug!("key '{:?}' evicted using random policy", key);
457 | handle.remove(&key);
458 | }
459 | },
460 | _ => unreachable!(),
461 | }
462 | }
463 | }
464 | }
465 | });
466 | }
467 |
468 | pub fn evictor(&self) -> Evictor {
469 | self.evictor
470 | }
471 | }
472 |
473 | impl Value {
474 | pub fn new(data: Bytes, expire_at: Option) -> Self {
475 | Value {
476 | data,
477 | last_accessed: Instant::now(),
478 | expire_at,
479 | }
480 | }
481 |
482 | pub fn touch(&mut self) {
483 | self.last_accessed = Instant::now();
484 | }
485 |
486 | pub fn data(&self) -> Bytes {
487 | self.data.clone()
488 | }
489 |
490 | pub fn expire_at(&self) -> Option {
491 | self.expire_at
492 | }
493 |
494 | pub fn last_accessed(&self) -> Instant {
495 | self.last_accessed
496 | }
497 | }
498 |
499 | impl Evictor {
500 | pub fn as_bytes(&self) -> &[u8] {
501 | match self {
502 | Evictor::Lru => b"LRU",
503 | Evictor::Nop => b"NOP",
504 | Evictor::Random => b"RANDOM",
505 | }
506 | }
507 | }
508 |
--------------------------------------------------------------------------------
/src/command/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::db::Evictor;
2 | use crate::frame::Frame;
3 | use bytes::Bytes;
4 | use std::iter::Peekable;
5 | use std::ops::Add;
6 | use std::str::{self, Utf8Error};
7 | use std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH};
8 | use std::vec::IntoIter;
9 | use thiserror::Error;
10 |
11 | #[cfg(test)]
12 | mod test;
13 |
14 | #[derive(Debug)]
15 | struct Parser {
16 | tokens: Peekable>,
17 | }
18 |
19 | #[derive(Debug, PartialEq)]
20 | pub struct Create {
21 | keyspace: Bytes,
22 | evictor: Evictor,
23 | if_not_exists: bool,
24 | }
25 |
26 | #[derive(Debug, PartialEq)]
27 | pub struct Set {
28 | keyspace: Bytes,
29 | key: Bytes,
30 | value: Bytes,
31 | expire_at: Option,
32 | if_not_exists: bool,
33 | if_exists: bool,
34 | }
35 |
36 | #[derive(Debug, PartialEq)]
37 | pub struct Get {
38 | keyspace: Bytes,
39 | key: Bytes,
40 | }
41 |
42 | #[derive(Debug, PartialEq)]
43 | pub struct Del {
44 | keyspace: Bytes,
45 | key: Bytes,
46 | }
47 |
48 | #[derive(Debug, PartialEq)]
49 | pub struct Drop {
50 | keyspace: Bytes,
51 | if_exists: bool,
52 | }
53 |
54 | #[derive(Debug, PartialEq)]
55 | pub struct Count {
56 | keyspace: Bytes,
57 | }
58 |
59 | #[derive(Debug, PartialEq)]
60 | pub struct Ttl {
61 | keyspace: Bytes,
62 | key: Bytes,
63 | }
64 |
65 | #[derive(Debug, PartialEq)]
66 | pub enum Command {
67 | Create(Create),
68 | Set(Set),
69 | Get(Get),
70 | Del(Del),
71 | Drop(Drop),
72 | Count(Count),
73 | Ttl(Ttl),
74 | Ping,
75 | Keyspaces,
76 | }
77 |
78 | #[derive(Debug, Error)]
79 | pub enum ParseCommandError {
80 | #[error("invalid command format")]
81 | InvalidFormat,
82 |
83 | #[error("wrong number of arguments for '{0}' command")]
84 | WrongArgCount(String),
85 |
86 | #[error(transparent)]
87 | Utf8Error(#[from] Utf8Error),
88 |
89 | #[error("invalid argument '{0}' for '{1}' command")]
90 | InvalidArg(String, String),
91 |
92 | #[error("invalid value '{0}' for argument '{1}' for '{2}' command")]
93 | InvalidArgValue(String, String, String),
94 |
95 | #[error(transparent)]
96 | SystemTimeError(#[from] SystemTimeError),
97 |
98 | #[error("unknown command '{0}'")]
99 | UnknownCommand(String),
100 | }
101 |
102 | impl Parser {
103 | pub fn new(frame: Frame) -> Result {
104 | match frame {
105 | Frame::Array(tokens) => Ok(Parser {
106 | tokens: tokens.into_iter().peekable(),
107 | }),
108 | _ => Err(ParseCommandError::InvalidFormat),
109 | }
110 | }
111 |
112 | pub fn next(&mut self) -> Option {
113 | self.tokens.next()
114 | }
115 |
116 | pub fn has_remaining(&mut self) -> bool {
117 | self.tokens.peek().is_some()
118 | }
119 |
120 | pub fn next_as_string(&mut self) -> Result