Compare commits
1 Commits
main
...
200d0c928d
Author | SHA1 | Date | |
---|---|---|---|
200d0c928d |
74
Cargo.lock
generated
74
Cargo.lock
generated
@@ -197,15 +197,6 @@ version = "0.9.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
|
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bincode"
|
|
||||||
version = "1.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.9.2"
|
version = "2.9.2"
|
||||||
@@ -221,12 +212,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byteorder"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@@ -661,28 +646,22 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "herocrypto"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "herodb"
|
name = "herodb"
|
||||||
version = "0.0.1"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"age",
|
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
|
||||||
"bincode",
|
|
||||||
"byteorder",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"chacha20poly1305",
|
|
||||||
"clap",
|
"clap",
|
||||||
"ed25519-dalek",
|
"libcryptoa",
|
||||||
"futures",
|
"libdbstorage",
|
||||||
"rand",
|
"log",
|
||||||
"redb",
|
|
||||||
"redis",
|
"redis",
|
||||||
"secrecy",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
"sha2",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -949,6 +928,39 @@ version = "0.2.175"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libcrypto"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20poly1305",
|
||||||
|
"libdbstorage",
|
||||||
|
"rand",
|
||||||
|
"sha2",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libcryptoa"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"age",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"rand",
|
||||||
|
"secrecy",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libdbstorage"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"redb",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -1530,6 +1542,10 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
name = "supervisor"
|
name = "supervisor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "supervisorrpc"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
|
37
Cargo.toml
37
Cargo.toml
@@ -1,12 +1,37 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
|
||||||
"herodb",
|
|
||||||
"supervisor",
|
|
||||||
]
|
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"crates/herodb",
|
||||||
|
"crates/libdbstorage",
|
||||||
|
"crates/libcrypto",
|
||||||
|
"crates/libcryptoa",
|
||||||
|
"crates/herocrypto",
|
||||||
|
"crates/supervisorrpc",
|
||||||
|
"crates/supervisor",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
# Central place for dependencies shared across the workspace
|
||||||
|
anyhow = "1.0"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
# Crypto deps
|
||||||
|
chacha20poly1305 = "0.10"
|
||||||
|
rand = "0.8"
|
||||||
|
sha2 = "0.10"
|
||||||
|
age = "0.10"
|
||||||
|
secrecy = "0.8"
|
||||||
|
ed25519-dalek = "2"
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
|
# DB
|
||||||
|
redb = "2.1"
|
||||||
|
|
||||||
# You can define shared profiles for all workspace members here
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units =1
|
||||||
strip = true
|
strip = true
|
6
crates/herocrypto/Cargo.toml
Normal file
6
crates/herocrypto/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "herocrypto"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
14
crates/herocrypto/src/lib.rs
Normal file
14
crates/herocrypto/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
pub fn add(left: u64, right: u64) -> u64 {
|
||||||
|
left + right
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let result = add(2, 2);
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
|
}
|
28
crates/herodb/Cargo.toml
Normal file
28
crates/herodb/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "herodb"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Pin Fang <fpfangpin@hotmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# THIS IS A BINARY, NOT A LIBRARY
|
||||||
|
[[bin]]
|
||||||
|
name = "herodb"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Workspace dependencies
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
|
||||||
|
# Local Crate Dependencies
|
||||||
|
libdbstorage = { path = "../libdbstorage" }
|
||||||
|
libcryptoa = { path = "../libcryptoa" }
|
||||||
|
|
||||||
|
# Other dependencies
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
bytes = "1.3.0" # Example, keep specific versions if needed
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
redis = { version = "0.24", features = ["aio", "tokio-comp"] }
|
267
crates/herodb/README.md
Normal file
267
crates/herodb/README.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# 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.
|
1
crates/herodb/src/main.rs
Normal file
1
crates/herodb/src/main.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
fn main() {}
|
@@ -12,14 +12,7 @@ impl Storage {
|
|||||||
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||||
|
|
||||||
let key_type = {
|
// Set the type to hash
|
||||||
let access_guard = types_table.get(key)?;
|
|
||||||
access_guard.map(|v| v.value().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_type.as_deref() {
|
|
||||||
Some("hash") | None => { // Proceed if hash or new key
|
|
||||||
// Set the type to hash (only if new key or existing hash)
|
|
||||||
types_table.insert(key, "hash")?;
|
types_table.insert(key, "hash")?;
|
||||||
|
|
||||||
for (field, value) in pairs {
|
for (field, value) in pairs {
|
||||||
@@ -35,9 +28,6 @@ impl Storage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(_) => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write_txn.commit()?;
|
write_txn.commit()?;
|
||||||
Ok(new_fields)
|
Ok(new_fields)
|
||||||
@@ -48,10 +38,8 @@ impl Storage {
|
|||||||
let read_txn = self.db.begin_read()?;
|
let read_txn = self.db.begin_read()?;
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
let key_type = types_table.get(key)?.map(|v| v.value().to_string());
|
match types_table.get(key)? {
|
||||||
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
match key_type.as_deref() {
|
|
||||||
Some("hash") => {
|
|
||||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
match hashes_table.get((key, field))? {
|
match hashes_table.get((key, field))? {
|
||||||
Some(data) => {
|
Some(data) => {
|
||||||
@@ -62,8 +50,7 @@ impl Storage {
|
|||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
_ => Ok(None),
|
||||||
None => Ok(None),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +58,9 @@ impl Storage {
|
|||||||
pub fn hgetall(&self, key: &str) -> Result<Vec<(String, String)>, DBError> {
|
pub fn hgetall(&self, key: &str) -> Result<Vec<(String, String)>, DBError> {
|
||||||
let read_txn = self.db.begin_read()?;
|
let read_txn = self.db.begin_read()?;
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
let key_type = {
|
|
||||||
let access_guard = types_table.get(key)?;
|
|
||||||
access_guard.map(|v| v.value().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_type.as_deref() {
|
match types_table.get(key)? {
|
||||||
Some("hash") => {
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
@@ -94,8 +77,7 @@ impl Storage {
|
|||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
_ => Ok(Vec::new()),
|
||||||
None => Ok(Vec::new()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,14 +86,16 @@ impl Storage {
|
|||||||
let mut deleted = 0i64;
|
let mut deleted = 0i64;
|
||||||
|
|
||||||
// First check if key exists and is a hash
|
// First check if key exists and is a hash
|
||||||
let key_type = {
|
let is_hash = {
|
||||||
let types_table = write_txn.open_table(TYPES_TABLE)?;
|
let types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
let access_guard = types_table.get(key)?;
|
let result = match types_table.get(key)? {
|
||||||
access_guard.map(|v| v.value().to_string())
|
Some(type_val) => type_val.value() == "hash",
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
result
|
||||||
};
|
};
|
||||||
|
|
||||||
match key_type.as_deref() {
|
if is_hash {
|
||||||
Some("hash") => {
|
|
||||||
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||||
|
|
||||||
for field in fields {
|
for field in fields {
|
||||||
@@ -138,9 +122,6 @@ impl Storage {
|
|||||||
types_table.remove(key)?;
|
types_table.remove(key)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(_) => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
|
||||||
None => {} // Key does not exist, nothing to delete, return 0 deleted
|
|
||||||
}
|
|
||||||
|
|
||||||
write_txn.commit()?;
|
write_txn.commit()?;
|
||||||
Ok(deleted)
|
Ok(deleted)
|
||||||
@@ -150,19 +131,12 @@ impl Storage {
|
|||||||
let read_txn = self.db.begin_read()?;
|
let read_txn = self.db.begin_read()?;
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
match types_table.get(key)? {
|
||||||
let key_type = {
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
let access_guard = types_table.get(key)?;
|
|
||||||
access_guard.map(|v| v.value().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_type.as_deref() {
|
|
||||||
Some("hash") => {
|
|
||||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
Ok(hashes_table.get((key, field))?.is_some())
|
Ok(hashes_table.get((key, field))?.is_some())
|
||||||
}
|
}
|
||||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
_ => Ok(false),
|
||||||
None => Ok(false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,14 +144,8 @@ impl Storage {
|
|||||||
let read_txn = self.db.begin_read()?;
|
let read_txn = self.db.begin_read()?;
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
match types_table.get(key)? {
|
||||||
let key_type = {
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
let access_guard = types_table.get(key)?;
|
|
||||||
access_guard.map(|v| v.value().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_type.as_deref() {
|
|
||||||
Some("hash") => {
|
|
||||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
@@ -192,8 +160,7 @@ impl Storage {
|
|||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
_ => Ok(Vec::new()),
|
||||||
None => Ok(Vec::new()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,14 +169,8 @@ impl Storage {
|
|||||||
let read_txn = self.db.begin_read()?;
|
let read_txn = self.db.begin_read()?;
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
match types_table.get(key)? {
|
||||||
let key_type = {
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
let access_guard = types_table.get(key)?;
|
|
||||||
access_guard.map(|v| v.value().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_type.as_deref() {
|
|
||||||
Some("hash") => {
|
|
||||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
@@ -226,8 +187,7 @@ impl Storage {
|
|||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
_ => Ok(Vec::new()),
|
||||||
None => Ok(Vec::new()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,14 +195,8 @@ impl Storage {
|
|||||||
let read_txn = self.db.begin_read()?;
|
let read_txn = self.db.begin_read()?;
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
match types_table.get(key)? {
|
||||||
let key_type = {
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
let access_guard = types_table.get(key)?;
|
|
||||||
access_guard.map(|v| v.value().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_type.as_deref() {
|
|
||||||
Some("hash") => {
|
|
||||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
let mut count = 0i64;
|
let mut count = 0i64;
|
||||||
|
|
||||||
@@ -257,8 +211,7 @@ impl Storage {
|
|||||||
|
|
||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
_ => Ok(0),
|
||||||
None => Ok(0),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,14 +220,8 @@ impl Storage {
|
|||||||
let read_txn = self.db.begin_read()?;
|
let read_txn = self.db.begin_read()?;
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
match types_table.get(key)? {
|
||||||
let key_type = {
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
let access_guard = types_table.get(key)?;
|
|
||||||
access_guard.map(|v| v.value().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_type.as_deref() {
|
|
||||||
Some("hash") => {
|
|
||||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
@@ -291,8 +238,7 @@ impl Storage {
|
|||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
_ => Ok(fields.into_iter().map(|_| None).collect()),
|
||||||
None => Ok(fields.into_iter().map(|_| None).collect()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,16 +251,9 @@ impl Storage {
|
|||||||
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
|
||||||
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
|
||||||
|
|
||||||
let key_type = {
|
|
||||||
let access_guard = types_table.get(key)?;
|
|
||||||
access_guard.map(|v| v.value().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_type.as_deref() {
|
|
||||||
Some("hash") | None => { // Proceed if hash or new key
|
|
||||||
// Check if field already exists
|
// Check if field already exists
|
||||||
if hashes_table.get((key, field))?.is_none() {
|
if hashes_table.get((key, field))?.is_none() {
|
||||||
// Set the type to hash (only if new key or existing hash)
|
// Set the type to hash
|
||||||
types_table.insert(key, "hash")?;
|
types_table.insert(key, "hash")?;
|
||||||
|
|
||||||
// Encrypt the value before storing
|
// Encrypt the value before storing
|
||||||
@@ -323,9 +262,6 @@ impl Storage {
|
|||||||
result = true;
|
result = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(_) => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write_txn.commit()?;
|
write_txn.commit()?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -336,14 +272,8 @@ impl Storage {
|
|||||||
let read_txn = self.db.begin_read()?;
|
let read_txn = self.db.begin_read()?;
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
||||||
|
|
||||||
let types_table = read_txn.open_table(TYPES_TABLE)?;
|
match types_table.get(key)? {
|
||||||
let key_type = {
|
Some(type_val) if type_val.value() == "hash" => {
|
||||||
let access_guard = types_table.get(key)?;
|
|
||||||
access_guard.map(|v| v.value().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_type.as_deref() {
|
|
||||||
Some("hash") => {
|
|
||||||
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut current_cursor = 0u64;
|
let mut current_cursor = 0u64;
|
||||||
@@ -382,8 +312,7 @@ impl Storage {
|
|||||||
let next_cursor = if result.len() < limit { 0 } else { current_cursor };
|
let next_cursor = if result.len() < limit { 0 } else { current_cursor };
|
||||||
Ok((next_cursor, result))
|
Ok((next_cursor, result))
|
||||||
}
|
}
|
||||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
_ => Ok((0, Vec::new())),
|
||||||
None => Ok((0, Vec::new())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -25,7 +25,7 @@ impl Storage {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add elements to the front (left)
|
// Add elements to the front (left)
|
||||||
for element in elements.into_iter() {
|
for element in elements.into_iter().rev() {
|
||||||
list.insert(0, element);
|
list.insert(0, element);
|
||||||
}
|
}
|
||||||
|
|
@@ -93,16 +93,9 @@ async fn test_basic_redis_functionality() {
|
|||||||
assert!(response.contains("string"));
|
assert!(response.contains("string"));
|
||||||
|
|
||||||
// Test QUIT to close connection gracefully
|
// Test QUIT to close connection gracefully
|
||||||
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).await.unwrap();
|
let response = send_redis_command(port, "*1\r\n$4\r\nQUIT\r\n").await;
|
||||||
stream.write_all("*1\r\n$4\r\nQUIT\r\n".as_bytes()).await.unwrap();
|
|
||||||
let mut buffer = [0; 1024];
|
|
||||||
let n = stream.read(&mut buffer).await.unwrap();
|
|
||||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
|
||||||
assert!(response.contains("OK"));
|
assert!(response.contains("OK"));
|
||||||
|
|
||||||
// Ensure the stream is closed
|
|
||||||
stream.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
// Stop the server
|
// Stop the server
|
||||||
server_handle.abort();
|
server_handle.abort();
|
||||||
|
|
||||||
@@ -156,8 +149,6 @@ async fn test_hash_operations() {
|
|||||||
assert!(response.contains("value2"));
|
assert!(response.contains("value2"));
|
||||||
|
|
||||||
// Stop the server
|
// Stop the server
|
||||||
// For hash operations, we don't have a persistent stream, so we'll just abort the server.
|
|
||||||
// The server should handle closing its connections.
|
|
||||||
server_handle.abort();
|
server_handle.abort();
|
||||||
|
|
||||||
println!("✅ All hash operations tests passed!");
|
println!("✅ All hash operations tests passed!");
|
||||||
@@ -211,16 +202,9 @@ async fn test_transaction_operations() {
|
|||||||
assert!(response.contains("OK")); // Should contain array of OK responses
|
assert!(response.contains("OK")); // Should contain array of OK responses
|
||||||
|
|
||||||
// Verify commands were executed
|
// Verify commands were executed
|
||||||
stream.write_all("*2\r\n$3\r\nGET\r\n$4\r\nkey1\r\n".as_bytes()).await.unwrap();
|
let response = send_redis_command(port, "*2\r\n$3\r\nGET\r\n$4\r\nkey1\r\n").await;
|
||||||
let n = stream.read(&mut buffer).await.unwrap();
|
|
||||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
|
||||||
assert!(response.contains("value1"));
|
assert!(response.contains("value1"));
|
||||||
|
|
||||||
stream.write_all("*2\r\n$3\r\nGET\r\n$4\r\nkey2\r\n".as_bytes()).await.unwrap();
|
|
||||||
let n = stream.read(&mut buffer).await.unwrap();
|
|
||||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
|
||||||
assert!(response.contains("value2"));
|
|
||||||
|
|
||||||
// Stop the server
|
// Stop the server
|
||||||
server_handle.abort();
|
server_handle.abort();
|
||||||
|
|
16
crates/libcrypto/Cargo.toml
Normal file
16
crates/libcrypto/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "libcrypto"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chacha20poly1305 = "0.10"
|
||||||
|
rand = { workspace = true }
|
||||||
|
sha2 = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
|
# Add this dependency for the From<CryptoError> for DBError
|
||||||
|
libdbstorage = { path = "../libdbstorage", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
storage_compat = ["dep:libdbstorage"]
|
14
crates/libcrypto/src/lib.rs
Normal file
14
crates/libcrypto/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
pub fn add(left: u64, right: u64) -> u64 {
|
||||||
|
left + right
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let result = add(2, 2);
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
|
}
|
12
crates/libcryptoa/Cargo.toml
Normal file
12
crates/libcryptoa/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "libcryptoa"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
age = { workspace = true }
|
||||||
|
secrecy = { workspace = true }
|
||||||
|
ed25519-dalek = { workspace = true }
|
||||||
|
base64 = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
14
crates/libcryptoa/src/lib.rs
Normal file
14
crates/libcryptoa/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
pub fn add(left: u64, right: u64) -> u64 {
|
||||||
|
left + right
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let result = add(2, 2);
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
|
}
|
12
crates/libdbstorage/Cargo.toml
Normal file
12
crates/libdbstorage/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "libdbstorage"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
redb = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
thiserror = { workspace = true }
|
14
crates/libdbstorage/src/lib.rs
Normal file
14
crates/libdbstorage/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
pub fn add(left: u64, right: u64) -> u64 {
|
||||||
|
left + right
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let result = add(2, 2);
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
|
}
|
6
crates/supervisorrpc/Cargo.toml
Normal file
6
crates/supervisorrpc/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "supervisorrpc"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
3
crates/supervisorrpc/src/main.rs
Normal file
3
crates/supervisorrpc/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
@@ -1,28 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "herodb"
|
|
||||||
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"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
bincode = "1.3.3"
|
|
||||||
chacha20poly1305 = "0.10.1"
|
|
||||||
rand = "0.8"
|
|
||||||
sha2 = "0.10"
|
|
||||||
age = "0.10"
|
|
||||||
secrecy = "0.8"
|
|
||||||
ed25519-dalek = "2"
|
|
||||||
base64 = "0.22"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
redis = { version = "0.24", features = ["aio", "tokio-comp"] }
|
|
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
export SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
echo "I am in $SCRIPT_DIR"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
cargo build
|
|
||||||
|
|
@@ -1,99 +0,0 @@
|
|||||||
|
|
||||||
### Cargo.toml
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
chacha20poly1305 = { version = "0.10", features = ["xchacha20"] }
|
|
||||||
rand = "0.8"
|
|
||||||
sha2 = "0.10"
|
|
||||||
```
|
|
||||||
|
|
||||||
### `crypto_factory.rs`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use chacha20poly1305::{
|
|
||||||
aead::{Aead, KeyInit, OsRng},
|
|
||||||
XChaCha20Poly1305, Key, XNonce,
|
|
||||||
};
|
|
||||||
use rand::RngCore;
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
const VERSION: u8 = 1;
|
|
||||||
const NONCE_LEN: usize = 24;
|
|
||||||
const TAG_LEN: usize = 16;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum CryptoError {
|
|
||||||
Format, // wrong length / header
|
|
||||||
Version(u8), // unknown version
|
|
||||||
Decrypt, // wrong key or corrupted data
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes)
|
|
||||||
pub struct CryptoFactory {
|
|
||||||
key: Key<XChaCha20Poly1305>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CryptoFactory {
|
|
||||||
/// Accepts any secret bytes; turns them into a 32-byte key (SHA-256).
|
|
||||||
/// (If your secret is already 32 bytes, this is still fine.)
|
|
||||||
pub fn new<S: AsRef<[u8]>>(secret: S) -> Self {
|
|
||||||
let mut h = Sha256::new();
|
|
||||||
h.update(b"xchacha20poly1305-factory:v1"); // domain separation
|
|
||||||
h.update(secret.as_ref());
|
|
||||||
let digest = h.finalize(); // 32 bytes
|
|
||||||
let key = Key::<XChaCha20Poly1305>::from_slice(&digest).to_owned();
|
|
||||||
Self { key }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Output layout: [version:1][nonce:24][ciphertext||tag]
|
|
||||||
pub fn encrypt(&self, plaintext: &[u8]) -> Vec<u8> {
|
|
||||||
let cipher = XChaCha20Poly1305::new(&self.key);
|
|
||||||
|
|
||||||
let mut nonce_bytes = [0u8; NONCE_LEN];
|
|
||||||
OsRng.fill_bytes(&mut nonce_bytes);
|
|
||||||
let nonce = XNonce::from_slice(&nonce_bytes);
|
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN);
|
|
||||||
out.push(VERSION);
|
|
||||||
out.extend_from_slice(&nonce_bytes);
|
|
||||||
|
|
||||||
let ct = cipher.encrypt(nonce, plaintext).expect("encrypt");
|
|
||||||
out.extend_from_slice(&ct);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrypt(&self, blob: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|
||||||
if blob.len() < 1 + NONCE_LEN + TAG_LEN {
|
|
||||||
return Err(CryptoError::Format);
|
|
||||||
}
|
|
||||||
let ver = blob[0];
|
|
||||||
if ver != VERSION {
|
|
||||||
return Err(CryptoError::Version(ver));
|
|
||||||
}
|
|
||||||
|
|
||||||
let nonce = XNonce::from_slice(&blob[1..1 + NONCE_LEN]);
|
|
||||||
let ct = &blob[1 + NONCE_LEN..];
|
|
||||||
|
|
||||||
let cipher = XChaCha20Poly1305::new(&self.key);
|
|
||||||
cipher.decrypt(nonce, ct).map_err(|_| CryptoError::Decrypt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tiny usage example
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn main() {
|
|
||||||
let f = CryptoFactory::new(b"super-secret-key-material");
|
|
||||||
let val = b"\x00\xFFbinary\x01\x02\x03";
|
|
||||||
|
|
||||||
let blob = f.encrypt(val);
|
|
||||||
let roundtrip = f.decrypt(&blob).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(roundtrip, val);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
That’s it: `new(secret)`, `encrypt(bytes)`, `decrypt(bytes)`.
|
|
||||||
You can stash the returned `blob` directly in your storage layer behind Redis.
|
|
@@ -1,80 +0,0 @@
|
|||||||
========================
|
|
||||||
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]).
|
|
@@ -1,150 +0,0 @@
|
|||||||
]
|
|
||||||
# INFO
|
|
||||||
|
|
||||||
**What it does**
|
|
||||||
Returns server stats in a human-readable text block, optionally filtered by sections. Typical sections: `server`, `clients`, `memory`, `persistence`, `stats`, `replication`, `cpu`, `commandstats`, `latencystats`, `cluster`, `modules`, `keyspace`, `errorstats`. Special args: `all`, `default`, `everything`. The reply is a **Bulk String** with `# <Section>` headers and `key:value` lines. ([Redis][1])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
INFO [section [section ...]]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Return (RESP2/RESP3)**: Bulk String. ([Redis][1])
|
|
||||||
|
|
||||||
**RESP request/response**
|
|
||||||
|
|
||||||
```
|
|
||||||
# Request: whole default set
|
|
||||||
*1\r\n$4\r\nINFO\r\n
|
|
||||||
|
|
||||||
# Request: a specific section, e.g., clients
|
|
||||||
*2\r\n$4\r\nINFO\r\n$7\r\nclients\r\n
|
|
||||||
|
|
||||||
# Response (prefix shown; body is long)
|
|
||||||
$1234\r\n# Server\r\nredis_version:7.4.0\r\n...\r\n# Clients\r\nconnected_clients:3\r\n...\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
(Reply type/format per RESP spec and the INFO page.) ([Redis][2])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Connection “name” (there is **no** top-level `NAME` command)
|
|
||||||
|
|
||||||
Redis doesn’t have a standalone `NAME` command. Connection names are handled via `CLIENT SETNAME` and retrieved via `CLIENT GETNAME`. ([Redis][3])
|
|
||||||
|
|
||||||
## CLIENT SETNAME
|
|
||||||
|
|
||||||
Assigns a human label to the current connection (shown in `CLIENT LIST`, logs, etc.). No spaces allowed in the name; empty string clears it. Length is limited by Redis string limits (practically huge). **Reply**: Simple String `OK`. ([Redis][4])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
CLIENT SETNAME connection-name
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
# Set the name "myapp"
|
|
||||||
*3\r\n$6\r\nCLIENT\r\n$7\r\nSETNAME\r\n$5\r\nmyapp\r\n
|
|
||||||
|
|
||||||
# Reply
|
|
||||||
+OK\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
## CLIENT GETNAME
|
|
||||||
|
|
||||||
Returns the current connection’s name or **Null Bulk String** if unset. ([Redis][5])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
CLIENT GETNAME
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
# Before SETNAME:
|
|
||||||
*2\r\n$6\r\nCLIENT\r\n$7\r\nGETNAME\r\n
|
|
||||||
$-1\r\n # nil (no name)
|
|
||||||
|
|
||||||
# After SETNAME myapp:
|
|
||||||
*2\r\n$6\r\nCLIENT\r\n$7\r\nGETNAME\r\n
|
|
||||||
$5\r\nmyapp\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
(Null/Bulk String encoding per RESP spec.) ([Redis][2])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# CLIENT (container command + key subcommands)
|
|
||||||
|
|
||||||
`CLIENT` is a **container**; use subcommands like `CLIENT LIST`, `CLIENT INFO`, `CLIENT ID`, `CLIENT KILL`, `CLIENT TRACKING`, etc. Call `CLIENT HELP` to enumerate them. ([Redis][3])
|
|
||||||
|
|
||||||
## CLIENT LIST
|
|
||||||
|
|
||||||
Shows all connections as a single **Bulk String**: one line per client with `field=value` pairs (includes `id`, `addr`, `name`, `db`, `user`, `resp`, and more). Filters: `TYPE` and `ID`. **Return**: Bulk String (RESP2/RESP3). ([Redis][6])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
CLIENT LIST [TYPE <NORMAL|MASTER|REPLICA|PUBSUB>] [ID client-id ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2\r\n$6\r\nCLIENT\r\n$4\r\nLIST\r\n
|
|
||||||
|
|
||||||
# Reply (single Bulk String; example with one line shown)
|
|
||||||
$188\r\nid=7 addr=127.0.0.1:60840 laddr=127.0.0.1:6379 fd=8 name=myapp age=12 idle=3 flags=N db=0 ...\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
## CLIENT INFO
|
|
||||||
|
|
||||||
Returns info for **this** connection only (same format/fields as a single line of `CLIENT LIST`). **Return**: Bulk String. Available since 6.2.0. ([Redis][7])
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
CLIENT INFO
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2\r\n$6\r\nCLIENT\r\n$4\r\nINFO\r\n
|
|
||||||
|
|
||||||
$160\r\nid=7 addr=127.0.0.1:60840 laddr=127.0.0.1:6379 fd=8 name=myapp db=0 user=default resp=2 ...\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# RESP notes you’ll need for your parser
|
|
||||||
|
|
||||||
* **Requests** are Arrays: `*N\r\n` followed by `N` Bulk Strings for verb/args.
|
|
||||||
* **Common replies here**: Simple String (`+OK\r\n`), Bulk String (`$<len>\r\n...\r\n`), and **Null Bulk String** (`$-1\r\n`). (These cover `INFO`, `CLIENT LIST/INFO`, `CLIENT GETNAME`, `CLIENT SETNAME`.) ([Redis][2])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sources (checked)
|
|
||||||
|
|
||||||
* INFO command (syntax, sections, behavior). ([Redis][1])
|
|
||||||
* RESP spec (request/response framing, Bulk/Null Bulk Strings). ([Redis][2])
|
|
||||||
* CLIENT container + subcommands index. ([Redis][3])
|
|
||||||
* CLIENT LIST (fields, bulk-string return, filters). ([Redis][6])
|
|
||||||
* CLIENT INFO (exists since 6.2, reply format). ([Redis][7])
|
|
||||||
* CLIENT SETNAME (no spaces; clears with empty string; huge length OK). ([Redis][4])
|
|
||||||
* CLIENT GETNAME (nil if unset). ([Redis][5])
|
|
||||||
|
|
||||||
If you want, I can fold this into a tiny Rust “command + RESP” test harness that exercises `INFO`, `CLIENT SETNAME/GETNAME`, `CLIENT LIST`, and `CLIENT INFO` against your in-mem RESP parser.
|
|
||||||
|
|
||||||
[1]: https://redis.io/docs/latest/commands/info/ "INFO | Docs"
|
|
||||||
[2]: https://redis.io/docs/latest/develop/reference/protocol-spec/?utm_source=chatgpt.com "Redis serialization protocol specification | Docs"
|
|
||||||
[3]: https://redis.io/docs/latest/commands/client/ "CLIENT | Docs"
|
|
||||||
[4]: https://redis.io/docs/latest/commands/client-setname/?utm_source=chatgpt.com "CLIENT SETNAME | Docs"
|
|
||||||
[5]: https://redis.io/docs/latest/commands/client-getname/?utm_source=chatgpt.com "CLIENT GETNAME | Docs"
|
|
||||||
[6]: https://redis.io/docs/latest/commands/client-list/ "CLIENT LIST | Docs"
|
|
||||||
[7]: https://redis.io/docs/latest/commands/client-info/?utm_source=chatgpt.com "CLIENT INFO | Docs"
|
|
@@ -1,251 +0,0 @@
|
|||||||
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]` |
|
|
||||||
|
|
||||||
##
|
|
@@ -1,307 +0,0 @@
|
|||||||
|
|
||||||
# 🔑 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
|
|
||||||
```
|
|
@@ -1,259 +0,0 @@
|
|||||||
|
|
||||||
# 1) Data model & basics
|
|
||||||
|
|
||||||
* A **queue** is a List at key `queue:<name>`.
|
|
||||||
* Common patterns:
|
|
||||||
|
|
||||||
* **Producer**: `LPUSH queue item` (or `RPUSH`)
|
|
||||||
* **Consumer (non-blocking)**: `RPOP queue` (or `LPOP`)
|
|
||||||
* **Consumer (blocking)**: `BRPOP queue timeout` (or `BLPOP`)
|
|
||||||
* If a key doesn’t exist, it’s treated as an **empty list**; push **creates** the list; when the **last element is popped, the key is deleted**. ([Redis][1])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 2) Commands to implement (queues via Lists)
|
|
||||||
|
|
||||||
## LPUSH / RPUSH
|
|
||||||
|
|
||||||
Prepend/append one or more elements. Create the list if it doesn’t exist.
|
|
||||||
**Return**: Integer = new length of the list.
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
LPUSH key element [element ...]
|
|
||||||
RPUSH key element [element ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP (example)**
|
|
||||||
|
|
||||||
```
|
|
||||||
*3\r\n$5\r\nLPUSH\r\n$5\r\nqueue\r\n$5\r\njob-1\r\n
|
|
||||||
:1\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: semantics & multi-arg ordering. ([Redis][1])
|
|
||||||
|
|
||||||
### LPUSHX / RPUSHX (optional but useful)
|
|
||||||
|
|
||||||
Like LPUSH/RPUSH, **but only if the list exists**.
|
|
||||||
**Return**: Integer = new length (0 if key didn’t exist).
|
|
||||||
|
|
||||||
```
|
|
||||||
LPUSHX key element [element ...]
|
|
||||||
RPUSHX key element [element ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: command index. ([Redis][2])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LPOP / RPOP
|
|
||||||
|
|
||||||
Remove & return one (default) or **up to COUNT** elements since Redis 6.2.
|
|
||||||
If the list is empty or missing, **Null** is returned (Null Bulk or Null Array if COUNT>1).
|
|
||||||
**Return**:
|
|
||||||
|
|
||||||
* No COUNT: Bulk String or Null Bulk.
|
|
||||||
* With COUNT: Array of Bulk Strings (possibly empty) or Null Array if key missing.
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
LPOP key [count]
|
|
||||||
RPOP key [count]
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP (no COUNT)**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2\r\n$4\r\nRPOP\r\n$5\r\nqueue\r\n
|
|
||||||
$5\r\njob-1\r\n # or $-1\r\n if empty
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP (COUNT=2)**
|
|
||||||
|
|
||||||
```
|
|
||||||
*3\r\n$4\r\nLPOP\r\n$5\r\nqueue\r\n$1\r\n2\r\n
|
|
||||||
*2\r\n$5\r\njob-2\r\n$5\r\njob-3\r\n # or *-1\r\n if key missing
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: LPOP w/ COUNT; general pop semantics. ([Redis][3])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BLPOP / BRPOP (blocking consumers)
|
|
||||||
|
|
||||||
Block until an element is available in any of the given lists or until `timeout` (seconds, **double**, `0` = forever).
|
|
||||||
**Return** on success: **Array \[key, element]**.
|
|
||||||
**Return** on timeout: **Null Array**.
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
BLPOP key [key ...] timeout
|
|
||||||
BRPOP key [key ...] timeout
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*3\r\n$5\r\nBRPOP\r\n$5\r\nqueue\r\n$1\r\n0\r\n # block forever
|
|
||||||
|
|
||||||
# Success reply
|
|
||||||
*2\r\n$5\r\nqueue\r\n$5\r\njob-4\r\n
|
|
||||||
|
|
||||||
# Timeout reply
|
|
||||||
*-1\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
**Implementation notes**
|
|
||||||
|
|
||||||
* If any listed key is non-empty at call time, reply **immediately** from the first non-empty key **by the command’s key order**.
|
|
||||||
* Otherwise, put the client into a **blocked state** (register per-key waiters). On any `LPUSH/RPUSH` to those keys, **wake the earliest waiter** and serve it atomically.
|
|
||||||
* If timeout expires, return **Null Array** and clear the blocked state.
|
|
||||||
Refs: timeout semantics and return shape. ([Redis][4])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LMOVE / BLMOVE (atomic move; replaces RPOPLPUSH/BRPOPLPUSH)
|
|
||||||
|
|
||||||
Atomically **pop from one side** of `source` and **push to one side** of `destination`.
|
|
||||||
|
|
||||||
* Use for **reliable queues** (move to a *processing* list).
|
|
||||||
* `BLMOVE` blocks like `BLPOP` when `source` is empty.
|
|
||||||
|
|
||||||
**Syntax**
|
|
||||||
|
|
||||||
```
|
|
||||||
LMOVE source destination LEFT|RIGHT LEFT|RIGHT
|
|
||||||
BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout
|
|
||||||
```
|
|
||||||
|
|
||||||
**Return**: Bulk String element moved, or Null if `source` empty (LMOVE); `BLMOVE` blocks/Null on timeout.
|
|
||||||
|
|
||||||
**RESP (LMOVE RIGHT->LEFT)**
|
|
||||||
|
|
||||||
```
|
|
||||||
*5\r\n$5\r\nLMOVE\r\n$6\r\nsource\r\n$3\r\ndst\r\n$5\r\nRIGHT\r\n$4\r\nLEFT\r\n
|
|
||||||
$5\r\njob-5\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
**Notes**
|
|
||||||
|
|
||||||
* Prefer `LMOVE/BLMOVE` over deprecated `RPOPLPUSH/BRPOPLPUSH`.
|
|
||||||
* Pattern: consumer `LMOVE queue processing RIGHT LEFT` → work → `LREM processing 1 <elem>` to ACK; a reaper can requeue stale items.
|
|
||||||
Refs: LMOVE/BLMOVE behavior and reliable-queue pattern; deprecation of RPOPLPUSH. ([Redis][5])
|
|
||||||
|
|
||||||
*(Compat: you can still implement `RPOPLPUSH source dest` and `BRPOPLPUSH source dest timeout`, but mark them deprecated and map to LMOVE/BLMOVE.)* ([Redis][6])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LLEN (length)
|
|
||||||
|
|
||||||
Useful for metrics/backpressure.
|
|
||||||
|
|
||||||
```
|
|
||||||
LLEN key
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*2\r\n$4\r\nLLEN\r\n$5\r\nqueue\r\n
|
|
||||||
:3\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: list overview mentioning LLEN. ([Redis][7])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LREM (ack for “reliable” processing)
|
|
||||||
|
|
||||||
Remove occurrences of `element` from the list (head→tail scan).
|
|
||||||
Use `count=1` to ACK a single processed item from `processing`.
|
|
||||||
|
|
||||||
```
|
|
||||||
LREM key count element
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*4\r\n$4\r\nLREM\r\n$9\r\nprocessing\r\n$1\r\n1\r\n$5\r\njob-5\r\n
|
|
||||||
:1\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: reliable pattern mentions LREM to ACK. ([Redis][5])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LTRIM (bounded queues / retention)
|
|
||||||
|
|
||||||
Keep only `[start, stop]` range; everything else is dropped.
|
|
||||||
Use to cap queue length after pushes.
|
|
||||||
|
|
||||||
```
|
|
||||||
LTRIM key start stop
|
|
||||||
```
|
|
||||||
|
|
||||||
**RESP**
|
|
||||||
|
|
||||||
```
|
|
||||||
*4\r\n$5\r\nLTRIM\r\n$5\r\nqueue\r\n$2\r\n0\r\n$3\r\n999\r\n
|
|
||||||
+OK\r\n
|
|
||||||
```
|
|
||||||
|
|
||||||
Refs: list overview includes LTRIM for retention. ([Redis][7])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LRANGE / LINDEX (debugging / peeking)
|
|
||||||
|
|
||||||
* `LRANGE key start stop` → Array of elements (non-destructive).
|
|
||||||
* `LINDEX key index` → one element or Null.
|
|
||||||
|
|
||||||
These aren’t required for queue semantics, but handy. ([Redis][7])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 3) Errors & types
|
|
||||||
|
|
||||||
* Wrong type: `-WRONGTYPE Operation against a key holding the wrong kind of value\r\n`
|
|
||||||
* Non-existing key:
|
|
||||||
|
|
||||||
* Push: creates the list (returns new length).
|
|
||||||
* Pop (non-blocking): returns **Null**.
|
|
||||||
* Blocking pop: **Null Array** on timeout. ([Redis][1])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 4) Blocking engine (implementation sketch)
|
|
||||||
|
|
||||||
1. **Call time**: scan keys in user order. If a non-empty list is found, pop & reply immediately.
|
|
||||||
2. **Otherwise**: register the client as **blocked** on those keys with `deadline = now + timeout` (or infinite).
|
|
||||||
3. **On push to any key**: if waiters exist, **wake one** (FIFO) and serve its pop **atomically** with the push result.
|
|
||||||
4. **On timer**: for each blocked client whose deadline passed, reply `Null Array` and clear state.
|
|
||||||
5. **Connection close**: remove from any wait queues.
|
|
||||||
|
|
||||||
Refs for timeout/block semantics. ([Redis][4])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 5) Reliable queue pattern (recommended)
|
|
||||||
|
|
||||||
* **Consume**: `LMOVE queue processing RIGHT LEFT` (or `BLMOVE ... 0`).
|
|
||||||
* **Process** the job.
|
|
||||||
* **ACK**: `LREM processing 1 <job>` when done.
|
|
||||||
* **Reaper**: auxiliary task that detects stale jobs (e.g., track job IDs + timestamps in a ZSET) and requeues them. (Lists don’t include timestamps; pairing with a ZSET is standard practice.)
|
|
||||||
Refs: LMOVE doc’s pattern. ([Redis][5])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 6) Minimal test matrix
|
|
||||||
|
|
||||||
* Push/pop happy path (both ends), with/without COUNT.
|
|
||||||
* Blocking pop: immediate availability, block + timeout, wake on push, multiple keys order, FIFO across multiple waiters.
|
|
||||||
* LMOVE/BLMOVE: RIGHT→LEFT pipeline, block + wake, cross-list atomicity, ACK via LREM.
|
|
||||||
* Type errors and key deletion on last pop.
|
|
||||||
|
|
@@ -12,6 +12,7 @@ echo ""
|
|||||||
echo "2️⃣ Running Comprehensive Redis Integration Tests (13 tests)..."
|
echo "2️⃣ Running Comprehensive Redis Integration Tests (13 tests)..."
|
||||||
echo "----------------------------------------------------------------"
|
echo "----------------------------------------------------------------"
|
||||||
cargo test -p herodb --test redis_integration_tests -- --nocapture
|
cargo test -p herodb --test redis_integration_tests -- --nocapture
|
||||||
|
cargo test -p herodb --test redis_basic_client -- --nocapture
|
||||||
cargo test -p herodb --test debug_hset -- --nocapture
|
cargo test -p herodb --test debug_hset -- --nocapture
|
||||||
cargo test -p herodb --test debug_hset_simple -- --nocapture
|
cargo test -p herodb --test debug_hset_simple -- --nocapture
|
||||||
|
|
1
src/main.rs
Normal file
1
src/main.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
fn main() {}
|
Reference in New Issue
Block a user