use std::sync::Arc;
use anyhow::{Context as _, Result};
use derive_builder::Builder;
use serde::Serialize;
use tokio::sync::RwLock;
use swiftide_core::indexing::{Chunk, Node};
mod message_history;
mod node_cache;
mod persist;
#[allow(clippy::type_complexity)]
#[derive(Builder, Clone)]
#[builder(pattern = "owned", setter(strip_option))]
pub struct Redis<T: Chunk = String> {
#[builder(setter(into))]
client: Arc<redis::Client>,
#[builder(default, setter(skip))]
connection_manager: Arc<RwLock<Option<redis::aio::ConnectionManager>>>,
#[builder(default, setter(into))]
cache_key_prefix: Arc<String>,
#[builder(default = "10")]
batch_size: usize,
#[builder(default)]
persist_key_fn: Option<fn(&Node<T>) -> Result<String>>,
#[builder(default)]
persist_value_fn: Option<fn(&Node<T>) -> Result<String>>,
#[builder(default = "message_history".to_string().into(), setter(into))]
message_history_key: Arc<String>,
}
impl Redis<String> {
pub fn try_from_url(url: impl AsRef<str>, prefix: impl AsRef<str>) -> Result<Redis<String>> {
let client = redis::Client::open(url.as_ref()).context("Failed to open redis client")?;
Ok(Redis::<String> {
client: client.into(),
connection_manager: Arc::new(RwLock::new(None)),
cache_key_prefix: prefix.as_ref().to_string().into(),
batch_size: 10,
persist_key_fn: None,
persist_value_fn: None,
message_history_key: format!("{}:message_history", prefix.as_ref()).into(),
})
}
}
impl<T: Chunk> Redis<T> {
pub fn try_build_from_url(url: impl AsRef<str>) -> Result<RedisBuilder<T>> {
Ok(RedisBuilder::default()
.client(redis::Client::open(url.as_ref()).context("Failed to open redis client")?))
}
pub fn builder() -> RedisBuilder<T> {
RedisBuilder::default()
}
pub fn with_message_history_key(&mut self, prefix: impl Into<String>) -> &mut Self {
self.message_history_key = Arc::new(prefix.into());
self
}
async fn lazy_connect(&self) -> Option<redis::aio::ConnectionManager> {
if self.connection_manager.read().await.is_none() {
self.ensure_tls_crypto_provider();
let result = self.client.get_connection_manager().await;
if let Err(e) = result {
tracing::error!("Failed to get connection manager: {}", e);
return None;
}
let mut cm = self.connection_manager.write().await;
*cm = result.ok();
}
self.connection_manager.read().await.clone()
}
fn ensure_tls_crypto_provider(&self) {
let uses_tls = matches!(
self.client.get_connection_info().addr(),
redis::ConnectionAddr::TcpTls { .. }
);
if uses_tls && rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
}
}
fn cache_key_for_node(&self, node: &Node<T>) -> String {
let cache_id = node.parent_id().unwrap_or_else(|| node.id());
format!("{}:{}", self.cache_key_prefix, cache_id)
}
fn cache_key_for_id(&self, id: uuid::Uuid) -> String {
format!("{}:{}", self.cache_key_prefix, id)
}
fn persist_key_for_node(&self, node: &Node<T>) -> Result<String> {
if let Some(key_fn) = self.persist_key_fn {
key_fn(node)
} else {
let hash = node.id();
Ok(format!("{}:{}", node.path.to_string_lossy(), hash))
}
}
#[allow(dead_code)]
async fn reset_cache(&self) {
if let Some(mut cm) = self.lazy_connect().await {
let keys: Vec<String> = redis::cmd("KEYS")
.arg(format!("{}:*", self.cache_key_prefix))
.query_async(&mut cm)
.await
.expect("Could not get keys");
for key in &keys {
let _: usize = redis::cmd("DEL")
.arg(key)
.query_async(&mut cm)
.await
.expect("Failed to reset cache");
}
}
}
#[allow(dead_code)]
async fn get_node(&self, node: &Node<T>) -> Result<Option<String>> {
if let Some(mut cm) = self.lazy_connect().await {
let key = self.persist_key_for_node(node)?;
let result: Option<String> = redis::cmd("GET")
.arg(key)
.query_async(&mut cm)
.await
.context("Error getting from redis")?;
Ok(result)
} else {
anyhow::bail!("Failed to connect to Redis")
}
}
}
impl<T: Chunk + Serialize> Redis<T> {
fn persist_value_for_node(&self, node: &Node<T>) -> Result<String> {
if let Some(value_fn) = self.persist_value_fn {
value_fn(node)
} else {
Ok(serde_json::to_string(node)?)
}
}
}
#[allow(clippy::missing_fields_in_debug)]
impl<T: Chunk> std::fmt::Debug for Redis<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Redis")
.field("client", &self.client)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test(tokio::test)]
async fn connecting_over_tls_works_without_an_installed_crypto_provider() {
let redis = Redis::try_from_url("rediss://localhost:1", "swiftide-test").unwrap();
assert!(redis.lazy_connect().await.is_none());
}
}