diff --git a/README.md b/README.md index 0a44c9b8..9f979d51 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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)** diff --git a/crates/ember-cli/src/commands.rs b/crates/ember-cli/src/commands.rs index 556e032e..48c83d7c 100644 --- a/crates/ember-cli/src/commands.rs +++ b/crates/ember-cli/src/commands.rs @@ -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]", diff --git a/crates/ember-core/src/keyspace/mod.rs b/crates/ember-core/src/keyspace/mod.rs index 18036204..21da495c 100644 --- a/crates/ember-core/src/keyspace/mod.rs +++ b/crates/ember-core/src/keyspace/mod.rs @@ -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 { + 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. diff --git a/crates/ember-core/src/shard/mod.rs b/crates/ember-core/src/shard/mod.rs index 3eca665f..0c879be7 100644 --- a/crates/ember-core/src/shard/mod.rs +++ b/crates/ember-core/src/shard/mod.rs @@ -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, }, @@ -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 } => { diff --git a/crates/ember-protocol/src/command/attributes.rs b/crates/ember-protocol/src/command/attributes.rs index 0079b184..d242184f 100644 --- a/crates/ember-protocol/src/command/attributes.rs +++ b/crates/ember-protocol/src/command/attributes.rs @@ -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", @@ -310,6 +312,7 @@ impl Command { | Command::SMove { .. } // server / persistence | Command::FlushDb { .. } + | Command::FlushAll { .. } | Command::ConfigSet { .. } | Command::Exec | Command::BgRewriteAof @@ -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 => { @@ -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), diff --git a/crates/ember-protocol/src/command/mod.rs b/crates/ember-protocol/src/command/mod.rs index f45ed0c3..3c5fe85c 100644 --- a/crates/ember-protocol/src/command/mod.rs +++ b/crates/ember-protocol/src/command/mod.rs @@ -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 }, diff --git a/crates/ember-protocol/src/command/parse.rs b/crates/ember-protocol/src/command/parse.rs index 1fc51656..04b48b78 100644 --- a/crates/ember-protocol/src/command/parse.rs +++ b/crates/ember-protocol/src/command/parse.rs @@ -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"), @@ -1021,6 +1023,39 @@ fn parse_flushdb(args: &[Frame]) -> Result { Err(wrong_arity("FLUSHDB")) } +fn parse_flushall(args: &[Frame]) -> Result { + 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 { + 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 { if args.is_empty() { return Err(wrong_arity("UNLINK")); diff --git a/crates/ember-server/src/acl.rs b/crates/ember-server/src/acl.rs index cb8857ea..eb5bec05 100644 --- a/crates/ember-server/src/acl.rs +++ b/crates/ember-server/src/acl.rs @@ -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(), }, diff --git a/crates/ember-server/src/connection/execute.rs b/crates/ember-server/src/connection/execute.rs index eebbd263..78e694e3 100644 --- a/crates/ember-server/src/connection/execute.rs +++ b/crates/ember-server/src/connection/execute.rs @@ -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 { diff --git a/docs/compatibility.md b/docs/compatibility.md index 6c785f47..67d54619 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -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 | @@ -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 | @@ -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** @@ -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.