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.
- 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
- 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)
- 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
- 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
- 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
git clone <repository-url>
cd RustKVStore
cargo build --releaseDefault Mode (REPL):
cargo runTCP 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 6379Hybrid 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!
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) 1if deleted,(integer) 0if key doesn't exist
EXPIRE key seconds
- Sets TTL in seconds on an existing key
- Returns:
(integer) 1if successful,(integer) 0if 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) Nwhere 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 databaseeviction_policy: Current eviction policy (NoEviction or AllKeysLRU)
QUIT / EXIT
- Gracefully exits the application
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 STATScommand to monitor usage - Background cleanup handles expired keys automatically
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 existInvalidArgument- Wrong number or type of argumentsInvalidInteger- Value cannot be parsed as integerInvalidCommand- Command not recognizedOutOfMemory- Memory limit exceededWrongType- Operation against wrong value typeIoError- File I/O errors (persistence)SerializationError- RDB serialization errors
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 writeeverysecond- 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
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
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
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 STATScommand 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)
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
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
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));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 dataMutex: 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 withexpect()for better error messages on mutex poisoning
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 runCrabCache handles shutdown signals (Ctrl+C, SIGTERM, SIGINT) gracefully:
- Signal Handler: Uses
ctrlccrate to catch termination signals - Atomic Flag: Background thread checks
AtomicBoolshutdown flag - Thread Cleanup: ExpirationManager joins background thread on shutdown
- 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
Run the comprehensive test suite (83 tests):
cargo test- 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)
# 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 -- --nocaptureRun the comprehensive benchmark suite:
cargo benchThis runs performance tests across all operations with varying dataset sizes, measuring throughput and latency. Results are saved in target/criterion/ with HTML reports.
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.