use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use anyhow::Result;
use lightcycle::{ConsistentRing, HasId, HashRing, RendezvousRing};
use redis::{Client, Commands, Connection, SetExpiry, SetOptions};
const HEALTH_CHECK_INTERVAL_SEC: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct CacheNode {
id: String,
uri: String,
capacity_gb: usize, healthy: Arc<Mutex<bool>>,
last_health_check: Arc<Mutex<Instant>>,
connection_count: Arc<Mutex<usize>>,
client: Arc<Mutex<Client>>,
}
#[allow(dead_code)]
impl CacheNode {
fn new(host: impl Into<String>, port: u16, db: u8, capacity_gb: usize) -> Result<Self> {
let host = host.into();
let id = format!("valkey://{host}:{port}/{db} ({capacity_gb}GB)");
let uri = format!("valkey://{host}:{port}/{db}");
let client = Client::open(uri.as_str())?;
Ok(Self {
id: id.clone(),
uri,
capacity_gb,
healthy: Arc::new(Mutex::new(true)),
last_health_check: Arc::new(Mutex::new(Instant::now())),
connection_count: Arc::new(Mutex::new(0)),
client: Arc::new(Mutex::new(client)),
})
}
fn connection(&self) -> Result<Connection> {
let guard = match self.client.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
Ok(guard.get_connection()?) }
fn incr_count(&self) {
let mut count = match self.connection_count.lock() {
Ok(v) => v,
Err(poisoned) => poisoned.into_inner(),
};
*count += 1;
}
fn check_health(&self) -> Result<bool> {
let mut last_check = self
.last_health_check
.lock()
.expect("poisoned mutex on last health check ts");
if last_check.elapsed() > HEALTH_CHECK_INTERVAL_SEC {
let mut conn = self.connection()?;
let is_healthy = match conn.ping::<String>() {
Ok(_v) => true,
Err(_) => false,
};
*last_check = Instant::now();
*self.healthy.lock().expect("poisoned mutex on the healthy field") = is_healthy;
}
Ok(*self.healthy.lock().expect("poisoned mutex on the healthy field"))
}
fn get(&self, key: &str) -> Option<String> {
if !self.check_health().ok()? {
return None;
}
self.incr_count();
let mut conn = self.connection().ok()?;
let value = conn.get::<&str, String>(key).ok()?;
Some(value)
}
fn set(&self, key: &str, value: &str, ttl: Duration) -> Result<bool> {
if !self.check_health()? {
return Ok(false);
}
let mut conn = self.connection()?;
let expiration = SetExpiry::EX(ttl.as_secs());
let options = SetOptions::default().with_expiration(expiration);
let was_set = conn.set_options(key, value, options)?;
Ok(was_set)
}
fn delete(&self, key: &str) -> Result<bool> {
if !self.check_health()? {
return Ok(false);
}
self.incr_count();
let mut conn = self.connection()?;
let count = conn.del::<&str, usize>(key)?;
Ok(count > 0)
}
fn stats(&self) -> CacheStats {
CacheStats {
id: self.id.clone(),
healthy: *self.healthy.lock().expect("poisoned mutex on health status field"),
connection_count: *self
.connection_count
.lock()
.expect("poisoned mutex on connection count"),
}
}
}
impl HasId for CacheNode {
fn id(&self) -> &str {
&self.id
}
}
#[derive(Debug)]
#[allow(dead_code)]
struct CacheStats {
id: String,
healthy: bool,
connection_count: usize,
}
#[allow(dead_code)]
struct DistributedCache {
ring: Arc<Mutex<ConsistentRing>>,
caches: Vec<CacheNode>,
}
#[allow(dead_code)]
impl DistributedCache {
fn new(caches: Vec<CacheNode>) -> Self {
let mut ring = ConsistentRing::new_with_replica_count(150);
for cache in &caches {
ring.add(Box::new(cache.clone()));
}
Self {
ring: Arc::new(Mutex::new(ring)),
caches,
}
}
fn get(&self, key: &str) -> Option<String> {
let ring = self.ring.lock().ok()?;
if let Some(cache_id_box) = ring.locate(key) {
let cache_id = cache_id_box.id();
for cache in &self.caches {
if cache.id() == cache_id {
return cache.get(key);
}
}
}
for cache in &self.caches {
if let Some(value) = cache.get(key) {
println!(" Found in fallback cache: {}", cache.id);
return Some(value);
}
}
None
}
fn set(&self, key: &str, value: &str, ttl: Duration) -> Result<bool> {
let ring = self.ring.lock().expect("poisoned mutex");
if let Some(cache_id_box) = ring.locate(key) {
let cache_id = cache_id_box.id();
for cache in &self.caches {
if cache.id() == cache_id {
return cache.set(key, value, ttl);
}
}
}
Ok(false)
}
fn delete(&self, key: &str) -> Result<bool> {
let ring = self.ring.lock().expect("poisoned mutex");
if let Some(cache_id_box) = ring.locate(key) {
let cache_id = cache_id_box.id();
for cache in &self.caches {
if cache.id() == cache_id {
return cache.delete(key);
}
}
}
Ok(false)
}
fn rebalance(&mut self) -> Result<()> {
println!("\n=== Rebalancing ring based on health ===");
let mut ring = self.ring.lock().expect("poisoned mutex");
for cache in &self.caches {
if !cache.check_health()? {
println!("Removing unhealthy cache: {}", cache.id);
ring.remove(cache.id()).ok();
}
}
for cache in &self.caches {
if cache.check_health()? && ring.locate(cache.id()).is_none() {
println!("Re-adding healthy cache: {}", cache.id);
ring.add(Box::new(cache.clone()));
}
}
Ok(())
}
fn stats(&self) -> Vec<CacheStats> {
self.caches.iter().map(|c| c.stats()).collect()
}
}
fn main() -> Result<()> {
println!("=== ConsistentRing vs RendezvousRing Comparison ===\n");
let caches = vec![
CacheNode::new("localhost", 6379, 0, 1)?, CacheNode::new("localhost", 6380, 0, 2)?, CacheNode::new("localhost", 6381, 0, 4)?, CacheNode::new("localhost", 6382, 0, 8)?, ];
demonstrate_consistent_ring(&caches);
println!();
demonstrate_rendezvous_ring(&caches);
println!();
compare_distributions(&caches);
Ok(())
}
fn demonstrate_consistent_ring(caches: &[CacheNode]) {
println!("=== ConsistentRing (Traditional Consistent Hashing) ===");
let mut consistent_ring = ConsistentRing::new_with_replica_count(150);
for cache in caches {
consistent_ring.add(Box::new(cache.clone()));
}
println!("✓ Even distribution regardless of cache size");
let mut distribution = HashMap::new();
for i in 0..1000 {
let key = format!("key-{}", i);
if let Some(cache) = consistent_ring.locate(&key) {
*distribution.entry(cache.id().to_string()).or_insert(0) += 1;
}
}
for (cache_id, count) in &distribution {
let percentage = (*count as f64 / 10.0).round();
println!(" {}: {} keys (~{}%)", cache_id, count, percentage);
}
}
fn demonstrate_rendezvous_ring(caches: &[CacheNode]) {
println!("=== RendezvousRing (Weighted Rendezvous Hashing) ===");
let mut rendezvous_ring = RendezvousRing::new();
for cache in caches {
rendezvous_ring.add_weighted(Box::new(cache.clone()), cache.capacity_gb as f64);
}
println!("✓ Weighted distribution based on cache capacity");
let mut distribution = HashMap::new();
for i in 0..1000 {
let key = format!("key-{}", i);
if let Some(cache) = rendezvous_ring.locate(&key) {
*distribution.entry(cache.id().to_string()).or_insert(0) += 1;
}
}
for (cache_id, count) in &distribution {
let percentage = (*count as f64 / 10.0).round();
println!(" {}: {} keys (~{}%)", cache_id, count, percentage);
}
}
fn compare_distributions(caches: &[CacheNode]) {
println!("=== Algorithm Comparison ===");
let mut consistent_ring = ConsistentRing::new_with_replica_count(100);
let mut rendezvous_ring = RendezvousRing::new();
for cache in caches {
consistent_ring.add(Box::new(cache.clone()));
rendezvous_ring.add_weighted(Box::new(cache.clone()), cache.capacity_gb as f64);
}
println!("Memory usage:");
println!(" ConsistentRing entries: {} (with replicas)", consistent_ring.len());
println!(" RendezvousRing entries: {} (no replicas)", rendezvous_ring.len());
println!("\nCapabilities:");
println!(" ConsistentRing: ✓ Fast lookup, ✗ No weighting");
println!(" RendezvousRing: ✓ Weighted nodes, ✓ Memory efficient, ✓ Dynamic weights");
let test_key = "test:generic:key";
if let Some(node) = consistent_ring.locate(test_key) {
println!(" ConsistentRing: '{}' -> {}", test_key, node.id());
}
if let Some(node) = rendezvous_ring.locate(test_key) {
println!(" RendezvousRing: '{}' -> {}", test_key, node.id());
}
}