From c09d28b3540bd355238ee614b97c1a33499d9350 Mon Sep 17 00:00:00 2001 From: runningwater Date: Wed, 31 Jul 2024 14:36:15 +0800 Subject: [PATCH] feature: support command and store function --- Cargo.lock | 166 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/backend/mod.rs | 68 +++++++++++++++++++ src/cmd/hmap.rs | 113 ++++++++++++++++++++++++++++++ src/cmd/map.rs | 111 ++++++++++++++++++++++++++++++ src/cmd/mod.rs | 126 ++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/resp/mod.rs | 27 +++++--- 8 files changed, 605 insertions(+), 11 deletions(-) create mode 100644 src/backend/mod.rs create mode 100644 src/cmd/hmap.rs create mode 100644 src/cmd/map.rs create mode 100644 src/cmd/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 19c2483..302759d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,50 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bytes" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "dashmap" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -26,12 +64,53 @@ dependencies = [ "syn", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -50,16 +129,39 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "simple-redis" version = "0.1.0" dependencies = [ "anyhow", "bytes", + "dashmap", "enum_dispatch", + "lazy_static", "thiserror", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "syn" version = "2.0.72" @@ -96,3 +198,67 @@ name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 19415c8..068dc8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ license = "MIT" [dependencies] anyhow = "1.0.86" bytes = "1.6.1" +dashmap = "6.0.1" enum_dispatch = "0.3.13" +lazy_static = "1.5.0" # This library provides a convenient derive macro for the standard library’s std::error::Error trait. thiserror = "1.0.63" diff --git a/src/backend/mod.rs b/src/backend/mod.rs new file mode 100644 index 0000000..efa685a --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,68 @@ +use std::{ops::Deref, sync::Arc}; + +use dashmap::DashMap; + +use crate::RespFrame; + +#[derive(Debug, Clone)] +pub struct Backend(Arc); + +// 使用 DashMap, 实现 Redis 存储 +#[derive(Debug)] +pub struct BackendInner { + map: DashMap, + hmap: DashMap>, +} + +impl Deref for Backend { + type Target = BackendInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl BackendInner { + pub fn new() -> Self { + BackendInner { + map: DashMap::new(), + hmap: DashMap::new(), + } + } +} +impl Default for Backend { + fn default() -> Self { + Self(Arc::new(BackendInner::default())) + } +} +impl Default for BackendInner { + fn default() -> Self { + BackendInner { + map: DashMap::new(), + hmap: DashMap::new(), + } + } +} + +impl Backend { + pub fn new() -> Self { + Self::default() + } + pub fn get(&self, key: &str) -> Option { + self.map.get(key).map(|v| v.value().clone()) + } + pub fn set(&self, key: String, value: RespFrame) { + self.map.insert(key, value); + } + pub fn hget(&self, key: &str, field: &str) -> Option { + self.hmap + .get(key) + .and_then(|v| v.get(field).map(|v| v.value().clone())) + } + pub fn hset(&mut self, key: String, field: String, value: RespFrame) { + let hmap = self.hmap.entry(key).or_default(); + hmap.insert(field, value); + } + pub fn hgetall(&self, key: &str) -> Option> { + self.hmap.get(key).map(|v| v.clone()) + } +} diff --git a/src/cmd/hmap.rs b/src/cmd/hmap.rs new file mode 100644 index 0000000..932340a --- /dev/null +++ b/src/cmd/hmap.rs @@ -0,0 +1,113 @@ +use crate::cmd::{extract_args, validate_command, HGetAll, HSet}; +use crate::{ + cmd::{CommandError, HGet}, + RespArray, RespFrame, +}; + +impl TryFrom for HGet { + type Error = CommandError; + + fn try_from(value: RespArray) -> Result { + validate_command(&value, &["HGET"], 2)?; + let mut args = extract_args(value, 1)?.into_iter(); + match (args.next(), args.next()) { + (Some(RespFrame::BulkString(key)), Some(RespFrame::BulkString(field))) => Ok(HGet { + key: String::from_utf8(key.0)?, + field: String::from_utf8(field.0)?, + }), + _ => Err(CommandError::InvalidCommand( + "Invalid key or field for HGET command".to_string(), + )), + } + } +} + +impl TryFrom for HGetAll { + type Error = CommandError; + fn try_from(value: RespArray) -> Result { + validate_command(&value, &["HGETALL"], 1)?; + let mut args = extract_args(value, 1)?.into_iter(); + match args.next() { + Some(RespFrame::BulkString(key)) => Ok(HGetAll { + key: String::from_utf8(key.0)?, + }), + _ => Err(CommandError::InvalidCommand( + "Invalid key for HGETALL command".to_string(), + )), + } + } +} + +impl TryFrom for HSet { + type Error = CommandError; + fn try_from(value: RespArray) -> Result { + validate_command(&value, &["HSET"], 3)?; + let mut args = extract_args(value, 1)?.into_iter(); + match (args.next(), args.next(), args.next()) { + (Some(RespFrame::BulkString(key)), Some(RespFrame::BulkString(field)), Some(value)) => { + Ok(HSet { + key: String::from_utf8(key.0)?, + field: String::from_utf8(field.0)?, + value, + }) + } + _ => Err(CommandError::InvalidCommand( + "Invalid key, field or value for HSET command".to_string(), + )), + } + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use bytes::BytesMut; + + use crate::{BulkString, RespDecode}; + + use super::*; + + #[test] + fn test_hget_from_resp_array() -> Result<()> { + let mut buf = BytesMut::new(); + buf.extend_from_slice(b"*3\r\n$4\r\nHGET\r\n$5\r\nmykey\r\n$7\r\nmyfield\r\n"); + + let frame = RespArray::decode(&mut buf).unwrap(); + let result: HGet = frame.try_into()?; + assert_eq!(result.key, "mykey"); + assert_eq!(result.field, "myfield"); + + Ok(()) + } + + #[test] + fn test_hgetall_from_resp_array() -> Result<()> { + let mut buf = BytesMut::new(); + buf.extend_from_slice(b"*2\r\n$7\r\nHGETALL\r\n$5\r\nmykey\r\n"); + + let frame = RespArray::decode(&mut buf)?; + let result: HGetAll = frame.try_into()?; + assert_eq!(result.key, "mykey"); + + Ok(()) + } + + #[test] + fn test_hset_from_resp_array() -> Result<()> { + let mut buf = BytesMut::new(); + buf.extend_from_slice( + b"*4\r\n$4\r\nHSET\r\n$5\r\nmykey\r\n$7\r\nmyfield\r\n$7\r\nmyvalue\r\n", + ); + + let frame = RespArray::decode(&mut buf)?; + let result: HSet = frame.try_into()?; + assert_eq!(result.key, "mykey"); + assert_eq!(result.field, "myfield"); + assert_eq!( + result.value, + RespFrame::BulkString(BulkString::new(b"myvalue".to_vec())) + ); + + Ok(()) + } +} diff --git a/src/cmd/map.rs b/src/cmd/map.rs new file mode 100644 index 0000000..febf974 --- /dev/null +++ b/src/cmd/map.rs @@ -0,0 +1,111 @@ +use crate::cmd::RESP_OK; +use crate::{ + cmd::{extract_args, validate_command, CommandError, CommandExecutor, Get, Set}, + Backend, RespArray, RespFrame, RespNull, +}; + +//=================== 实现 CommandExecutor trait for Command +impl CommandExecutor for Get { + fn execute(&self, backend: &Backend) -> RespFrame { + backend.get(&self.key).unwrap_or(RespFrame::Null(RespNull)) + } +} +impl CommandExecutor for Set { + fn execute(&self, backend: &Backend) -> RespFrame { + backend.set(self.key.clone(), self.value.clone()); + RESP_OK.clone() + } +} + +// =========================== 实现 TryFrom trait for Command +impl TryFrom for Get { + type Error = CommandError; + + fn try_from(value: RespArray) -> Result { + validate_command(&value, &["get"], 1)?; + + let mut args = extract_args(value, 1)?.into_iter(); + + match args.next() { + Some(RespFrame::BulkString(key)) => Ok(Get { + key: String::from_utf8(key.0)?, + }), + _ => Err(CommandError::InvalidCommand("Invalid key".to_string())), + } + } +} +impl TryFrom for Set { + type Error = CommandError; + + fn try_from(value: RespArray) -> Result { + validate_command(&value, &["set"], 2)?; + let args = extract_args(value, 1)?; + let mut args = args.into_iter(); + match (args.next(), args.next()) { + (Some(RespFrame::BulkString(key)), Some(value)) => Ok(Set { + key: String::from_utf8(key.0)?, + value, + }), + _ => Err(CommandError::InvalidCommand( + "Invalid key or value".to_string(), + )), + } + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use bytes::BytesMut; + + use crate::{BulkString, RespDecode}; + + use super::*; + + #[test] + fn test_get_from_resp_array() -> Result<()> { + let mut buf = BytesMut::new(); + buf.extend_from_slice(b"*2\r\n$3\r\nget\r\n$5\r\nhello\r\n"); + + let frame = RespArray::decode(&mut buf)?; + let result: Get = frame.try_into()?; // Get::try_from(frame)? + assert_eq!(result.key, "hello"); + + Ok(()) + } + #[test] + fn test_set_from_resp_array() -> Result<()> { + let mut buf = BytesMut::new(); + buf.extend_from_slice(b"*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n"); + + let frame = RespArray::decode(&mut buf)?; + let result: Set = frame.try_into()?; + assert_eq!(result.key, "hello"); + assert_eq!( + result.value, + RespFrame::BulkString(BulkString::new(b"world".to_vec())) + ); + + Ok(()) + } + #[test] + fn test_set_get_execute() -> Result<()> { + let backend = Backend::new(); + let set_cmd = Set { + key: "hello".to_string(), + value: RespFrame::BulkString(BulkString::new(b"world".to_vec())), + }; + let result = set_cmd.execute(&backend); + assert_eq!(result, RESP_OK.clone()); + + let get_cmd = Get { + key: "hello".to_string(), + }; + let result = get_cmd.execute(&backend); + assert_eq!( + result, + RespFrame::BulkString(BulkString::new(b"world".to_vec())) + ); + Ok(()) + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs new file mode 100644 index 0000000..d1d4705 --- /dev/null +++ b/src/cmd/mod.rs @@ -0,0 +1,126 @@ +use lazy_static::lazy_static; +use thiserror::Error; + +use crate::{Backend, RespArray, RespError, RespFrame, SimpleString}; + +mod hmap; +mod map; + +lazy_static! { + static ref RESP_OK: RespFrame = RespFrame::SimpleString(SimpleString::new("OK".to_string())); +} +#[derive(Error, Debug)] +pub enum CommandError { + #[error("Invalid command: {0}")] + InvalidCommand(String), + #[error("Invalid command format: {0}")] + InvalidCommandFormat(String), + #[error("Invalid command arguments: {0}")] + InvalidCommandArguments(String), + #[error("Invalid command arguments length: {0}")] + InvalidCommandArgumentsLength(usize), + #[error("Command execution error: {0}")] + ExecutionError(#[from] anyhow::Error), + #[error("Command not found: {0}")] + CommandNotFound(String), + + #[error("{0}")] + RespError(#[from] RespError), + #[error("FromUtf8Error: {0}")] + FromUtf8Error(#[from] std::string::FromUtf8Error), +} + +pub trait CommandExecutor { + fn execute(&self, backend: &Backend) -> RespFrame; +} + +#[derive(Debug)] +pub enum Command { + Get(Get), + Set(Set), + HGet(HGet), + HSet(HSet), + HGetAll(HGetAll), +} + +#[allow(dead_code)] +#[derive(Debug)] +pub struct Get { + key: String, +} +#[allow(dead_code)] +#[derive(Debug)] +pub struct Set { + key: String, + value: RespFrame, +} +#[allow(dead_code)] +#[derive(Debug)] +pub struct HGet { + key: String, + field: String, +} +#[allow(dead_code)] +#[derive(Debug)] +pub struct HSet { + key: String, + field: String, + value: RespFrame, +} +#[allow(dead_code)] +#[derive(Debug)] +pub struct HGetAll { + key: String, +} + +impl TryFrom for Command { + type Error = CommandError; + fn try_from(_frame: RespArray) -> Result { + todo!() + } +} + +fn validate_command( + value: &RespArray, + names: &[&'static str], + n_args: usize, +) -> Result<(), CommandError> { + // 校验个数 + if value.len() != n_args + names.len() { + return Err(CommandError::InvalidCommandArguments(format!( + "{} command must have {} arguments, but got {}", + names.join(" "), + n_args + 1, + value.len() + ))); + } + for (i, name) in names.iter().enumerate() { + match value[i] { + RespFrame::BulkString(ref cmd) => { + if cmd.as_ref().to_ascii_lowercase() != name.to_ascii_lowercase().as_bytes() { + return Err(CommandError::InvalidCommand(format!( + "Invalid command: expect {}, got {}", + name, + String::from_utf8_lossy(cmd.as_ref()) + ))); + } + } + _ => { + return Err(CommandError::InvalidCommand( + "Command must a BulkString as the first argument".to_string(), + )); + } + } + } + + Ok(()) +} + +// 抽取参数 +fn extract_args(value: RespArray, start: usize) -> Result, CommandError> { + Ok(value + .iter() + .skip(start) + .cloned() + .collect::>()) +} diff --git a/src/lib.rs b/src/lib.rs index 62bcc21..2f3f08a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +mod backend; +pub mod cmd; mod resp; +pub use backend::*; pub use resp::*; diff --git a/src/resp/mod.rs b/src/resp/mod.rs index 65987e4..3667735 100644 --- a/src/resp/mod.rs +++ b/src/resp/mod.rs @@ -59,7 +59,7 @@ pub enum RespError { /// - set "~\r\n..." /// #[enum_dispatch(RespEncode)] -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum RespFrame { SimpleString(SimpleString), Error(SimpleError), @@ -76,23 +76,23 @@ pub enum RespFrame { Set(RespSet), } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct SimpleString(String); -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct SimpleError(String); -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct RespNull; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct RespNullArray; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct RespNullBulkString; -#[derive(Debug, PartialEq)] -pub struct BulkString(Vec); -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] +pub struct BulkString(pub(crate) Vec); +#[derive(Debug, PartialEq, Clone)] pub struct RespArray(Vec); -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct RespMap(HashMap); -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct RespSet(Vec); impl Deref for BulkString { @@ -102,6 +102,11 @@ impl Deref for BulkString { &self.0 } } +impl AsRef> for BulkString { + fn as_ref(&self) -> &Vec { + &self.0 + } +} impl Deref for RespArray { type Target = Vec;