Skip to content

Commit c2761ce

Browse files
committed
redis RDB file format parser
1 parent a46fbaa commit c2761ce

10 files changed

Lines changed: 288 additions & 52 deletions

File tree

README.md

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Build Your Own Redis
1+
# Build Your Own Redis in Rust
22

33
This project is to build a toy Redis-Server clone that's capable of handling
44
basic commands like `PING`, `SET` and `GET`. Also implement the event loops, the Redis protocol and more.
@@ -9,6 +9,8 @@ install `redis-cli` first (an implementation of redis client for test purpose)
99
cargo install mini-redis
1010
```
1111

12+
Know about [Redis protocoal](https://redis.io/docs/latest/develop/reference/protocol-spec)
13+
1214
## Start the Redis-rs server
1315
```sh
1416
cargo run -- --dir /tmp/redis-files --dbfilename dump.rdb
@@ -18,28 +20,136 @@ cargo run -- --dir /tmp/redis-files --dbfilename dump.rdb
1820
## Supported Commands
1921
```sh
2022
redis-cli PING
23+
redis-cli ECHO hey
24+
redis-cli SET foo bar
25+
redis-cli SET foo bar px 100
26+
redis-cli GET foo
27+
redis-cli CONFIG GET dbfilename
28+
redis-cli KEYS "*"
2129
```
2230

31+
## RDB Persistence
32+
Get Redis-rs server config
2333
```sh
24-
redis-cli ECHO hey
34+
redis-cli CONFIG GET dbfilename
35+
```
36+
### RDB file format overview
37+
Here are the different sections of the [RDB file](https://rdb.fnordig.de/file_format.html), in order:
38+
+ Header section
39+
+ Metadata section
40+
+ Database section
41+
+ End of file section
42+
#### Header section
43+
start with some magic number
44+
```sh
45+
52 45 44 49 53 30 30 31 31 // Magic string + version number (ASCII): "REDIS0011".
46+
```
47+
#### Metadata section
48+
contains zero or more "metadata subsections", which each specify a single metadata attribute
49+
e.g.
50+
```sh
51+
FA // Indicates the start of a metadata subsection.
52+
09 72 65 64 69 73 2D 76 65 72 // The name of the metadata attribute (string encoded): "redis-ver".
53+
06 36 2E 30 2E 31 36 // The value of the metadata attribute (string encoded): "6.0.16".
54+
```
55+
#### Database section
56+
contains zero or more "database subsections," which each describe a single database.
57+
e.g.
58+
```sh
59+
FE // Indicates the start of a database subsection.
60+
00 /* The index of the database (size encoded). Here, the index is 0. */
61+
62+
FB // Indicates that hash table size information follows.
63+
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. */
64+
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. */
2565
```
2666

2767
```sh
28-
redis-cli SET foo bar
68+
00 /* The 1-byte flag that specifies the value’s type and encoding. Here, the flag is 0, which means "string." */
69+
06 66 6F 6F 62 61 72 // The name of the key (string encoded). Here, it's "foobar".
70+
06 62 61 7A 71 75 78 // The value (string encoded). Here, it's "bazqux".
2971
```
3072

3173
```sh
32-
redis-cli SET foo bar px 100
74+
FC /* Indicates that this key ("foo") has an expire, and that the expire timestamp is expressed in milliseconds. */
75+
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. */
76+
00 // Value type is string.
77+
03 66 6F 6F // Key name is "foo".
78+
03 62 61 72 // Value is "bar".
3379
```
3480

3581
```sh
36-
redis-cli GET foo
82+
FD /* Indicates that this key ("baz") has an expire, and that the expire timestamp is expressed in seconds. */
83+
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. */
84+
00 // Value type is string.
85+
03 62 61 7A // Key name is "baz".
86+
03 71 75 78 // Value is "qux".
3787
```
3888

89+
In summary,
90+
- Optional expire information (one of the following):
91+
- Timestamp in seconds:
92+
- FD
93+
- Expire timestamp in seconds (4-byte unsigned integer)
94+
- Timestamp in milliseconds:
95+
- FC
96+
- Expire timestamp in milliseconds (8-byte unsigned long)
97+
- Value type (1-byte flag)
98+
- Key (string encoded)
99+
- Value (encoding depends on value type)
39100

101+
#### End of file section
102+
```sh
103+
FF /* Indicates that the file is ending, and that the checksum follows. */
104+
89 3b b7 4e f8 0f 77 19 // An 8-byte CRC64 checksum of the entire file.
105+
```
40106

41-
## RDB Persistence
42-
Get Redis-rs server config
107+
#### Size encoding
43108
```sh
44-
redis-cli CONFIG GET dbfilename
109+
/* If the first two bits are 0b00:
110+
The size is the remaining 6 bits of the byte.
111+
In this example, the size is 10: */
112+
0A
113+
00001010
114+
115+
/* If the first two bits are 0b01:
116+
The size is the next 14 bits
117+
(remaining 6 bits in the first byte, combined with the next byte),
118+
in big-endian (read left-to-right).
119+
In this example, the size is 700: */
120+
42 BC
121+
01000010 10111100
122+
123+
/* If the first two bits are 0b10:
124+
Ignore the remaining 6 bits of the first byte.
125+
The size is the next 4 bytes, in big-endian (read left-to-right).
126+
In this example, the size is 17000: */
127+
80 00 00 42 68
128+
10000000 00000000 00000000 01000010 01101000
129+
130+
/* If the first two bits are 0b11:
131+
The remaining 6 bits specify a type of string encoding.
132+
See string encoding section. */
133+
```
134+
135+
#### String encoding
136+
+ The size of the string (size encoded).
137+
+ The string.
138+
```sh
139+
/* The 0x0D size specifies that the string is 13 characters long. The remaining characters spell out "Hello, World!". */
140+
0D 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21
141+
```
142+
For sizes that begin with 0b11, the remaining 6 bits indicate a type of string format:
143+
```sh
144+
/* The 0xC0 size indicates the string is an 8-bit integer. In this example, the string is "123". */
145+
C0 7B
146+
147+
/* 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". */
148+
C1 39 30
149+
150+
/* 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". */
151+
C2 87 D6 12 00
152+
153+
/* The 0xC3 size indicates that the string is compressed with the LZF algorithm. You will not encounter LZF-compressed strings in this challenge. */
154+
C3 ...
45155
```

src/cmd.rs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::{protocol::Protocol, server::Server};
2-
use anyhow::Result;
1+
use crate::{error::Error, protocol::Protocol, server::Server};
32

43
pub enum Cmd {
54
Ping,
@@ -8,17 +7,18 @@ pub enum Cmd {
87
Set(String, String),
98
SetPx(String, String, u128),
109
SetEx(String, String, u128),
10+
Keys,
1111
ConfigGet(String),
1212
}
1313

1414
impl Cmd {
15-
pub fn from(s: &str) -> Result<Self> {
15+
pub fn from(s: &str) -> Result<Self, Error> {
1616
let protocol = Protocol::from(s)?;
1717
match protocol.0 {
1818
Protocol::Array(p) => {
1919
let cmd = p.into_iter().map(|x| x.decode()).collect::<Vec<_>>();
2020
if cmd.len() == 0 {
21-
return Err(anyhow::anyhow!("cmd length is 0"));
21+
return Err(Error::E("cmd length is 0".to_string()));
2222
}
2323
Ok(match cmd[0].as_str() {
2424
"echo" => Cmd::Echo(cmd[1].clone()),
@@ -35,24 +35,34 @@ impl Cmd {
3535
}
3636
"config" => {
3737
if cmd.len() != 3 || cmd[1] != "get" {
38-
return Err(anyhow::anyhow!("unsupported cmd {:?}", cmd));
38+
return Err(Error::E(format!("unsupported cmd {:?}", cmd)));
3939
} else {
4040
Cmd::ConfigGet(cmd[2].clone())
4141
}
4242
}
43-
_ => return Err(anyhow::anyhow!("unknown cmd {:?}", cmd[0])),
43+
"keys" => {
44+
if cmd.len() != 2 || cmd[1] != "*" {
45+
return Err(Error::E(format!("unsupported cmd {:?}", cmd)));
46+
} else {
47+
Cmd::Keys
48+
}
49+
}
50+
_ => return Err(Error::E(format!("unknown cmd {:?}", cmd[0]))),
4451
})
4552
}
46-
_ => Err(anyhow::anyhow!("fail to parse as cmd for {:?}", protocol.0)),
53+
_ => Err(Error::E(format!(
54+
"fail to parse as cmd for {:?}",
55+
protocol.0
56+
))),
4757
}
4858
}
4959

50-
pub fn run(self: &Self, server: &mut Server) -> Result<Protocol> {
60+
pub fn run(self: &Self, server: &mut Server) -> Result<Protocol, String> {
5161
match self {
5262
Cmd::Ping => Ok(Protocol::SimpleString("PONG".to_string())),
5363
Cmd::Echo(s) => Ok(Protocol::SimpleString(s.clone())),
5464
Cmd::Get(k) => {
55-
let s = server.storage.lock().unwrap();
65+
let s = server.storage.read().unwrap();
5666
Ok(if let Some(v) = s.get(k) {
5767
Protocol::SimpleString(v.clone())
5868
} else {
@@ -61,21 +71,21 @@ impl Cmd {
6171
}
6272
Cmd::Set(k, v) => {
6373
{
64-
let mut s = server.storage.lock().unwrap();
74+
let mut s = server.storage.write().unwrap();
6575
s.set(k.clone(), v.clone());
6676
}
6777
Ok(Protocol::ok())
6878
}
6979
Cmd::SetPx(k, v, x) => {
7080
{
71-
let mut s = server.storage.lock().unwrap();
81+
let mut s = server.storage.write().unwrap();
7282
s.setx(k.clone(), v.clone(), *x);
7383
}
7484
Ok(Protocol::ok())
7585
}
7686
Cmd::SetEx(k, v, x) => {
7787
{
78-
let mut s = server.storage.lock().unwrap();
88+
let mut s = server.storage.write().unwrap();
7989
s.setx(k.clone(), v.clone(), *x * 1000);
8090
}
8191
Ok(Protocol::ok())
@@ -89,8 +99,14 @@ impl Cmd {
8999
Protocol::BulkString(name.clone()),
90100
Protocol::BulkString(server.option.db_file_name.clone()),
91101
])),
92-
_ => Err(anyhow::anyhow!("unsupported config {:?}", name)),
102+
_ => Err(format!("unsupported config {:?}", name)),
93103
},
104+
Cmd::Keys => {
105+
let keys = { server.storage.read().unwrap().keys() };
106+
Ok(Protocol::Array(
107+
keys.into_iter().map(|x| Protocol::BulkString(x)).collect(),
108+
))
109+
}
94110
}
95111
}
96112
}

src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// todo: more error types
2+
pub enum Error {
3+
E(String),
4+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod cmd;
2+
pub mod error;
23
pub mod options;
34
mod protocol;
5+
mod rdb;
46
pub mod server;
57
mod storage;

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![allow(unused_imports)]
1+
// #![allow(unused_imports)]
22

33
use redis_rs::server;
44

src/options.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
pub struct DBOption {
33
pub dir: String,
44
pub db_file_name: String,
5+
pub redis_version: String,
56
}

0 commit comments

Comments
 (0)