Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ a low-latency, memory-efficient, distributed cache written in Rust. designed to
- **sets** — SADD, SREM, SMEMBERS, SISMEMBER, SCARD, SMISMEMBER, SUNION, SINTER, SDIFF, SUNIONSTORE, SINTERSTORE, SDIFFSTORE, SRANDMEMBER, SPOP, SSCAN, SMOVE, SINTERCARD
- **bitmaps** — GETBIT, SETBIT, BITCOUNT, BITPOS, BITOP
- **key commands** — DEL, UNLINK, EXISTS, EXPIRE, EXPIREAT, EXPIRETIME, TTL, PEXPIRE, PEXPIREAT, PEXPIRETIME, PTTL, PERSIST, TYPE, SCAN, KEYS, RENAME, COPY, TOUCH, RANDOMKEY, SORT, OBJECT ENCODING/REFCOUNT, WAIT
- **server commands** — PING, ECHO, INFO, DBSIZE, FLUSHDB, BGSAVE, BGREWRITEAOF, AUTH, QUIT, CONFIG GET/SET/REWRITE, SLOWLOG, CLIENT ID/SETNAME/GETNAME/LIST, TIME, LASTSAVE, ROLE, MONITOR
- **server commands** — PING, ECHO, INFO, DBSIZE, FLUSHDB, FLUSHALL, MEMORY USAGE, BGSAVE, BGREWRITEAOF, AUTH, QUIT, CONFIG GET/SET/REWRITE, SLOWLOG, CLIENT ID/SETNAME/GETNAME/LIST, TIME, LASTSAVE, ROLE, MONITOR
- **transactions** — MULTI, EXEC, DISCARD, WATCH/UNWATCH for optimistic locking
- **acl** — per-user command permissions and key pattern restrictions: ACL SETUSER, GETUSER, DELUSER, LIST, WHOAMI, CAT, USERS
- **pub/sub** — SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, plus PUBSUB introspection
Expand All @@ -46,6 +46,24 @@ a low-latency, memory-efficient, distributed cache written in Rust. designed to
- **interactive CLI** — `ember-cli` with REPL, syntax highlighting, tab-completion, inline hints, cluster subcommands, and built-in benchmark
- **graceful shutdown** — drains active connections on SIGINT/SIGTERM before exiting

## not supported

ember is purpose-built for caching. some Redis features are intentionally excluded:

- **lua scripting** — `EVAL`, `EVALSHA`, `SCRIPT *` are not implemented and not planned. server-side logic may be supported via WASM extensions in a future release.
- **streams** — `XADD`, `XREAD`, and related commands are not implemented. use a dedicated stream store (Kafka, Redpanda, etc.) for this.
- **multiple databases** — `SELECT`, `MOVE`, `SWAPDB` are not supported. ember is single-database by design.
- **bitfield** — `BITFIELD` and `BITFIELD_RO` are not planned; use application-level serialization instead.

see [docs/compatibility.md](docs/compatibility.md) for the full command support matrix.

## notable differences from redis

- **RENAME** requires the source and destination keys to hash to the same shard. cross-shard renames return an error.
- **transactions** (MULTI/EXEC) are fully atomic within a single shard. cross-shard transactions execute in order but are not globally atomic — the same limitation as Redis Cluster.
- **geo commands** are coming in a future release.
- `redis-cli` connects on port 6379 (RESP3). the typed gRPC API is on port 6380 (when `--features grpc` is compiled in).

## install

**homebrew (macOS/Linux)**
Expand Down
12 changes: 12 additions & 0 deletions crates/ember-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,18 @@ pub static COMMANDS: &[CommandInfo] = &[
group: "server",
summary: "remove all keys from the current database",
},
CommandInfo {
name: "FLUSHALL",
args: "[ASYNC]",
group: "server",
summary: "remove all keys from all databases (alias for FLUSHDB in ember)",
},
CommandInfo {
name: "MEMORY USAGE",
args: "key [SAMPLES count]",
group: "server",
summary: "estimate memory usage for a key in bytes",
},
CommandInfo {
name: "INFO",
args: "[section]",
Expand Down
13 changes: 13 additions & 0 deletions crates/ember-core/src/keyspace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,19 @@ impl Keyspace {
}
}

/// Returns the estimated memory usage in bytes for the given key,
/// or `None` if the key does not exist or is expired.
///
/// Uses the cached value size for O(1) cost. The estimate covers the
/// key string, the serialized value, and the per-entry overhead.
pub fn memory_usage(&mut self, key: &str) -> Option<usize> {
if self.remove_if_expired(key) {
return None;
}
let entry = self.entries.get(key)?;
Some(entry.entry_size(key))
}

/// Removes the expiration from a key.
///
/// Returns `true` if the key existed and had a timeout that was removed.
Expand Down
7 changes: 7 additions & 0 deletions crates/ember-core/src/shard/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ pub enum ShardRequest {
Ttl {
key: String,
},
/// MEMORY USAGE. Returns the estimated memory footprint of a key in bytes.
MemoryUsage {
key: String,
},
Persist {
key: String,
},
Expand Down Expand Up @@ -1773,6 +1777,9 @@ fn dispatch(
ShardResponse::Bool(ks.expireat(key, *timestamp))
}
ShardRequest::Ttl { key } => ShardResponse::Ttl(ks.ttl(key)),
ShardRequest::MemoryUsage { key } => {
ShardResponse::Integer(ks.memory_usage(key).map(|n| n as i64).unwrap_or(-1))
}
ShardRequest::Persist { key } => ShardResponse::Bool(ks.persist(key)),
ShardRequest::Pttl { key } => ShardResponse::Ttl(ks.pttl(key)),
ShardRequest::Pexpire { key, milliseconds } => {
Expand Down
8 changes: 7 additions & 1 deletion crates/ember-protocol/src/command/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ impl Command {
Command::BgSave => "bgsave",
Command::BgRewriteAof => "bgrewriteaof",
Command::FlushDb { .. } => "flushdb",
Command::FlushAll { .. } => "flushall",
Command::MemoryUsage { .. } => "memory",
Command::ConfigGet { .. } => "config",
Command::ConfigSet { .. } => "config",
Command::ConfigRewrite => "config",
Expand Down Expand Up @@ -310,6 +312,7 @@ impl Command {
| Command::SMove { .. }
// server / persistence
| Command::FlushDb { .. }
| Command::FlushAll { .. }
| Command::ConfigSet { .. }
| Command::Exec
| Command::BgRewriteAof
Expand Down Expand Up @@ -514,6 +517,8 @@ impl Command {
}
Command::BgSave | Command::BgRewriteAof => SERVER | ADMIN | SLOW,
Command::FlushDb { .. } => KEYSPACE | WRITE | ADMIN | DANGEROUS | SLOW,
Command::FlushAll { .. } => KEYSPACE | WRITE | ADMIN | DANGEROUS | SLOW,
Command::MemoryUsage { .. } => READ | KEYSPACE | SLOW,
Command::ConfigGet { .. } => SERVER | ADMIN | SLOW,
Command::ConfigSet { .. } | Command::ConfigRewrite => SERVER | ADMIN | SLOW,
Command::SlowLogGet { .. } | Command::SlowLogLen | Command::SlowLogReset => {
Expand Down Expand Up @@ -676,7 +681,8 @@ impl Command {
| Command::Restore { key, .. }
| Command::Sort { key, .. }
| Command::GetDel { key }
| Command::GetEx { key, .. } => Some(key),
| Command::GetEx { key, .. }
| Command::MemoryUsage { key } => Some(key),
Command::LMove { source, .. } => Some(source),
Command::Copy { source, .. } => Some(source),
Command::SMove { source, .. } => Some(source),
Expand Down
6 changes: 6 additions & 0 deletions crates/ember-protocol/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ pub enum Command {
/// FLUSHDB \[ASYNC\]. Removes all keys from the database.
FlushDb { async_mode: bool },

/// FLUSHALL \[ASYNC\]. Removes all keys (alias for FLUSHDB — Ember is single-database).
FlushAll { async_mode: bool },

/// MEMORY USAGE key \[SAMPLES count\]. Returns estimated memory usage in bytes.
MemoryUsage { key: String },

/// CONFIG GET `pattern`. Returns matching server configuration parameters.
ConfigGet { pattern: String },

Expand Down
35 changes: 35 additions & 0 deletions crates/ember-protocol/src/command/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ impl Command {
"BGSAVE" => parse_bgsave(&frames[1..]),
"BGREWRITEAOF" => parse_bgrewriteaof(&frames[1..]),
"FLUSHDB" => parse_flushdb(&frames[1..]),
"FLUSHALL" => parse_flushall(&frames[1..]),
"MEMORY" => parse_memory_cmd(&frames[1..]),
"SCAN" => parse_scan(&frames[1..]),
"SSCAN" => parse_key_scan(&frames[1..], "SSCAN"),
"HSCAN" => parse_key_scan(&frames[1..], "HSCAN"),
Expand Down Expand Up @@ -1021,6 +1023,39 @@ fn parse_flushdb(args: &[Frame]) -> Result<Command, ProtocolError> {
Err(wrong_arity("FLUSHDB"))
}

fn parse_flushall(args: &[Frame]) -> Result<Command, ProtocolError> {
if args.is_empty() {
return Ok(Command::FlushAll { async_mode: false });
}
if args.len() == 1 {
let arg = extract_string(&args[0])?;
if arg.eq_ignore_ascii_case("ASYNC") {
return Ok(Command::FlushAll { async_mode: true });
}
}
Err(wrong_arity("FLUSHALL"))
}

fn parse_memory_cmd(args: &[Frame]) -> Result<Command, ProtocolError> {
if args.is_empty() {
return Err(ProtocolError::WrongArity("memory".into()));
}
let subcommand = extract_string(&args[0])?;
if subcommand.eq_ignore_ascii_case("USAGE") {
if args.len() < 2 {
return Err(wrong_arity("MEMORY USAGE"));
}
let key = extract_string(&args[1])?;
// Accept but ignore SAMPLES count — we always use the cached value size.
Ok(Command::MemoryUsage { key })
} else {
Err(ProtocolError::InvalidCommandFrame(format!(
"unknown subcommand '{}' for 'memory' command",
subcommand
)))
}
}

fn parse_unlink(args: &[Frame]) -> Result<Command, ProtocolError> {
if args.is_empty() {
return Err(wrong_arity("UNLINK"));
Expand Down
1 change: 1 addition & 0 deletions crates/ember-server/src/acl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@ fn commands_in_category(flag: u64) -> Vec<&'static str> {
Command::BgSave,
Command::BgRewriteAof,
Command::FlushDb { async_mode: false },
Command::FlushAll { async_mode: false },
Command::ConfigGet {
pattern: String::new(),
},
Expand Down
24 changes: 24 additions & 0 deletions crates/ember-server/src/connection/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,30 @@ pub(super) async fn execute(
}
}

// Ember is single-database, so FLUSHALL is identical to FLUSHDB.
Command::FlushAll { async_mode } => {
let req = if async_mode {
|| ShardRequest::FlushDbAsync
} else {
|| ShardRequest::FlushDb
};
match engine.broadcast(req).await {
Ok(_) => Frame::Simple("OK".into()),
Err(e) => Frame::Error(format!("ERR {e}")),
}
}

Command::MemoryUsage { key } => {
let idx = engine.shard_for_key(&key);
let req = ShardRequest::MemoryUsage { key: key.clone() };
match engine.send_to_shard(idx, req).await {
Ok(ShardResponse::Integer(-1)) => Frame::Null,
Ok(ShardResponse::Integer(n)) => Frame::Integer(n),
Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")),
Err(e) => Frame::Error(format!("ERR {e}")),
}
}

Command::Keys { pattern } => {
match engine
.broadcast(|| ShardRequest::Keys {
Expand Down
8 changes: 4 additions & 4 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ Ember also exposes port `6379` by default, the same as Redis, so most default co
| CLIENT SETNAME | ✓ | |
| CLIENT GETNAME | ✓ | |
| CLIENT LIST | ✓ | |
| FLUSHALL | | use FLUSHDB instead |
| FLUSHALL | | ASYNC mode supported; single-database, equivalent to FLUSHDB |
| SAVE | ✗ | use BGSAVE instead |
| SHUTDOWN | ✗ | use SIGTERM instead |
| DEBUG | ✗ | not implemented |
Expand All @@ -257,7 +257,7 @@ Ember also exposes port `6379` by default, the same as Redis, so most default co
| CLIENT NO-EVICT | ✗ | not implemented |
| CLIENT NO-TOUCH | ✗ | not implemented |
| LATENCY | ✗ | not implemented |
| MEMORY USAGE | | not implemented |
| MEMORY USAGE | | returns estimated key memory in bytes; SAMPLES option accepted and ignored |
| MEMORY STATS | ✗ | not implemented |
| MEMORY DOCTOR | ✗ | not implemented |
| RESET | ✗ | not implemented |
Expand Down Expand Up @@ -334,7 +334,7 @@ all cluster commands are implemented. see the cluster documentation for operatio
some Redis commands are explicitly not planned for Ember:

**scripting**
- `EVAL`, `EVALSHA`, `EVALRO`, `SCRIPT LOAD`, `SCRIPT EXISTS`, `SCRIPT FLUSH` — Lua scripting is an anti-goal. We may support WASM-based extensions in the future instead.
- `EVAL`, `EVALSHA`, `EVALRO`, `SCRIPT LOAD`, `SCRIPT EXISTS`, `SCRIPT FLUSH` — Lua scripting is explicitly not planned. if you need server-side logic, WASM-based extensions may be supported in a future release.
- `FCALL`, `FUNCTION LOAD`, `FUNCTION LIST`, `FUNCTION DELETE` — same reasoning as EVAL.

**streams**
Expand All @@ -344,7 +344,7 @@ some Redis commands are explicitly not planned for Ember:
- `BITFIELD`, `BITFIELD_RO` — not planned; use application-level serialization if needed.

**geo**
- `GEOADD`, `GEOPOS`, `GEODIST`, `GEORADIUS`, `GEORADIUSBYMEMBER`, `GEOSEARCH`, `GEOSEARCHSTORE`, `GEOHASH` — not implemented yet.
- `GEOADD`, `GEOPOS`, `GEODIST`, `GEORADIUS`, `GEORADIUSBYMEMBER`, `GEOSEARCH`, `GEOSEARCHSTORE`, `GEOHASH` — coming in a future release.

**other**
- `LOLWUT` — not implemented.
Loading