use crate::infrastructure::config::redact_userinfo;
use crate::infrastructure::config::redis::RedisConfig;
use redis::aio::{ConnectionManager, ConnectionManagerConfig};
use redis::{AsyncCommands, Client, FromRedisValue, RedisError, RedisResult, ToSingleRedisArg};
use std::time::Duration;
use tracing::{debug, error, info, instrument};
#[derive(Clone)]
pub struct RedisClient {
manager: ConnectionManager,
config: RedisConfig,
}
impl RedisClient {
#[instrument(skip(config), level = "debug")]
pub async fn new(config: RedisConfig) -> Result<Self, RedisError> {
let url = config.url();
info!("Connecting to Redis at {}", config);
let client = Client::open(url).inspect_err(|e| {
error!(
"Failed to open Redis client: {}",
redact_userinfo(&e.to_string())
);
})?;
let manager_config = ConnectionManagerConfig::new()
.set_response_timeout(Some(Duration::from_secs(config.timeout)))
.set_connection_timeout(Some(Duration::from_secs(config.connect_timeout)));
let manager = ConnectionManager::new_with_config(client, manager_config)
.await
.inspect_err(|e| {
error!(
"Failed to connect to Redis: {}",
redact_userinfo(&e.to_string())
);
})?;
info!("Successfully connected to Redis");
Ok(Self { manager, config })
}
#[instrument(skip(self), level = "debug")]
pub async fn get<T: FromRedisValue>(&self, key: &str) -> RedisResult<Option<T>> {
let mut conn = self.manager.clone();
debug!("Retrieving key '{}' from Redis", key);
let value: Option<T> = conn.get(key).await?;
Ok(value)
}
#[instrument(skip(self, value), level = "debug")]
pub async fn set<T: ToSingleRedisArg + Send + Sync>(
&self,
key: &str,
value: T,
expiry_secs: Option<u64>,
) -> RedisResult<()> {
let mut conn = self.manager.clone();
match expiry_secs {
Some(secs) => {
debug!(
"Setting key '{}' in Redis with expiry of {} seconds",
key, secs
);
conn.set_ex(key, value, secs).await
}
None => {
debug!("Setting key '{}' in Redis without expiry", key);
conn.set(key, value).await
}
}
}
#[instrument(skip(self, value), level = "debug")]
pub async fn set_nx<T: ToSingleRedisArg + Send + Sync>(
&self,
key: &str,
value: T,
expiry_secs: Option<u64>,
) -> RedisResult<bool> {
let mut conn = self.manager.clone();
let outcome: Option<String> = match expiry_secs {
Some(secs) => {
debug!(
"Setting key '{}' in Redis with NX and expiry of {} seconds",
key, secs
);
redis::cmd("SET")
.arg(key)
.arg(value)
.arg("NX")
.arg("EX")
.arg(secs)
.query_async(&mut conn)
.await?
}
None => {
debug!("Setting key '{}' in Redis with NX and no expiry", key);
redis::cmd("SET")
.arg(key)
.arg(value)
.arg("NX")
.query_async(&mut conn)
.await?
}
};
Ok(outcome.is_some())
}
#[instrument(skip(self), level = "debug")]
pub async fn delete(&self, key: &str) -> RedisResult<bool> {
let mut conn = self.manager.clone();
debug!("Deleting key '{}' from Redis", key);
let deleted: usize = conn.del(key).await?;
Ok(deleted > 0)
}
#[instrument(skip(self), level = "debug")]
pub async fn keys(&self, pattern: &str) -> RedisResult<Vec<String>> {
let mut conn = self.manager.clone();
debug!("Getting keys matching pattern '{}' from Redis", pattern);
conn.keys(pattern).await
}
#[instrument(skip(self), level = "debug")]
pub async fn exists(&self, key: &str) -> RedisResult<bool> {
let mut conn = self.manager.clone();
debug!("Checking if key '{}' exists in Redis", key);
conn.exists(key).await
}
pub fn get_config(&self) -> &RedisConfig {
&self.config
}
pub fn connection_manager(&self) -> ConnectionManager {
self.manager.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redis_client_is_clone_send_sync() {
fn assert_clone<T: Clone>() {}
fn assert_send_sync<T: Send + Sync>() {}
assert_clone::<RedisClient>();
assert_send_sync::<RedisClient>();
}
#[allow(dead_code)]
async fn concurrent_ops_compile_check(client: &RedisClient) {
let a = client.get::<String>("session:a");
let b = client.get::<String>("session:b");
let _ = tokio::join!(a, b);
let _ = client.exists("session:a").await;
let _ = client.keys("session:*").await;
let _ = client.delete("session:a").await;
let _ = client.set("session:a", "v".to_string(), None).await;
let _ = client.set_nx("session:b", "v".to_string(), Some(1)).await;
}
}