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..d79c975 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.inserted_at.cmp(&a.inserted_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..980fd1d 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 inserted_at = removed.map(|v| v.inserted_at).unwrap_or(Instant::now()); let value = Value { data, expires_at: None, + inserted_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 inserted_at = removed.map(|v| v.inserted_at).unwrap_or(Instant::now()); let expires_at = Instant::now() + ttl; let value = Value { data, expires_at: Some(expires_at), + inserted_at, }; self.state.keys.insert(key.clone(), value); @@ -142,11 +148,11 @@ impl<'a> InnerStoreLocked<'a> { self.state.keys.keys() } - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.state .keys .iter() - .map(|(key, value)| (key, &value.data)) + .map(|(key, value)| (key, value)) } pub fn incr_by(&mut self, key: &str, increment: T) -> Result @@ -232,6 +238,7 @@ type Key = String; pub struct Value { pub data: Bytes, pub expires_at: Option, + pub inserted_at: Instant, } pub struct State { diff --git a/tests/integration.rs b/tests/integration.rs index 0e12570..99434ae 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -349,9 +349,11 @@ 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 deterministice but not guaranteed. + // We sort the keys by insertion order to make the test deterministic. + // Testing with a different set of keys produces different results, + // but 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");