diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2af687..8c14b86c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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) diff --git a/crates/ember-core/src/shard/blocking.rs b/crates/ember-core/src/shard/blocking.rs index a96d7328..9e6be8dd 100644 --- a/crates/ember-core/src/shard/blocking.rs +++ b/crates/ember-core/src/shard/blocking.rs @@ -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")); } @@ -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")); } diff --git a/crates/ember-core/src/shard/persistence.rs b/crates/ember-core/src/shard/persistence.rs index d52778d3..6b0e8c06 100644 --- a/crates/ember-core/src/shard/persistence.rs +++ b/crates/ember-core/src/shard/persistence.rs @@ -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 { diff --git a/crates/ember-core/src/types/sorted_set.rs b/crates/ember-core/src/types/sorted_set.rs index 998bf600..e002ba39 100644 --- a/crates/ember-core/src/types/sorted_set.rs +++ b/crates/ember-core/src/types/sorted_set.rs @@ -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) { @@ -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); diff --git a/crates/ember-server/src/connection/exec/strings.rs b/crates/ember-server/src/connection/exec/strings.rs index 3ca4c990..6e1b83cd 100644 --- a/crates/ember-server/src/connection/exec/strings.rs +++ b/crates/ember-server/src/connection/exec/strings.rs @@ -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 = pairs.iter().map(|(k, _)| k.clone()).collect();