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
34 changes: 33 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# changelog

## 0.4.9 (2026-02-27)

### features
- bitmap commands: `GETBIT`, `SETBIT`, `BITCOUNT`, `BITPOS`, `BITOP` (#316, #330)
- `LMPOP` / `ZMPOP` multi-key pops (#317)
- `EXPIREAT`, `PEXPIREAT`, `EXPIRETIME`, `PEXPIRETIME`, `GETSET`, `MSETNX`, `SMOVE`, `SINTERCARD`, count args for `LPOP`/`RPOP` (#315, #318)
- `HRANDFIELD` and `ZRANDMEMBER` (#319)
- Redis 6.2+ commands: `LMOVE`, `GETDEL`, `GETEX`, `ZDIFF`, `ZINTER`, `ZUNION` (#299)
- `COMMAND`, `HINCRBYFLOAT`, `ZDIFFSTORE`/`ZINTERSTORE`/`ZUNIONSTORE`, `FLUSHALL`, `MEMORY USAGE`, `WAIT` (#312, #324, #331)
- keyspace notifications (`__keyevent@0__:expired` and write events) (#313)
- automatic snapshot scheduling (#300)
- `CONFIG SET` live-apply for maxmemory and maxmemory-policy (#312)
- ember-client: typed async RESP3 API with pipelining; CLI now reuses it (#297, #302, #303)
- ember-ts client with full API parity; npm package renamed to `emberdb` with OIDC trusted publishing (#308, #309, #332)
- grpc: 39 new rpcs closing the RESP/gRPC api gap; 17 new commands surfaced across cli, grpc, and clients (#306, #320)
- CLI watch mode and batch mode (#298)

### fixes
- BITOP cross-shard correctness (#330)
- cluster reports `cluster_state:ok` on bootstrap (#339)
- eliminated production panics in client decoder and server startup (#322)
- auth failure metrics, command memory budget, migration progress reporting (#323)
- replication send-failure counter; go client subscribe error propagation (#325)
- raft log persistence switched from bincode to postcard; dropped atomic-polyfill (#296, #329)
- prometheus counters, crash recovery hardening, TLS tests (#314)
- go client: regenerated protobuf stubs for the 17 new rpcs (#337)

### docs
- command count updated to 190+; compatibility guide refreshed (#335)

---

## 0.4.8 (2026-02-25)

### performance
Expand All @@ -13,7 +45,7 @@
- rate-limited ENOSPC handling for AOF writes (#249)

### docs
- removed concurrent mode references — sharded is now the only execution mode (#275, #291)
- pruned concurrent mode from the docs (#275, #291) — correction: the mode itself was not removed and is still available behind `--concurrent`; sharded remains the default execution mode
- refreshed all benchmark numbers from 2026-02-25 GCP run (#288-290)
- added documentation section, code of conduct, performance tuning guide, production checklist (#270, #281-282)

Expand Down
14 changes: 6 additions & 8 deletions crates/ember-core/src/shard/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,10 @@ mod tests {
})
.await;

// give the shard a moment to process
tokio::time::sleep(Duration::from_millis(50)).await;

let result = rx.try_recv();
assert!(result.is_ok());
let (key, data) = result.unwrap();
// recv with a generous timeout instead of a fixed sleep so a busy
// runner can't flake the test
let result = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await;
let (key, data) = result.expect("timed out waiting for BLPop").unwrap();
assert_eq!(key, "mylist");
assert_eq!(data, Bytes::from("hello"));
}
Expand Down Expand Up @@ -267,8 +265,8 @@ mod tests {
})
.await;

tokio::time::sleep(Duration::from_millis(50)).await;
let (key, data) = rx.try_recv().unwrap();
let result = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await;
let (key, data) = result.expect("timed out waiting for BRPop").unwrap();
assert_eq!(key, "mylist");
assert_eq!(data, Bytes::from("b"));
}
Expand Down
5 changes: 3 additions & 2 deletions crates/ember-core/src/shard/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,9 @@ mod tests {
// drop handle to shut down shard
}

// give it a moment to flush
tokio::time::sleep(Duration::from_millis(50)).await;
// the final AOF sync runs in the shard task after the handle drops;
// wait generously so a loaded runner can't observe a partial flush
tokio::time::sleep(Duration::from_millis(500)).await;

// start a new shard with the same config — should recover
{
Expand Down
4 changes: 4 additions & 0 deletions crates/ember-core/src/types/sorted_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ impl SortedSet {

/// Adds or updates a member with ZADD flag semantics.
pub fn add_with_flags(&mut self, member: &str, score: f64, flags: &ZAddFlags) -> AddResult {
// the unwraps below rely on the score map and sorted vec staying in
// lockstep; catch any desync early in debug/test builds
debug_assert_eq!(self.scores.len(), self.sorted.len());
let new_score = OrderedFloat(score);

if let Some(&old_score) = self.scores.get(member) {
Expand Down Expand Up @@ -137,6 +140,7 @@ impl SortedSet {

/// Removes a member from the sorted set. Returns `true` if it existed.
pub fn remove(&mut self, member: &str) -> bool {
debug_assert_eq!(self.scores.len(), self.sorted.len());
if let Some((name, score)) = self.scores.remove_entry(member) {
let idx = self.search_idx(score, &name).unwrap();
self.sorted.remove(idx);
Expand Down
4 changes: 3 additions & 1 deletion crates/ember-server/src/connection/exec/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,9 @@ pub(in crate::connection) async fn msetnx(pairs: Vec<(String, Bytes)>, cx: &Exec
// but matches Redis cluster semantics where MSETNX pairs must
// share a hash slot. For single-node mode it is correct.
if pairs.is_empty() {
return Frame::Error("ERR wrong number of arguments for 'MSETNX'".into());
// unreachable in practice (the parser enforces arity); message kept
// in the canonical WrongArity format for consistency
return Frame::Error("ERR wrong number of arguments for 'MSETNX' command".into());
}

let keys: Vec<String> = pairs.iter().map(|(k, _)| k.clone()).collect();
Expand Down
Loading