Skip to content

A high-performance in-memory key-value store written in Rust, inspired by Redis. CrabCache provides a Redis-compatible RESP protocol server with production-grade features including dual persistence (AOF + RDB), background expiration cleanup, lock-free counters, and comprehensive observability.

Notifications You must be signed in to change notification settings

wjake/crabcache-rust

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CrabCache

A high-performance in-memory key-value store written in Rust, inspired by Redis. CrabCache provides a Redis-compatible RESP protocol server with production-grade features including dual persistence (AOF + RDB), background expiration cleanup, lock-free counters, and comprehensive observability.

Features

Core Commands

  • SET key value - Store a key-value pair in memory
  • GET key - Retrieve a value by key (returns (nil) if not found or expired)
  • DEL key - Delete a key from the database
  • EXPIRE key seconds - Set a time-to-live (TTL) on a key
  • TTL key - Get remaining time-to-live in seconds (-1: no expiry, -2: key doesn't exist)
  • EXISTS key [key ...] - Check how many keys exist (skips expired keys)
  • MSET key value [key value ...] - Set multiple key-value pairs atomically
  • MGET key [key ...] - Get multiple values atomically
  • MEMORY STATS - Show memory usage, max memory, keys count, and eviction policy
  • QUIT/EXIT - Close the application

String Operations

  • APPEND key value - Append a value to a key, returns new length
  • STRLEN key - Get the length of the value stored at key
  • GETRANGE key start end - Get substring (supports negative indices)
  • SETRANGE key offset value - Overwrite part of string at offset
  • INCR key - Increment integer value by 1
  • DECR key - Decrement integer value by 1
  • INCRBY key increment - Increment by specified amount
  • DECRBY key decrement - Decrement by specified amount
  • GETSET key value - Set new value and return old value
  • SETNX key value - Set only if key doesn't exist (returns 1 if set, 0 otherwise)

Statistics & Management

  • INFO - Comprehensive server statistics (uptime, memory, hits/misses, keyspace)
  • DBSIZE - Get the number of keys in the database
  • FLUSHDB - Delete all keys from the database

Network & Persistence

  • TCP Server - Redis-compatible RESP protocol server (tokio-based async)
  • AOF (Append-Only File) - Durability with configurable fsync policies
  • RDB Snapshots - Point-in-time binary snapshots with automatic intervals
  • Configuration - JSON-based config with three modes: REPL, TCP, or Both

Infrastructure

  • Enhanced CLI - Powered by Rustyline with command history, line editing, and persistent history file
  • Structured Logging - Tracing integration for debugging and observability
  • Hit/Miss Tracking - Automatic tracking of cache hits and misses with hit rate calculation
  • Memory Management - Configurable max memory with LRU eviction policy
  • Error Handling - Custom error types with Redis-compatible error messages
  • Background cleanup - Automatic removal of expired keys (configurable interval)
  • Thread-safe operations - Concurrent access via Arc<Mutex<>>
  • Lock-free counters - AtomicU64 for hit/miss/command tracking (zero contention)
  • Graceful shutdown - Handles SIGTERM/SIGINT signals, cleanly stops background threads
  • Modular architecture - Separated concerns: storage, commands, expiration, error handling, networking, persistence

Installation

git clone <repository-url>
cd RustKVStore
cargo build --release

Usage

Running CrabCache

Default Mode (REPL):

cargo run

TCP Server Mode (Redis-compatible):

# Create config file
cp crabcache.json.example crabcache.json
# Edit to set "mode": "tcp"
cargo run

# Connect with redis-cli
redis-cli -p 6379

Hybrid Mode (TCP + REPL):

{
  "server": {
    "mode": "both",
    "port": 6379
  }
}

The CLI supports:

  • Arrow keys for navigation
  • Ctrl-R for reverse history search
  • History persistence across sessions (stored in .crabcache_history)
  • Ctrl-C or Ctrl-D to exit gracefully

Example session:

CrabCache v0.1.0 - A Blazing Fast In-Memory Database
Commands: SET, GET, DEL, EXPIRE, TTL, EXISTS, MSET, MGET, MEMORY, QUIT
Type your commands below:

> SET user:1:name Alice
OK
> GET user:1:name
Alice
> MSET session:1 active session:2 idle session:3 expired
OK
> MGET session:1 session:2 session:3
1) "active"
2) "idle"
3) "expired"
> EXISTS session:1 session:2 session:3 session:4
(integer) 3
> MEMORY STATS
used_memory: 312 bytes
max_memory: unlimited
keys: 4
eviction_policy: NoEviction
> EXPIRE session:3 10
(integer) 1
> TTL session:3
(integer) 9
> # Wait 10 seconds...
> EXISTS session:1 session:2 session:3
(integer) 2
> DEL user:1:name
(integer) 1
> QUIT
Goodbye!

Command Details

SET key value

  • Stores a key-value pair
  • Multi-word values are supported: SET greeting Hello World
  • Returns: OK

GET key

  • Retrieves the value for a key
  • Automatically removes expired keys
  • Returns: value string or (nil) if not found

DEL key

  • Deletes a key from the database
  • Returns: (integer) 1 if deleted, (integer) 0 if key doesn't exist

EXPIRE key seconds

  • Sets TTL in seconds on an existing key
  • Returns: (integer) 1 if successful, (integer) 0 if key doesn't exist

TTL key

  • Returns remaining time-to-live in seconds
  • Returns:
    • Positive integer: seconds remaining
    • -1: key exists but has no expiration
    • -2: key doesn't exist or is expired

EXISTS key [key ...]

  • Checks how many of the specified keys exist
  • Automatically skips expired keys
  • Returns: (integer) N where N is the count of existing keys

MSET key value [key value ...]

  • Atomically sets multiple key-value pairs
  • Requires an even number of arguments
  • Returns: OK

MGET key [key ...]

  • Atomically retrieves multiple values
  • Returns values in order, (nil) for missing/expired keys
  • Output format:
    1) "value1"
    2) (nil)
    3) "value3"
    

MEMORY STATS

  • Shows memory usage statistics
  • Returns:
    • used_memory: Current memory usage (approximate)
    • max_memory: Maximum memory limit or "unlimited"
    • keys: Number of keys in database
    • eviction_policy: Current eviction policy (NoEviction or AllKeysLRU)

QUIT / EXIT

  • Gracefully exits the application

Advanced Configuration

Memory Management

Configure memory limits with LRU eviction:

use crabcache::{CrabCacheStorage, CrabCache};

// Create storage with 10MB memory limit (uses LRU eviction by default)
let storage = CrabCacheStorage::with_max_memory(10); // 10MB

// Create cache with custom storage
let cache = CrabCache::with_storage(storage);

Or via JSON configuration:

{
  "memory": {
    "max_memory_mb": 100,
    "eviction_policy": "allkeys-lru"
  }
}

Eviction Policy:

  • AllKeysLRU - Automatically evicts least recently used keys when limit reached

Memory Tracking:

  • Approximate memory calculation includes key names and values
  • Use MEMORY STATS command to monitor usage
  • Background cleanup handles expired keys automatically

Error Handling

CrabCache uses custom error types for precise error handling:

use crabcache::{CrabCacheError, Result};

match cache.set("key", "value") {
    Ok(()) => println!("Success"),
    Err(CrabCacheError::OutOfMemory) => println!("Memory limit exceeded"),
    Err(e) => println!("Error: {}", e),
}

Error Types:

  • KeyNotFound - Key does not exist
  • InvalidArgument - Wrong number or type of arguments
  • InvalidInteger - Value cannot be parsed as integer
  • InvalidCommand - Command not recognized
  • OutOfMemory - Memory limit exceeded
  • WrongType - Operation against wrong value type
  • IoError - File I/O errors (persistence)
  • SerializationError - RDB serialization errors

Persistence Configuration

Enable AOF and RDB persistence:

{
  "persistence": {
    "enabled": true,
    "aof_enabled": true,
    "aof_path": "data/appendonly.aof",
    "aof_fsync": "everysecond",
    "rdb_enabled": true,
    "rdb_path": "data/dump.rdb",
    "snapshot_interval_secs": 300
  }
}

AOF Options:

  • always - Safest, fsync after every write
  • everysecond - Balanced performance/durability (default)
  • never - Fastest, let OS handle fsync

RDB Options:

  • Automatic snapshots at intervals
  • Binary format for fast loading
  • Point-in-time backups

QUIT / EXIT

  • Gracefully exits the application

Implementation

Architecture

CrabCache is built with production Redis principles and modern Rust async architecture:

┌─────────────────────────────────────┐
│  Rustyline CLI (main thread)        │
│  - Command history                  │
│  - Line editing                     │
│  - Persistent history file          │
└────────────┬────────────────────────┘
             │
             ▼
┌─────────────────────────────────────┐
│   Tokio Async TCP Server             │
│   - Multi-client handling           │
│   - RESP protocol parser            │
│   - Non-blocking I/O                │
└────────────┬────────────────────────┘
             │
             ▼
┌─────────────────────────────────────┐
│   Arc<Mutex<HashMap<K, V>>>         │
│   - Thread-safe key-value store     │
│   - O(1) operations                 │
│   - AtomicU64 counters (lock-free)  │
│   - Memory usage tracking           │
│   - LRU access tracking             │
└────────────┬────────────────────────┘
             │
             ├──► Background Cleanup Thread
             │    - Runs every 1 second
             │    - Scans and removes expired keys
             │
             └──► Persistence Layer
                  - AOF: Append-only file
                  - RDB: Binary snapshots

Data Structure

CrabCache uses Rust's HashMap<String, ValueWithExpiry> wrapped in Arc<Mutex<>> for thread-safe O(1) average-case performance:

  • SET/GET/DEL: O(1) - Direct hash operations
  • MSET: O(n) where n = number of pairs
  • MGET: O(n) where n = number of keys
  • EXISTS: O(n) where n = number of keys to check
  • EXPIRE/TTL: O(1) - Updates/reads existing entry's timestamp
  • MEMORY STATS: O(n) where n = number of keys (memory calculation)
  • LRU eviction: O(k) where k = number of keys to evict
  • Background cleanup: O(n) every second (n = number of keys)

The Arc<Mutex<>> wrapper enables:

  • Shared ownership between main thread and cleanup thread
  • Safe concurrent access with automatic lock management
  • Zero-cost abstraction over manual synchronization

Memory Management

CrabCache implements configurable memory limits with LRU eviction:

Memory Tracking:

  • Approximate memory calculation includes key names and values
  • Real-time tracking updated on every SET/MSET/DEL operation
  • MEMORY STATS command provides visibility into usage

Eviction Policies:

  • NoEviction (default): Returns error when memory limit exceeded
  • AllKeysLRU: Automatically evicts least recently used keys

LRU Implementation:

  • Tracks key access order in a separate vector
  • Updates on GET operations and SET of existing keys
  • Evicts oldest accessed keys first when memory limit reached
  • O(1) access tracking using swap_remove (no iteration needed)

Network Protocol

CrabCache implements the RESP (Redis Serialization Protocol) for network communication:

Supported Types:

  • Simple Strings: +OK\r\n
  • Errors: -ERR message\r\n
  • Integers: :42\r\n
  • Bulk Strings: $5\r\nhello\r\n
  • Arrays: *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

Features:

  • Streaming parser for partial messages
  • Compatible with redis-cli and Redis clients
  • Async I/O with tokio
  • Concurrent client connections

Persistence

CrabCache implements dual persistence strategies:

1. AOF (Append-Only File)

  • Logs every write command for durability
  • Three fsync policies: always, everysecond, never
  • AOF rewrite to compact logs
  • Automatic replay on startup

2. RDB (Snapshot)

  • Binary point-in-time snapshots
  • Configurable intervals (default: 5 minutes)
  • Fast loading on restart
  • Atomic file operations

Expiration Strategy

CrabCache implements a dual-expiration model matching production Redis:

1. Lazy Expiration (Passive)

  • Triggered on GET operations
  • Checks expiry timestamp before returning value
  • O(1) overhead per access
  • Ensures stale data is never returned

2. Active Expiration (Background)

  • Dedicated thread spawned at startup
  • Default: runs cleanup every 1 second (configurable)
  • Uses HashMap::retain() for efficient scanning
  • Prevents memory leaks from unaccessed keys

Configuring Cleanup Interval

// Default: 1 second
let db = CrabCache::new();

// Custom interval: 500ms
let db = CrabCache::with_cleanup_interval(Duration::from_millis(500));

Thread Safety

The store uses Arc<Mutex<HashMap>> for concurrent access:

struct CrabCache {
    storage: CrabCacheStorage,
    expiration_manager: ExpirationManager,
}
  • Arc (Atomic Reference Counting): Multiple threads can own the same data
  • Mutex: Ensures only one thread accesses HashMap at a time
  • Lock is held briefly (microseconds) during operations
  • Background thread and main thread coordinate automatically
  • All unwrap() calls replaced with expect() for better error messages on mutex poisoning

Observability

CrabCache includes structured logging via the tracing crate:

  • Startup/Shutdown: Logs initialization and cleanup events
  • Command Processing: Debug logs for each command with argument counts
  • Expiration Cleanup: Statistics on removed expired keys
  • Error Conditions: Warning/error logs for exceptional cases

Enable detailed logs:

RUST_LOG=debug cargo run

Graceful Shutdown

CrabCache handles shutdown signals (Ctrl+C, SIGTERM, SIGINT) gracefully:

  1. Signal Handler: Uses ctrlc crate to catch termination signals
  2. Atomic Flag: Background thread checks AtomicBool shutdown flag
  3. Thread Cleanup: ExpirationManager joins background thread on shutdown
  4. Drop Implementation: Automatic cleanup if manager goes out of scope
# Press Ctrl+C
^C
Received shutdown signal. Shutting down gracefully...
Cleanup complete. Goodbye!

This ensures:

  • No data corruption
  • Background threads terminate cleanly
  • Resources are properly released
  • No zombie processes

Testing

Run the comprehensive test suite (83 tests):

cargo test

Test Coverage

  • Core Operations: SET, GET, DEL (8 tests)
  • Expiration: EXPIRE, TTL, lazy/active cleanup (12 tests)
  • Batch Operations: MSET, MGET, EXISTS (8 tests)
  • String Operations: APPEND, STRLEN, GETRANGE, SETRANGE, INCR/DECR, GETSET, SETNX (7 tests)
  • Statistics: INFO, DBSIZE, FLUSHDB, hit/miss tracking (4 tests)
  • Memory Management: MEMORY STATS, LRU eviction (3 tests)
  • Command Processing: Parser, error cases (10 tests)
  • Persistence: AOF write/read, RDB save/load (2 tests)
  • RESP Protocol: Serialization, parsing, commands (10 tests)
  • Server: Response conversion (5 tests)
  • Concurrency: Thread safety (4 tests)

Run Specific Tests

# Test only expiration logic
cargo test expire

# Test memory management
cargo test memory

# Test new batch operations
cargo test mget
cargo test mset
cargo test exists

# Test command processing
cargo test process_command

# Run with output
cargo test -- --nocapture

Performance

Benchmarking

Run the comprehensive benchmark suite:

cargo bench

This runs performance tests across all operations with varying dataset sizes, measuring throughput and latency. Results are saved in target/criterion/ with HTML reports.

Benchmark Results

Measured on Apple M1 with optimized release builds (Rust 1.83+):

Core Operations (with lock-free atomic counters):

  • SET: ~165-189ns per operation (53-61 million ops/sec)
  • GET: ~100-127ns per operation (78-100 million ops/sec)
  • DEL: ~946ns for 10 keys, ~91µs for 1000 keys (11 million ops/sec)
  • EXPIRE: ~90-93ns per operation (107-111 million ops/sec)
  • TTL: ~76-81ns per operation (124-132 million ops/sec)

Batch Operations:

  • EXISTS: ~124ns for 1 key, ~2.6µs for 50 keys (19 million checks/sec)
  • MSET: ~165ns for 1 pair, ~6.4µs for 50 pairs (7.8 million pairs/sec)
  • MGET: ~179ns for 1 key, ~4.4µs for 50 keys (11.2 million fetches/sec)

Memory Management:

  • Memory calculation: ~14.6ns for 10 keys, ~21µs for 10k keys (473 million keys/sec)
  • LRU eviction: ~1.9ms for 1000 insertions with 1MB limit

Expiration:

  • Expired key access: ~22-23µs for 1000 keys (includes lazy expiration checks)

Performance Optimizations:

  • Lock-free atomic counters (AtomicU64) eliminate mutex contention on hit/miss tracking
  • O(1) LRU updates using swap_remove instead of remove
  • Single Instant::now() call per GET operation (reduces syscalls)
  • Static error strings to avoid allocations
  • Pre-allocated string buffers for batch operations
  • The Arc<Mutex<>> wrapper adds only ~5ns overhead for thread safety

All benchmarks run with cargo bench using Criterion.rs for statistical rigor.

About

A high-performance in-memory key-value store written in Rust, inspired by Redis. CrabCache provides a Redis-compatible RESP protocol server with production-grade features including dual persistence (AOF + RDB), background expiration cleanup, lock-free counters, and comprehensive observability.

Topics

Resources

Stars

Watchers

Forks

Languages