use async_trait::async_trait;
use deadpool_redis::cluster::Config;
use deadpool_redis::{Runtime, Timeouts};
use crate::CacheError;
use crate::redis_cache::{RedisOptions, RedisPoolStatus, conn_err, to_pool_status};
use crate::redis_ha::{HaCacheBackend, impl_cache_for_ha};
pub struct RedisClusterCache {
backend: ClusterBackend,
}
struct ClusterBackend {
pool: deadpool_redis::cluster::Pool,
}
#[async_trait]
impl HaCacheBackend for ClusterBackend {
type Conn = deadpool_redis::cluster::Connection;
async fn checkout(&self) -> Result<Self::Conn, CacheError> {
self.pool.get().await.map_err(conn_err)
}
fn status(&self) -> deadpool_redis::Status {
self.pool.status()
}
}
impl RedisClusterCache {
pub async fn new(urls: &[&str], max_size: usize) -> Result<Self, CacheError> {
Self::with_options(urls, RedisOptions { max_size, ..RedisOptions::default() }).await
}
pub async fn with_options(urls: &[&str], options: RedisOptions) -> Result<Self, CacheError> {
let pool = connect_pool(urls, options.max_size).await?;
Ok(Self { backend: ClusterBackend { pool } })
}
pub fn status(&self) -> deadpool_redis::Status {
self.backend.status()
}
pub fn pool_status(&self) -> RedisPoolStatus {
to_pool_status(self.status())
}
}
async fn connect_pool(
urls: &[&str],
max_size: usize,
) -> Result<deadpool_redis::cluster::Pool, CacheError> {
let urls = urls.iter().map(|url| url.to_string()).collect::<Vec<_>>();
let pool = Config::from_urls(urls)
.builder()
.map_err(conn_err)?
.max_size(max_size.max(1))
.timeouts(Timeouts {
wait: Some(std::time::Duration::from_secs(30)),
create: Some(std::time::Duration::from_secs(5)),
recycle: Some(std::time::Duration::from_secs(5)),
})
.runtime(Runtime::Tokio1)
.build()
.map_err(conn_err)?;
let mut conn = pool.get().await.map_err(conn_err)?;
let ping = redis::cmd("PING");
let reply = ping.query_async::<String>(&mut conn);
tokio::time::timeout(std::time::Duration::from_secs(30), reply)
.await
.map_err(|_| {
CacheError::ConnectionError(
"cluster: PING timed out after 30s during construction".into(),
)
})?
.map_err(conn_err)?;
Ok(pool)
}
impl_cache_for_ha!(RedisClusterCache);
#[cfg(test)]
mod tests {
use super::*;
fn config() -> Config {
Config::from_urls(vec!["redis://127.0.0.1:7000".to_string()])
}
#[test]
fn config_keeps_the_seed_urls() {
let config = config();
assert_eq!(config.urls.as_deref(), Some(&["redis://127.0.0.1:7000".to_string()][..]));
assert!(!config.read_from_replicas);
}
#[test]
fn pool_status_reports_the_configured_size_before_any_connection() {
let pool =
config().builder().unwrap().max_size(3).runtime(Runtime::Tokio1).build().unwrap();
let cache = RedisClusterCache { backend: ClusterBackend { pool } };
let status = cache.pool_status();
assert_eq!(status.max_size, 3);
assert_eq!(status.size, 0);
assert_eq!(status.available, 0);
}
#[tokio::test]
async fn an_empty_seed_list_is_a_connection_error() {
match RedisClusterCache::new(&[], 1).await {
Err(CacheError::ConnectionError(_)) => {}
Err(other) => panic!("expected a connection error, got {other:?}"),
Ok(_) => panic!("an empty seed list must not build"),
}
}
}