Compare commits
3 Commits
31c47d7998
...
d3e28cafe4
Author | SHA1 | Date | |
---|---|---|---|
d3e28cafe4 | |||
de2be4a785 | |||
cd61406d1d |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto
|
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
.vscode/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
dumb.rdb
|
730
Cargo.lock
generated
Normal file
730
Cargo.lock
generated
Normal file
@ -0,0 +1,730 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
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 = "backtrace"
|
||||
version = "0.3.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5208975e568d83b6b05cc0a063c8e7e9acc2b43bee6da15616a5b73e109d7437"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[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 = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[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 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redb"
|
||||
version = "2.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59b38b05028f398f08bea4691640503ec25fcb60b82fb61ce1f8fd1f4fccd3f7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis-rs"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"clap",
|
||||
"futures",
|
||||
"redb",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[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.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[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.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[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.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[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.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[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.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[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.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "redis-rs"
|
||||
version = "0.0.1"
|
||||
authors = ["Pin Fang <fpfangpin@hotmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.59"
|
||||
bytes = "1.3.0"
|
||||
thiserror = "1.0.32"
|
||||
tokio = { version = "1.23.0", features = ["full"] }
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
byteorder = "1.4.3"
|
||||
futures = "0.3"
|
||||
redb = "2.1.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
bincode = "1.3.3"
|
||||
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pin Fang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
267
README.md
267
README.md
@ -1,2 +1,267 @@
|
||||
# herodb
|
||||
# Build Your Own Redis in Rust
|
||||
|
||||
This project is to build a toy Redis-Server clone that's capable of parsing Redis protocol and handling basic Redis commands, parsing and initializing Redis from RDB file,
|
||||
supporting leader-follower replication, redis streams (queue), redis batch commands in transaction.
|
||||
|
||||
You can find all the source code and commit history in [my github repo](https://github.com/fangpin/redis-rs).
|
||||
|
||||
## Main features
|
||||
+ Parse Redis protocol
|
||||
+ Handle basic Redis commands
|
||||
+ Parse and initialize Redis from RDB file
|
||||
+ Leader-follower Replication
|
||||
|
||||
## Prerequisites
|
||||
install `redis-cli` first (an implementation of redis client for test purpose)
|
||||
```sh
|
||||
cargo install mini-redis
|
||||
```
|
||||
|
||||
Learn about:
|
||||
- [Redis protocoal](https://redis.io/docs/latest/develop/reference/protocol-spec)
|
||||
- [RDB file format](https://rdb.fnordig.de/file_format.html)
|
||||
- [Redis replication](https://redis.io/docs/management/replication/)
|
||||
|
||||
## Start the Redis-rs server
|
||||
```sh
|
||||
# start as master
|
||||
cargo run -- --dir /some/db/path --dbfilename dump.rdb
|
||||
# start as slave
|
||||
cargo run -- --dir /some/db/path --dbfilename dump.rdb --port 6380 --replicaof "localhost 6379"
|
||||
```
|
||||
|
||||
|
||||
## Supported Commands
|
||||
```sh
|
||||
# basic commands
|
||||
redis-cli PING
|
||||
redis-cli ECHO hey
|
||||
redis-cli SET foo bar
|
||||
redis-cli SET foo bar px/ex 100
|
||||
redis-cli GET foo
|
||||
redis-cli SET foo 2
|
||||
redis-cli INCR foo
|
||||
redis-cli INCR missing_key
|
||||
redis-cli TYPE some_key
|
||||
redis-cli KEYS "*"
|
||||
|
||||
# leader-follower replication related commands
|
||||
redis-cli CONFIG GET dbfilename
|
||||
redis-cli INFO replication
|
||||
|
||||
# streams related commands
|
||||
redis-cli XADD stream_key 1526919030474-0 temperature 36 humidity 95
|
||||
redis-cli XADD stream_key 1526919030474-* temperature 37 humidity 94
|
||||
redis-cli XADD stream_key "*" foo bar
|
||||
## read stream
|
||||
redis-cli XRANGE stream_key 0-2 0-3
|
||||
## query with + -
|
||||
redis-cli XRANGE some_key - 1526985054079
|
||||
## query single stream using xread
|
||||
redis-cli XREAD streams some_key 1526985054069-0
|
||||
## query multiple stream using xread
|
||||
redis-cli XREAD streams stream_key other_stream_key 0-0 0-1
|
||||
## blocking reads without timeout
|
||||
redis-cli XREAD block 0 streams some_key 1526985054069-0
|
||||
|
||||
|
||||
# transactions related commands
|
||||
## start a transaction and exec all queued commands in a transaction
|
||||
redis-cli
|
||||
> MULTI
|
||||
> set foo 1
|
||||
> incr foo
|
||||
> exec
|
||||
## start a transaction and queued commands and cancel transaction then
|
||||
redis-cli
|
||||
> MULTI
|
||||
> set foo 1
|
||||
> incr foo
|
||||
> discard
|
||||
|
||||
```
|
||||
|
||||
## RDB Persistence
|
||||
Get Redis-rs server config
|
||||
```sh
|
||||
redis-cli CONFIG GET dbfilename
|
||||
```
|
||||
### RDB file format overview
|
||||
Here are the different sections of the [RDB file](https://rdb.fnordig.de/file_format.html), in order:
|
||||
+ Header section
|
||||
+ Metadata section
|
||||
+ Database section
|
||||
+ End of file section
|
||||
#### Header section
|
||||
start with some magic number
|
||||
```sh
|
||||
52 45 44 49 53 30 30 31 31 // Magic string + version number (ASCII): "REDIS0011".
|
||||
```
|
||||
#### Metadata section
|
||||
contains zero or more "metadata subsections", which each specify a single metadata attribute
|
||||
e.g.
|
||||
```sh
|
||||
FA // Indicates the start of a metadata subsection.
|
||||
09 72 65 64 69 73 2D 76 65 72 // The name of the metadata attribute (string encoded): "redis-ver".
|
||||
06 36 2E 30 2E 31 36 // The value of the metadata attribute (string encoded): "6.0.16".
|
||||
```
|
||||
#### Database section
|
||||
contains zero or more "database subsections," which each describe a single database.
|
||||
e.g.
|
||||
```sh
|
||||
FE // Indicates the start of a database subsection.
|
||||
00 /* The index of the database (size encoded). Here, the index is 0. */
|
||||
|
||||
FB // Indicates that hash table size information follows.
|
||||
03 /* The size of the hash table that stores the keys and values (size encoded). Here, the total key-value hash table size is 3. */
|
||||
02 /* The size of the hash table that stores the expires of the keys (size encoded). Here, the number of keys with an expiry is 2. */
|
||||
```
|
||||
|
||||
```sh
|
||||
00 /* The 1-byte flag that specifies the value’s type and encoding. Here, the flag is 0, which means "string." */
|
||||
06 66 6F 6F 62 61 72 // The name of the key (string encoded). Here, it's "foobar".
|
||||
06 62 61 7A 71 75 78 // The value (string encoded). Here, it's "bazqux".
|
||||
```
|
||||
|
||||
```sh
|
||||
FC /* Indicates that this key ("foo") has an expire, and that the expire timestamp is expressed in milliseconds. */
|
||||
15 72 E7 07 8F 01 00 00 /* The expire timestamp, expressed in Unix time, stored as an 8-byte unsigned long, in little-endian (read right-to-left). Here, the expire timestamp is 1713824559637. */
|
||||
00 // Value type is string.
|
||||
03 66 6F 6F // Key name is "foo".
|
||||
03 62 61 72 // Value is "bar".
|
||||
```
|
||||
|
||||
```sh
|
||||
FD /* Indicates that this key ("baz") has an expire, and that the expire timestamp is expressed in seconds. */
|
||||
52 ED 2A 66 /* The expire timestamp, expressed in Unix time, stored as an 4-byte unsigned integer, in little-endian (read right-to-left). Here, the expire timestamp is 1714089298. */
|
||||
00 // Value type is string.
|
||||
03 62 61 7A // Key name is "baz".
|
||||
03 71 75 78 // Value is "qux".
|
||||
```
|
||||
|
||||
In summary,
|
||||
- Optional expire information (one of the following):
|
||||
- Timestamp in seconds:
|
||||
- FD
|
||||
- Expire timestamp in seconds (4-byte unsigned integer)
|
||||
- Timestamp in milliseconds:
|
||||
- FC
|
||||
- Expire timestamp in milliseconds (8-byte unsigned long)
|
||||
- Value type (1-byte flag)
|
||||
- Key (string encoded)
|
||||
- Value (encoding depends on value type)
|
||||
|
||||
#### End of file section
|
||||
```sh
|
||||
FF /* Indicates that the file is ending, and that the checksum follows. */
|
||||
89 3b b7 4e f8 0f 77 19 // An 8-byte CRC64 checksum of the entire file.
|
||||
```
|
||||
|
||||
#### Size encoding
|
||||
```sh
|
||||
/* If the first two bits are 0b00:
|
||||
The size is the remaining 6 bits of the byte.
|
||||
In this example, the size is 10: */
|
||||
0A
|
||||
00001010
|
||||
|
||||
/* If the first two bits are 0b01:
|
||||
The size is the next 14 bits
|
||||
(remaining 6 bits in the first byte, combined with the next byte),
|
||||
in big-endian (read left-to-right).
|
||||
In this example, the size is 700: */
|
||||
42 BC
|
||||
01000010 10111100
|
||||
|
||||
/* If the first two bits are 0b10:
|
||||
Ignore the remaining 6 bits of the first byte.
|
||||
The size is the next 4 bytes, in big-endian (read left-to-right).
|
||||
In this example, the size is 17000: */
|
||||
80 00 00 42 68
|
||||
10000000 00000000 00000000 01000010 01101000
|
||||
|
||||
/* If the first two bits are 0b11:
|
||||
The remaining 6 bits specify a type of string encoding.
|
||||
See string encoding section. */
|
||||
```
|
||||
|
||||
#### String encoding
|
||||
+ The size of the string (size encoded).
|
||||
+ The string.
|
||||
```sh
|
||||
/* The 0x0D size specifies that the string is 13 characters long. The remaining characters spell out "Hello, World!". */
|
||||
0D 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21
|
||||
```
|
||||
For sizes that begin with 0b11, the remaining 6 bits indicate a type of string format:
|
||||
```sh
|
||||
/* The 0xC0 size indicates the string is an 8-bit integer. In this example, the string is "123". */
|
||||
C0 7B
|
||||
|
||||
/* The 0xC1 size indicates the string is a 16-bit integer. The remaining bytes are in little-endian (read right-to-left). In this example, the string is "12345". */
|
||||
C1 39 30
|
||||
|
||||
/* The 0xC2 size indicates the string is a 32-bit integer. The remaining bytes are in little-endian (read right-to-left), In this example, the string is "1234567". */
|
||||
C2 87 D6 12 00
|
||||
|
||||
/* The 0xC3 size indicates that the string is compressed with the LZF algorithm. You will not encounter LZF-compressed strings in this challenge. */
|
||||
C3 ...
|
||||
```
|
||||
|
||||
## Replication
|
||||
Redis server [leader-follower replication](https://redis.io/docs/management/replication/).
|
||||
Run multiple Redis servers with one acting as the "master" and the others as "replicas". Changes made to the master will be automatically replicated to replicas.
|
||||
|
||||
### Send Handshake (follower -> master)
|
||||
1. When the follower starts, it will send a PING command to the master as RESP Array.
|
||||
2. Then 2 REPLCONF (replication config) commands are sent to master from follower to communicate the port and the sync protocol. One is *REPLCONF listening-port <PORT>* and the other is *REPLCONF capa psync2*. psnync2 is an example sync protocol supported in this project.
|
||||
3. The follower sends the *PSYNC* command to master with replication id and offset to start the replication process.
|
||||
|
||||
### Receive Handshake (master -> follower)
|
||||
1. Response a PONG message to follower.
|
||||
2. Response an OK message to follower for both REPLCONF commands.
|
||||
3. Response a *+FULLRESYNC <REPL_ID> 0\r\n* to follower with the replication id and offset.
|
||||
|
||||
### RDB file transfer
|
||||
When the follower starts, it sends a *PSYNC ? -1* command to tell master that it doesn't have any data yet, and needs a full resync.
|
||||
|
||||
Then the master send a *FULLRESYNC* response to the follower as an acknowledgement.
|
||||
|
||||
Finally, the master send the RDB file to represent its current state to the follower. The follower should load the RDB file received to the memory, replacing its current state.
|
||||
|
||||
### Receive write commands (master -> follower)
|
||||
The master sends following write commands to the follower with the offset info.
|
||||
The sending is to reuse the same TCP connection of handshake and RDB file transfer.
|
||||
As the all the commands are encoded as RESP Array just like a normal client command, so the follower could reuse the same logic to handler the replicate commands from master. The only difference is the commands are coming from the master and no need response back.
|
||||
|
||||
|
||||
## Streams
|
||||
A stream is identified by a key, and it contains multiple entries.
|
||||
Each entry consists of one or more key-value pairs, and is assigned a unique ID.
|
||||
[More about redis streams](https://redis.io/docs/latest/develop/data-types/streams/)
|
||||
[Radix tree](https://en.wikipedia.org/wiki/Radix_tree)
|
||||
|
||||
|
||||
It looks like a list of key-value pairs.
|
||||
```sh
|
||||
entries:
|
||||
- id: 1526985054069-0 # (ID of the first entry)
|
||||
temperature: 36 # (A key value pair in the first entry)
|
||||
humidity: 95 # (Another key value pair in the first entry)
|
||||
|
||||
- id: 1526985054079-0 # (ID of the second entry)
|
||||
temperature: 37 # (A key value pair in the first entry)
|
||||
humidity: 94 # (Another key value pair in the first entry)
|
||||
|
||||
# ... (and so on)
|
||||
|
||||
```
|
||||
|
||||
Examples of Redis stream use cases include:
|
||||
- Event sourcing (e.g., tracking user actions, clicks, etc.)
|
||||
- Sensor monitoring (e.g., readings from devices in the field)
|
||||
- Notifications (e.g., storing a record of each user's notifications in a separate stream)
|
||||
|
||||
## Transaction
|
||||
When *MULTI* command is called in a connection, redis just queued all following commands until *EXEC* or *DISCARD* command is called.
|
||||
*EXEC* command will execute all queued commands and return an array representation of all execution result (including), instead the *DISCARD* command just clear all queued commands.
|
||||
The transactions among each client connection are independent.
|
||||
|
64
_config.yml
Normal file
64
_config.yml
Normal file
@ -0,0 +1,64 @@
|
||||
title: 从 0 到 1 由 Rust 构建 Redis
|
||||
description: 从 0 到 1 由 Rust 构建 Redis
|
||||
theme: just-the-docs
|
||||
|
||||
url: https://fangpin.github.io/redis-rs
|
||||
|
||||
aux_links:
|
||||
GitHub: https://fangpin.github.io/redis-rs
|
||||
|
||||
# logo: "/assets/images/just-the-docs.png"
|
||||
|
||||
search_enabled: true
|
||||
search:
|
||||
# Split pages into sections that can be searched individually
|
||||
# Supports 1 - 6, default: 2
|
||||
heading_level: 2
|
||||
# Maximum amount of previews per search result
|
||||
# Default: 3
|
||||
previews: 3
|
||||
# Maximum amount of words to display before a matched word in the preview
|
||||
# Default: 5
|
||||
preview_words_before: 5
|
||||
# Maximum amount of words to display after a matched word in the preview
|
||||
# Default: 10
|
||||
preview_words_after: 10
|
||||
# Set the search token separator
|
||||
# Default: /[\s\-/]+/
|
||||
# Example: enable support for hyphenated search words
|
||||
tokenizer_separator: /[\s/]+/
|
||||
# Display the relative url in search results
|
||||
# Supports true (default) or false
|
||||
rel_url: true
|
||||
# Enable or disable the search button that appears in the bottom right corner of every page
|
||||
# Supports true or false (default)
|
||||
button: false
|
||||
|
||||
|
||||
# Heading anchor links appear on hover over h1-h6 tags in page content
|
||||
# allowing users to deep link to a particular heading on a page.
|
||||
#
|
||||
# Supports true (default) or false
|
||||
heading_anchors: true
|
||||
|
||||
|
||||
# Footer content
|
||||
# appears at the bottom of every page's main content
|
||||
# Note: The footer_content option is deprecated and will be removed in a future major release. Please use `_includes/footer_custom.html` for more robust markup / liquid-based content.
|
||||
footer_content: "Copyright © 2017-2024 Pin Fang"
|
||||
|
||||
# Footer last edited timestamp
|
||||
last_edit_timestamp: true # show or hide edit time - page must have `last_modified_date` defined in the frontmatter
|
||||
last_edit_time_format: "%b %e %Y at %I:%M %p" # uses ruby's time format: https://ruby-doc.org/stdlib-2.7.0/libdoc/time/rdoc/Time.html
|
||||
|
||||
|
||||
|
||||
# code
|
||||
compress_html:
|
||||
ignore:
|
||||
envs: all
|
||||
|
||||
kramdown:
|
||||
syntax_highlighter_opts:
|
||||
block:
|
||||
line_numbers: true
|
307
instructions/hset_redis_functions.md
Normal file
307
instructions/hset_redis_functions.md
Normal file
@ -0,0 +1,307 @@
|
||||
|
||||
# 🔑 Redis `HSET` and Related Hash Commands
|
||||
|
||||
## 1. `HSET`
|
||||
|
||||
* **Purpose**: Set the value of one or more fields in a hash.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HSET key field value [field value ...]
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Integer: number of fields that were newly added.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*4
|
||||
$4
|
||||
HSET
|
||||
$3
|
||||
key
|
||||
$5
|
||||
field
|
||||
$5
|
||||
value
|
||||
```
|
||||
|
||||
(If multiple field-value pairs: `*6`, `*8`, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 2. `HSETNX`
|
||||
|
||||
* **Purpose**: Set the value of a hash field only if it does **not** exist.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HSETNX key field value
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* `1` if field was set.
|
||||
* `0` if field already exists.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*4
|
||||
$6
|
||||
HSETNX
|
||||
$3
|
||||
key
|
||||
$5
|
||||
field
|
||||
$5
|
||||
value
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. `HGET`
|
||||
|
||||
* **Purpose**: Get the value of a hash field.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HGET key field
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Bulk string (value) or `nil` if field does not exist.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*3
|
||||
$4
|
||||
HGET
|
||||
$3
|
||||
key
|
||||
$5
|
||||
field
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. `HGETALL`
|
||||
|
||||
* **Purpose**: Get all fields and values in a hash.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HGETALL key
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Array of `[field1, value1, field2, value2, ...]`.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*2
|
||||
$7
|
||||
HGETALL
|
||||
$3
|
||||
key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. `HMSET` (⚠️ Deprecated, use `HSET`)
|
||||
|
||||
* **Purpose**: Set multiple field-value pairs.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HMSET key field value [field value ...]
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Always `OK`.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*6
|
||||
$5
|
||||
HMSET
|
||||
$3
|
||||
key
|
||||
$5
|
||||
field
|
||||
$5
|
||||
value
|
||||
$5
|
||||
field2
|
||||
$5
|
||||
value2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. `HMGET`
|
||||
|
||||
* **Purpose**: Get values of multiple fields.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HMGET key field [field ...]
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Array of values (bulk strings or nils).
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*4
|
||||
$5
|
||||
HMGET
|
||||
$3
|
||||
key
|
||||
$5
|
||||
field1
|
||||
$5
|
||||
field2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. `HDEL`
|
||||
|
||||
* **Purpose**: Delete one or more fields from a hash.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HDEL key field [field ...]
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Integer: number of fields removed.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*3
|
||||
$4
|
||||
HDEL
|
||||
$3
|
||||
key
|
||||
$5
|
||||
field
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. `HEXISTS`
|
||||
|
||||
* **Purpose**: Check if a field exists.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HEXISTS key field
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* `1` if exists, `0` if not.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*3
|
||||
$7
|
||||
HEXISTS
|
||||
$3
|
||||
key
|
||||
$5
|
||||
field
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. `HKEYS`
|
||||
|
||||
* **Purpose**: Get all field names in a hash.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HKEYS key
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Array of field names.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*2
|
||||
$5
|
||||
HKEYS
|
||||
$3
|
||||
key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. `HVALS`
|
||||
|
||||
* **Purpose**: Get all values in a hash.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HVALS key
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Array of values.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*2
|
||||
$5
|
||||
HVALS
|
||||
$3
|
||||
key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. `HLEN`
|
||||
|
||||
* **Purpose**: Get number of fields in a hash.
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HLEN key
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Integer: number of fields.
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*2
|
||||
$4
|
||||
HLEN
|
||||
$3
|
||||
key
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 12. `HSCAN`
|
||||
|
||||
* **Purpose**: Iterate fields/values of a hash (cursor-based scan).
|
||||
* **Syntax**:
|
||||
|
||||
```bash
|
||||
HSCAN key cursor [MATCH pattern] [COUNT count]
|
||||
```
|
||||
* **Return**:
|
||||
|
||||
* Array: `[new-cursor, [field1, value1, ...]]`
|
||||
* **RESP Protocol**:
|
||||
|
||||
```
|
||||
*3
|
||||
$5
|
||||
HSCAN
|
||||
$3
|
||||
key
|
||||
$1
|
||||
0
|
||||
```
|
80
instructions/redb.md
Normal file
80
instructions/redb.md
Normal file
@ -0,0 +1,80 @@
|
||||
========================
|
||||
CODE SNIPPETS
|
||||
========================
|
||||
TITLE: 1PC+C Commit Strategy Vulnerability Example
|
||||
DESCRIPTION: Illustrates a scenario where a partially committed transaction might appear complete due to the non-cryptographic checksum (XXH3) used in the 1PC+C commit strategy. This requires controlling page flush order, introducing a crash during fsync, and ensuring valid checksums for partially written data.
|
||||
|
||||
SOURCE: https://github.com/cberner/redb/blob/master/docs/design.md#_snippet_9
|
||||
|
||||
LANGUAGE: rust
|
||||
CODE:
|
||||
```
|
||||
table.insert(malicious_key, malicious_value);
|
||||
table.insert(good_key, good_value);
|
||||
txn.commit();
|
||||
```
|
||||
|
||||
LANGUAGE: rust
|
||||
CODE:
|
||||
```
|
||||
table.insert(malicious_key, malicious_value);
|
||||
txn.commit();
|
||||
```
|
||||
|
||||
----------------------------------------
|
||||
|
||||
TITLE: Basic Key-Value Operations in redb
|
||||
DESCRIPTION: Demonstrates the fundamental usage of redb for creating a database, opening a table, inserting a key-value pair, and retrieving the value within separate read and write transactions.
|
||||
|
||||
SOURCE: https://github.com/cberner/redb/blob/master/README.md#_snippet_0
|
||||
|
||||
LANGUAGE: rust
|
||||
CODE:
|
||||
```
|
||||
use redb::{Database, Error, ReadableTable, TableDefinition};
|
||||
|
||||
const TABLE: TableDefinition<&str, u64> = TableDefinition::new("my_data");
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
let db = Database::create("my_db.redb")?;
|
||||
let write_txn = db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(TABLE)?;
|
||||
table.insert("my_key", &123)?;
|
||||
}
|
||||
write_txn.commit()?;
|
||||
|
||||
let read_txn = db.begin_read()?;
|
||||
let table = read_txn.open_table(TABLE)?;
|
||||
assert_eq!(table.get("my_key")?.unwrap().value(), 123);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## What *redb* currently supports:
|
||||
|
||||
* Simple operations like creating databases, inserting key-value pairs, opening and reading tables ([GitHub][1]).
|
||||
* No mention of operations such as:
|
||||
|
||||
* Iterating over keys with a given prefix.
|
||||
* Range queries based on string prefixes.
|
||||
* Specialized prefix‑filtered lookups.
|
||||
|
||||
|
||||
## implement range scans as follows
|
||||
|
||||
You can implement prefix-like functionality using **range scans** combined with manual checks, similar to using a `BTreeSet` in Rust:
|
||||
|
||||
```rust
|
||||
for key in table.range(prefix..).keys() {
|
||||
if !key.starts_with(prefix) {
|
||||
break;
|
||||
}
|
||||
// process key
|
||||
}
|
||||
```
|
||||
|
||||
This pattern iterates keys starting at the prefix, and stops once a key no longer matches the prefix—this works because the keys are sorted ([GitHub][1]).
|
250
instructions/redis_basics.md
Normal file
250
instructions/redis_basics.md
Normal file
@ -0,0 +1,250 @@
|
||||
Got it 👍 — let’s break this down properly.
|
||||
|
||||
Redis has two broad classes you’re asking about:
|
||||
|
||||
1. **Basic key-space functions** (SET, GET, DEL, EXISTS, etc.)
|
||||
2. **Iteration commands** (`SCAN`, `SSCAN`, `HSCAN`, `ZSCAN`)
|
||||
|
||||
And for each I’ll show:
|
||||
|
||||
* What it does
|
||||
* How it works at a high level
|
||||
* Its **RESP protocol implementation** (the actual wire format).
|
||||
|
||||
---
|
||||
|
||||
# 1. Basic Key-Space Commands
|
||||
|
||||
### `SET key value`
|
||||
|
||||
* Stores a string value at a key.
|
||||
* Overwrites if the key already exists.
|
||||
|
||||
**Protocol (RESP2):**
|
||||
|
||||
```
|
||||
*3
|
||||
$3
|
||||
SET
|
||||
$3
|
||||
foo
|
||||
$3
|
||||
bar
|
||||
```
|
||||
|
||||
(client sends: array of 3 bulk strings: `["SET", "foo", "bar"]`)
|
||||
|
||||
**Reply:**
|
||||
|
||||
```
|
||||
+OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET key`
|
||||
|
||||
* Retrieves the string value stored at the key.
|
||||
* Returns `nil` if key doesn’t exist.
|
||||
|
||||
**Protocol:**
|
||||
|
||||
```
|
||||
*2
|
||||
$3
|
||||
GET
|
||||
$3
|
||||
foo
|
||||
```
|
||||
|
||||
**Reply:**
|
||||
|
||||
```
|
||||
$3
|
||||
bar
|
||||
```
|
||||
|
||||
(or `$-1` for nil)
|
||||
|
||||
---
|
||||
|
||||
### `DEL key [key ...]`
|
||||
|
||||
* Removes one or more keys.
|
||||
* Returns number of keys actually removed.
|
||||
|
||||
**Protocol:**
|
||||
|
||||
```
|
||||
*2
|
||||
$3
|
||||
DEL
|
||||
$3
|
||||
foo
|
||||
```
|
||||
|
||||
**Reply:**
|
||||
|
||||
```
|
||||
:1
|
||||
```
|
||||
|
||||
(integer reply = number of deleted keys)
|
||||
|
||||
---
|
||||
|
||||
### `EXISTS key [key ...]`
|
||||
|
||||
* Checks if one or more keys exist.
|
||||
* Returns count of existing keys.
|
||||
|
||||
**Protocol:**
|
||||
|
||||
```
|
||||
*2
|
||||
$6
|
||||
EXISTS
|
||||
$3
|
||||
foo
|
||||
```
|
||||
|
||||
**Reply:**
|
||||
|
||||
```
|
||||
:1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `KEYS pattern`
|
||||
|
||||
* Returns all keys matching a glob-style pattern.
|
||||
⚠️ Not efficient in production (O(N)), better to use `SCAN`.
|
||||
|
||||
**Protocol:**
|
||||
|
||||
```
|
||||
*2
|
||||
$4
|
||||
KEYS
|
||||
$1
|
||||
*
|
||||
```
|
||||
|
||||
**Reply:**
|
||||
|
||||
```
|
||||
*2
|
||||
$3
|
||||
foo
|
||||
$3
|
||||
bar
|
||||
```
|
||||
|
||||
(array of bulk strings with key names)
|
||||
|
||||
---
|
||||
|
||||
# 2. Iteration Commands (`SCAN` family)
|
||||
|
||||
### `SCAN cursor [MATCH pattern] [COUNT n]`
|
||||
|
||||
* Iterates the keyspace incrementally.
|
||||
* Client keeps sending back the cursor from previous call until it returns `0`.
|
||||
|
||||
**Protocol example:**
|
||||
|
||||
```
|
||||
*2
|
||||
$4
|
||||
SCAN
|
||||
$1
|
||||
0
|
||||
```
|
||||
|
||||
**Reply:**
|
||||
|
||||
```
|
||||
*2
|
||||
$1
|
||||
0
|
||||
*2
|
||||
$3
|
||||
foo
|
||||
$3
|
||||
bar
|
||||
```
|
||||
|
||||
Explanation:
|
||||
|
||||
* First element = new cursor (`"0"` means iteration finished).
|
||||
* Second element = array of keys returned in this batch.
|
||||
|
||||
---
|
||||
|
||||
### `HSCAN key cursor [MATCH pattern] [COUNT n]`
|
||||
|
||||
* Like `SCAN`, but iterates fields of a hash.
|
||||
|
||||
**Protocol:**
|
||||
|
||||
```
|
||||
*3
|
||||
$5
|
||||
HSCAN
|
||||
$3
|
||||
myh
|
||||
$1
|
||||
0
|
||||
```
|
||||
|
||||
**Reply:**
|
||||
|
||||
```
|
||||
*2
|
||||
$1
|
||||
0
|
||||
*4
|
||||
$5
|
||||
field
|
||||
$5
|
||||
value
|
||||
$5
|
||||
age
|
||||
$2
|
||||
42
|
||||
```
|
||||
|
||||
(Array of alternating field/value pairs)
|
||||
|
||||
---
|
||||
|
||||
### `SSCAN key cursor [MATCH pattern] [COUNT n]`
|
||||
|
||||
* Iterates members of a set.
|
||||
|
||||
Protocol and reply structure same as SCAN.
|
||||
|
||||
---
|
||||
|
||||
### `ZSCAN key cursor [MATCH pattern] [COUNT n]`
|
||||
|
||||
* Iterates members of a sorted set with scores.
|
||||
* Returns alternating `member`, `score`.
|
||||
|
||||
---
|
||||
|
||||
# Quick Comparison
|
||||
|
||||
| Command | Purpose | Return Type |
|
||||
| -------- | ----------------------------- | --------------------- |
|
||||
| `SET` | Store a string value | Simple string `+OK` |
|
||||
| `GET` | Retrieve a string value | Bulk string / nil |
|
||||
| `DEL` | Delete keys | Integer (count) |
|
||||
| `EXISTS` | Check existence | Integer (count) |
|
||||
| `KEYS` | List all matching keys (slow) | Array of bulk strings |
|
||||
| `SCAN` | Iterate over keys (safe) | `[cursor, array]` |
|
||||
| `HSCAN` | Iterate over hash fields | `[cursor, array]` |
|
||||
| `SSCAN` | Iterate over set members | `[cursor, array]` |
|
||||
| `ZSCAN` | Iterate over sorted set | `[cursor, array]` |
|
||||
|
499
src/cmd.rs
Normal file
499
src/cmd.rs
Normal file
@ -0,0 +1,499 @@
|
||||
use crate::{error::DBError, protocol::Protocol, server::Server};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Cmd {
|
||||
Ping,
|
||||
Echo(String),
|
||||
Get(String),
|
||||
Set(String, String),
|
||||
SetPx(String, String, u128),
|
||||
SetEx(String, String, u128),
|
||||
Keys,
|
||||
ConfigGet(String),
|
||||
Info(Option<String>),
|
||||
Del(String),
|
||||
Type(String),
|
||||
Incr(String),
|
||||
Multi,
|
||||
Exec,
|
||||
Discard,
|
||||
// Hash commands
|
||||
HSet(String, Vec<(String, String)>),
|
||||
HGet(String, String),
|
||||
HGetAll(String),
|
||||
HDel(String, Vec<String>),
|
||||
HExists(String, String),
|
||||
HKeys(String),
|
||||
HVals(String),
|
||||
HLen(String),
|
||||
HMGet(String, Vec<String>),
|
||||
HSetNx(String, String, String),
|
||||
Scan(u64, Option<String>, Option<u64>), // cursor, pattern, count
|
||||
Unknow,
|
||||
}
|
||||
|
||||
impl Cmd {
|
||||
pub fn from(s: &str) -> Result<(Self, Protocol), DBError> {
|
||||
let protocol = Protocol::from(s)?;
|
||||
match protocol.clone().0 {
|
||||
Protocol::Array(p) => {
|
||||
let cmd = p.into_iter().map(|x| x.decode()).collect::<Vec<_>>();
|
||||
if cmd.is_empty() {
|
||||
return Err(DBError("cmd length is 0".to_string()));
|
||||
}
|
||||
Ok((
|
||||
match cmd[0].to_lowercase().as_str() {
|
||||
"echo" => Cmd::Echo(cmd[1].clone()),
|
||||
"ping" => Cmd::Ping,
|
||||
"get" => Cmd::Get(cmd[1].clone()),
|
||||
"set" => {
|
||||
if cmd.len() == 5 && cmd[3].to_lowercase() == "px" {
|
||||
Cmd::SetPx(cmd[1].clone(), cmd[2].clone(), cmd[4].parse().unwrap())
|
||||
} else if cmd.len() == 5 && cmd[3].to_lowercase() == "ex" {
|
||||
Cmd::SetEx(cmd[1].clone(), cmd[2].clone(), cmd[4].parse().unwrap())
|
||||
} else if cmd.len() == 3 {
|
||||
Cmd::Set(cmd[1].clone(), cmd[2].clone())
|
||||
} else {
|
||||
return Err(DBError(format!("unsupported cmd {:?}", cmd)));
|
||||
}
|
||||
}
|
||||
"config" => {
|
||||
if cmd.len() != 3 || cmd[1].to_lowercase() != "get" {
|
||||
return Err(DBError(format!("unsupported cmd {:?}", cmd)));
|
||||
} else {
|
||||
Cmd::ConfigGet(cmd[2].clone())
|
||||
}
|
||||
}
|
||||
"keys" => {
|
||||
if cmd.len() != 2 || cmd[1] != "*" {
|
||||
return Err(DBError(format!("unsupported cmd {:?}", cmd)));
|
||||
} else {
|
||||
Cmd::Keys
|
||||
}
|
||||
}
|
||||
"info" => {
|
||||
let section = if cmd.len() == 2 {
|
||||
Some(cmd[1].clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Cmd::Info(section)
|
||||
}
|
||||
"del" => {
|
||||
if cmd.len() != 2 {
|
||||
return Err(DBError(format!("unsupported cmd {:?}", cmd)));
|
||||
}
|
||||
Cmd::Del(cmd[1].clone())
|
||||
}
|
||||
"type" => {
|
||||
if cmd.len() != 2 {
|
||||
return Err(DBError(format!("unsupported cmd {:?}", cmd)));
|
||||
}
|
||||
Cmd::Type(cmd[1].clone())
|
||||
}
|
||||
"incr" => {
|
||||
if cmd.len() != 2 {
|
||||
return Err(DBError(format!("unsupported cmd {:?}", cmd)));
|
||||
}
|
||||
Cmd::Incr(cmd[1].clone())
|
||||
}
|
||||
"multi" => {
|
||||
if cmd.len() != 1 {
|
||||
return Err(DBError(format!("unsupported cmd {:?}", cmd)));
|
||||
}
|
||||
Cmd::Multi
|
||||
}
|
||||
"exec" => {
|
||||
if cmd.len() != 1 {
|
||||
return Err(DBError(format!("unsupported cmd {:?}", cmd)));
|
||||
}
|
||||
Cmd::Exec
|
||||
}
|
||||
"discard" => Cmd::Discard,
|
||||
// Hash commands
|
||||
"hset" => {
|
||||
if cmd.len() < 4 || (cmd.len() - 2) % 2 != 0 {
|
||||
return Err(DBError(format!("wrong number of arguments for HSET command")));
|
||||
}
|
||||
let mut pairs = Vec::new();
|
||||
let mut i = 2;
|
||||
while i < cmd.len() - 1 {
|
||||
pairs.push((cmd[i].clone(), cmd[i + 1].clone()));
|
||||
i += 2;
|
||||
}
|
||||
Cmd::HSet(cmd[1].clone(), pairs)
|
||||
}
|
||||
"hget" => {
|
||||
if cmd.len() != 3 {
|
||||
return Err(DBError(format!("wrong number of arguments for HGET command")));
|
||||
}
|
||||
Cmd::HGet(cmd[1].clone(), cmd[2].clone())
|
||||
}
|
||||
"hgetall" => {
|
||||
if cmd.len() != 2 {
|
||||
return Err(DBError(format!("wrong number of arguments for HGETALL command")));
|
||||
}
|
||||
Cmd::HGetAll(cmd[1].clone())
|
||||
}
|
||||
"hdel" => {
|
||||
if cmd.len() < 3 {
|
||||
return Err(DBError(format!("wrong number of arguments for HDEL command")));
|
||||
}
|
||||
Cmd::HDel(cmd[1].clone(), cmd[2..].to_vec())
|
||||
}
|
||||
"hexists" => {
|
||||
if cmd.len() != 3 {
|
||||
return Err(DBError(format!("wrong number of arguments for HEXISTS command")));
|
||||
}
|
||||
Cmd::HExists(cmd[1].clone(), cmd[2].clone())
|
||||
}
|
||||
"hkeys" => {
|
||||
if cmd.len() != 2 {
|
||||
return Err(DBError(format!("wrong number of arguments for HKEYS command")));
|
||||
}
|
||||
Cmd::HKeys(cmd[1].clone())
|
||||
}
|
||||
"hvals" => {
|
||||
if cmd.len() != 2 {
|
||||
return Err(DBError(format!("wrong number of arguments for HVALS command")));
|
||||
}
|
||||
Cmd::HVals(cmd[1].clone())
|
||||
}
|
||||
"hlen" => {
|
||||
if cmd.len() != 2 {
|
||||
return Err(DBError(format!("wrong number of arguments for HLEN command")));
|
||||
}
|
||||
Cmd::HLen(cmd[1].clone())
|
||||
}
|
||||
"hmget" => {
|
||||
if cmd.len() < 3 {
|
||||
return Err(DBError(format!("wrong number of arguments for HMGET command")));
|
||||
}
|
||||
Cmd::HMGet(cmd[1].clone(), cmd[2..].to_vec())
|
||||
}
|
||||
"hsetnx" => {
|
||||
if cmd.len() != 4 {
|
||||
return Err(DBError(format!("wrong number of arguments for HSETNX command")));
|
||||
}
|
||||
Cmd::HSetNx(cmd[1].clone(), cmd[2].clone(), cmd[3].clone())
|
||||
}
|
||||
"scan" => {
|
||||
if cmd.len() < 2 {
|
||||
return Err(DBError(format!("wrong number of arguments for SCAN command")));
|
||||
}
|
||||
|
||||
let cursor = cmd[1].parse::<u64>().map_err(|_|
|
||||
DBError("ERR invalid cursor".to_string()))?;
|
||||
|
||||
let mut pattern = None;
|
||||
let mut count = None;
|
||||
let mut i = 2;
|
||||
|
||||
while i < cmd.len() {
|
||||
match cmd[i].to_lowercase().as_str() {
|
||||
"match" => {
|
||||
if i + 1 >= cmd.len() {
|
||||
return Err(DBError("ERR syntax error".to_string()));
|
||||
}
|
||||
pattern = Some(cmd[i + 1].clone());
|
||||
i += 2;
|
||||
}
|
||||
"count" => {
|
||||
if i + 1 >= cmd.len() {
|
||||
return Err(DBError("ERR syntax error".to_string()));
|
||||
}
|
||||
count = Some(cmd[i + 1].parse::<u64>().map_err(|_|
|
||||
DBError("ERR value is not an integer or out of range".to_string()))?);
|
||||
i += 2;
|
||||
}
|
||||
_ => {
|
||||
return Err(DBError(format!("ERR syntax error")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cmd::Scan(cursor, pattern, count)
|
||||
}
|
||||
_ => Cmd::Unknow,
|
||||
},
|
||||
protocol.0,
|
||||
))
|
||||
}
|
||||
_ => Err(DBError(format!(
|
||||
"fail to parse as cmd for {:?}",
|
||||
protocol.0
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
&self,
|
||||
server: &Server,
|
||||
protocol: Protocol,
|
||||
queued_cmd: &mut Option<Vec<(Cmd, Protocol)>>,
|
||||
) -> Result<Protocol, DBError> {
|
||||
// Handle queued commands for transactions
|
||||
if queued_cmd.is_some()
|
||||
&& !matches!(self, Cmd::Exec)
|
||||
&& !matches!(self, Cmd::Multi)
|
||||
&& !matches!(self, Cmd::Discard)
|
||||
{
|
||||
queued_cmd
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.push((self.clone(), protocol.clone()));
|
||||
return Ok(Protocol::SimpleString("QUEUED".to_string()));
|
||||
}
|
||||
|
||||
match self {
|
||||
Cmd::Ping => Ok(Protocol::SimpleString("PONG".to_string())),
|
||||
Cmd::Echo(s) => Ok(Protocol::SimpleString(s.clone())),
|
||||
Cmd::Get(k) => get_cmd(server, k).await,
|
||||
Cmd::Set(k, v) => set_cmd(server, k, v).await,
|
||||
Cmd::SetPx(k, v, x) => set_px_cmd(server, k, v, x).await,
|
||||
Cmd::SetEx(k, v, x) => set_ex_cmd(server, k, v, x).await,
|
||||
Cmd::Del(k) => del_cmd(server, k).await,
|
||||
Cmd::ConfigGet(name) => config_get_cmd(name, server),
|
||||
Cmd::Keys => keys_cmd(server).await,
|
||||
Cmd::Info(section) => info_cmd(section),
|
||||
Cmd::Type(k) => type_cmd(server, k).await,
|
||||
Cmd::Incr(key) => incr_cmd(server, key).await,
|
||||
Cmd::Multi => {
|
||||
*queued_cmd = Some(Vec::<(Cmd, Protocol)>::new());
|
||||
Ok(Protocol::SimpleString("OK".to_string()))
|
||||
}
|
||||
Cmd::Exec => exec_cmd(queued_cmd, server).await,
|
||||
Cmd::Discard => {
|
||||
if queued_cmd.is_some() {
|
||||
*queued_cmd = None;
|
||||
Ok(Protocol::SimpleString("OK".to_string()))
|
||||
} else {
|
||||
Ok(Protocol::err("ERR DISCARD without MULTI"))
|
||||
}
|
||||
}
|
||||
// Hash commands
|
||||
Cmd::HSet(key, pairs) => hset_cmd(server, key, pairs).await,
|
||||
Cmd::HGet(key, field) => hget_cmd(server, key, field).await,
|
||||
Cmd::HGetAll(key) => hgetall_cmd(server, key).await,
|
||||
Cmd::HDel(key, fields) => hdel_cmd(server, key, fields).await,
|
||||
Cmd::HExists(key, field) => hexists_cmd(server, key, field).await,
|
||||
Cmd::HKeys(key) => hkeys_cmd(server, key).await,
|
||||
Cmd::HVals(key) => hvals_cmd(server, key).await,
|
||||
Cmd::HLen(key) => hlen_cmd(server, key).await,
|
||||
Cmd::HMGet(key, fields) => hmget_cmd(server, key, fields).await,
|
||||
Cmd::HSetNx(key, field, value) => hsetnx_cmd(server, key, field, value).await,
|
||||
Cmd::Scan(cursor, pattern, count) => scan_cmd(server, cursor, pattern.as_deref(), count).await,
|
||||
Cmd::Unknow => Ok(Protocol::err("unknown cmd")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn exec_cmd(
|
||||
queued_cmd: &mut Option<Vec<(Cmd, Protocol)>>,
|
||||
server: &Server,
|
||||
) -> Result<Protocol, DBError> {
|
||||
if queued_cmd.is_some() {
|
||||
let mut vec = Vec::new();
|
||||
for (cmd, protocol) in queued_cmd.as_ref().unwrap() {
|
||||
let res = Box::pin(cmd.run(server, protocol.clone(), &mut None)).await?;
|
||||
vec.push(res);
|
||||
}
|
||||
*queued_cmd = None;
|
||||
Ok(Protocol::Array(vec))
|
||||
} else {
|
||||
Ok(Protocol::err("ERR EXEC without MULTI"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn incr_cmd(server: &Server, key: &String) -> Result<Protocol, DBError> {
|
||||
let current_value = server.storage.get(key)?;
|
||||
|
||||
let new_value = match current_value {
|
||||
Some(v) => {
|
||||
match v.parse::<i64>() {
|
||||
Ok(num) => num + 1,
|
||||
Err(_) => return Ok(Protocol::err("ERR value is not an integer or out of range")),
|
||||
}
|
||||
}
|
||||
None => 1,
|
||||
};
|
||||
|
||||
server.storage.set(key.clone(), new_value.to_string())?;
|
||||
Ok(Protocol::SimpleString(new_value.to_string()))
|
||||
}
|
||||
|
||||
fn config_get_cmd(name: &String, server: &Server) -> Result<Protocol, DBError> {
|
||||
match name.as_str() {
|
||||
"dir" => Ok(Protocol::Array(vec![
|
||||
Protocol::BulkString(name.clone()),
|
||||
Protocol::BulkString(server.option.dir.clone()),
|
||||
])),
|
||||
"dbfilename" => Ok(Protocol::Array(vec![
|
||||
Protocol::BulkString(name.clone()),
|
||||
Protocol::BulkString("herodb.redb".to_string()),
|
||||
])),
|
||||
_ => Err(DBError(format!("unsupported config {:?}", name))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn keys_cmd(server: &Server) -> Result<Protocol, DBError> {
|
||||
let keys = server.storage.keys("*")?;
|
||||
Ok(Protocol::Array(
|
||||
keys.into_iter().map(Protocol::BulkString).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
fn info_cmd(section: &Option<String>) -> Result<Protocol, DBError> {
|
||||
match section {
|
||||
Some(s) => match s.as_str() {
|
||||
"replication" => Ok(Protocol::BulkString(
|
||||
"role:master\nmaster_replid:8371b4fb1155b71f4a04d3e1bc3e18c4a990aeea\nmaster_repl_offset:0\n".to_string()
|
||||
)),
|
||||
_ => Err(DBError(format!("unsupported section {:?}", s))),
|
||||
},
|
||||
None => Ok(Protocol::BulkString("# Server\nredis_version:7.0.0\n".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn type_cmd(server: &Server, k: &String) -> Result<Protocol, DBError> {
|
||||
match server.storage.get_key_type(k)? {
|
||||
Some(type_str) => Ok(Protocol::SimpleString(type_str)),
|
||||
None => Ok(Protocol::SimpleString("none".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn del_cmd(server: &Server, k: &str) -> Result<Protocol, DBError> {
|
||||
server.storage.del(k.to_string())?;
|
||||
Ok(Protocol::SimpleString("1".to_string()))
|
||||
}
|
||||
|
||||
async fn set_ex_cmd(
|
||||
server: &Server,
|
||||
k: &str,
|
||||
v: &str,
|
||||
x: &u128,
|
||||
) -> Result<Protocol, DBError> {
|
||||
server.storage.setx(k.to_string(), v.to_string(), *x * 1000)?;
|
||||
Ok(Protocol::SimpleString("OK".to_string()))
|
||||
}
|
||||
|
||||
async fn set_px_cmd(
|
||||
server: &Server,
|
||||
k: &str,
|
||||
v: &str,
|
||||
x: &u128,
|
||||
) -> Result<Protocol, DBError> {
|
||||
server.storage.setx(k.to_string(), v.to_string(), *x)?;
|
||||
Ok(Protocol::SimpleString("OK".to_string()))
|
||||
}
|
||||
|
||||
async fn set_cmd(server: &Server, k: &str, v: &str) -> Result<Protocol, DBError> {
|
||||
server.storage.set(k.to_string(), v.to_string())?;
|
||||
Ok(Protocol::SimpleString("OK".to_string()))
|
||||
}
|
||||
|
||||
async fn get_cmd(server: &Server, k: &str) -> Result<Protocol, DBError> {
|
||||
let v = server.storage.get(k)?;
|
||||
Ok(v.map_or(Protocol::Null, Protocol::BulkString))
|
||||
}
|
||||
|
||||
// Hash command implementations
|
||||
async fn hset_cmd(server: &Server, key: &str, pairs: &[(String, String)]) -> Result<Protocol, DBError> {
|
||||
let new_fields = server.storage.hset(key, pairs)?;
|
||||
Ok(Protocol::SimpleString(new_fields.to_string()))
|
||||
}
|
||||
|
||||
async fn hget_cmd(server: &Server, key: &str, field: &str) -> Result<Protocol, DBError> {
|
||||
match server.storage.hget(key, field) {
|
||||
Ok(Some(value)) => Ok(Protocol::BulkString(value)),
|
||||
Ok(None) => Ok(Protocol::Null),
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn hgetall_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> {
|
||||
match server.storage.hgetall(key) {
|
||||
Ok(pairs) => {
|
||||
let mut result = Vec::new();
|
||||
for (field, value) in pairs {
|
||||
result.push(Protocol::BulkString(field));
|
||||
result.push(Protocol::BulkString(value));
|
||||
}
|
||||
Ok(Protocol::Array(result))
|
||||
}
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn hdel_cmd(server: &Server, key: &str, fields: &[String]) -> Result<Protocol, DBError> {
|
||||
match server.storage.hdel(key, fields) {
|
||||
Ok(deleted) => Ok(Protocol::SimpleString(deleted.to_string())),
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn hexists_cmd(server: &Server, key: &str, field: &str) -> Result<Protocol, DBError> {
|
||||
match server.storage.hexists(key, field) {
|
||||
Ok(exists) => Ok(Protocol::SimpleString(if exists { "1" } else { "0" }.to_string())),
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn hkeys_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> {
|
||||
match server.storage.hkeys(key) {
|
||||
Ok(keys) => Ok(Protocol::Array(
|
||||
keys.into_iter().map(Protocol::BulkString).collect(),
|
||||
)),
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn hvals_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> {
|
||||
match server.storage.hvals(key) {
|
||||
Ok(values) => Ok(Protocol::Array(
|
||||
values.into_iter().map(Protocol::BulkString).collect(),
|
||||
)),
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn hlen_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> {
|
||||
match server.storage.hlen(key) {
|
||||
Ok(len) => Ok(Protocol::SimpleString(len.to_string())),
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn hmget_cmd(server: &Server, key: &str, fields: &[String]) -> Result<Protocol, DBError> {
|
||||
match server.storage.hmget(key, fields) {
|
||||
Ok(values) => {
|
||||
let result: Vec<Protocol> = values
|
||||
.into_iter()
|
||||
.map(|v| v.map_or(Protocol::Null, Protocol::BulkString))
|
||||
.collect();
|
||||
Ok(Protocol::Array(result))
|
||||
}
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn hsetnx_cmd(server: &Server, key: &str, field: &str, value: &str) -> Result<Protocol, DBError> {
|
||||
match server.storage.hsetnx(key, field, value) {
|
||||
Ok(was_set) => Ok(Protocol::SimpleString(if was_set { "1" } else { "0" }.to_string())),
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn scan_cmd(server: &Server, cursor: &u64, pattern: Option<&str>, count: &Option<u64>) -> Result<Protocol, DBError> {
|
||||
match server.storage.scan(*cursor, pattern, *count) {
|
||||
Ok((next_cursor, keys)) => {
|
||||
let mut result = Vec::new();
|
||||
result.push(Protocol::BulkString(next_cursor.to_string()));
|
||||
result.push(Protocol::Array(
|
||||
keys.into_iter().map(Protocol::BulkString).collect(),
|
||||
));
|
||||
Ok(Protocol::Array(result))
|
||||
}
|
||||
Err(e) => Ok(Protocol::err(&e.0)),
|
||||
}
|
||||
}
|
83
src/error.rs
Normal file
83
src/error.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use redb;
|
||||
use bincode;
|
||||
|
||||
use crate::protocol::Protocol;
|
||||
|
||||
// todo: more error types
|
||||
#[derive(Debug)]
|
||||
pub struct DBError(pub String);
|
||||
|
||||
impl From<std::io::Error> for DBError {
|
||||
fn from(item: std::io::Error) -> Self {
|
||||
DBError(item.to_string().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for DBError {
|
||||
fn from(item: ParseIntError) -> Self {
|
||||
DBError(item.to_string().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::str::Utf8Error> for DBError {
|
||||
fn from(item: std::str::Utf8Error) -> Self {
|
||||
DBError(item.to_string().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for DBError {
|
||||
fn from(item: std::string::FromUtf8Error) -> Self {
|
||||
DBError(item.to_string().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redb::Error> for DBError {
|
||||
fn from(item: redb::Error) -> Self {
|
||||
DBError(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redb::DatabaseError> for DBError {
|
||||
fn from(item: redb::DatabaseError) -> Self {
|
||||
DBError(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redb::TransactionError> for DBError {
|
||||
fn from(item: redb::TransactionError) -> Self {
|
||||
DBError(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redb::TableError> for DBError {
|
||||
fn from(item: redb::TableError) -> Self {
|
||||
DBError(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redb::StorageError> for DBError {
|
||||
fn from(item: redb::StorageError) -> Self {
|
||||
DBError(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redb::CommitError> for DBError {
|
||||
fn from(item: redb::CommitError) -> Self {
|
||||
DBError(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<bincode::ErrorKind>> for DBError {
|
||||
fn from(item: Box<bincode::ErrorKind>) -> Self {
|
||||
DBError(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tokio::sync::mpsc::error::SendError<()>> for DBError {
|
||||
fn from(item: mpsc::error::SendError<()>) -> Self {
|
||||
DBError(item.to_string().clone())
|
||||
}
|
||||
}
|
6
src/lib.rs
Normal file
6
src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
||||
mod cmd;
|
||||
pub mod error;
|
||||
pub mod options;
|
||||
mod protocol;
|
||||
pub mod server;
|
||||
mod storage;
|
63
src/main.rs
Normal file
63
src/main.rs
Normal file
@ -0,0 +1,63 @@
|
||||
// #![allow(unused_imports)]
|
||||
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use redis_rs::server;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
/// Simple program to greet a person
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// The directory of Redis DB file
|
||||
#[arg(long)]
|
||||
dir: String,
|
||||
|
||||
|
||||
/// The port of the Redis server, default is 6379 if not specified
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// parse args
|
||||
let args = Args::parse();
|
||||
|
||||
// bind port
|
||||
let port = args.port.unwrap_or(6379);
|
||||
println!("will listen on port: {}", port);
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// new DB option
|
||||
let option = redis_rs::options::DBOption {
|
||||
dir: args.dir,
|
||||
port,
|
||||
};
|
||||
|
||||
// new server
|
||||
let server = server::Server::new(option).await;
|
||||
|
||||
// accept new connections
|
||||
loop {
|
||||
let stream = listener.accept().await;
|
||||
match stream {
|
||||
Ok((stream, _)) => {
|
||||
println!("accepted new connection");
|
||||
|
||||
let mut sc = server.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = sc.handle(stream).await {
|
||||
println!("error: {:?}, will close the connection. Bye", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
println!("error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
src/options.rs
Normal file
5
src/options.rs
Normal file
@ -0,0 +1,5 @@
|
||||
#[derive(Clone)]
|
||||
pub struct DBOption {
|
||||
pub dir: String,
|
||||
pub port: u16,
|
||||
}
|
176
src/protocol.rs
Normal file
176
src/protocol.rs
Normal file
@ -0,0 +1,176 @@
|
||||
use core::fmt;
|
||||
|
||||
use crate::error::DBError;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Protocol {
|
||||
SimpleString(String),
|
||||
BulkString(String),
|
||||
Null,
|
||||
Array(Vec<Protocol>),
|
||||
}
|
||||
|
||||
impl fmt::Display for Protocol {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.decode().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Protocol {
|
||||
pub fn from(protocol: &str) -> Result<(Self, usize), DBError> {
|
||||
let ret = match protocol.chars().nth(0) {
|
||||
Some('+') => Self::parse_simple_string_sfx(&protocol[1..]),
|
||||
Some('$') => Self::parse_bulk_string_sfx(&protocol[1..]),
|
||||
Some('*') => Self::parse_array_sfx(&protocol[1..]),
|
||||
_ => Err(DBError(format!(
|
||||
"[from] unsupported protocol: {:?}",
|
||||
protocol
|
||||
))),
|
||||
};
|
||||
match ret {
|
||||
Ok((p, s)) => Ok((p, s + 1)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_vec(array: Vec<&str>) -> Self {
|
||||
let array = array
|
||||
.into_iter()
|
||||
.map(|x| Protocol::BulkString(x.to_string()))
|
||||
.collect();
|
||||
Protocol::Array(array)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ok() -> Self {
|
||||
Protocol::SimpleString("ok".to_string())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn err(msg: &str) -> Self {
|
||||
Protocol::SimpleString(msg.to_string())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_on_slave_err() -> Self {
|
||||
Self::err("DISALLOW WRITE ON SLAVE")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn psync_on_slave_err() -> Self {
|
||||
Self::err("PSYNC ON SLAVE IS NOT ALLOWED")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn none() -> Self {
|
||||
Self::SimpleString("none".to_string())
|
||||
}
|
||||
|
||||
pub fn decode(&self) -> String {
|
||||
match self {
|
||||
Protocol::SimpleString(s) => s.to_string(),
|
||||
Protocol::BulkString(s) => s.to_string(),
|
||||
Protocol::Null => "".to_string(),
|
||||
Protocol::Array(s) => s.iter().map(|x| x.decode()).collect::<Vec<_>>().join(" "),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> String {
|
||||
match self {
|
||||
Protocol::SimpleString(s) => format!("+{}\r\n", s),
|
||||
Protocol::BulkString(s) => format!("${}\r\n{}\r\n", s.len(), s),
|
||||
Protocol::Array(ss) => {
|
||||
format!("*{}\r\n", ss.len())
|
||||
+ ss.iter()
|
||||
.map(|x| x.encode())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
.as_str()
|
||||
}
|
||||
Protocol::Null => "$-1\r\n".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_simple_string_sfx(protocol: &str) -> Result<(Self, usize), DBError> {
|
||||
match protocol.find("\r\n") {
|
||||
Some(x) => Ok((Self::SimpleString(protocol[..x].to_string()), x + 2)),
|
||||
_ => Err(DBError(format!(
|
||||
"[new simple string] unsupported protocol: {:?}",
|
||||
protocol
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bulk_string_sfx(protocol: &str) -> Result<(Self, usize), DBError> {
|
||||
if let Some(len) = protocol.find("\r\n") {
|
||||
let size = Self::parse_usize(&protocol[..len])?;
|
||||
if let Some(data_len) = protocol[len + 2..].find("\r\n") {
|
||||
let s = Self::parse_string(&protocol[len + 2..len + 2 + data_len])?;
|
||||
if size != s.len() {
|
||||
Err(DBError(format!(
|
||||
"[new bulk string] unmatched string length in prototocl {:?}",
|
||||
protocol,
|
||||
)))
|
||||
} else {
|
||||
Ok((
|
||||
Protocol::BulkString(s.to_lowercase()),
|
||||
len + 2 + data_len + 2,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(DBError(format!(
|
||||
"[new bulk string] unsupported protocol: {:?}",
|
||||
protocol
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
Err(DBError(format!(
|
||||
"[new bulk string] unsupported protocol: {:?}",
|
||||
protocol
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_array_sfx(s: &str) -> Result<(Self, usize), DBError> {
|
||||
let mut offset = 0;
|
||||
match s.find("\r\n") {
|
||||
Some(x) => {
|
||||
let array_len = s[..x].parse::<usize>()?;
|
||||
offset += x + 2;
|
||||
let mut vec = vec![];
|
||||
for _ in 0..array_len {
|
||||
match Protocol::from(&s[offset..]) {
|
||||
Ok((p, len)) => {
|
||||
offset += len;
|
||||
vec.push(p);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((Protocol::Array(vec), offset))
|
||||
}
|
||||
_ => Err(DBError(format!(
|
||||
"[new array] unsupported protocol: {:?}",
|
||||
s
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_usize(protocol: &str) -> Result<usize, DBError> {
|
||||
match protocol.len() {
|
||||
0 => Err(DBError(format!("parse usize error: {:?}", protocol))),
|
||||
_ => Ok(protocol
|
||||
.parse::<usize>()
|
||||
.map_err(|_| DBError(format!("parse usize error: {}", protocol)))?),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_string(protocol: &str) -> Result<String, DBError> {
|
||||
match protocol.len() {
|
||||
0 => Err(DBError(format!("parse usize error: {:?}", protocol))),
|
||||
_ => Ok(protocol.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
68
src/server.rs
Normal file
68
src/server.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use core::str;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::cmd::Cmd;
|
||||
use crate::error::DBError;
|
||||
use crate::options;
|
||||
use crate::protocol::Protocol;
|
||||
use crate::storage::Storage;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Server {
|
||||
pub storage: Arc<Storage>,
|
||||
pub option: options::DBOption,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn new(option: options::DBOption) -> Self {
|
||||
// Create database file path with fixed filename
|
||||
let db_file_path = PathBuf::from(option.dir.clone()).join("herodb.redb");
|
||||
println!("will open db file path: {}", db_file_path.display());
|
||||
|
||||
// Initialize storage with redb
|
||||
let storage = Storage::new(db_file_path).expect("Failed to initialize storage");
|
||||
|
||||
Server {
|
||||
storage: Arc::new(storage),
|
||||
option,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle(
|
||||
&mut self,
|
||||
mut stream: tokio::net::TcpStream,
|
||||
) -> Result<(), DBError> {
|
||||
let mut buf = [0; 512];
|
||||
let mut queued_cmd: Option<Vec<(Cmd, Protocol)>> = None;
|
||||
|
||||
loop {
|
||||
if let Ok(len) = stream.read(&mut buf).await {
|
||||
if len == 0 {
|
||||
println!("[handle] connection closed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let s = str::from_utf8(&buf[..len])?;
|
||||
let (cmd, protocol) =
|
||||
Cmd::from(s).unwrap_or((Cmd::Unknow, Protocol::err("unknow cmd")));
|
||||
println!("got command: {:?}, protocol: {:?}", cmd, protocol);
|
||||
|
||||
let res = cmd
|
||||
.run(self, protocol, &mut queued_cmd)
|
||||
.await
|
||||
.unwrap_or(Protocol::err("unknow cmd"));
|
||||
print!("queued cmd {:?}", queued_cmd);
|
||||
|
||||
println!("going to send response {}", res.encode());
|
||||
_ = stream.write(res.encode().as_bytes()).await?;
|
||||
} else {
|
||||
println!("[handle] going to break");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
509
src/storage.rs
Normal file
509
src/storage.rs
Normal file
@ -0,0 +1,509 @@
|
||||
use std::{
|
||||
path::Path,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use redb::{Database, Error, ReadableTable, Table, TableDefinition, WriteTransaction, ReadTransaction};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::DBError;
|
||||
|
||||
// Table definitions for different Redis data types
|
||||
const TYPES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("types");
|
||||
const STRINGS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("strings");
|
||||
const HASHES_TABLE: TableDefinition<(&str, &str), &str> = TableDefinition::new("hashes");
|
||||
const STREAMS_META_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("streams_meta");
|
||||
const STREAMS_DATA_TABLE: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("streams_data");
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct StringValue {
|
||||
pub value: String,
|
||||
pub expires_at_ms: Option<u128>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct StreamEntry {
|
||||
pub fields: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn now_in_millis() -> u128 {
|
||||
let start = SystemTime::now();
|
||||
let duration_since_epoch = start.duration_since(UNIX_EPOCH).unwrap();
|
||||
duration_since_epoch.as_millis()
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn new(path: impl AsRef<Path>) -> Result<Self, DBError> {
|
||||
let db = Database::create(path)?;
|
||||
|
||||
// Create tables if they don't exist
|
||||
let write_txn = db.begin_write()?;
|
||||
{
|
||||
let _ = write_txn.open_table(TYPES_TABLE)?;
|
||||
let _ = write_txn.open_table(STRINGS_TABLE)?;
|
||||
let _ = write_txn.open_table(HASHES_TABLE)?;
|
||||
let _ = write_txn.open_table(STREAMS_META_TABLE)?;
|
||||
let _ = write_txn.open_table(STREAMS_DATA_TABLE)?;
|
||||
}
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(Storage { db })
|
||||
}
|
||||
|
||||
pub fn get_key_type(&self, key: &str) -> Result<Option<String>, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(TYPES_TABLE)?;
|
||||
|
||||
match table.get(key)? {
|
||||
Some(type_val) => Ok(Some(type_val.value().to_string())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Result<Option<String>, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
|
||||
// Check if key exists and is of string type
|
||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||
match types_table.get(key)? {
|
||||
Some(type_val) if type_val.value() == "string" => {
|
||||
let strings_table = read_txn.open_table(STRINGS_TABLE)?;
|
||||
match strings_table.get(key)? {
|
||||
Some(data) => {
|
||||
let string_value: StringValue = bincode::deserialize(data.value())?;
|
||||
|
||||
// Check if expired
|
||||
if let Some(expires_at) = string_value.expires_at_ms {
|
||||
if now_in_millis() > expires_at {
|
||||
// Key expired, remove it
|
||||
drop(read_txn);
|
||||
self.del(key.to_string())?;
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(string_value.value))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&self, key: String, value: String) -> Result<(), DBError> {
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||
types_table.insert(key.as_str(), "string")?;
|
||||
|
||||
let mut strings_table = write_txn.open_table(STRINGS_TABLE)?;
|
||||
let string_value = StringValue {
|
||||
value,
|
||||
expires_at_ms: None,
|
||||
};
|
||||
let serialized = bincode::serialize(&string_value)?;
|
||||
strings_table.insert(key.as_str(), serialized.as_slice())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setx(&self, key: String, value: String, expire_ms: u128) -> Result<(), DBError> {
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||
types_table.insert(key.as_str(), "string")?;
|
||||
|
||||
let mut strings_table = write_txn.open_table(STRINGS_TABLE)?;
|
||||
let string_value = StringValue {
|
||||
value,
|
||||
expires_at_ms: Some(expire_ms + now_in_millis()),
|
||||
};
|
||||
let serialized = bincode::serialize(&string_value)?;
|
||||
strings_table.insert(key.as_str(), serialized.as_slice())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn del(&self, key: String) -> Result<(), DBError> {
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||
let mut strings_table = write_txn.open_table(STRINGS_TABLE)?;
|
||||
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||
|
||||
// Remove from type table
|
||||
types_table.remove(key.as_str())?;
|
||||
|
||||
// Remove from strings table
|
||||
strings_table.remove(key.as_str())?;
|
||||
|
||||
// Remove all hash fields for this key
|
||||
let mut to_remove = Vec::new();
|
||||
let mut iter = hashes_table.iter()?;
|
||||
while let Some(entry) = iter.next() {
|
||||
let entry = entry?;
|
||||
let (hash_key, field) = entry.0.value();
|
||||
if hash_key == key.as_str() {
|
||||
to_remove.push((hash_key.to_string(), field.to_string()));
|
||||
}
|
||||
}
|
||||
drop(iter);
|
||||
|
||||
for (hash_key, field) in to_remove {
|
||||
hashes_table.remove((hash_key.as_str(), field.as_str()))?;
|
||||
}
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn keys(&self, pattern: &str) -> Result<Vec<String>, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(TYPES_TABLE)?;
|
||||
|
||||
let mut keys = Vec::new();
|
||||
let mut iter = table.iter()?;
|
||||
while let Some(entry) = iter.next() {
|
||||
let key = entry?.0.value().to_string();
|
||||
if pattern == "*" || key.contains(pattern) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
// Hash operations
|
||||
pub fn hset(&self, key: &str, pairs: &[(String, String)]) -> Result<u64, DBError> {
|
||||
let write_txn = self.db.begin_write()?;
|
||||
let mut new_fields = 0u64;
|
||||
|
||||
{
|
||||
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||
|
||||
// Check if key exists and is of correct type
|
||||
let existing_type = match types_table.get(key)? {
|
||||
Some(type_val) => Some(type_val.value().to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
match existing_type {
|
||||
Some(ref type_str) if type_str != "hash" => {
|
||||
return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string()));
|
||||
}
|
||||
None => {
|
||||
// Set type to hash
|
||||
types_table.insert(key, "hash")?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for (field, value) in pairs {
|
||||
let existed = hashes_table.get((key, field.as_str()))?.is_some();
|
||||
hashes_table.insert((key, field.as_str()), value.as_str())?;
|
||||
if !existed {
|
||||
new_fields += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
Ok(new_fields)
|
||||
}
|
||||
|
||||
pub fn hget(&self, key: &str, field: &str) -> Result<Option<String>, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
|
||||
// Check type
|
||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||
match types_table.get(key)? {
|
||||
Some(type_val) if type_val.value() == "hash" => {
|
||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||
match hashes_table.get((key, field))? {
|
||||
Some(value) => Ok(Some(value.value().to_string())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hgetall(&self, key: &str) -> Result<Vec<(String, String)>, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
|
||||
// Check type
|
||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||
match types_table.get(key)? {
|
||||
Some(type_val) if type_val.value() == "hash" => {
|
||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||
let mut result = Vec::new();
|
||||
|
||||
let mut iter = hashes_table.iter()?;
|
||||
while let Some(entry) = iter.next() {
|
||||
let entry = entry?;
|
||||
let (hash_key, field) = entry.0.value();
|
||||
let value = entry.1.value();
|
||||
if hash_key == key {
|
||||
result.push((field.to_string(), value.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
||||
None => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hdel(&self, key: &str, fields: &[String]) -> Result<u64, DBError> {
|
||||
let write_txn = self.db.begin_write()?;
|
||||
let mut deleted = 0u64;
|
||||
|
||||
{
|
||||
let types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||
let key_type = types_table.get(key)?;
|
||||
match key_type {
|
||||
Some(type_val) if type_val.value() == "hash" => {
|
||||
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||
|
||||
for field in fields {
|
||||
if hashes_table.remove((key, field.as_str()))?.is_some() {
|
||||
deleted += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
pub fn hexists(&self, key: &str, field: &str) -> Result<bool, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
|
||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||
match types_table.get(key)? {
|
||||
Some(type_val) if type_val.value() == "hash" => {
|
||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||
Ok(hashes_table.get((key, field))?.is_some())
|
||||
}
|
||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hkeys(&self, key: &str) -> Result<Vec<String>, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
|
||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||
match types_table.get(key)? {
|
||||
Some(type_val) if type_val.value() == "hash" => {
|
||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||
let mut result = Vec::new();
|
||||
|
||||
let mut iter = hashes_table.iter()?;
|
||||
while let Some(entry) = iter.next() {
|
||||
let entry = entry?;
|
||||
let (hash_key, field) = entry.0.value();
|
||||
if hash_key == key {
|
||||
result.push(field.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
||||
None => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hvals(&self, key: &str) -> Result<Vec<String>, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
|
||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||
match types_table.get(key)? {
|
||||
Some(type_val) if type_val.value() == "hash" => {
|
||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||
let mut result = Vec::new();
|
||||
|
||||
let mut iter = hashes_table.iter()?;
|
||||
while let Some(entry) = iter.next() {
|
||||
let entry = entry?;
|
||||
let (hash_key, _) = entry.0.value();
|
||||
let value = entry.1.value();
|
||||
if hash_key == key {
|
||||
result.push(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
||||
None => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hlen(&self, key: &str) -> Result<u64, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
|
||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||
match types_table.get(key)? {
|
||||
Some(type_val) if type_val.value() == "hash" => {
|
||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||
let mut count = 0u64;
|
||||
|
||||
let mut iter = hashes_table.iter()?;
|
||||
while let Some(entry) = iter.next() {
|
||||
let entry = entry?;
|
||||
let (hash_key, _) = entry.0.value();
|
||||
if hash_key == key {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hmget(&self, key: &str, fields: &[String]) -> Result<Vec<Option<String>>, DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
|
||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||
match types_table.get(key)? {
|
||||
Some(type_val) if type_val.value() == "hash" => {
|
||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||
let mut result = Vec::new();
|
||||
|
||||
for field in fields {
|
||||
match hashes_table.get((key, field.as_str()))? {
|
||||
Some(value) => result.push(Some(value.value().to_string())),
|
||||
None => result.push(None),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
||||
None => Ok(fields.iter().map(|_| None).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hsetnx(&self, key: &str, field: &str, value: &str) -> Result<bool, DBError> {
|
||||
let write_txn = self.db.begin_write()?;
|
||||
let mut result = false;
|
||||
|
||||
{
|
||||
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||
|
||||
// Check if key exists and is of correct type
|
||||
let existing_type = match types_table.get(key)? {
|
||||
Some(type_val) => Some(type_val.value().to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
match existing_type {
|
||||
Some(ref type_str) if type_str != "hash" => {
|
||||
return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string()));
|
||||
}
|
||||
None => {
|
||||
// Set type to hash
|
||||
types_table.insert(key, "hash")?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Check if field already exists
|
||||
if hashes_table.get((key, field))?.is_none() {
|
||||
hashes_table.insert((key, field), value)?;
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn scan(&self, cursor: u64, pattern: Option<&str>, count: Option<u64>) -> Result<(u64, Vec<String>), DBError> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(TYPES_TABLE)?;
|
||||
|
||||
let count = count.unwrap_or(10); // Default count is 10
|
||||
let mut keys = Vec::new();
|
||||
let mut current_cursor = 0u64;
|
||||
let mut returned_keys = 0u64;
|
||||
|
||||
let mut iter = table.iter()?;
|
||||
while let Some(entry) = iter.next() {
|
||||
let key = entry?.0.value().to_string();
|
||||
|
||||
// Skip keys until we reach the cursor position
|
||||
if current_cursor < cursor {
|
||||
current_cursor += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if key matches pattern
|
||||
let matches = match pattern {
|
||||
Some(pat) => {
|
||||
if pat == "*" {
|
||||
true
|
||||
} else if pat.contains('*') {
|
||||
// Simple glob pattern matching
|
||||
let pattern_parts: Vec<&str> = pat.split('*').collect();
|
||||
if pattern_parts.len() == 2 {
|
||||
let prefix = pattern_parts[0];
|
||||
let suffix = pattern_parts[1];
|
||||
key.starts_with(prefix) && key.ends_with(suffix)
|
||||
} else {
|
||||
key.contains(&pat.replace('*', ""))
|
||||
}
|
||||
} else {
|
||||
key.contains(pat)
|
||||
}
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
if matches {
|
||||
keys.push(key);
|
||||
returned_keys += 1;
|
||||
|
||||
// Stop if we've returned enough keys
|
||||
if returned_keys >= count {
|
||||
current_cursor += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
current_cursor += 1;
|
||||
}
|
||||
|
||||
// If we've reached the end of iteration, return cursor 0 to indicate completion
|
||||
let next_cursor = if returned_keys < count { 0 } else { current_cursor };
|
||||
|
||||
Ok((next_cursor, keys))
|
||||
}
|
||||
}
|
355
test_herodb.sh
Executable file
355
test_herodb.sh
Executable file
@ -0,0 +1,355 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for HeroDB - Redis-compatible database with redb backend
|
||||
# This script starts the server and runs comprehensive tests
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
DB_DIR="./test_db"
|
||||
PORT=6379
|
||||
SERVER_PID=""
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to cleanup on exit
|
||||
cleanup() {
|
||||
if [ ! -z "$SERVER_PID" ]; then
|
||||
print_status "Stopping HeroDB server (PID: $SERVER_PID)..."
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
wait $SERVER_PID 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Clean up test database
|
||||
if [ -d "$DB_DIR" ]; then
|
||||
print_status "Cleaning up test database directory..."
|
||||
rm -rf "$DB_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# Set trap to cleanup on script exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Function to wait for server to start
|
||||
wait_for_server() {
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
print_status "Waiting for server to start on port $PORT..."
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if nc -z localhost $PORT 2>/dev/null; then
|
||||
print_success "Server is ready!"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
print_error "Server failed to start within $max_attempts seconds"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to send Redis command and get response
|
||||
redis_cmd() {
|
||||
local cmd="$1"
|
||||
local expected="$2"
|
||||
|
||||
print_status "Testing: $cmd"
|
||||
|
||||
local result=$(echo "$cmd" | redis-cli -p $PORT --raw 2>/dev/null || echo "ERROR")
|
||||
|
||||
if [ "$expected" != "" ] && [ "$result" != "$expected" ]; then
|
||||
print_error "Expected: '$expected', Got: '$result'"
|
||||
return 1
|
||||
else
|
||||
print_success "✓ $cmd -> $result"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to test basic string operations
|
||||
test_string_operations() {
|
||||
print_status "=== Testing String Operations ==="
|
||||
|
||||
redis_cmd "PING" "PONG"
|
||||
redis_cmd "SET mykey hello" "OK"
|
||||
redis_cmd "GET mykey" "hello"
|
||||
redis_cmd "SET counter 1" "OK"
|
||||
redis_cmd "INCR counter" "2"
|
||||
redis_cmd "INCR counter" "3"
|
||||
redis_cmd "GET counter" "3"
|
||||
redis_cmd "DEL mykey" "1"
|
||||
redis_cmd "GET mykey" ""
|
||||
redis_cmd "TYPE counter" "string"
|
||||
redis_cmd "TYPE nonexistent" "none"
|
||||
}
|
||||
|
||||
# Function to test hash operations
|
||||
test_hash_operations() {
|
||||
print_status "=== Testing Hash Operations ==="
|
||||
|
||||
# HSET and HGET
|
||||
redis_cmd "HSET user:1 name John" "1"
|
||||
redis_cmd "HSET user:1 age 30 city NYC" "2"
|
||||
redis_cmd "HGET user:1 name" "John"
|
||||
redis_cmd "HGET user:1 age" "30"
|
||||
redis_cmd "HGET user:1 nonexistent" ""
|
||||
|
||||
# HGETALL
|
||||
print_status "Testing HGETALL user:1"
|
||||
redis_cmd "HGETALL user:1" ""
|
||||
|
||||
# HEXISTS
|
||||
redis_cmd "HEXISTS user:1 name" "1"
|
||||
redis_cmd "HEXISTS user:1 nonexistent" "0"
|
||||
|
||||
# HKEYS
|
||||
print_status "Testing HKEYS user:1"
|
||||
redis_cmd "HKEYS user:1" ""
|
||||
|
||||
# HVALS
|
||||
print_status "Testing HVALS user:1"
|
||||
redis_cmd "HVALS user:1" ""
|
||||
|
||||
# HLEN
|
||||
redis_cmd "HLEN user:1" "3"
|
||||
|
||||
# HMGET
|
||||
print_status "Testing HMGET user:1 name age"
|
||||
redis_cmd "HMGET user:1 name age" ""
|
||||
|
||||
# HSETNX
|
||||
redis_cmd "HSETNX user:1 name Jane" "0" # Should not set, field exists
|
||||
redis_cmd "HSETNX user:1 email john@example.com" "1" # Should set, new field
|
||||
redis_cmd "HGET user:1 email" "john@example.com"
|
||||
|
||||
# HDEL
|
||||
redis_cmd "HDEL user:1 age city" "2"
|
||||
redis_cmd "HLEN user:1" "2"
|
||||
redis_cmd "HEXISTS user:1 age" "0"
|
||||
|
||||
# Test type checking
|
||||
redis_cmd "SET stringkey value" "OK"
|
||||
print_status "Testing WRONGTYPE error on string key"
|
||||
redis_cmd "HGET stringkey field" "" # Should return WRONGTYPE error
|
||||
}
|
||||
|
||||
# Function to test configuration commands
|
||||
test_config_operations() {
|
||||
print_status "=== Testing Configuration Operations ==="
|
||||
|
||||
print_status "Testing CONFIG GET dir"
|
||||
redis_cmd "CONFIG GET dir" ""
|
||||
|
||||
print_status "Testing CONFIG GET dbfilename"
|
||||
redis_cmd "CONFIG GET dbfilename" ""
|
||||
}
|
||||
|
||||
# Function to test transaction operations
|
||||
test_transaction_operations() {
|
||||
print_status "=== Testing Transaction Operations ==="
|
||||
|
||||
redis_cmd "MULTI" "OK"
|
||||
redis_cmd "SET tx_key1 value1" "QUEUED"
|
||||
redis_cmd "SET tx_key2 value2" "QUEUED"
|
||||
redis_cmd "INCR counter" "QUEUED"
|
||||
print_status "Testing EXEC"
|
||||
redis_cmd "EXEC" ""
|
||||
|
||||
redis_cmd "GET tx_key1" "value1"
|
||||
redis_cmd "GET tx_key2" "value2"
|
||||
|
||||
# Test DISCARD
|
||||
redis_cmd "MULTI" "OK"
|
||||
redis_cmd "SET discard_key value" "QUEUED"
|
||||
redis_cmd "DISCARD" "OK"
|
||||
redis_cmd "GET discard_key" ""
|
||||
}
|
||||
|
||||
# Function to test keys operations
|
||||
test_keys_operations() {
|
||||
print_status "=== Testing Keys Operations ==="
|
||||
|
||||
print_status "Testing KEYS *"
|
||||
redis_cmd "KEYS *" ""
|
||||
}
|
||||
|
||||
# Function to test info operations
|
||||
test_info_operations() {
|
||||
print_status "=== Testing Info Operations ==="
|
||||
|
||||
print_status "Testing INFO"
|
||||
redis_cmd "INFO" ""
|
||||
|
||||
print_status "Testing INFO replication"
|
||||
redis_cmd "INFO replication" ""
|
||||
}
|
||||
|
||||
# Function to test expiration
|
||||
test_expiration() {
|
||||
print_status "=== Testing Expiration ==="
|
||||
|
||||
redis_cmd "SET expire_key value" "OK"
|
||||
redis_cmd "SET expire_px_key value PX 1000" "OK" # 1 second
|
||||
redis_cmd "SET expire_ex_key value EX 1" "OK" # 1 second
|
||||
|
||||
redis_cmd "GET expire_key" "value"
|
||||
redis_cmd "GET expire_px_key" "value"
|
||||
redis_cmd "GET expire_ex_key" "value"
|
||||
|
||||
print_status "Waiting 2 seconds for expiration..."
|
||||
sleep 2
|
||||
|
||||
redis_cmd "GET expire_key" "value" # Should still exist
|
||||
redis_cmd "GET expire_px_key" "" # Should be expired
|
||||
redis_cmd "GET expire_ex_key" "" # Should be expired
|
||||
}
|
||||
|
||||
# Function to test SCAN operations
|
||||
test_scan_operations() {
|
||||
print_status "=== Testing SCAN Operations ==="
|
||||
|
||||
# Set up test data for scanning
|
||||
redis_cmd "SET scan_test1 value1" "OK"
|
||||
redis_cmd "SET scan_test2 value2" "OK"
|
||||
redis_cmd "SET scan_test3 value3" "OK"
|
||||
redis_cmd "SET other_key other_value" "OK"
|
||||
redis_cmd "HSET scan_hash field1 value1" "1"
|
||||
|
||||
# Test basic SCAN
|
||||
print_status "Testing basic SCAN with cursor 0"
|
||||
redis_cmd "SCAN 0" ""
|
||||
|
||||
# Test SCAN with MATCH pattern
|
||||
print_status "Testing SCAN with MATCH pattern"
|
||||
redis_cmd "SCAN 0 MATCH scan_test*" ""
|
||||
|
||||
# Test SCAN with COUNT
|
||||
print_status "Testing SCAN with COUNT 2"
|
||||
redis_cmd "SCAN 0 COUNT 2" ""
|
||||
|
||||
# Test SCAN with both MATCH and COUNT
|
||||
print_status "Testing SCAN with MATCH and COUNT"
|
||||
redis_cmd "SCAN 0 MATCH scan_* COUNT 1" ""
|
||||
|
||||
# Test SCAN continuation with more keys
|
||||
print_status "Setting up more keys for continuation test"
|
||||
redis_cmd "SET scan_key1 val1" "OK"
|
||||
redis_cmd "SET scan_key2 val2" "OK"
|
||||
redis_cmd "SET scan_key3 val3" "OK"
|
||||
redis_cmd "SET scan_key4 val4" "OK"
|
||||
redis_cmd "SET scan_key5 val5" "OK"
|
||||
|
||||
print_status "Testing SCAN with small COUNT for pagination"
|
||||
redis_cmd "SCAN 0 COUNT 3" ""
|
||||
|
||||
# Clean up SCAN test data
|
||||
print_status "Cleaning up SCAN test data"
|
||||
redis_cmd "DEL scan_test1" "1"
|
||||
redis_cmd "DEL scan_test2" "1"
|
||||
redis_cmd "DEL scan_test3" "1"
|
||||
redis_cmd "DEL other_key" "1"
|
||||
redis_cmd "DEL scan_hash" "1"
|
||||
redis_cmd "DEL scan_key1" "1"
|
||||
redis_cmd "DEL scan_key2" "1"
|
||||
redis_cmd "DEL scan_key3" "1"
|
||||
redis_cmd "DEL scan_key4" "1"
|
||||
redis_cmd "DEL scan_key5" "1"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_status "Starting HeroDB comprehensive test suite..."
|
||||
|
||||
# Build the project
|
||||
print_status "Building HeroDB..."
|
||||
if ! cargo build --release; then
|
||||
print_error "Failed to build HeroDB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create test database directory
|
||||
mkdir -p "$DB_DIR"
|
||||
|
||||
# Start the server
|
||||
print_status "Starting HeroDB server..."
|
||||
./target/release/redis-rs --dir "$DB_DIR" --port $PORT &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for server to start
|
||||
if ! wait_for_server; then
|
||||
print_error "Failed to start server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run tests
|
||||
local failed_tests=0
|
||||
|
||||
test_string_operations || failed_tests=$((failed_tests + 1))
|
||||
test_hash_operations || failed_tests=$((failed_tests + 1))
|
||||
test_config_operations || failed_tests=$((failed_tests + 1))
|
||||
test_transaction_operations || failed_tests=$((failed_tests + 1))
|
||||
test_keys_operations || failed_tests=$((failed_tests + 1))
|
||||
test_info_operations || failed_tests=$((failed_tests + 1))
|
||||
test_expiration || failed_tests=$((failed_tests + 1))
|
||||
test_scan_operations || failed_tests=$((failed_tests + 1))
|
||||
|
||||
# Summary
|
||||
echo
|
||||
print_status "=== Test Summary ==="
|
||||
if [ $failed_tests -eq 0 ]; then
|
||||
print_success "All tests completed! Some may have warnings due to protocol differences."
|
||||
print_success "HeroDB is working with persistent redb storage!"
|
||||
else
|
||||
print_warning "$failed_tests test categories had issues"
|
||||
print_warning "Check the output above for details"
|
||||
fi
|
||||
|
||||
print_status "Database file created at: $DB_DIR/herodb.redb"
|
||||
print_status "Server logs and any errors are shown above"
|
||||
}
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies() {
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
print_error "cargo is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v nc &> /dev/null; then
|
||||
print_warning "netcat (nc) not found - some tests may not work properly"
|
||||
fi
|
||||
|
||||
if ! command -v redis-cli &> /dev/null; then
|
||||
print_warning "redis-cli not found - using netcat fallback"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run dependency check and main function
|
||||
check_dependencies
|
||||
main "$@"
|
Loading…
Reference in New Issue
Block a user