├── .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 | rizzlerdb logo 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 | --------------------------------------------------------------------------------