├── .gitattributes
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── README.md
├── assets
└── banner.jpg
└── src
├── client.rs
├── main.rs
├── persistence
├── json.rs
├── mod.rs
└── persister.rs
├── pubsub
├── commands
│ ├── cmd.rs
│ └── mod.rs
├── engine.rs
└── mod.rs
├── resp
├── commands
│ ├── hash_set.rs
│ ├── key.rs
│ ├── list.rs
│ ├── mod.rs
│ ├── number.rs
│ ├── set.rs
│ └── string.rs
├── handler.rs
├── mod.rs
└── utils.rs
├── server.rs
├── store
├── commands
│ ├── hash_set.rs
│ ├── key.rs
│ ├── list.rs
│ ├── mod.rs
│ ├── number.rs
│ ├── set.rs
│ └── string.rs
├── db.rs
├── expiry.rs
└── mod.rs
├── types.rs
└── utils.rs
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | debug/
4 | target/
5 |
6 | # These are backup files generated by rustfmt
7 | **/*.rs.bk
8 |
9 | # MSVC Windows builds of rustc generate these, which store debugging information
10 | *.pdb
11 |
12 | #Json persistent storage for the database
13 | db.json
14 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.24.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler2"
16 | version = "2.0.0"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
19 |
20 | [[package]]
21 | name = "autocfg"
22 | version = "1.4.0"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
25 |
26 | [[package]]
27 | name = "backtrace"
28 | version = "0.3.74"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
31 | dependencies = [
32 | "addr2line",
33 | "cfg-if",
34 | "libc",
35 | "miniz_oxide",
36 | "object",
37 | "rustc-demangle",
38 | "windows-targets",
39 | ]
40 |
41 | [[package]]
42 | name = "bitflags"
43 | version = "2.9.0"
44 | source = "registry+https://github.com/rust-lang/crates.io-index"
45 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
46 |
47 | [[package]]
48 | name = "bytes"
49 | version = "1.10.1"
50 | source = "registry+https://github.com/rust-lang/crates.io-index"
51 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
52 |
53 | [[package]]
54 | name = "cfg-if"
55 | version = "1.0.0"
56 | source = "registry+https://github.com/rust-lang/crates.io-index"
57 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
58 |
59 | [[package]]
60 | name = "gimli"
61 | version = "0.31.1"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
64 |
65 | [[package]]
66 | name = "itoa"
67 | version = "1.0.15"
68 | source = "registry+https://github.com/rust-lang/crates.io-index"
69 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
70 |
71 | [[package]]
72 | name = "libc"
73 | version = "0.2.172"
74 | source = "registry+https://github.com/rust-lang/crates.io-index"
75 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
76 |
77 | [[package]]
78 | name = "lock_api"
79 | version = "0.4.12"
80 | source = "registry+https://github.com/rust-lang/crates.io-index"
81 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
82 | dependencies = [
83 | "autocfg",
84 | "scopeguard",
85 | ]
86 |
87 | [[package]]
88 | name = "memchr"
89 | version = "2.7.4"
90 | source = "registry+https://github.com/rust-lang/crates.io-index"
91 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
92 |
93 | [[package]]
94 | name = "miniz_oxide"
95 | version = "0.8.8"
96 | source = "registry+https://github.com/rust-lang/crates.io-index"
97 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
98 | dependencies = [
99 | "adler2",
100 | ]
101 |
102 | [[package]]
103 | name = "mio"
104 | version = "1.0.3"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
107 | dependencies = [
108 | "libc",
109 | "wasi",
110 | "windows-sys",
111 | ]
112 |
113 | [[package]]
114 | name = "object"
115 | version = "0.36.7"
116 | source = "registry+https://github.com/rust-lang/crates.io-index"
117 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
118 | dependencies = [
119 | "memchr",
120 | ]
121 |
122 | [[package]]
123 | name = "parking_lot"
124 | version = "0.12.3"
125 | source = "registry+https://github.com/rust-lang/crates.io-index"
126 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
127 | dependencies = [
128 | "lock_api",
129 | "parking_lot_core",
130 | ]
131 |
132 | [[package]]
133 | name = "parking_lot_core"
134 | version = "0.9.10"
135 | source = "registry+https://github.com/rust-lang/crates.io-index"
136 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
137 | dependencies = [
138 | "cfg-if",
139 | "libc",
140 | "redox_syscall",
141 | "smallvec",
142 | "windows-targets",
143 | ]
144 |
145 | [[package]]
146 | name = "pin-project-lite"
147 | version = "0.2.16"
148 | source = "registry+https://github.com/rust-lang/crates.io-index"
149 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
150 |
151 | [[package]]
152 | name = "proc-macro2"
153 | version = "1.0.94"
154 | source = "registry+https://github.com/rust-lang/crates.io-index"
155 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
156 | dependencies = [
157 | "unicode-ident",
158 | ]
159 |
160 | [[package]]
161 | name = "quote"
162 | version = "1.0.40"
163 | source = "registry+https://github.com/rust-lang/crates.io-index"
164 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
165 | dependencies = [
166 | "proc-macro2",
167 | ]
168 |
169 | [[package]]
170 | name = "redis_rust"
171 | version = "0.1.0"
172 | dependencies = [
173 | "serde",
174 | "serde_json",
175 | "tokio",
176 | ]
177 |
178 | [[package]]
179 | name = "redox_syscall"
180 | version = "0.5.11"
181 | source = "registry+https://github.com/rust-lang/crates.io-index"
182 | checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
183 | dependencies = [
184 | "bitflags",
185 | ]
186 |
187 | [[package]]
188 | name = "rustc-demangle"
189 | version = "0.1.24"
190 | source = "registry+https://github.com/rust-lang/crates.io-index"
191 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
192 |
193 | [[package]]
194 | name = "ryu"
195 | version = "1.0.20"
196 | source = "registry+https://github.com/rust-lang/crates.io-index"
197 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
198 |
199 | [[package]]
200 | name = "scopeguard"
201 | version = "1.2.0"
202 | source = "registry+https://github.com/rust-lang/crates.io-index"
203 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
204 |
205 | [[package]]
206 | name = "serde"
207 | version = "1.0.219"
208 | source = "registry+https://github.com/rust-lang/crates.io-index"
209 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
210 | dependencies = [
211 | "serde_derive",
212 | ]
213 |
214 | [[package]]
215 | name = "serde_derive"
216 | version = "1.0.219"
217 | source = "registry+https://github.com/rust-lang/crates.io-index"
218 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
219 | dependencies = [
220 | "proc-macro2",
221 | "quote",
222 | "syn",
223 | ]
224 |
225 | [[package]]
226 | name = "serde_json"
227 | version = "1.0.140"
228 | source = "registry+https://github.com/rust-lang/crates.io-index"
229 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
230 | dependencies = [
231 | "itoa",
232 | "memchr",
233 | "ryu",
234 | "serde",
235 | ]
236 |
237 | [[package]]
238 | name = "signal-hook-registry"
239 | version = "1.4.2"
240 | source = "registry+https://github.com/rust-lang/crates.io-index"
241 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
242 | dependencies = [
243 | "libc",
244 | ]
245 |
246 | [[package]]
247 | name = "smallvec"
248 | version = "1.15.0"
249 | source = "registry+https://github.com/rust-lang/crates.io-index"
250 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
251 |
252 | [[package]]
253 | name = "socket2"
254 | version = "0.5.9"
255 | source = "registry+https://github.com/rust-lang/crates.io-index"
256 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
257 | dependencies = [
258 | "libc",
259 | "windows-sys",
260 | ]
261 |
262 | [[package]]
263 | name = "syn"
264 | version = "2.0.100"
265 | source = "registry+https://github.com/rust-lang/crates.io-index"
266 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
267 | dependencies = [
268 | "proc-macro2",
269 | "quote",
270 | "unicode-ident",
271 | ]
272 |
273 | [[package]]
274 | name = "tokio"
275 | version = "1.44.2"
276 | source = "registry+https://github.com/rust-lang/crates.io-index"
277 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
278 | dependencies = [
279 | "backtrace",
280 | "bytes",
281 | "libc",
282 | "mio",
283 | "parking_lot",
284 | "pin-project-lite",
285 | "signal-hook-registry",
286 | "socket2",
287 | "tokio-macros",
288 | "windows-sys",
289 | ]
290 |
291 | [[package]]
292 | name = "tokio-macros"
293 | version = "2.5.0"
294 | source = "registry+https://github.com/rust-lang/crates.io-index"
295 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
296 | dependencies = [
297 | "proc-macro2",
298 | "quote",
299 | "syn",
300 | ]
301 |
302 | [[package]]
303 | name = "unicode-ident"
304 | version = "1.0.18"
305 | source = "registry+https://github.com/rust-lang/crates.io-index"
306 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
307 |
308 | [[package]]
309 | name = "wasi"
310 | version = "0.11.0+wasi-snapshot-preview1"
311 | source = "registry+https://github.com/rust-lang/crates.io-index"
312 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
313 |
314 | [[package]]
315 | name = "windows-sys"
316 | version = "0.52.0"
317 | source = "registry+https://github.com/rust-lang/crates.io-index"
318 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
319 | dependencies = [
320 | "windows-targets",
321 | ]
322 |
323 | [[package]]
324 | name = "windows-targets"
325 | version = "0.52.6"
326 | source = "registry+https://github.com/rust-lang/crates.io-index"
327 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
328 | dependencies = [
329 | "windows_aarch64_gnullvm",
330 | "windows_aarch64_msvc",
331 | "windows_i686_gnu",
332 | "windows_i686_gnullvm",
333 | "windows_i686_msvc",
334 | "windows_x86_64_gnu",
335 | "windows_x86_64_gnullvm",
336 | "windows_x86_64_msvc",
337 | ]
338 |
339 | [[package]]
340 | name = "windows_aarch64_gnullvm"
341 | version = "0.52.6"
342 | source = "registry+https://github.com/rust-lang/crates.io-index"
343 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
344 |
345 | [[package]]
346 | name = "windows_aarch64_msvc"
347 | version = "0.52.6"
348 | source = "registry+https://github.com/rust-lang/crates.io-index"
349 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
350 |
351 | [[package]]
352 | name = "windows_i686_gnu"
353 | version = "0.52.6"
354 | source = "registry+https://github.com/rust-lang/crates.io-index"
355 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
356 |
357 | [[package]]
358 | name = "windows_i686_gnullvm"
359 | version = "0.52.6"
360 | source = "registry+https://github.com/rust-lang/crates.io-index"
361 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
362 |
363 | [[package]]
364 | name = "windows_i686_msvc"
365 | version = "0.52.6"
366 | source = "registry+https://github.com/rust-lang/crates.io-index"
367 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
368 |
369 | [[package]]
370 | name = "windows_x86_64_gnu"
371 | version = "0.52.6"
372 | source = "registry+https://github.com/rust-lang/crates.io-index"
373 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
374 |
375 | [[package]]
376 | name = "windows_x86_64_gnullvm"
377 | version = "0.52.6"
378 | source = "registry+https://github.com/rust-lang/crates.io-index"
379 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
380 |
381 | [[package]]
382 | name = "windows_x86_64_msvc"
383 | version = "0.52.6"
384 | source = "registry+https://github.com/rust-lang/crates.io-index"
385 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
386 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 |
2 | [package]
3 | name = "redis_rust"
4 | version = "0.1.0"
5 | edition = "2021"
6 |
7 | [dependencies]
8 | serde_json = "1.0.140"
9 | serde = { version = "1.0", features = ["derive"] }
10 | tokio = { version = "1", features = ["full"] }
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🔥 rizzlerdb — A Redis-Inspired Server in Rust (Async + Pub/Sub Ready)
2 |
3 |
4 |
5 |
6 |
7 | **rizzlerdb** is a lightweight, high-performance Redis-like server built from the ground up in Rust.
8 |
9 | No frameworks. No shortcuts. Just raw TCP, a hand-crafted RESP protocol parser, and deep control over memory and concurrency using Tokio. Designed for learning, performance, and backend mastery.
10 |
11 | ---
12 |
13 | 🚀 **Architecture Overview**
14 |
15 | - 🤩 Clients connect over raw TCP on port `6379`
16 | - 🥵 Each connection is handled asynchronously via Tokio tasks
17 | - 🧠 Commands are parsed with a custom-built RESP protocol parser
18 | - 🧱 Shared state is managed with an `Arc>`-based central `Database`
19 | - 📂 All mutations are persisted using a pluggable `Persister` trait
20 | - Default: `JsonPersister`, writing to `db.json`
21 | - 🔄 On startup, previous state is rehydrated from disk
22 | - 📣 Full support for Pub/Sub using async channel broadcasting
23 |
24 | ---
25 |
26 | ✅ **Implemented Features**
27 |
28 | ### ☑ Infrastructure
29 |
30 | - [x] Async TCP server using Tokio
31 | - [x] Manual RESP parser (zero dependencies)
32 | - [x] In-memory storage with `HashMap`, `Vec`, and other core types
33 | - [x] Multithreaded, safe concurrency with Tokio + `Arc>`
34 | - [x] Background expiry workers for keys with TTL
35 | - [x] Auto persistence via `JsonPersister`
36 | - [x] Disk hydration at boot
37 | - [x] Real-time Pub/Sub system
38 |
39 | ### ☑ Supported Commands
40 |
41 | #### 🧠 String Operations
42 | `PING`, `ECHO`, `SET`, `GET`, `DEL`, `EXISTS`, `INCR`, `INCRBY`, `DECR`, `DECRBY`
43 |
44 | #### ⏳ Expiry & TTL
45 | `EXPIRE`, `TTL`, `PERSIST`
46 |
47 | #### 🧺 List Operations
48 | `LPUSH`, `RPUSH`, `LPOP`, `RPOP`, `LRANGE`, `LLEN`, `LINDEX`, `LSET`
49 |
50 | #### 📐 Set Operations
51 | `SADD`, `SREM`, `SMEMBERS`, `SISMEMBER`, `SCARD`
52 |
53 | #### 💃 Hash Operations
54 | `HSET`, `HGET`, `HDEL`, `HKEYS`, `HVALS`, `HGETALL`, `HEXISTS`, `HLEN`
55 |
56 | #### 📡 Pub/Sub
57 | `PUBLISH`, `SUBSCRIBE` — instant message delivery across clients
58 |
59 | #### 🔍 Miscellaneous
60 | `KEYS` with basic pattern matching
61 |
62 | ---
63 |
64 | 📂 **Running Locally**
65 |
66 | Start the server:
67 | ```bash
68 | cargo run
69 | ```
70 |
71 | Connect using the Redis CLI:
72 | ```bash
73 | redis-cli -p 6379
74 | ```
75 |
76 | Sample session:
77 | ```redis
78 | > SET name gigachad
79 | > GET name
80 | > INCR count
81 | > LPUSH queue task1
82 | > HSET user name yash
83 | > SUBSCRIBE news
84 | > PUBLISH news "the backend villain strikes again"
85 | ```
86 |
87 | ---
88 |
89 | 🔮 **Planned Enhancements**
90 |
91 | - [ ] Advanced Pub/Sub features (patterns, multi-channel, unsubscribe)
92 | - [ ] Config file support (e.g., custom ports, persistence settings)
93 | - [ ] Key eviction strategies (LRU / LFU)
94 | - [ ] RDB-style memory snapshots
95 | - [ ] AOF-style persistence (append-only)
96 |
97 | ---
98 |
99 | 🤔 **Motivation**
100 |
101 | Redis is a cornerstone of high-performance backend architecture. Rust offers powerful tools for safe concurrency, low-level control, and memory safety. Building rizzlerdb merges both worlds, delivering a hands-on learning journey through network programming, protocol design, persistence strategies, and async architectures.
102 |
103 | This isn’t just a clone. It’s a deep dive. A backend developer's training ground.
104 |
105 | ---
106 |
107 | 📌 **Repository**
108 |
109 | GitHub: [github.com/pixperk/redis_in_rust](https://github.com/pixperk/redis_in_rust)
110 |
111 | ---
112 |
113 | 🙏 **Acknowledgements**
114 |
115 | - Redis Official Documentation
116 | - RESP Protocol Specification
117 | - Tokio and the broader Rust community
118 |
119 | ---
120 |
121 | Star it ⭐ | Fork it 🍴 | Hack it 🧠 | Rizz it 🭝 | Deploy it 💥
122 |
123 |
--------------------------------------------------------------------------------
/assets/banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pixperk/redis_in_rust/b3bc2c5cc19e0fcd6a68a72a166e7a4b35292a2b/assets/banner.jpg
--------------------------------------------------------------------------------
/src/client.rs:
--------------------------------------------------------------------------------
1 | use crate::pubsub::PubSub;
2 | use crate::{
3 | persistence::Persister, resp::handler::handle_command, store::db::Database,
4 | utils::is_mutating_command,
5 | };
6 |
7 | use std::sync::Arc;
8 | use tokio::io::BufReader;
9 | use tokio::{
10 | io::{AsyncReadExt, AsyncWriteExt},
11 | net::TcpStream,
12 | sync::Mutex,
13 | };
14 |
15 | pub async fn handle_connection(
16 | stream: TcpStream,
17 | db: Arc>,
18 | persister: Arc,
19 | pubsub: Arc,
20 | ) {
21 | let (reader, writer) = stream.into_split();
22 | let writer = Arc::new(Mutex::new(writer));
23 | let mut reader = BufReader::new(reader);
24 | let mut buf = [0; 512];
25 |
26 | loop {
27 | let bytes_read = match reader.read(&mut buf).await {
28 | Ok(0) => {
29 | println!("⚠️ Client disconnected");
30 | break;
31 | }
32 | Ok(n) => n,
33 | Err(e) => {
34 | eprintln!("Read error: {e}");
35 | break;
36 | }
37 | };
38 |
39 | let input = String::from_utf8_lossy(&buf[..bytes_read]);
40 | println!("📥 Received:\n{input}");
41 |
42 | let command_name = input
43 | .lines()
44 | .find(|line| line.starts_with('$'))
45 | .map(|_| input.lines().nth(2).unwrap_or(""))
46 | .unwrap_or("")
47 | .to_uppercase();
48 |
49 |
50 |
51 | let mut db = db.lock().await;
52 |
53 | let response = handle_command(
54 | &input,
55 | &mut db,
56 | Arc::clone(&pubsub),
57 | Arc::clone(&writer),
58 | )
59 | .await;
60 |
61 |
62 |
63 | // Save to disk if mutating
64 | if is_mutating_command(&command_name) {
65 | if let Err(e) = persister.save(&db) {
66 | eprintln!("❌ Failed to save database: {e}");
67 | } else {
68 | println!("💾 Database saved to disk");
69 | }
70 | }
71 |
72 | if command_name!= "SUBSCRIBE"{
73 | let mut s = writer.lock().await;
74 | if let Err(e) = s.write_all(response.as_bytes()).await {
75 | eprintln!("❌ Write error: {e}");
76 | break;
77 | }
78 | if let Err(e) = s.flush().await {
79 | eprintln!("❌ Flush error: {e}");
80 | break;
81 | }
82 | drop(s); // Explicitly drop the lock to avoid holding it longer than necessary
83 |
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | mod server;
4 | mod store;
5 | mod resp;
6 | mod types;
7 | mod client;
8 | mod persistence;
9 | mod utils;
10 | mod pubsub;
11 |
12 | use pubsub::PubSub;
13 |
14 | use crate::persistence::JsonPersister;
15 |
16 | #[tokio::main]
17 | async fn main(){
18 | const REDIS_PORT: &str = "127.0.0.1:6379";
19 |
20 |
21 | println!("🚀 Redis (Rust Edition) listening on {REDIS_PORT}");
22 |
23 | let persister = Arc::new(JsonPersister::new("db.json"));
24 | let pubsub = PubSub::new();
25 |
26 | server::run(REDIS_PORT, persister, pubsub).await;
27 | }
28 |
--------------------------------------------------------------------------------
/src/persistence/json.rs:
--------------------------------------------------------------------------------
1 | use std::{fs, path::Path};
2 |
3 | use serde::{Deserialize, Serialize};
4 |
5 | use crate::store::Database;
6 |
7 | use super::persister::Persister;
8 |
9 |
10 | #[derive(Serialize, Deserialize, Debug)]
11 | pub struct JsonPersister {
12 | path: String,
13 | }
14 |
15 | impl JsonPersister{
16 | pub fn new(path : &str) -> Self{
17 | Self { path: path.to_string() }
18 | }
19 | }
20 |
21 | impl Persister for JsonPersister{
22 | fn load(&self) -> Option{
23 | if !Path::new(&self.path).exists(){
24 | return None;
25 | }
26 |
27 | let data = fs::read_to_string(&self.path).ok()?;
28 | match serde_json::from_str(&data) {
29 | Ok(db) => Some(db),
30 | Err(e) => {
31 | eprintln!("Failed to parse db.json: {e}");
32 | None
33 | }
34 | }
35 | }
36 |
37 | fn save(&self, db:&Database) -> Result<(), Box> {
38 | let data = serde_json::to_string_pretty(db)?;
39 | fs::write(&self.path, data)?;
40 |
41 | Ok(())
42 |
43 | }
44 |
45 |
46 | }
--------------------------------------------------------------------------------
/src/persistence/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod json;
2 | pub mod persister;
3 |
4 | pub use json::JsonPersister;
5 | pub use persister::Persister;
6 |
--------------------------------------------------------------------------------
/src/persistence/persister.rs:
--------------------------------------------------------------------------------
1 | use std::error::Error;
2 |
3 | use crate::store::Database;
4 |
5 | pub trait Persister {
6 | fn load(&self) -> Option;
7 | fn save(&self, db: &Database) -> Result<(), Box>;
8 | }
9 |
--------------------------------------------------------------------------------
/src/pubsub/commands/cmd.rs:
--------------------------------------------------------------------------------
1 | use std:: sync::Arc;
2 | use tokio::{io::AsyncWriteExt, net::tcp::OwnedWriteHalf, sync::Mutex};
3 |
4 | use crate::pubsub::PubSub;
5 |
6 | pub async fn handle_subscribe(
7 | parts: Vec<&str>,
8 | writer: Arc>,
9 | pubsub: Arc,
10 | ) {
11 | if parts.len() != 2 {
12 | let mut w = writer.lock().await;
13 | let _ = w.write_all(b"-ERR usage: SUBSCRIBE \r\n").await;
14 | let _ = w.flush().await;
15 | return;
16 | }
17 |
18 | let channel = parts[1].to_string();
19 | let mut rx = pubsub.subscribe(&channel).await;
20 |
21 | // Spawn listener for published messages
22 | let writer_clone = Arc::clone(&writer);
23 | tokio::spawn(async move {
24 | while let Some(msg) = rx.recv().await {
25 | let mut w = writer_clone.lock().await;
26 | println!("Received message: {} on channel: {}", msg, channel);
27 | let msg_type = "message";
28 |
29 | let response = format!(
30 | "*3\r\n${}\r\n{}\r\n${}\r\n{}\r\n${}\r\n{}\r\n",
31 | msg_type.len(),
32 | msg_type,
33 | channel.len(),
34 | channel,
35 | msg.len(),
36 | msg
37 | );
38 |
39 | if let Err(e) = w.write_all(response.as_bytes()).await {
40 | eprintln!("Failed to write message: {:?}", e);
41 | break;
42 | }
43 | if let Err(e) = w.flush().await {
44 | eprintln!("Failed to flush: {:?}", e);
45 | break;
46 | }
47 | }
48 | });
49 | }
50 |
51 | pub async fn handle_publish(
52 | parts: Vec<&str>,
53 | pubsub: Arc,
54 | ) {
55 |
56 |
57 | let channel = parts[1];
58 | let message = parts[2..].join(" ");
59 | pubsub.publish(channel, message).await;
60 |
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/pubsub/commands/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod cmd;
2 |
--------------------------------------------------------------------------------
/src/pubsub/engine.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, sync::Arc};
2 | use tokio::sync::{
3 | mpsc::{self, unbounded_channel, UnboundedSender},
4 | Mutex,
5 | };
6 |
7 | type Subscriber = UnboundedSender;
8 |
9 | #[derive(Debug, Default)]
10 | pub struct PubSub {
11 | channels: Mutex>>,
12 | }
13 |
14 | impl PubSub {
15 | pub fn new() -> Arc {
16 | Arc::new(Self::default())
17 | }
18 |
19 | pub async fn subscribe(&self, channel: &str) -> mpsc::UnboundedReceiver
20 | {
21 | let (tx, rx) = unbounded_channel();
22 | let mut channels = self.channels.lock().await;
23 | channels.entry(channel.to_string()).or_default().push(tx);
24 | rx
25 | }
26 |
27 |
28 | pub async fn publish(&self, channel: &str, message: String) {
29 | let mut channels = self.channels.lock().await;
30 | let mut delivered = 0;
31 |
32 | if let Some(subscribers) = channels.get_mut(channel) {
33 | subscribers.retain(|subscriber| {
34 | if subscriber.is_closed() {
35 | // Remove dead subscribers without publishing
36 | false
37 | } else {
38 | match subscriber.send(message.clone()) {
39 | Ok(_) => {
40 | delivered += 1;
41 | true
42 | }
43 | Err(_) => false,
44 | }
45 | }
46 | });
47 | }
48 |
49 | println!(
50 | "{{ \"channel\": \"{}\", \"message\": \"{}\", \"subscribers\": {} }}",
51 | channel, message, delivered
52 | );
53 | }
54 |
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/src/pubsub/mod.rs:
--------------------------------------------------------------------------------
1 | mod engine;
2 | mod commands;
3 | pub use engine::PubSub;
4 | pub use commands::cmd;
--------------------------------------------------------------------------------
/src/resp/commands/hash_set.rs:
--------------------------------------------------------------------------------
1 | use crate::{resp::utils::{bulk_string, format_array, wrong_args}, store::Database};
2 |
3 | pub fn handle_hash_set(cmd: &str, parts: &[String], db: &mut Database) -> String {
4 | match cmd {
5 |
6 | "HSET" => {
7 | if let Some(key) = parts.get(1) {
8 | if parts.len() < 4 || parts.len() % 2 != 0 {
9 | return wrong_args("HSET");
10 | }
11 |
12 | let mut inserted = 0;
13 | let mut i = 2;
14 | while i < parts.len() {
15 | if let (Some(field), Some(value)) = (parts.get(i), parts.get(i + 1)) {
16 | inserted += db.hset(key, field, value);
17 | i += 2;
18 | } else {
19 | return wrong_args("HSET");
20 | }
21 | }
22 |
23 | format!(":{}\r\n", inserted)
24 | } else {
25 | wrong_args("HSET")
26 | }
27 | }
28 |
29 | "HGET" => {
30 | if let (Some(key), Some(field)) = (parts.get(1), parts.get(2)) {
31 | match db.hget(key, field) {
32 | Some(val) => bulk_string(&val),
33 | None => "$-1\r\n".to_string(),
34 | }
35 | } else {
36 | wrong_args("HGET")
37 | }
38 | }
39 |
40 | "HDEL" => {
41 | if let Some(key) = parts.get(1) {
42 | let fields: Vec = parts.iter().skip(2).cloned().collect();
43 | if fields.is_empty() {
44 | wrong_args("HDEL")
45 | } else {
46 | let removed = db.hdel(key, &fields);
47 | format!(":{}\r\n", removed)
48 | }
49 | } else {
50 | wrong_args("HDEL")
51 | }
52 | }
53 |
54 | "HKEYS" => {
55 | if let Some(key) = parts.get(1) {
56 | format_array(db.hkeys(key))
57 | } else {
58 | wrong_args("HKEYS")
59 | }
60 | }
61 |
62 | "HVALS" => {
63 | if let Some(key) = parts.get(1) {
64 | format_array(db.hvals(key))
65 | } else {
66 | wrong_args("HVALS")
67 | }
68 | }
69 |
70 | "HLEN" => {
71 | if let Some(key) = parts.get(1) {
72 | let len = db.hlen(key);
73 | format!(":{}\r\n", len)
74 | } else {
75 | wrong_args("HLEN")
76 | }
77 | }
78 |
79 | "HGETALL" => {
80 | if let Some(key) = parts.get(1) {
81 | let hash = db.hgetall(key);
82 | let mut resp = format!("*{}\r\n", hash.len() * 2);
83 | for (k, v) in hash {
84 | resp.push_str(&bulk_string(&k));
85 | resp.push_str(&bulk_string(&v));
86 | }
87 | resp
88 | } else {
89 | wrong_args("HGETALL")
90 | }
91 | }
92 |
93 | "HEXISTS" => {
94 | if let (Some(key), Some(field)) = (parts.get(1), parts.get(2)) {
95 | let exists = db.hexists(key, field);
96 | format!(":{}\r\n", if exists { 1 } else { 0 })
97 | } else {
98 | wrong_args("HEXISTS")
99 | }
100 | }
101 |
102 | _ => {
103 | format!("-ERR unknown command '{}'\r\n", cmd)
104 | }
105 |
106 | }
107 | }
--------------------------------------------------------------------------------
/src/resp/commands/key.rs:
--------------------------------------------------------------------------------
1 | use crate::{resp::utils::{bulk_string, wrong_args}, store::Database};
2 |
3 | pub fn handle_key (cmd: &str, parts: &[String], db: &mut Database) -> String {
4 | match cmd{
5 | "EXISTS" => {
6 | if parts.len() < 2 {
7 | wrong_args("EXISTS")
8 | } else {
9 | let exists = db.exists(&parts[1..]);
10 | format!(":{}\r\n", exists)
11 | }
12 | }
13 |
14 | "KEYS" => {
15 | let keys = db.keys();
16 | let mut response = format!("*{}\r\n", keys.len());
17 | for key in keys {
18 | response.push_str(&bulk_string(&key));
19 | }
20 | response
21 | }
22 |
23 | "EXPIRE" => {
24 | if let (Some(key), Some(seconds_str)) = (parts.get(1), parts.get(2)) {
25 | if let Ok(seconds) = seconds_str.parse::() {
26 | db.expire(key, seconds);
27 | "+OK\r\n".to_string()
28 | } else {
29 | "-ERR invalid seconds\r\n".to_string()
30 | }
31 | } else {
32 | wrong_args("EXPIRE")
33 | }
34 | }
35 | "TTL" => {
36 | if let Some(key) = parts.get(1) {
37 | let ttl = db.ttl(key);
38 | format!(":{}\r\n", ttl)
39 | } else {
40 | wrong_args("TTL")
41 | }
42 | }
43 | "PERSIST" => {
44 | if let Some(key) = parts.get(1) {
45 | db.persist(key);
46 | "+OK\r\n".to_string()
47 | } else {
48 | wrong_args("PERSIST")
49 | }
50 | }
51 |
52 | _ => {
53 | format!("-ERR unknown command '{}'\r\n", cmd)
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/src/resp/commands/list.rs:
--------------------------------------------------------------------------------
1 | use crate::{resp::utils::{bulk_string, wrong_args}, store::Database};
2 |
3 | pub fn handle_list(cmd: &str, parts: &[String], db: &mut Database) -> String {
4 | match cmd {
5 |
6 | "LPOP" => {
7 | if let Some(key) = parts.get(1) {
8 | if let Some(value) = db.lpop(key) {
9 | bulk_string(&value)
10 | } else {
11 | "$-1\r\n".to_string()
12 | }
13 | } else {
14 | wrong_args("LPOP")
15 | }
16 | }
17 |
18 | "RPOP" => {
19 | if let Some(key) = parts.get(1) {
20 | if let Some(value) = db.rpop(key) {
21 | bulk_string(&value)
22 | } else {
23 | "$-1\r\n".to_string()
24 | }
25 | } else {
26 | wrong_args("RPOP")
27 | }
28 | }
29 |
30 | "LPUSH" => {
31 | if let Some(key) = parts.get(1) {
32 | let len = db.lpush(key, &parts[2..]);
33 | format!(":{}\r\n", len)
34 | } else {
35 | wrong_args("LPUSH")
36 | }
37 | }
38 |
39 | "RPUSH" => {
40 | if let Some(key) = parts.get(1) {
41 | let len = db.rpush(key, &parts[2..]);
42 | format!(":{}\r\n", len)
43 | } else {
44 | wrong_args("RPUSH")
45 | }
46 | }
47 |
48 | "LLEN" => {
49 | if let Some(key) = parts.get(1) {
50 | let len = db.llen(key);
51 | format!(":{}\r\n", len)
52 | } else {
53 | wrong_args("LLEN")
54 | }
55 | }
56 |
57 | "LINDEX" => {
58 | if let (Some(key), Some(index_str)) = (parts.get(1), parts.get(2)) {
59 | if let Ok(index) = index_str.parse::() {
60 | if let Some(value) = db.lindex(key, index) {
61 | bulk_string(&value)
62 | } else {
63 | "$-1\r\n".to_string()
64 | }
65 | } else {
66 | "-ERR invalid index\r\n".to_string()
67 | }
68 | } else {
69 | wrong_args("LINDEX")
70 | }
71 | }
72 |
73 | "LSET" => {
74 | if let (Some(key), Some(index_str), Some(value)) =
75 | (parts.get(1), parts.get(2), parts.get(3))
76 | {
77 | if let Ok(index) = index_str.parse::() {
78 | match db.lset(key, index, value.to_string()) {
79 | Ok(()) => "+OK\r\n".to_string(),
80 | Err(e) => format!("-ERR {}\r\n", e),
81 | }
82 | } else {
83 | "-ERR invalid index\r\n".to_string()
84 | }
85 | } else {
86 | wrong_args("LSET")
87 | }
88 | }
89 |
90 | "LRANGE" => {
91 | if let (Some(key), Some(start_str), Some(end_str)) =
92 | (parts.get(1), parts.get(2), parts.get(3))
93 | {
94 | if let (Ok(start), Ok(end)) = (start_str.parse::(), end_str.parse::())
95 | {
96 | let values = db.lrange(key, start, end);
97 | let mut response = format!("*{}\r\n", values.len());
98 | for value in values {
99 | response.push_str(&bulk_string(&value));
100 | }
101 | response
102 | } else {
103 | "-ERR invalid range\r\n".to_string()
104 | }
105 | } else {
106 | wrong_args("LRANGE")
107 | }
108 | }
109 |
110 | _ => {
111 | format!("-ERR unknown command '{}'\r\n", cmd)
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/src/resp/commands/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod string;
2 | pub mod hash_set;
3 | pub mod list;
4 | pub mod set;
5 | pub mod number;
6 | pub mod key;
7 |
--------------------------------------------------------------------------------
/src/resp/commands/number.rs:
--------------------------------------------------------------------------------
1 | use crate::{resp::utils::wrong_args, store::Database};
2 |
3 | pub fn handle_number (cmd: &str, parts: &[String], db: &mut Database) -> String {
4 | match cmd {
5 | "INCR" => {
6 | if let Some(key) = parts.get(1) {
7 | match db.incr(key) {
8 | Ok(val) => format!(":{}\r\n", val),
9 | Err(e) => format!("-ERR {}\r\n", e),
10 | }
11 | } else {
12 | wrong_args("INCR")
13 | }
14 | }
15 |
16 | "INCRBY" => {
17 | if let (Some(key), Some(arg)) = (parts.get(1), parts.get(2)) {
18 | let by: i64 = arg.parse().unwrap_or(0);
19 | match db.incr_by(key, by) {
20 | Ok(val) => format!(":{}\r\n", val),
21 | Err(e) => format!("-ERR {}\r\n", e),
22 | }
23 | } else {
24 | wrong_args("INCRBY")
25 | }
26 | }
27 |
28 | "DECRBY" => {
29 | if let (Some(key), Some(arg)) = (parts.get(1), parts.get(2)) {
30 | let by: i64 = arg.parse().unwrap_or(0);
31 | match db.incr_by(key, -by) {
32 | Ok(val) => format!(":{}\r\n", val),
33 | Err(e) => format!("-ERR {}\r\n", e),
34 | }
35 | } else {
36 | wrong_args("DECRBY")
37 | }
38 | }
39 |
40 | "DECR" => {
41 | if let Some(key) = parts.get(1) {
42 | match db.incr_by(key, -1) {
43 | Ok(val) => format!(":{}\r\n", val),
44 | Err(e) => format!("-ERR {}\r\n", e),
45 | }
46 | } else {
47 | wrong_args("DECR")
48 | }
49 | }
50 |
51 | _ => {
52 | format!("-ERR unknown command '{}'\r\n", cmd)
53 | }
54 | }
55 |
56 | }
--------------------------------------------------------------------------------
/src/resp/commands/set.rs:
--------------------------------------------------------------------------------
1 | use crate::{resp::utils::{format_array, wrong_args}, store::Database};
2 |
3 | pub fn handle_set(cmd: &str, parts: &[String], db: &mut Database) -> String {
4 | match cmd {
5 | "SADD" => {
6 | if let Some(key) = parts.get(1) {
7 | let len = db.sadd(key, &parts[2..]);
8 | format!(":{}\r\n", len)
9 | } else {
10 | wrong_args("SADD")
11 | }
12 | }
13 |
14 | "SREM" => {
15 | if let Some(key) = parts.get(1) {
16 | let len = db.srem(key, &parts[2..]);
17 | format!(":{}\r\n", len)
18 | } else {
19 | wrong_args("SREM")
20 | }
21 | }
22 |
23 | "SMEMBERS" => {
24 | if let Some(key) = parts.get(1) {
25 | let members = db.smembers(key);
26 | format_array(members)
27 | } else {
28 | wrong_args("SMEMBERS")
29 | }
30 | }
31 |
32 | "SISMEMBER" => {
33 | if let (Some(key), Some(member)) = (parts.get(1), parts.get(2)) {
34 | let is_member = db.sismember(key, member);
35 | format!(":{}\r\n", if is_member { 1 } else { 0 })
36 | } else {
37 | wrong_args("SISMEMBER")
38 | }
39 | }
40 |
41 | "SCARD" => {
42 | if let Some(key) = parts.get(1) {
43 | let count = db.scard(key);
44 | format!(":{}\r\n", count)
45 | } else {
46 | wrong_args("SCARD")
47 | }
48 | }
49 |
50 | _ => {
51 | format!("-ERR unknown command '{}'\r\n", cmd)
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/resp/commands/string.rs:
--------------------------------------------------------------------------------
1 | use crate::{resp::utils::{bulk_string, wrong_args}, store::Database};
2 |
3 | pub fn handle_string(cmd: &str, parts: &[String], db: &mut Database) -> String {
4 | match cmd{
5 | "PING" => "+PONG\r\n".to_string(),
6 |
7 | "ECHO" => {
8 | if let Some(arg) = parts.get(1) {
9 | bulk_string(arg)
10 | } else {
11 | wrong_args("ECHO")
12 | }
13 | }
14 |
15 | "SET" => {
16 | if let (Some(key), Some(value)) = (parts.get(1), parts.get(2)) {
17 | let mut expiry = None;
18 |
19 | if let (Some(option), Some(seconds)) = (parts.get(3), parts.get(4)) {
20 | if option.to_uppercase() == "EX" {
21 | if let Ok(sec) = seconds.parse::() {
22 | expiry = Some(sec);
23 | }
24 | }
25 | }
26 |
27 | db.set(key, value.clone(), expiry);
28 | "+OK\r\n".to_string()
29 | } else {
30 | wrong_args("SET")
31 | }
32 | }
33 |
34 | "GET" => {
35 | if let Some(key) = parts.get(1) {
36 | if let Some(value) = db.get(key) {
37 | bulk_string(&value)
38 | } else {
39 | "$-1\r\n".to_string()
40 | }
41 | } else {
42 | wrong_args("GET")
43 | }
44 | }
45 |
46 | "DEL" => {
47 | if parts.len() < 2 {
48 | wrong_args("DEL")
49 | } else {
50 | let deleted = db.delete(&parts[1..]);
51 | format!(":{}\r\n", deleted)
52 | }
53 | },
54 | _ => "-ERR unknown command\r\n".to_string()
55 |
56 | }
57 |
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/src/resp/handler.rs:
--------------------------------------------------------------------------------
1 | use crate::pubsub::cmd::handle_publish;
2 | use crate::pubsub::{cmd::handle_subscribe, PubSub};
3 | use crate::store::db::Database;
4 | use std::sync::Arc;
5 | use tokio::net::tcp::OwnedWriteHalf;
6 | use tokio::sync::Mutex;
7 |
8 | use super::{
9 | commands::{
10 | hash_set::handle_hash_set,
11 | key::handle_key,
12 | list::handle_list,
13 | number::handle_number,
14 | set::handle_set,
15 | string::handle_string,
16 | },
17 | utils::parse_resp,
18 | };
19 |
20 | pub async fn handle_command(
21 | input: &str,
22 | db: &mut Database,
23 | pubsub: Arc,
24 | writer: Arc>,
25 | ) -> String {
26 | let parts = parse_resp(input);
27 | if parts.is_empty() {
28 | return "-ERR empty command\r\n".to_string();
29 | }
30 |
31 | let cmd = parts[0].to_uppercase();
32 | println!(" Command: {}", cmd);
33 |
34 | match cmd.as_str() {
35 | // Regular commands
36 | "PING" | "ECHO" | "SET" | "GET" | "DEL" => handle_string(&cmd, &parts, db),
37 | "INCR" | "INCRBY" | "DECR" | "DECRBY" => handle_number(&cmd, &parts, db),
38 | "EXISTS" | "KEYS" | "EXPIRE" | "TTL" | "PERSIST" => handle_key(&cmd, &parts, db),
39 | "LPOP" | "RPOP" | "LPUSH" | "RPUSH" | "LLEN" | "LINDEX" | "LRANGE" | "LSET" => {
40 | handle_list(&cmd, &parts, db)
41 | }
42 | "SADD" | "SREM" | "SMEMBERS" | "SISMEMBER" | "SCARD" => handle_set(&cmd, &parts, db),
43 | "HSET" | "HGET" | "HDEL" | "HKEYS" | "HVALS" | "HLEN" | "HGETALL" | "HEXISTS" => {
44 | handle_hash_set(&cmd, &parts, db)
45 | }
46 | "FLUSHDB" => {
47 | db.flushdb();
48 | "+OK\r\n".to_string()
49 | }
50 |
51 | "SUBSCRIBE" => {
52 | handle_subscribe(parts.iter().map(|s| s.as_str()).collect(), writer, pubsub).await;
53 | "".to_string()
54 | }
55 |
56 | "PUBLISH" => {
57 |
58 | if parts.len() < 3 {
59 | return "-ERR usage: PUBLISH \n".to_string();
60 | }
61 | // Spawn a new async task for publishing
62 | tokio::spawn(async move {
63 | handle_publish(parts.iter().map(|s: &String| s.as_str()).collect(), pubsub).await;
64 |
65 | });
66 |
67 |
68 | "+OK published\r\n".to_string()
69 | }
70 |
71 | _ => "-ERR unknown command\r\n".to_string(),
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/resp/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod handler;
2 | pub mod utils;
3 | pub mod commands;
--------------------------------------------------------------------------------
/src/resp/utils.rs:
--------------------------------------------------------------------------------
1 | pub fn wrong_args(cmd: &str) -> String {
2 | format!("-ERR wrong number of arguments for '{}'\r\n", cmd)
3 | }
4 |
5 | pub fn bulk_string(s: &str) -> String {
6 | format!("${}\r\n{}\r\n", s.len(), s)
7 | }
8 |
9 | pub fn format_array(values: Vec) -> String {
10 | let mut resp = format!("*{}\r\n", values.len());
11 | for v in values {
12 | resp.push_str(&bulk_string(&v));
13 | }
14 | resp
15 | }
16 |
17 |
18 | pub fn parse_resp(input: &str) -> Vec {
19 | input
20 | .lines()
21 | .filter(|line| !line.starts_with('*') && !line.starts_with('$'))
22 | .map(|line| line.to_string())
23 | .collect()
24 | }
25 |
--------------------------------------------------------------------------------
/src/server.rs:
--------------------------------------------------------------------------------
1 | use crate::pubsub::PubSub;
2 | use crate::{client, persistence::Persister, store::db::Database, utils::start_expiry_worker};
3 | use std::sync::Arc;
4 | use tokio::net::TcpListener;
5 | use tokio::sync::Mutex;
6 |
7 | pub async fn run(addr: &str, persister: Arc, pubsub: Arc) {
8 | let listener = TcpListener::bind(addr)
9 | .await
10 | .expect("Failed to bind to address");
11 |
12 | let db = match persister.load() {
13 | Some(db) => {
14 | println!("🔄 Loaded database from file");
15 | db
16 | }
17 | None => {
18 | println!("🗄️ No database file found, starting with an empty database");
19 | Database::new()
20 | }
21 | };
22 |
23 | let db = Arc::new(Mutex::new(db));
24 |
25 | let db_worker = Arc::clone(&db);
26 | let persister_worker = Arc::clone(&persister);
27 |
28 | start_expiry_worker(db_worker, persister_worker);
29 |
30 | loop {
31 | match listener.accept().await {
32 | Ok((stream, _)) => {
33 | println!("🔗 Accepted new connection");
34 |
35 | let db = Arc::clone(&db);
36 | let persister = Arc::clone(&persister);
37 | let pubsub = Arc::clone(&pubsub);
38 |
39 | tokio::spawn(async move {
40 | client::handle_connection(stream, db, persister, pubsub).await;
41 | });
42 | }
43 | Err(e) => eprintln!("Failed to accept connection: {e}"),
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/store/commands/hash_set.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use crate::{store::Database, types::RedisValue};
4 |
5 | impl Database{
6 |
7 | pub fn hset(&mut self, key: &str, field: &str, value: &str) -> usize {
8 |
9 | let entry = self
10 | .store_mut()
11 | .entry(key.to_string())
12 | .or_insert(RedisValue::Hash(HashMap::new()));
13 | if let RedisValue::Hash(hash) = entry {
14 | hash.insert(field.to_string(), value.to_string());
15 | hash.len()
16 | } else {
17 | 0
18 | }
19 | }
20 |
21 | pub fn hget(&mut self, key: &str, field: &str) -> Option {
22 | if self.is_expired(key) {
23 | return None;
24 | }
25 | match self.store_ref().get(key) {
26 | Some(RedisValue::Hash(hash)) => hash.get(field).cloned(),
27 | _ => None,
28 | }
29 | }
30 |
31 | pub fn hdel(&mut self, key: &str, fields: &[String]) -> usize {
32 | if self.is_expired(key) {
33 | return 0;
34 | }
35 | match self.store_mut().get_mut(key) {
36 | Some(RedisValue::Hash(ref mut hash)) => {
37 | let mut removed = 0;
38 | for field in fields.iter() {
39 | if hash.remove(field).is_some() {
40 | removed += 1;
41 | }
42 | }
43 | removed
44 | }
45 | _ => 0,
46 | }
47 | }
48 |
49 | pub fn hkeys(&mut self, key: &str) -> Vec {
50 | if self.is_expired(key) {
51 | return vec![];
52 | }
53 | match self.store_ref().get(key) {
54 | Some(RedisValue::Hash(hash)) => hash.keys().cloned().collect(),
55 | _ => vec![],
56 | }
57 | }
58 |
59 | pub fn hvals(&mut self, key: &str) -> Vec {
60 | if self.is_expired(key) {
61 | return vec![];
62 | }
63 | match self.store_ref().get(key) {
64 | Some(RedisValue::Hash(hash)) => hash.values().cloned().collect(),
65 | _ => vec![],
66 | }
67 | }
68 |
69 | pub fn hlen(&mut self, key: &str) -> usize {
70 | if self.is_expired(key) {
71 | return 0;
72 | }
73 | match self.store_ref().get(key) {
74 | Some(RedisValue::Hash(hash)) => hash.len(),
75 | _ => 0,
76 | }
77 | }
78 |
79 | pub fn hgetall(&mut self, key: &str) -> HashMap {
80 | if self.is_expired(key) {
81 | return HashMap::new();
82 | }
83 | match self.store_ref().get(key) {
84 | Some(RedisValue::Hash(hash)) => hash.clone(),
85 | _ => HashMap::new(),
86 | }
87 | }
88 |
89 | pub fn hexists(&mut self, key: &str, field: &str) -> bool {
90 | if self.is_expired(key) {
91 | return false;
92 | }
93 | match self.store_ref().get(key) {
94 | Some(RedisValue::Hash(hash)) => hash.contains_key(field),
95 | _ => false,
96 | }
97 | }
98 |
99 |
100 |
101 | }
--------------------------------------------------------------------------------
/src/store/commands/key.rs:
--------------------------------------------------------------------------------
1 |
2 |
3 | use crate::{store::Database, utils::current_unix_timestamp};
4 |
5 | impl Database{
6 | pub fn exists(&mut self, keys: &[String]) -> usize {
7 | keys.iter()
8 | .filter(|key| {
9 | self.is_expired(key);
10 | self.store_ref().contains_key(*key)
11 | })
12 | .count()
13 | }
14 |
15 | // Return only non expired keys.
16 | pub fn keys(&mut self) -> Vec {
17 | // Check all keys for expiry first.
18 | let current_keys: Vec = self.store_ref().keys().cloned().collect();
19 | for key in current_keys.iter() {
20 | self.is_expired(key);
21 | }
22 | self.store_ref().keys().cloned().collect()
23 | }
24 |
25 | pub fn expire(&mut self, key: &str, seconds: u64) -> usize {
26 | if self.is_expired(key) || !self.store_ref().contains_key(key) {
27 | 0
28 | } else {
29 | let now = current_unix_timestamp();
30 | let expire_at = now + seconds;
31 | self.expiry_mut().insert(key.to_string(), expire_at);
32 | 1
33 | }
34 | }
35 |
36 |
37 | pub fn ttl(&mut self, key: &str) -> isize {
38 | if self.is_expired(key) || !self.store_ref().contains_key(key) {
39 | -2 // Key does not exist
40 | } else if let Some(&expire_at) = self.expiry_ref().get(key) {
41 | let now = current_unix_timestamp();
42 | let ttl = expire_at.saturating_sub(now);
43 | ttl as isize
44 | } else {
45 | -1 // Key exists, no expiry
46 | }
47 | }
48 |
49 |
50 |
51 | pub fn persist(&mut self, key: &str) -> usize {
52 | if self.is_expired(key) || !self.store_mut().contains_key(key) {
53 | 0
54 | } else if self.expiry_mut().remove(key).is_some() {
55 | 1
56 | } else {
57 | 0
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/store/commands/list.rs:
--------------------------------------------------------------------------------
1 | use crate::{store::Database, types::RedisValue};
2 |
3 | impl Database{
4 | pub fn lpush(&mut self, key: &str, values: &[String]) -> usize {
5 | self.is_expired(key);
6 | let entry = self
7 | .store_mut()
8 | .entry(key.to_string())
9 | .or_insert(RedisValue::List(vec![]));
10 | if let RedisValue::List(list) = entry {
11 | for value in values.iter().rev() {
12 | list.insert(0, value.clone());
13 | }
14 | list.len()
15 | } else {
16 | 0
17 | }
18 | }
19 |
20 | pub fn rpush(&mut self, key: &str, values: &[String]) -> usize {
21 | self.is_expired(key);
22 | let entry = self
23 | .store_mut()
24 | .entry(key.to_string())
25 | .or_insert(RedisValue::List(vec![]));
26 | if let RedisValue::List(list) = entry {
27 | for value in values.iter() {
28 | list.push(value.clone());
29 | }
30 | list.len()
31 | } else {
32 | 0
33 | }
34 | }
35 |
36 | // Returns None if the key is expired.
37 | pub fn lpop(&mut self, key: &str) -> Option {
38 | if self.is_expired(key) {
39 | return None;
40 | }
41 | match self.store_mut().get_mut(key) {
42 | Some(RedisValue::List(ref mut list)) => {
43 | if list.is_empty() {
44 | None
45 | } else {
46 | Some(list.remove(0))
47 | }
48 | }
49 | _ => None,
50 | }
51 | }
52 |
53 | // Returns None if the key is expired.
54 | pub fn rpop(&mut self, key: &str) -> Option {
55 | if self.is_expired(key) {
56 | return None;
57 | }
58 | match self.store_mut().get_mut(key) {
59 | Some(RedisValue::List(ref mut list)) => list.pop(),
60 | _ => None,
61 | }
62 | }
63 |
64 | pub fn llen(&mut self, key: &str) -> usize {
65 | if self.is_expired(key) {
66 | return 0;
67 | }
68 | match self.store_mut().get(key) {
69 | Some(RedisValue::List(list)) => list.len(),
70 | _ => 0,
71 | }
72 | }
73 |
74 | pub fn lindex(&mut self, key: &str, index: usize) -> Option {
75 | if self.is_expired(key) {
76 | return None;
77 | }
78 | match self.store_ref().get(key) {
79 | Some(RedisValue::List(list)) => list.get(index).cloned(),
80 | _ => None,
81 | }
82 | }
83 |
84 | pub fn lset(&mut self, key: &str, index: usize, value: String) -> Result<(), &'static str> {
85 | if self.is_expired(key) {
86 | return Err("Key does not exist");
87 | }
88 | match self.store_mut().get_mut(key) {
89 | Some(RedisValue::List(ref mut list)) => {
90 | if index < list.len() {
91 | list[index] = value;
92 | Ok(())
93 | } else {
94 | Err("Index out of range")
95 | }
96 | }
97 | _ => Err("Key does not exist or is not a list"),
98 | }
99 | }
100 |
101 | pub fn lrange(&mut self, key: &str, start: isize, end: isize) -> Vec {
102 | if self.is_expired(key) {
103 | return vec![];
104 | }
105 | match self.store_ref().get(key) {
106 | Some(RedisValue::List(list)) => {
107 | let len = list.len() as isize;
108 | let start = if start < 0 { len + start } else { start };
109 | let end = if end < 0 { len + end } else { end };
110 | if start < 0 || end < 0 || start >= len || end >= len || start > end {
111 | vec![]
112 | } else {
113 | list[start as usize..=end as usize].to_vec()
114 | }
115 | }
116 | _ => vec![],
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/src/store/commands/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod string;
2 | pub mod set;
3 | pub mod hash_set;
4 | pub mod key;
5 | pub mod number;
6 | pub mod list;
--------------------------------------------------------------------------------
/src/store/commands/number.rs:
--------------------------------------------------------------------------------
1 | use crate::{store::Database, types::RedisValue};
2 |
3 | impl Database{
4 | pub fn incr(&mut self, key: &str) -> Result {
5 | self.incr_by(key, 1)
6 | }
7 | pub fn incr_by(&mut self, key: &str, by: i64) -> Result {
8 | // Even for incr, check expiry first.
9 | self.is_expired(key);
10 | let val = self.store_mut().entry(key.to_string()).or_insert(RedisValue::String("0".to_string()));
11 | match val {
12 | RedisValue::String(ref mut s) => {
13 | let current_value: i64 = s.parse().map_err(|_| "Value is not an integer")?;
14 | let new_value = current_value + by;
15 | *s = new_value.to_string();
16 | Ok(new_value)
17 | }
18 | _ => Err("Value is not an integer"),
19 | }
20 | }
21 |
22 |
23 | }
--------------------------------------------------------------------------------
/src/store/commands/set.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 |
3 | use crate::{store::Database, types::RedisValue};
4 |
5 | impl Database{
6 |
7 |
8 | pub fn sadd(&mut self, key: &str, values: &[String]) -> usize {
9 |
10 | let entry = self
11 | .store_mut()
12 | .entry(key.to_string())
13 | .or_insert(RedisValue::Set(HashSet::new()));
14 | if let RedisValue::Set(set) = entry {
15 | for value in values.iter() {
16 | set.insert(value.clone());
17 | }
18 | set.len()
19 | } else {
20 | 0
21 | }
22 | }
23 |
24 | pub fn srem(&mut self, key: &str, values: &[String]) -> usize {
25 | if self.is_expired(key) {
26 | return 0;
27 | }
28 | match self.store_mut().get_mut(key) {
29 | Some(RedisValue::Set(ref mut set)) => {
30 | let mut removed = 0;
31 | for value in values.iter() {
32 | if set.remove(value) {
33 | removed += 1;
34 | }
35 | }
36 | removed
37 | }
38 | _ => 0,
39 | }
40 | }
41 |
42 | pub fn smembers(&mut self, key: &str) -> Vec {
43 | if self.is_expired(key) {
44 | return vec![];
45 | }
46 | match self.store_ref().get(key) {
47 | Some(RedisValue::Set(set)) => set.iter().cloned().collect(),
48 | _ => vec![],
49 | }
50 | }
51 |
52 | pub fn sismember(&mut self, key: &str, value: &str) -> bool {
53 | if self.is_expired(key) {
54 | return false;
55 | }
56 | match self.store_ref().get(key) {
57 | Some(RedisValue::Set(set)) => set.contains(value),
58 | _ => false,
59 | }
60 | }
61 |
62 | pub fn scard(&mut self, key: &str) -> usize {
63 | if self.is_expired(key) {
64 | return 0;
65 | }
66 | match self.store_ref().get(key) {
67 | Some(RedisValue::Set(set)) => set.len(),
68 | _ => 0,
69 | }
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/src/store/commands/string.rs:
--------------------------------------------------------------------------------
1 |
2 |
3 | use crate::{store::Database, types::RedisValue, utils::current_unix_timestamp};
4 |
5 | impl Database{
6 | pub fn set(&mut self, key: &str, value: String, ttl: Option) {
7 | self.store_mut()
8 | .insert(key.to_string(), RedisValue::String(value));
9 |
10 | if let Some(seconds) = ttl {
11 | let expire_at = current_unix_timestamp() + seconds;
12 | self.expiry_mut().insert(key.to_string(), expire_at);
13 | } else {
14 | self.expiry_mut().remove(key);
15 | }
16 | }
17 |
18 |
19 | // Now returns None if key is expired.
20 | pub fn get(&mut self, key: &str) -> Option {
21 | if self.is_expired(key) {
22 | return None;
23 | }
24 | match self.store_ref().get(key) {
25 | Some(RedisValue::String(value)) => Some(value.clone()),
26 | _ => None,
27 | }
28 | }
29 |
30 | pub fn delete(&mut self, keys: &[String]) -> usize {
31 | let mut removed = 0;
32 | for key in keys {
33 | self.is_expired(key);
34 | if self.store_mut().remove(key).is_some() {
35 | removed += 1;
36 | }
37 | }
38 | removed
39 | }
40 | }
--------------------------------------------------------------------------------
/src/store/db.rs:
--------------------------------------------------------------------------------
1 | use std:: collections::HashMap;
2 |
3 |
4 | use serde::{Deserialize, Serialize};
5 |
6 | use crate::types::RedisValue;
7 |
8 |
9 | #[derive(Serialize, Deserialize, Debug)]
10 | pub struct Database {
11 | store: HashMap, // key: value
12 | expiry: HashMap, // key: expiry time
13 | }
14 |
15 | impl Database {
16 | pub fn new() -> Self {
17 | Database {
18 | store: HashMap::new(),
19 | expiry: HashMap::new(),
20 | }
21 | }
22 |
23 | pub fn store_ref(&self) -> &HashMap {
24 | &self.store
25 | }
26 |
27 | pub fn expiry_ref(&self) -> &HashMap {
28 | &self.expiry
29 | }
30 |
31 | pub fn store_mut(&mut self) -> &mut HashMap {
32 | &mut self.store
33 | }
34 |
35 | pub fn expiry_mut(&mut self) -> &mut HashMap {
36 | &mut self.expiry
37 | }
38 |
39 |
40 | pub fn flushdb(&mut self) {
41 | self.store.clear();
42 | self.expiry.clear();
43 | }
44 |
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/store/expiry.rs:
--------------------------------------------------------------------------------
1 |
2 |
3 | use crate::{persistence::Persister, utils::current_unix_timestamp};
4 |
5 | use super::db::Database;
6 |
7 | impl Database{
8 |
9 | // Checks expiration; if expired, removes the key from both store and expiry.
10 | pub fn is_expired(&mut self, key: &str) -> bool {
11 | if let Some(&expire_time) = self.expiry_mut().get(key) {
12 | if current_unix_timestamp() >= expire_time {
13 | self.store_mut().remove(key);
14 | self.expiry_mut().remove(key);
15 | return true;
16 | }
17 | }
18 | false
19 | }
20 |
21 | pub fn remove_expired_keys(&mut self, persister : &dyn Persister){
22 | let now = current_unix_timestamp();
23 |
24 |
25 |
26 | let expired_keys : Vec = self.expiry_mut()
27 | .iter()
28 | .filter(|(_, &exp)| exp<= now)
29 | .map(|(key, _)| key.clone())
30 | .collect();
31 |
32 |
33 |
34 | for key in expired_keys {
35 | self.store_mut().remove(&key);
36 | self.expiry_mut().remove(&key);
37 | println!("Key expired, thus removed: {}", key);
38 | }
39 |
40 | if let Err(e) = persister.save(self) {
41 | eprintln!("Failed to persist DB after expiry cleanup: {e}");
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/src/store/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod db;
2 | pub mod expiry;
3 | pub mod commands;
4 |
5 | pub use db::Database;
--------------------------------------------------------------------------------
/src/types.rs:
--------------------------------------------------------------------------------
1 | use std::collections::{HashMap, HashSet};
2 |
3 | use serde::{Deserialize, Serialize};
4 |
5 | #[derive(Serialize, Deserialize, Debug, Clone)]
6 | pub enum RedisValue {
7 | String(String),
8 | List(Vec),
9 | Set(HashSet),
10 | Hash(HashMap),
11 | }
--------------------------------------------------------------------------------
/src/utils.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 | use tokio::sync::Mutex;
3 |
4 |
5 |
6 | use crate::{persistence::Persister, store::Database};
7 |
8 | pub fn is_mutating_command(input: &str) -> bool {
9 | let cmd = input.trim().split_whitespace().next();
10 |
11 | match cmd {
12 | Some(cmd) => {
13 | matches!(
14 | cmd.to_uppercase().as_str(),
15 | "SET"
16 | | "DEL"
17 | | "INCR"
18 | | "INCRBY"
19 | | "DECR"
20 | | "DECRBY"
21 | | "EXPIRE"
22 | | "PERSIST"
23 | | "LPOP"
24 | | "RPOP"
25 | | "LPUSH"
26 | | "RPUSH"
27 | | "LSET"
28 | | "SADD"
29 | | "SREM"
30 | | "HSET"
31 | | "HDEL"
32 | | "FLUSHDB"
33 | )
34 | }
35 | None => false,
36 | }
37 | }
38 |
39 | pub fn current_unix_timestamp() -> u64 {
40 | use std::time::{SystemTime, UNIX_EPOCH};
41 | SystemTime::now()
42 | .duration_since(UNIX_EPOCH)
43 | .unwrap()
44 | .as_secs()
45 | }
46 | pub fn start_expiry_worker(db: Arc>, persister: Arc) {
47 | tokio::spawn(async move {
48 | loop {
49 | {
50 | let mut db = db.lock().await;
51 | db.remove_expired_keys(&*persister);
52 | }
53 | tokio::time::sleep(std::time::Duration::from_secs(1)).await;
54 | }
55 | });
56 | }
57 |
--------------------------------------------------------------------------------