mod constants;
mod utilities;
pub(crate) mod test_cache {
use super::*;
use glide_core::connection_request::ClientSideCache;
use glide_core::connection_request::EvictionPolicy;
use glide_core::connection_request::ProtocolVersion;
use redis::Value;
use redis::cache::glide_cache::CachedKeyType;
use rstest::rstest;
use utilities::cluster::*;
use utilities::*;
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_basic_cache_hit_with_metrics(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: true,
client_side_cache: Some(ClientSideCache {
cache_id: "test_cache".to_string().into(),
max_cache_kb: 1,
entry_ttl_ms: 0,
eviction_policy: None,
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut reset_cmd = redis::Cmd::new();
reset_cmd.arg("CONFIG").arg("RESETSTAT");
test_basics
.client
.send_command(&mut reset_cmd, None)
.await
.ok();
let mut set_cmd = redis::Cmd::new();
set_cmd
.arg("SET")
.arg("cache_test_key")
.arg("cache_test_value");
let set_result = test_basics.client.send_command(&mut set_cmd, None).await;
assert!(set_result.is_ok());
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("cache_test_key");
let get_result = test_basics.client.send_command(&mut get_cmd, None).await;
assert!(get_result.is_ok());
assert_eq!(
get_result.unwrap(),
Value::BulkString(b"cache_test_value".to_vec())
);
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(1));
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("cache_test_key");
let get_result = test_basics.client.send_command(&mut get_cmd, None).await;
assert!(get_result.is_ok());
assert_eq!(
get_result.unwrap(),
Value::BulkString(b"cache_test_value".to_vec())
);
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("cache_test_key");
let get_result = test_basics.client.send_command(&mut get_cmd, None).await;
assert!(get_result.is_ok());
assert_eq!(
get_result.unwrap(),
Value::BulkString(b"cache_test_value".to_vec())
);
assert_command_count(&mut test_basics.client, "GET", 1, use_cluster).await;
let hit_rate = test_basics.client.cache_hit_rate().unwrap();
let miss_rate = test_basics.client.cache_miss_rate().unwrap();
let hit_rate = match hit_rate {
Value::Double(d) => d,
_ => panic!("Expected Value::Double, got {:?}", hit_rate),
};
let miss_rate = match miss_rate {
Value::Double(d) => d,
_ => panic!("Expected Value::Double, got {:?}", miss_rate),
};
assert_eq!(hit_rate, 2.0 / 3.0, "Expected 66.67% hit rate");
assert_eq!(miss_rate, 1.0 / 3.0, "Expected 33.33% miss rate");
assert!(
(hit_rate + miss_rate - 1.0).abs() < 0.0001,
"Rates should sum to 1.0"
);
let total_lookups = test_basics.client.cache_total_lookups().unwrap();
assert_eq!(total_lookups, Value::Int(3), "Expected 3 total lookups");
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_cache_without_metrics(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: true,
client_side_cache: Some(ClientSideCache {
cache_id: "test_cache_no_metrics".to_string().into(),
max_cache_kb: 10 * 1024,
entry_ttl_ms: 60_000,
eviction_policy: None,
enable_metrics: false, ..Default::default()
}),
..Default::default()
},
)
.await;
let mut reset_cmd = redis::Cmd::new();
reset_cmd.arg("CONFIG").arg("RESETSTAT");
test_basics
.client
.send_command(&mut reset_cmd, None)
.await
.ok();
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("key").arg("value");
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("key");
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_command_count(&mut test_basics.client, "GET", 1, use_cluster).await;
let hit_rate = test_basics.client.cache_hit_rate();
assert!(hit_rate.is_err());
assert!(
hit_rate
.unwrap_err()
.to_string()
.contains("Cache metrics tracking is not enabled")
);
let miss_rate = test_basics.client.cache_miss_rate();
assert!(miss_rate.is_err());
assert!(
miss_rate
.unwrap_err()
.to_string()
.contains("Cache metrics tracking is not enabled")
);
let evictions = test_basics.client.cache_evictions();
assert!(evictions.is_err());
assert!(
evictions
.unwrap_err()
.to_string()
.contains("Cache metrics tracking is not enabled")
);
let expirations = test_basics.client.cache_expirations();
assert!(expirations.is_err());
assert!(
expirations
.unwrap_err()
.to_string()
.contains("Cache metrics tracking is not enabled")
);
let total_lookups = test_basics.client.cache_total_lookups();
assert!(total_lookups.is_err());
assert!(
total_lookups
.unwrap_err()
.to_string()
.contains("Cache metrics tracking is not enabled")
);
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(1));
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_cache_nil_values_not_cached(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: true,
client_side_cache: Some(ClientSideCache {
cache_id: "test_cache_nil".to_string().into(),
max_cache_kb: 1,
entry_ttl_ms: 60_000,
eviction_policy: None,
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut reset_cmd = redis::Cmd::new();
reset_cmd.arg("CONFIG").arg("RESETSTAT");
test_basics
.client
.send_command(&mut reset_cmd, None)
.await
.ok();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("nonexistent_key");
let result = test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(result, Value::Nil);
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(0));
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("nonexistent_key");
let result = test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(result, Value::Nil);
assert_command_count(&mut test_basics.client, "GET", 2, use_cluster).await;
let miss_rate = test_basics.client.cache_miss_rate().unwrap();
let miss_rate = match miss_rate {
Value::Double(d) => d,
_ => panic!("Expected Value::Double, got {:?}", miss_rate),
};
assert_eq!(miss_rate, 1.0, "Expected 100% miss rate");
let total_lookups = test_basics.client.cache_total_lookups().unwrap();
assert_eq!(total_lookups, Value::Int(2), "Expected 2 total lookups");
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_cache_ttl_expiration(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: true,
client_side_cache: Some(ClientSideCache {
cache_id: "test_cache_ttl".to_string().into(),
max_cache_kb: 1,
entry_ttl_ms: 2_000, eviction_policy: None,
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut reset_cmd = redis::Cmd::new();
reset_cmd.arg("CONFIG").arg("RESETSTAT");
test_basics
.client
.send_command(&mut reset_cmd, None)
.await
.ok();
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("ttl_key").arg("ttl_value");
let set_res = test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
assert_eq!(set_res, Value::Okay);
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("ttl_key");
let mut get_res = test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(get_res, Value::BulkString(b"ttl_value".to_vec()));
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(1));
get_res = test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(get_res, Value::BulkString(b"ttl_value".to_vec()));
assert_command_count(&mut test_basics.client, "GET", 1, use_cluster).await;
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("ttl_key");
get_res = test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(get_res, Value::BulkString(b"ttl_value".to_vec()));
assert_command_count(&mut test_basics.client, "GET", 2, use_cluster).await;
let expirations = test_basics.client.cache_expirations().unwrap();
assert_eq!(expirations, Value::Int(1));
let miss_rate = test_basics.client.cache_miss_rate().unwrap();
let miss_rate = match miss_rate {
Value::Double(d) => d,
_ => panic!("Expected Value::Double, got {:?}", miss_rate),
};
assert_eq!(miss_rate, (2.0 / 3.0), "Expected 66.67% miss rate");
let total_lookups = test_basics.client.cache_total_lookups().unwrap();
assert_eq!(total_lookups, Value::Int(3), "Expected 3 total lookups");
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_cache_multiple_keys(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
client_side_cache: Some(ClientSideCache {
cache_id: "test_cache_multi".to_string().into(),
max_cache_kb: 1,
entry_ttl_ms: 60_000,
eviction_policy: None,
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut reset_cmd = redis::Cmd::new();
reset_cmd.arg("CONFIG").arg("RESETSTAT");
test_basics
.client
.send_command(&mut reset_cmd, None)
.await
.ok();
for i in 1..=3 {
let mut set_cmd = redis::Cmd::new();
set_cmd
.arg("SET")
.arg(format!("key{}", i))
.arg(format!("value{}", i));
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
}
for i in 1..=3 {
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg(format!("key{}", i));
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
}
assert_command_count(&mut test_basics.client, "GET", 3, use_cluster).await;
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(3));
let hit_rate = test_basics.client.cache_hit_rate().unwrap();
let hit_rate = match hit_rate {
Value::Double(d) => d,
_ => panic!("Expected Value::Double, got {:?}", hit_rate),
};
assert_eq!(hit_rate, 0.5);
let total_lookups = test_basics.client.cache_total_lookups().unwrap();
assert_eq!(total_lookups, Value::Int(6), "Expected 6 total lookups");
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_no_cache_all_requests_hit_server(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
client_side_cache: None, ..Default::default()
},
)
.await;
let mut reset_cmd = redis::Cmd::new();
reset_cmd.arg("CONFIG").arg("RESETSTAT");
test_basics
.client
.send_command(&mut reset_cmd, None)
.await
.ok();
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("key").arg("value");
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
for _ in 0..3 {
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("key");
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
}
assert_command_count(&mut test_basics.client, "GET", 3, use_cluster).await;
let mut result = test_basics.client.cache_hit_rate();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Client-side caching is not enabled")
);
result = test_basics.client.cache_miss_rate();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Client-side caching is not enabled")
);
result = test_basics.client.cache_evictions();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Client-side caching is not enabled")
);
result = test_basics.client.cache_expirations();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Client-side caching is not enabled")
);
result = test_basics.client.cache_total_lookups();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Client-side caching is not enabled")
);
result = test_basics.client.cache_entry_count();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Client-side caching is not enabled")
);
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_cache_eviction_policy_lru(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let cache_id = "test_cache_lru";
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
client_side_cache: Some(ClientSideCache {
cache_id: cache_id.to_string().into(),
max_cache_kb: 1, entry_ttl_ms: 0,
eviction_policy: Some(EvictionPolicy::LRU.into()),
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let value = "x".repeat(250);
for i in 1..=3 {
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg(format!("lru_key{}", i)).arg(&value);
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg(format!("lru_key{}", i));
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
}
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(3));
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("lru_key1");
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
for i in 4..=5 {
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg(format!("lru_key{}", i)).arg(&value);
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg(format!("lru_key{}", i));
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
}
let evictions = test_basics.client.cache_evictions().unwrap();
assert_eq!(evictions, Value::Int(2));
assert!(
is_key_cached(cache_id, b"lru_key1", CachedKeyType::String),
"lru_key1 should still be cached (recently accessed)"
);
assert!(
!is_key_cached(cache_id, b"lru_key2", CachedKeyType::String)
&& !is_key_cached(cache_id, b"lru_key3", CachedKeyType::String),
"lru_key2 and lru_key3 should be evicted"
);
assert!(
is_key_cached(cache_id, b"lru_key4", CachedKeyType::String)
&& is_key_cached(cache_id, b"lru_key5", CachedKeyType::String),
"lru_key4 and lru_key5 should be in cache"
);
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_cache_eviction_policy_lfu(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let cache_id = "test_cache_lfu";
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
client_side_cache: Some(ClientSideCache {
cache_id: cache_id.to_string().into(),
max_cache_kb: 1, entry_ttl_ms: 0,
eviction_policy: Some(EvictionPolicy::LFU.into()),
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let value = "x".repeat(250);
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("lfu_key1").arg(&value);
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
for _ in 0..5 {
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("lfu_key1");
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
}
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("lfu_key2").arg(&value);
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
for _ in 0..2 {
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("lfu_key2");
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
}
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("lfu_key3").arg(&value);
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("lfu_key3");
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(3), "Cache should have 3 entries");
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("lfu_key4").arg(&value);
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("lfu_key4");
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(
entry_count,
Value::Int(3),
"Cache should still have 3 entries"
);
let evictions = test_basics.client.cache_evictions().unwrap();
assert_eq!(
evictions,
Value::Int(1),
"One entry should have been evicted"
);
assert!(
is_key_cached(cache_id, b"lfu_key1", CachedKeyType::String)
&& is_key_cached(cache_id, b"lfu_key2", CachedKeyType::String)
&& is_key_cached(cache_id, b"lfu_key4", CachedKeyType::String),
"lfu_key1, lfu_key2 and lfu_key4 should be cached"
);
assert!(
!is_key_cached(cache_id, b"lfu_key3", CachedKeyType::String),
"lfu_key3 should be evicted"
);
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_shared_cache_between_clients(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let shared_cache_id = "shared_test_cache";
let mut test_basics1 = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
client_side_cache: Some(ClientSideCache {
cache_id: shared_cache_id.to_string().into(),
max_cache_kb: 10 * 1024,
entry_ttl_ms: 60_000,
eviction_policy: None,
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut test_basics2 = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
client_side_cache: Some(ClientSideCache {
cache_id: shared_cache_id.to_string().into(),
max_cache_kb: 10 * 1024,
entry_ttl_ms: 60_000,
eviction_policy: None,
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut reset_cmd = redis::Cmd::new();
reset_cmd.arg("CONFIG").arg("RESETSTAT");
test_basics1
.client
.send_command(&mut reset_cmd, None)
.await
.ok();
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("shared_key").arg("shared_value");
test_basics1
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("shared_key");
let result = test_basics1
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(result, Value::BulkString(b"shared_value".to_vec()));
let entry_count = test_basics2.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(1));
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("shared_key");
let result = test_basics2
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(result, Value::BulkString(b"shared_value".to_vec()));
assert_command_count(&mut test_basics1.client, "GET", 1, use_cluster).await;
let hit_rate1 = test_basics1.client.cache_hit_rate().unwrap();
let hit_rate2 = test_basics2.client.cache_hit_rate().unwrap();
let hit_rate1 = match hit_rate1 {
Value::Double(d) => d,
_ => panic!("Expected Value::Double"),
};
let hit_rate2 = match hit_rate2 {
Value::Double(d) => d,
_ => panic!("Expected Value::Double"),
};
assert_eq!(hit_rate1, 0.5);
assert_eq!(hit_rate2, 0.5);
let total_lookups = test_basics1.client.cache_total_lookups().unwrap();
assert_eq!(
total_lookups,
Value::Int(2),
"Expected 2 total lookups on shared cache"
);
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_cache_wrong_key_type_raises_error(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let cache_id = "test_cache_wrong_key_type_raises_error";
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
client_side_cache: Some(ClientSideCache {
cache_id: cache_id.to_string().into(),
max_cache_kb: 1, entry_ttl_ms: 0,
eviction_policy: None,
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("string_key").arg("string_value");
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("string_key");
let result = test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(result, Value::BulkString(b"string_value".to_vec()));
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(1), "Cache should have 1 entry");
let mut hgetall_cmd = redis::Cmd::new();
hgetall_cmd.arg("HGETALL").arg("string_key");
let hgetall_result = test_basics
.client
.send_command(&mut hgetall_cmd, None)
.await;
assert!(
hgetall_result.is_err(),
"HGETALL on string key should error"
);
assert!(
hgetall_result
.unwrap_err()
.to_string()
.contains("WRONGTYPE"),
"Error should indicate WRONGTYPE"
);
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(
entry_count,
Value::Int(1),
"Cache should still have 1 entry"
);
assert_command_count(&mut test_basics.client, "HGETALL", 1, use_cluster).await;
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_cache_cacheable_commands(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let cache_id = "test_cache_cacheable_commands";
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
client_side_cache: Some(ClientSideCache {
cache_id: cache_id.to_string().into(),
max_cache_kb: 1, entry_ttl_ms: 0,
eviction_policy: None,
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut set_cmd = redis::Cmd::new();
set_cmd
.arg("SET")
.arg("cacheable_key")
.arg("cacheable_value");
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("cacheable_key");
let result = test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(result, Value::BulkString(b"cacheable_value".to_vec()));
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(1), "Cache should have 1 entry");
let mut hset_cmd = redis::Cmd::new();
hset_cmd
.arg("HSET")
.arg("hash_key")
.arg("field1")
.arg("value1");
test_basics
.client
.send_command(&mut hset_cmd, None)
.await
.unwrap();
let mut hgetall_cmd = redis::Cmd::new();
hgetall_cmd.arg("HGETALL").arg("hash_key");
let hgetall_result = test_basics
.client
.send_command(&mut hgetall_cmd, None)
.await
.unwrap();
assert_eq!(
hgetall_result,
Value::Map(vec![(
Value::BulkString(b"field1".to_vec()),
Value::BulkString(b"value1".to_vec())
)])
);
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(2), "Cache should have 2 entries");
let mut sadd_cmd = redis::Cmd::new();
sadd_cmd.arg("SADD").arg("set_key").arg("member1");
test_basics
.client
.send_command(&mut sadd_cmd, None)
.await
.unwrap();
let mut smembers_cmd = redis::Cmd::new();
smembers_cmd.arg("SMEMBERS").arg("set_key");
let smembers_result = test_basics
.client
.send_command(&mut smembers_cmd, None)
.await
.unwrap();
assert_eq!(
smembers_result,
Value::Set(vec![Value::BulkString(b"member1".to_vec())])
);
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(entry_count, Value::Int(3), "Cache should have 3 entries");
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_cache_oversized_entry_not_cached(#[values(false, true)] use_cluster: bool) {
block_on_all(async {
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
client_side_cache: Some(ClientSideCache {
cache_id: "test_cache_oversized".to_string().into(),
max_cache_kb: 1, entry_ttl_ms: 0,
eviction_policy: None,
enable_metrics: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut reset_cmd = redis::Cmd::new();
reset_cmd.arg("CONFIG").arg("RESETSTAT");
test_basics
.client
.send_command(&mut reset_cmd, None)
.await
.ok();
let large_value = "x".repeat(2048);
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg("large_key").arg(&large_value);
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg("large_key");
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
let entry_count = test_basics.client.cache_entry_count().unwrap();
assert_eq!(
entry_count,
Value::Int(0),
"Oversized entry should not be cached"
);
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_command_count(&mut test_basics.client, "GET", 2, use_cluster).await;
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_server_assisted_cache_invalidation(#[values(false, true)] use_cluster: bool) {
block_on_all(async move {
let mut cached_client = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: true,
protocol: ProtocolVersion::RESP3,
client_side_cache: Some(ClientSideCache {
cache_id: "server_assisted_invalidation_test".to_string().into(),
max_cache_kb: 1024,
entry_ttl_ms: 0,
eviction_policy: None,
enable_metrics: true,
server_assisted: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let mut writer = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: true,
..Default::default()
},
)
.await;
let key = generate_random_string(10);
let value1 = "original";
let value2 = "updated";
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg(&key).arg(value1);
writer
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg(&key);
let result1 = cached_client
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(
result1,
Value::BulkString(value1.as_bytes().to_vec()),
"first GET should return original value"
);
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg(&key);
let result2 = cached_client
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(
result2,
Value::BulkString(value1.as_bytes().to_vec()),
"second GET should return cached value"
);
let mut set_cmd2 = redis::Cmd::new();
set_cmd2.arg("SET").arg(&key).arg(value2);
writer
.client
.send_command(&mut set_cmd2, None)
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg(&key);
let result3 = cached_client
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
assert_eq!(
result3,
Value::BulkString(value2.as_bytes().to_vec()),
"GET after invalidation should return updated value"
);
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_server_assisted_nil_invalidation_flushes_cache(
#[values(false, true)] use_cluster: bool,
) {
block_on_all(async move {
let mut test_basics = setup_test_basics(
use_cluster,
TestConfiguration {
shared_server: false,
protocol: ProtocolVersion::RESP3,
client_side_cache: Some(ClientSideCache {
cache_id: "nil_invalidation_test".to_string().into(),
max_cache_kb: 1024,
entry_ttl_ms: 0,
eviction_policy: None,
enable_metrics: true,
server_assisted: true,
..Default::default()
}),
..Default::default()
},
)
.await;
let key = generate_random_string(10);
let mut set_cmd = redis::Cmd::new();
set_cmd.arg("SET").arg(&key).arg("value");
test_basics
.client
.send_command(&mut set_cmd, None)
.await
.unwrap();
let mut get_cmd = redis::Cmd::new();
get_cmd.arg("GET").arg(&key);
test_basics
.client
.send_command(&mut get_cmd, None)
.await
.unwrap();
let entry_count_before = test_basics.client.cache_entry_count().unwrap();
assert_eq!(
entry_count_before,
Value::Int(1),
"cache should have entries before flush"
);
let mut flushdb = redis::Cmd::new();
flushdb.arg("FLUSHDB");
let _ = test_basics.client.send_command(&mut flushdb, None).await;
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let entry_count_after = test_basics.client.cache_entry_count().unwrap();
assert_eq!(
entry_count_after,
Value::Int(0),
"cache should be empty after nil invalidation"
);
});
}
#[rstest]
#[serial_test::serial]
#[timeout(SHORT_CLUSTER_TEST_TIMEOUT)]
fn test_server_assisted_requires_resp3(#[values(false, true)] use_cluster: bool) {
block_on_all(async move {
let configuration = TestConfiguration {
shared_server: false,
protocol: ProtocolVersion::RESP2,
client_side_cache: Some(ClientSideCache {
cache_id: "resp2_server_assisted_test".to_string().into(),
max_cache_kb: 1024,
entry_ttl_ms: 0,
eviction_policy: None,
enable_metrics: false,
server_assisted: true,
..Default::default()
}),
..Default::default()
};
let _server;
let addr = if use_cluster {
cluster::get_shared_cluster_addresses(false)
.into_iter()
.next()
.expect("cluster address")
} else {
_server = RedisServer::new(ServerType::Tcp { tls: false });
_server.get_client_addr()
};
let connection_request =
create_connection_request(std::slice::from_ref(&addr), &configuration);
let result = glide_core::client::Client::new(connection_request.into(), None).await;
assert!(
result.is_err(),
"server_assisted=true with RESP2 should fail at connection time"
);
});
}
}