Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. CLI argument parsing--bind, --port, --maxmemory, --maxmemory-policy
  2. Environment variable fallbackMINIREDIS_MAXMEMORY, MINIREDIS_MAXMEMORY_POLICY
  3. Shared state initialization:
    • DBArc<RwLock<HashMap<String, Entry>>> for key-value storage
    • HeapArc<Mutex<BinaryHeap<MinHeap>>> for TTL expiration tracking
    • LruManager — approximate LRU tracking and memory accounting
  4. Background task launchasync_clean_db_heap spawns a periodic TTL cleanup task
  5. TCP accept loop — each connection spawns a dedicated tokio::spawn task

Client Handler (handle_client.rs)

process_client() is the per-client async loop:

  1. Reads up to 4096 bytes into a ring buffer
  2. Validates the first byte is a valid RESP type (+, -, :, $, *)
  3. Parses RESP messages incrementally (returns Ok(None) on partial data)
  4. Converts RESP arrays into Command enum variants
  5. Records key access for LRU tracking before dispatch
  6. Dispatches to the appropriate controller
  7. Flushes the access batch after each command
  8. Writes RESP response back to the socket

Background Cleanup (async_heap_delete.rs)

A dedicated tokio task runs every 100ms:

  1. Locks the heap (mutex) and DB (write lock)
  2. Pops entries where expires_at <= Instant::now()
  3. Removes expired keys from the DB
  4. Calculates freed bytes and adjusts the LRU memory tracker
  5. Uses Instant for precise, monotonic timestamps

Concurrency Model

Shared State

ComponentTypePurpose
DBArc<RwLock<HashMap>>Concurrent reads, exclusive writes
TTL HeapArc<Mutex<BinaryHeap>>Exclusive access only
LRU ManagerArc<AtomicU*> + mpsc::SenderLock-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:

  1. Each client loop collects key accesses into a Vec<String> buffer
  2. After each command, the buffer is flushed via an mpsc::channel (capacity 1024)
  3. A dedicated background task receives batches and updates the last_access map
  4. 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

PrefixTypeExample
+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 parsed
  • Ok(None) — insufficient data, need more bytes
  • Err(...) — 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

FileHandlesFormat
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 SETINFO are 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

ScenarioResponse
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 string
  • List(deque) → array of bulk strings (empty list → *0\r\n)

Protocol Limitations

FeatureStatus
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-policynoeviction, allkeys-lru, or volatile-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

CommandArityTypeFlags
PING1fast
QUIT1fast
GET2readonlyfast
SET-3write
SETEX4write
PSETEX4write
DEL-2write
EXISTS-2readonlyfast
EXPIRE3writefast
PERSIST2writefast
TTL2readonlyfast
PTTL2readonlyfast
TYPE2readonlyfast
LPUSH-3write
RPUSH-3write
LPOP2writefast
RPOP2writefast
CONFIG-2admin, readonly
INFO-1readonly
HELLO-1readonlyfast
COMMAND0readonly
CLIENT-2readonly

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:

  1. Check if used_bytes > maxmemory
  2. Sample SAMPLE_SIZE = 8 random keys from the DB
  3. For each sampled key, look up its last access tick in the LRU map
  4. Evict the key with the lowest access tick (least recently used)
  5. Adjust used_bytes and 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:

  1. Check if used_bytes > maxmemory
  2. Pop from the TTL min-heap (earliest expiration first)
  3. Verify the popped expires_at matches the DB entry’s expires_at (handles duplicates)
  4. Remove the key from DB
  5. 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
                          ┘
  1. Collection: Each client loop maintains a Vec<String> buffer
  2. Recording: lru.record_access(key) pushes to the buffer
  3. Flushing: After each command, lru.flush_access_batch() sends the buffer through the channel
  4. 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

StrategyMechanismFrequency
LazyCheck on access (GET, EXISTS, TYPE, etc.)Per-request
EagerBackground heap cleanup taskEvery ~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:

  • maxmemoryAtomicUsize::store()
  • policyAtomicU8::store()

The LRU background task reads these values on each batch processing — no restart required.

Environment Variables

VariablePurposeDefault
MINIREDIS_MAXMEMORYByte limit (0 = disabled)0
MINIREDIS_MAXMEMORY_POLICYEviction policy namenoeviction

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 Command enum
  • 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 sender
  • EvictionPolicy — enum: NoEviction, AllKeysLru, VolatileTtl

Key functions:

  • new(maxmemory, policy) — create manager + spawn background access task
  • record_access(key) — buffer a key access
  • flush_access_batch() — send buffered keys through channel
  • evict_if_needed() — check limit and evict per policy
  • adjust_used_bytes(delta) — CAS-loop atomic update
  • estimate_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 RESP
  • Value::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 arguments
  • CommandInfo — 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 calls parse_resp for 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/

FunctionPurpose
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

AliasResolves To
DBArc<RwLock<HashMap<String, Entry>>>
HeapArc<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 clientredis-cli is 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

FlagDescriptionDefault
--bind <ip>Bind address127.0.0.1
--port <port>Port number6379
--maxmemory <bytes>Approximate memory limit (0 = disabled)0
--maxmemory-policy <policy>Eviction policynoeviction
--help, -hShow help and exit

Environment Variables

VariableDescriptionDefault
MINIREDIS_MAXMEMORYByte limit (overridden by CLI flag if both set)0
MINIREDIS_MAXMEMORY_POLICYEviction policy namenoeviction

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)