Architecture
miniRedis follows a layered, async-first architecture built on Tokio’s runtime. This page covers the overall system design, concurrency model, and request flow.
System Overview
┌─────────────┐ TCP (RESP) ┌──────────────────────────────┐
│ redis-cli │ ──────────────────▶│ miniRedis Server │
│ or any │ ◀───────────────── │ │
│ RESP client│ │ ┌────────────────────────┐ │
└─────────────┘ │ │ TcpListener (:6379) │ │
│ └──────────┬─────────────┘ │
│ │ accept │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ tokio::spawn │ │
│ │ process_client() │ │
│ └──────────┬─────────────┘ │
│ │ dispatch │
│ ┌─────────┼──────────┐ │
│ ▼ ▼ ▼ │
│ ┌────┐ ┌──────┐ ┌───────┐ │
│ │GET │ │ SET │ │ LPUSH │ │
│ └──┬─┘ └──┬───┘ └───┬───┘ │
│ │ │ │ │
│ └───────┼──────────┘ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ DB (HashMap + RWLock) │ │
│ │ TTL Heap (MinHeap) │ │
│ │ LRU Manager │ │
│ └────────────────────────┘ │
└──────────────────────────────┘
Core Components
Entry Point (main.rs)
The server bootstrap performs:
- CLI argument parsing —
--bind,--port,--maxmemory,--maxmemory-policy - Environment variable fallback —
MINIREDIS_MAXMEMORY,MINIREDIS_MAXMEMORY_POLICY - Shared state initialization:
DB—Arc<RwLock<HashMap<String, Entry>>>for key-value storageHeap—Arc<Mutex<BinaryHeap<MinHeap>>>for TTL expiration trackingLruManager— approximate LRU tracking and memory accounting
- Background task launch —
async_clean_db_heapspawns a periodic TTL cleanup task - TCP accept loop — each connection spawns a dedicated
tokio::spawntask
Client Handler (handle_client.rs)
process_client() is the per-client async loop:
- Reads up to 4096 bytes into a ring buffer
- Validates the first byte is a valid RESP type (
+,-,:,$,*) - Parses RESP messages incrementally (returns
Ok(None)on partial data) - Converts RESP arrays into
Commandenum variants - Records key access for LRU tracking before dispatch
- Dispatches to the appropriate controller
- Flushes the access batch after each command
- Writes RESP response back to the socket
Background Cleanup (async_heap_delete.rs)
A dedicated tokio task runs every 100ms:
- Locks the heap (mutex) and DB (write lock)
- Pops entries where
expires_at <= Instant::now() - Removes expired keys from the DB
- Calculates freed bytes and adjusts the LRU memory tracker
- Uses
Instantfor precise, monotonic timestamps
Concurrency Model
Shared State
| Component | Type | Purpose |
|---|---|---|
| DB | Arc<RwLock<HashMap>> | Concurrent reads, exclusive writes |
| TTL Heap | Arc<Mutex<BinaryHeap>> | Exclusive access only |
| LRU Manager | Arc<AtomicU*> + mpsc::Sender | Lock-free counters, batched channel |
Lock Ordering
The code avoids deadlocks by dropping guards before acquiring other locks. For example, in set_cmd:
#![allow(unused)]
fn main() {
{
let mut db = db.write().await;
// ... perform insert ...
db.drop(); // explicit drop before eviction
}
evict_if_needed(...).await; // acquires its own DB lock
}
LRU Access Batching
To avoid locking the LRU map on every key access:
- Each client loop collects key accesses into a
Vec<String>buffer - After each command, the buffer is flushed via an
mpsc::channel(capacity 1024) - A dedicated background task receives batches and updates the
last_accessmap - Batch size is 32 accesses per flush
Data Types
Value (model/db.rs)
#![allow(unused)]
fn main() {
pub enum Value {
String(Vec<u8>),
List(VecDeque<Vec<u8>>),
}
}
Entry (model/db.rs)
#![allow(unused)]
fn main() {
pub struct Entry {
pub value: Value,
pub expires_at: Option<Instant>,
}
}
Command (model/command.rs)
An enum with 22 variants covering all supported Redis commands. Each variant carries its typed arguments:
#![allow(unused)]
fn main() {
pub enum Command {
PING,
QUIT,
SET { key: String, value: Vec<u8> },
SETEX { key: String, value: Vec<u8>, seconds: u64 },
GET { key: String },
DEL { keys: Vec<String> },
// ... etc
}
}
Request Lifecycle
1. TCP bytes arrive
2. Buffer accumulates (partial read handling)
3. find_crlf() locates \r\n delimiters
4. parse_resp() → RESP enum (recursive descent)
5. parse_command() → Command enum (type-safe dispatch)
6. Controller executes (acquires DB lock as needed)
7. Response serialized as RESP bytes
8. Bytes written to socket
9. LRU access batch flushed
10. Loop back to step 1
RESP Protocol Implementation
miniRedis implements the RESP (REdis Serialization Protocol) for client-server communication. This page documents the protocol support, parsing strategy, and serialization.
Overview
RESP is a typed, line-oriented protocol where each message begins with a type prefix character followed by \r\n-terminated data. miniRedis supports RESP clients only — inline commands (plain text like GET foo) are explicitly rejected.
Type Prefixes
| Prefix | Type | Example |
|---|---|---|
+ | Simple String | +OK\r\n |
- | Simple Error | -ERR unknown command\r\n |
: | Integer | :1\r\n |
$ | Bulk String | $5\r\nhello\r\n |
* | Array | *2\r\n$3\r\nGET\r\n$3\r\nfoo\r\n |
RESP Parsing
Parser Architecture
The parser is split into two layers:
Layer 1 — Wire Format (parser/parse_resp/):
Recursive descent parser that dispatches by first byte to the appropriate type parser.
Layer 2 — Command Parsing (parser/parse_command/):
Maps a Vec<RESP> (array) into a typed Command enum.
Incremental Parsing
All RESP parsers return Result<Option<RESP>>:
Ok(Some(resp))— complete message parsedOk(None)— insufficient data, need more bytesErr(...)— protocol error
This enables partial read handling: if a TCP read returns incomplete data, the parser simply returns None and the client loop accumulates more bytes.
Finding Delimiters (util/find_crlf.rs)
#![allow(unused)]
fn main() {
pub fn find_crlf(buf: &[u8]) -> Option<usize> {
buf.windows(2).position(|w| w == b"\r\n")
}
}
Used by every parser to locate \r\n boundaries.
Parser Modules
| File | Handles | Format |
|---|---|---|
simple_strings.rs | + | +<string>\r\n |
simple_errors.rs | - | -<string>\r\n |
integers.rs | : | :<number>\r\n |
bulkstings.rs | $ | $<len>\r\n<data>\r\n or $-1\r\n (nil) |
arrays.rs | * | *<count>\r\n followed by N RESP elements |
Bulk String Nil Handling
$-1\r\n represents a nil bulk string. The parser produces RESP::BulkStrings(None), which controllers interpret as a missing key or null value.
Command Parsing (parser/parse_command/)
Takes a parsed RESP::Arrays(Vec<RESP>) and matches on the first element (the command name):
["SET", "foo", "bar"] → Command::SET { key: "foo", value: [104, 97, 114] }
["GET", "foo"] → Command::GET { key: "foo" }
["DEL", "a", "b"] → Command::DEL { keys: ["a", "b"] }
Validation
- Argument count: Each command validates its minimum/maximum arity
- Type checking: Arguments must be bulk strings; wrong types produce parse errors
- Subcommand routing:
CONFIG GET,CONFIG SET,CLIENT SETINFOare routed by matching subsequent array elements
Error Messages
Parse errors follow Redis conventions:
-wrong number of arguments for 'get' command
-unknown command
-expected bulk string at index 1
Response Serialization
RESP Encoding (util/resp_encode.rs)
Three helper functions serialize values back to RESP:
#![allow(unused)]
fn main() {
pub fn array_len(len: usize) -> Vec<u8> // *<count>\r\n
pub fn bulk_str(s: &str) -> Vec<u8> // $<len>\r\n<data>\r\n
pub fn integer(n: i64) -> Vec<u8> // :<n>\r\n
}
Response Patterns
| Scenario | Response |
|---|---|
| Success (no data) | +OK\r\n |
| Success with value | $<len>\r\n<value>\r\n |
| Key not found | $-1\r\n |
| Integer result | :<n>\r\n |
| Error | -ERR <message>\r\n |
| Array result | *<count>\r\n... |
| Empty list | *0\r\n |
Value Serialization (model/db.rs)
Value::to_resp_bytes() converts stored values to RESP:
String(bytes)→ bulk stringList(deque)→ array of bulk strings (empty list →*0\r\n)
Protocol Limitations
| Feature | Status |
|---|---|
| RESP2 | ✅ Full support |
| RESP3 | ❌ Not supported |
| Inline commands | ❌ Rejected with protocol error |
| Pipelining | ✅ Works (messages parsed sequentially from buffer) |
| Pub/Sub | ❌ Not implemented |
| Transactions (MULTI/EXEC) | ❌ Not implemented |
| Lua scripting | ❌ Not implemented |
Wire Example
SET Command
Client sends:
*3\r\n
$3\r\n
SET\r\n
$3\r\n
foo\r\n
$3\r\n
bar\r\n
Server responds:
+OK\r\n
GET Command
Client sends:
*2\r\n
$3\r\n
GET\r\n
$3\r\n
foo\r\n
Server responds (key exists):
$3\r\n
bar\r\n
Server responds (key missing):
$-1\r\n
Supported Commands
miniRedis supports 22 Redis-compatible commands across strings, lists, key management, and administration.
String Commands
SET
SET <key> <value>
Sets <key> to hold the string <value>. Overwrites existing values.
Response: +OK\r\n
Memory: Tracks byte size; triggers eviction if over maxmemory. On OOM failure, rolls back the insert.
SETEX
SETEX <key> <seconds> <value>
Sets <key> with a TTL in seconds. Atomically sets value and expiry.
Response: +OK\r\n
Internals: Creates Entry with expires_at = Instant::now() + Duration::from_secs(seconds), pushes to TTL min-heap.
PSETEX
PSETEX <key> <milliseconds> <value>
Sets <key> with a TTL in milliseconds.
Response: +OK\r\n
Internals: Same as SETEX but uses Duration::from_millis().
GET
GET <key>
Returns the value of <key>. Performs lazy expiration on access.
Response: $<len>\r\n<value>\r\n (or $-1\r\n if missing/expired)
List Commands
LPUSH
LPUSH <key> <value> [value ...]
Pushes one or more values to the head (left) of the list. Creates a new list if the key doesn’t exist.
Response: :<length>\r\n (list length after push)
Error: -WRONGTYPE\r\n if key holds a non-list value.
Memory: Tracks byte delta; rolls back on OOM.
RPUSH
RPUSH <key> <value> [value ...]
Pushes one or more values to the tail (right) of the list.
Response: :<length>\r\n
Error: -WRONGTYPE\r\n if key holds a non-list value.
LPOP
LPOP <key>
Removes and returns the first element from the list.
Response: $<len>\r\n<value>\r\n (or $-1\r\n if list is empty or key missing)
Internals: Removes the key entirely when the list becomes empty. Adjusts memory tracking.
RPOP
RPOP <key>
Removes and returns the last element from the list.
Response: $<len>\r\n<value>\r\n (or $-1\r\n if list is empty or key missing)
Key Management
DEL
DEL <key> [key ...]
Removes the specified keys. A key is ignored if it does not exist.
Response: :<count>\r\n (number of keys actually removed)
Memory: Calculates freed bytes and adjusts the LRU tracker.
EXISTS
EXISTS <key> [key ...]
Returns the number of keys that exist (non-expired).
Response: :<count>\r\n
Internals: Performs lazy expiration — expired keys are re-pushed to the heap for background cleanup.
EXPIRE
EXPIRE <key> <seconds>
Sets a TTL on <key> in seconds. Overwrites any existing TTL.
Response: :1\r\n (TTL set) or :0\r\n (key doesn’t exist)
PERSIST
PERSIST <key>
Removes the TTL from <key>, making it persistent.
Response: :1\r\n (TTL removed) or :0\r\n (key doesn’t exist or had no TTL)
Internals: Performs lazy expiration check before removing TTL.
TTL
TTL <key>
Returns the remaining time to live in seconds.
Response:
:<seconds>\r\n— remaining TTL:-1\r\n— key exists but has no TTL:-2\r\n— key doesn’t exist or is expired
PTTL
PTTL <key>
Returns the remaining time to live in milliseconds.
Response: Same format as TTL but in milliseconds.
TYPE
TYPE <key>
Returns the type of value stored at <key>.
Response:
+string\r\n— value is a string+list\r\n— value is a list+none\r\n— key doesn’t exist
Protocol & Administration
PING
PING
Tests if the connection is alive.
Response: +PONG\r\n
QUIT
QUIT
Closes the client connection.
Response: +OK\r\n (then server closes the socket)
HELLO
HELLO [protover]
Protocol handshake. Accepts version 2 or 3.
Response: RESP2-style map with server metadata:
*1\r\n
*6\r\n
$4\r\n
name\r\n
$9\r\n
miniRedis\r\n
...
COMMAND
COMMAND
Returns metadata for all 22 supported commands.
Response: Array of CommandInfo entries, each containing:
- Command name
- Arity (negative = variable args)
- Flags (
readonly,write,fast,admin) - First key position
- Last key position
- Key step
INFO
INFO [section]
Returns server statistics. Supported sections: server, clients, memory, stats.
Response: Bulk string in Redis INFO format:
# Server
redis_version:0.1.0
...
# Clients
connected_clients:3
...
CONFIG GET
CONFIG GET <pattern>
Returns configuration values matching <pattern>. Supports * wildcard.
Response: Array of [key, value, key, value, ...] pairs.
Retrievable keys: maxmemory, maxmemory-policy
CONFIG SET
CONFIG SET <key> <value>
Sets a configuration parameter at runtime.
Response: +OK\r\n
Settable keys:
maxmemory— byte limit (0 = disabled)maxmemory-policy—noeviction,allkeys-lru, orvolatile-ttl
CLIENT SETINFO
CLIENT SETINFO <attr> <value>
Accepted but not stored (compatibility shim for Redis clients that send this on connect).
Response: +OK\r\n
All other CLIENT subcommands are rejected.
Command Summary Table
| Command | Arity | Type | Flags |
|---|---|---|---|
| PING | 1 | fast | — |
| QUIT | 1 | fast | — |
| GET | 2 | readonly | fast |
| SET | -3 | write | — |
| SETEX | 4 | write | — |
| PSETEX | 4 | write | — |
| DEL | -2 | write | — |
| EXISTS | -2 | readonly | fast |
| EXPIRE | 3 | write | fast |
| PERSIST | 2 | write | fast |
| TTL | 2 | readonly | fast |
| PTTL | 2 | readonly | fast |
| TYPE | 2 | readonly | fast |
| LPUSH | -3 | write | — |
| RPUSH | -3 | write | — |
| LPOP | 2 | write | fast |
| RPOP | 2 | write | fast |
| CONFIG | -2 | admin, readonly | — |
| INFO | -1 | readonly | — |
| HELLO | -1 | readonly | fast |
| COMMAND | 0 | readonly | — |
| CLIENT | -2 | readonly | — |
Arity note: Negative values indicate variable-length argument lists. For example, -3 means “at least 3 arguments.”
Memory Management & Eviction
miniRedis provides approximate memory tracking with configurable eviction policies. This page covers the LRU manager, TTL heap, and memory accounting.
Overview
┌───────────────────────────────────────────────────────┐
│ LruManager │
│ │
│ maxmemory: AtomicUsize │
│ used_bytes: AtomicUsize ◄─── adjust via CAS loops │
│ policy: AtomicU8 ◄─── runtime configurable │
│ last_access: Mutex<HashMap<String, u64>> │
│ access_tx: mpsc::Sender<Vec<String>> │
└───────────────────────┬───────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌────────────┐
│NoEviction│ │AllKeysLRU│ │VolatileTTL │
└──────────┘ └──────────┘ └────────────┘
Memory Tracking
used_bytes (AtomicUsize)
An approximate counter for total memory usage. Updated via compare-and-swap (CAS) loops — no locks required:
#![allow(unused)]
fn main() {
fn adjust_used_bytes(&self, delta: isize) {
let mut current = self.used_bytes.load(Ordering::Relaxed);
loop {
let new = if delta >= 0 {
current.saturating_add(delta as usize)
} else {
current.saturating_sub(delta.unsigned_abs())
};
match self.used_bytes.compare_exchange(current, new, ...) {
Ok(_) => break,
Err(actual) => current = actual, // retry
}
}
}
}
Memory Estimation (estimate_entry_bytes)
Estimates the size of a DB entry:
key_string.capacity()
+ size_of::<Entry>()
+ value capacity:
String → vec.capacity()
List → size_of::<VecDeque>()
+ deque.capacity() * size_of::<Option<Vec<u8>>>()
+ Σ element.capacity()
Design choice: Uses .capacity() instead of .len(). This overestimates actual data but reflects real allocation size, making eviction more accurate.
Eviction Policies
NoEviction (default)
When used_bytes > maxmemory:
- Returns OOM error to the client
- Controllers roll back mutations (restore old value or remove newly created key)
- OOM command not allowed when used memory > 'maxmemory'.
AllKeysLru
Sample-based approximate LRU eviction across all keys.
Algorithm:
- Check if
used_bytes > maxmemory - Sample
SAMPLE_SIZE = 8random keys from the DB - For each sampled key, look up its last access tick in the LRU map
- Evict the key with the lowest access tick (least recently used)
- Adjust
used_bytesand repeat until under limit
Why sample-based? True LRU requires tracking every access with ordering overhead. Sampling 8 keys provides a good approximation with minimal cost — matching Redis’s maxmemory-samples approach.
VolatileTTL
Evicts keys with the soonest TTL expiration.
Algorithm:
- Check if
used_bytes > maxmemory - Pop from the TTL min-heap (earliest expiration first)
- Verify the popped
expires_atmatches the DB entry’sexpires_at(handles duplicates) - Remove the key from DB
- Repeat until under limit
Duplicate handling: When a key’s TTL is updated, a new heap entry is pushed. The old entry remains with a stale timestamp. Step 3 filters these out by verifying timestamps match.
LRU Access Tracking
Batching via mpsc Channel
To avoid locking the access map on every key lookup:
Client Loop Background Task
─────────── ───────────────
record_access("foo") ──┐
record_access("bar") ──┤
flush_access_batch() ──┼──▶ mpsc::channel (cap: 1024) ──▶ update last_access map
┘
- Collection: Each client loop maintains a
Vec<String>buffer - Recording:
lru.record_access(key)pushes to the buffer - Flushing: After each command,
lru.flush_access_batch()sends the buffer through the channel - Processing: A dedicated background task receives batches and updates
last_access: HashMap<String, u64>with an incrementing tick counter
Access Ticks
Each key maps to a u64 tick counter that increments on every access:
#![allow(unused)]
fn main() {
let mut tick = self.current_tick;
for key in batch {
*self.last_access.entry(key).or_insert(tick) = tick;
tick += 1;
}
self.current_tick = tick;
}
During LRU eviction, the key with the lowest tick is considered least recently used.
TTL Expiration
Dual Strategy
| Strategy | Mechanism | Frequency |
|---|---|---|
| Lazy | Check on access (GET, EXISTS, TYPE, etc.) | Per-request |
| Eager | Background heap cleanup task | Every ~100ms |
Lazy Expiration
When accessing a key:
#![allow(unused)]
fn main() {
if is_expired(entry) {
// Push stale entry back to heap for background cleanup
heap.push(entry.clone());
return nil;
}
}
The expired key returns nil but stays in the DB until the background task removes it.
Background Cleanup (async_heap_delete.rs)
loop {
tokio::time::sleep(100ms).await;
lock heap + lock db (write);
while heap.peek().expires_at <= now {
entry = heap.pop();
if entry.expires_at == db[key].expires_at {
db.remove(key);
freed_bytes += estimate_entry_bytes(key);
}
}
adjust_used_bytes(-freed_bytes);
}
Uses Instant for monotonic, precise timestamps — immune to system clock adjustments.
Runtime Configuration
CONFIG SET
CONFIG SET maxmemory 1048576
CONFIG SET maxmemory-policy allkeys-lru
Changes are applied atomically:
maxmemory→AtomicUsize::store()policy→AtomicU8::store()
The LRU background task reads these values on each batch processing — no restart required.
Environment Variables
| Variable | Purpose | Default |
|---|---|---|
MINIREDIS_MAXMEMORY | Byte limit (0 = disabled) | 0 |
MINIREDIS_MAXMEMORY_POLICY | Eviction policy name | noeviction |
CLI Flags
--maxmemory <bytes>
--maxmemory-policy <noeviction|allkeys-lru|volatile-ttl>
OOM Rollback
When eviction fails under noeviction policy, controllers undo mutations:
SET Rollback
#![allow(unused)]
fn main() {
let old = db.insert(key, new_entry);
if evict_if_needed().await == false {
if let Some(old_entry) = old {
db.insert(key, old_entry); // restore
} else {
db.remove(key); // remove new key
}
return OOM_ERROR;
}
}
LPUSH/RPUSH Rollback
#![allow(unused)]
fn main() {
// If list was newly created, remove it
// If list existed, restore to previous state
}
This ensures no partial state on OOM.
Project Structure
A module-by-module reference for navigating the miniRedis codebase.
Source Tree
src/
├── main.rs # Entry point, CLI parsing, server bootstrap
├── handle_client.rs # Per-client TCP handling loop
├── async_heap_delete.rs # Background TTL cleanup task
├── lru.rs # LRU tracking, eviction, memory accounting
│
├── model/
│ ├── mod.rs # Module re-exports
│ ├── db.rs # DB, Entry, Value types
│ ├── resp.rs # RESP enum (wire format types)
│ ├── command.rs # Command enum + CommandInfo
│ └── min_heap.rs # MinHeap (TTL min-heap wrapper)
│
├── parser/
│ ├── mod.rs # Module re-exports
│ ├── parse_resp/
│ │ ├── mod.rs # RESP dispatch (first-byte routing)
│ │ ├── simple_strings.rs # + parser
│ │ ├── simple_errors.rs # - parser
│ │ ├── integers.rs # : parser
│ │ ├── bulkstings.rs # $ parser
│ │ └── arrays.rs # * parser (recursive)
│ └── parse_command/
│ └── mod.rs # RESP array → Command enum
│
├── controllers/
│ ├── mod.rs # Module re-exports
│ ├── get.rs # GET
│ ├── set.rs # SET
│ ├── setex.rs # SETEX
│ ├── psetex.rs # PSETEX
│ ├── del.rs # DEL
│ ├── exists.rs # EXISTS
│ ├── expire.rs # EXPIRE
│ ├── persist.rs # PERSIST
│ ├── ttl.rs # TTL
│ ├── pttl.rs # PTTL
│ ├── type_cmd.rs # TYPE
│ ├── info.rs # INFO
│ ├── config.rs # CONFIG GET / CONFIG SET
│ ├── hello.rs # HELLO
│ ├── command_cmd.rs # COMMAND
│ ├── lpush.rs # LPUSH
│ ├── rpush.rs # RPUSH
│ ├── lpop.rs # LPOP
│ └── rpop.rs # RPOP
│
└── util/
├── mod.rs # Module re-exports
├── bulk_to_string.rs # Vec<u8> → String helper
├── expect_bulk.rs # Validate/extract bulk string at index
├── find_crlf.rs # Find \r\n in byte slice
├── is_expired.rs # Check if Entry has expired
└── resp_encode.rs # RESP serialization helpers
Module Details
main.rs
Purpose: Server bootstrap and accept loop.
Key responsibilities:
- Parse CLI args and env vars
- Create
TcpListener - Initialize shared state (
DB,Heap,LruManager) - Launch background cleanup task
- Accept connections and spawn per-client tasks
handle_client.rs
Purpose: Main loop for a single client connection.
Key responsibilities:
- Read bytes from socket (up to 4096 per read)
- Validate RESP first byte
- Parse RESP messages incrementally
- Convert to
Commandenum - Record LRU access
- Dispatch to controller
- Write RESP response
- Flush access batch
async_heap_delete.rs
Purpose: Periodic TTL cleanup.
Key responsibilities:
- Sleep 100ms between cycles
- Pop expired entries from heap
- Remove from DB
- Adjust memory counter
lru.rs
Purpose: Memory tracking and eviction.
Key types:
LruManager— holds counters, access map, and channel senderEvictionPolicy— enum:NoEviction,AllKeysLru,VolatileTtl
Key functions:
new(maxmemory, policy)— create manager + spawn background access taskrecord_access(key)— buffer a key accessflush_access_batch()— send buffered keys through channelevict_if_needed()— check limit and evict per policyadjust_used_bytes(delta)— CAS-loop atomic updateestimate_entry_bytes(key, db)— approximate entry size
model/db.rs
Key types:
#![allow(unused)]
fn main() {
pub enum Value {
String(Vec<u8>),
List(VecDeque<Vec<u8>>),
}
pub struct Entry {
pub value: Value,
pub expires_at: Option<Instant>,
}
pub type DB = Arc<RwLock<HashMap<String, Entry>>>;
}
Key methods:
Value::to_resp_bytes()— serialize to RESPValue::as_list_mut()— downcast to mutable VecDeque
model/resp.rs
#![allow(unused)]
fn main() {
pub enum RESP {
SimpleStrings(String),
SimpleErrors(String),
Integers(i64),
BulkStrings(Option<Vec<u8>>), // None = nil
Arrays(Vec<RESP>),
}
}
model/command.rs
Key types:
Command— enum with 22 variants, each carrying typed argumentsCommandInfo— metadata for the COMMAND response (name, arity, flags, key positions)
model/min_heap.rs
#![allow(unused)]
fn main() {
pub struct MinHeap {
pub expires_at: Instant,
pub key: String,
}
pub type Heap = Arc<Mutex<BinaryHeap<MinHeap>>>;
}
Key detail: Ord is reversed (other.expires_at.cmp(&self.expires_at)) so Rust’s max-heap behaves as a min-heap — earliest expiration bubbles to the top.
parser/parse_resp/
Entry point: parse_resp(buf: &[u8]) -> Result<Option<RESP>>
Dispatches by first byte:
+→simple_strings.rs-→simple_errors.rs:→integers.rs$→bulkstings.rs*→arrays.rs(recursively callsparse_respfor each element)
All return Ok(None) on insufficient data.
parser/parse_command/
Entry point: parse_command(resp_array: Vec<RESP>) -> Result<Command>
Matches first element (command name) as string, then validates argument count and extracts typed fields using expect_bulk().
controllers/
One file per command. Common pattern:
#![allow(unused)]
fn main() {
pub async fn <cmd>_cmd(socket: &mut TcpStream, db: DB, heap: Heap, lru: LruManager) -> Result<()> {
// 1. Acquire lock
// 2. Validate / check expiration
// 3. Mutate or read
// 4. Adjust memory tracking
// 5. Trigger eviction (on writes)
// 6. Rollback on OOM
// 7. Serialize response
// 8. Write to socket
}
}
util/
| Function | Purpose |
|---|---|
find_crlf(buf) | Locate \r\n boundary |
bulk_to_string(bytes) | Vec<u8> → String (lossy UTF-8) |
expect_bulk(array, index) | Validate element at index is a bulk string and extract it |
is_expired(entry) | Check entry.expires_at <= Instant::now() |
array_len(n) | Serialize *<n>\r\n |
bulk_str(s) | Serialize $<len>\r\n<data>\r\n |
integer(n) | Serialize :<n>\r\n |
Type Aliases
| Alias | Resolves To |
|---|---|
DB | Arc<RwLock<HashMap<String, Entry>>> |
Heap | Arc<Mutex<BinaryHeap<MinHeap>>> |
Defined in model/mod.rs and re-exported at the crate root.
Getting Started
How to build, run, and connect to miniRedis.
Prerequisites
- Rust & Cargo — install via rustup
- A RESP client —
redis-cliis recommended but any RESP-compatible client works
Building
cargo build
For an optimized release build:
cargo build --release
Running
Default
cargo run
The server starts on 127.0.0.1:6379.
With Options
cargo run -- --bind 0.0.0.0 --port 6380 --maxmemory 1048576 --maxmemory-policy allkeys-lru
Or run the binary directly:
./target/debug/miniRedis --help
./target/debug/miniRedis --bind 0.0.0.0 --port 6380
CLI Flags
| Flag | Description | Default |
|---|---|---|
--bind <ip> | Bind address | 127.0.0.1 |
--port <port> | Port number | 6379 |
--maxmemory <bytes> | Approximate memory limit (0 = disabled) | 0 |
--maxmemory-policy <policy> | Eviction policy | noeviction |
--help, -h | Show help and exit | — |
Environment Variables
| Variable | Description | Default |
|---|---|---|
MINIREDIS_MAXMEMORY | Byte limit (overridden by CLI flag if both set) | 0 |
MINIREDIS_MAXMEMORY_POLICY | Eviction policy name | noeviction |
Connecting with redis-cli
# Basic connection
redis-cli -h 127.0.0.1 -p 6379
# Test connectivity
redis-cli PING
# → PONG
Example Session
# Set a string
redis-cli SET greeting "Hello, miniRedis!"
# → OK
# Get it back
redis-cli GET greeting
# → "Hello, miniRedis!"
# Set with TTL
redis-cli SETEX temp_key 10 "expires soon"
# → OK
# Check TTL
redis-cli TTL temp_key
# → (integer) 8
# List operations
redis-cli LPUSH mylist a b c
# → (integer) 3
redis-cli LPOP mylist
# → "c"
# Delete
redis-cli DEL greeting temp_key
# → (integer) 2
# Server info
redis-cli INFO memory
# → # Memory
# → used_memory:1234
# → ...
Configuration at Runtime
Use CONFIG GET and CONFIG SET after the server is running:
# Check current settings
redis-cli CONFIG GET maxmemory
# → 1) "maxmemory"
# → 2) "0"
redis-cli CONFIG GET maxmemory-policy
# → 1) "maxmemory-policy"
# → 2) "noeviction"
# Change memory limit
redis-cli CONFIG SET maxmemory 2097152
# → OK
# Change eviction policy
redis-cli CONFIG SET maxmemory-policy allkeys-lru
# → OK
Supported vs Unsupported Features
Supported
- String GET/SET with TTL
- List push/pop (LPUSH, RPUSH, LPOP, RPOP)
- Key management (DEL, EXISTS, EXPIRE, PERSIST, TTL, PTTL, TYPE)
- Approximate LRU and TTL-based eviction
- Runtime configuration via CONFIG
- COMMAND metadata
- INFO sections (server, clients, memory, stats)
- HELLO handshake (v2/v3)
Not Implemented
- Hashes, Sets, Sorted Sets, Bitmaps, HyperLogLog, Streams
- Transactions (MULTI/EXEC/DISCARD)
- Pub/Sub
- Lua scripting
- Persistence (RDB snapshots, AOF)
- Replication / clustering
- RESP3 protocol
- Inline commands (plain text)