diff --git a/Cargo.toml b/Cargo.toml index 74c1c7c..d86f30b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ strum = "0.26.2" strum_macros = "0.26.2" clap = { version = "4.5.7", features = ["derive", "env"] } num-traits = "0.2.19" +itertools = "0.13.0" [dev-dependencies] rand = "0.8.5" diff --git a/src/commands/keys.rs b/src/commands/keys.rs index 203c8fe..89578e8 100644 --- a/src/commands/keys.rs +++ b/src/commands/keys.rs @@ -1,5 +1,6 @@ use bytes::Bytes; use glob_match::glob_match; +use itertools::Itertools; use crate::commands::executable::Executable; use crate::commands::CommandParser; @@ -20,11 +21,12 @@ pub struct Keys { impl Executable for Keys { fn exec(self, store: Store) -> Result { let store = store.lock(); - let matching_keys: Vec = store - .keys() - .filter(|key| glob_match(self.pattern.as_str(), key)) - .map(|key| Frame::Bulk(Bytes::from(key.to_string()))) - .collect(); + let matching_keys = store + .iter() + .filter(|(key, _)| glob_match(self.pattern.as_str(), key)) + .sorted_by(|(_, a), (_, b)| b.created_at.cmp(&a.created_at)) + .map(|(key, _)| Frame::Bulk(Bytes::from(key.to_string()))) + .collect::>(); Ok(Frame::Array(matching_keys)) } diff --git a/src/store.rs b/src/store.rs index b83a575..a0ab6b3 100644 --- a/src/store.rs +++ b/src/store.rs @@ -59,23 +59,29 @@ pub struct InnerStoreLocked<'a> { impl<'a> InnerStoreLocked<'a> { pub fn set(&mut self, key: String, data: Bytes) { // Ensure any previous TTL is removed. - self.remove(&key); + let removed = self.remove(&key); + + let created_at = removed.map(|v| v.created_at).unwrap_or(Instant::now()); let value = Value { data, expires_at: None, + created_at, }; self.state.keys.insert(key, value); } pub fn set_with_ttl(&mut self, key: Key, data: Bytes, ttl: Duration) { // Ensure any previous TTL is removed. - self.remove(&key); + let removed = self.remove(&key); + + let created_at = removed.map(|v| v.created_at).unwrap_or(Instant::now()); let expires_at = Instant::now() + ttl; let value = Value { data, expires_at: Some(expires_at), + created_at, }; self.state.keys.insert(key.clone(), value); @@ -142,11 +148,8 @@ impl<'a> InnerStoreLocked<'a> { self.state.keys.keys() } - pub fn iter(&self) -> impl Iterator { - self.state - .keys - .iter() - .map(|(key, value)| (key, &value.data)) + pub fn iter(&self) -> impl Iterator { + self.state.keys.iter().map(|(key, value)| (key, value)) } pub fn incr_by(&mut self, key: &str, increment: T) -> Result @@ -232,6 +235,7 @@ type Key = String; pub struct Value { pub data: Bytes, pub expires_at: Option, + pub created_at: Instant, } pub struct State { diff --git a/tests/integration.rs b/tests/integration.rs index fa19c88..5d5fc7e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -363,15 +363,22 @@ async fn test_getrange() { #[tokio::test] #[serial] async fn test_keys() { - // TODO: The response order from the server is not guaranteed, to ensure accurate comparison - // with the expected result, we need to sort the response before performing the comparison. test_compare::>(|p| { + // Redis keys order is deterministic (always returning the same order for + // a given set of keys) but not guaranteed (it may change between runs). + // + // We sort in backward chronological order to get deterministic results. + // Matching the implementation is out of the scope of the project. p.cmd("SET").arg("keys_key_1").arg("Argentina"); - p.cmd("SET").arg("keys_key_2").arg("Spain"); - p.cmd("SET").arg("keys_key_3").arg("Netherlands"); p.cmd("KEYS").arg("*"); p.cmd("KEYS").arg("*key*"); + + p.cmd("SET").arg("keys_key_2").arg("Spain"); + p.cmd("SET").arg("keys_key_3").arg("Netherlands"); + + p.cmd("KEYS").arg("*1"); + p.cmd("KEYS").arg("*2"); p.cmd("KEYS").arg("*3"); }) .await;