swiftide_integrations/redis/
mod.rs

1//! This module provides the integration with Redis for caching nodes in the Swiftide system.
2//!
3//! The primary component of this module is the `Redis`, which is re-exported for use
4//! in other parts of the system. The `Redis` struct is responsible for managing and
5//! caching nodes during the indexing process, leveraging Redis for efficient storage and retrieval.
6//!
7//! # Overview
8//!
9//! The `Redis` struct provides methods for:
10//! - Connecting to a Redis database
11//! - Checking if a node is cached
12//! - Setting a node in the cache
13//! - Resetting the cache (primarily for testing purposes)
14//!
15//! This integration is essential for ensuring efficient node management and caching in the Swiftide
16//! system.
17
18use anyhow::{Context as _, Result};
19use derive_builder::Builder;
20use tokio::sync::RwLock;
21
22use swiftide_core::indexing::Node;
23
24mod node_cache;
25mod persist;
26
27/// `Redis` provides a caching mechanism for nodes using Redis.
28/// It helps in optimizing the indexing process by skipping nodes that have already been processed.
29///
30/// # Fields
31///
32/// * `client` - The Redis client used to interact with the Redis server.
33/// * `connection_manager` - Manages the Redis connections asynchronously.
34/// * `key_prefix` - A prefix used for keys stored in Redis to avoid collisions.
35#[derive(Builder)]
36#[builder(pattern = "owned", setter(strip_option))]
37pub struct Redis {
38    client: redis::Client,
39    #[builder(default, setter(skip))]
40    connection_manager: RwLock<Option<redis::aio::ConnectionManager>>,
41    #[builder(default)]
42    cache_key_prefix: String,
43    #[builder(default = "10")]
44    /// The batch size used for persisting nodes. Defaults to a safe 10.
45    batch_size: usize,
46    #[builder(default)]
47    /// Customize the key used for persisting nodes
48    persist_key_fn: Option<fn(&Node) -> Result<String>>,
49    #[builder(default)]
50    /// Customize the value used for persisting nodes
51    persist_value_fn: Option<fn(&Node) -> Result<String>>,
52}
53
54impl Redis {
55    /// Creates a new `Redis` instance from a given Redis URL and key prefix.
56    ///
57    /// # Parameters
58    ///
59    /// * `url` - The URL of the Redis server.
60    /// * `prefix` - The prefix to be used for keys stored in Redis.
61    ///
62    /// # Returns
63    ///
64    /// A `Result` containing the `Redis` instance or an error if the client could not be created.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the Redis client cannot be opened.
69    pub fn try_from_url(url: impl AsRef<str>, prefix: impl AsRef<str>) -> Result<Self> {
70        let client = redis::Client::open(url.as_ref()).context("Failed to open redis client")?;
71        Ok(Self {
72            client,
73            connection_manager: RwLock::new(None),
74            cache_key_prefix: prefix.as_ref().to_string(),
75            batch_size: 10,
76            persist_key_fn: None,
77            persist_value_fn: None,
78        })
79    }
80
81    /// # Errors
82    ///
83    /// Returns an error if the Redis client cannot be opened
84    pub fn try_build_from_url(url: impl AsRef<str>) -> Result<RedisBuilder> {
85        Ok(RedisBuilder::default()
86            .client(redis::Client::open(url.as_ref()).context("Failed to open redis client")?))
87    }
88
89    /// Builds a new `Redis` instance from the builder.
90    pub fn builder() -> RedisBuilder {
91        RedisBuilder::default()
92    }
93
94    /// Lazily connects to the Redis server and returns the connection manager.
95    ///
96    /// # Returns
97    ///
98    /// An `Option` containing the `ConnectionManager` if the connection is successful, or `None` if
99    /// it fails.
100    ///
101    /// # Errors
102    ///
103    /// Logs an error and returns `None` if the connection manager cannot be obtained.
104    async fn lazy_connect(&self) -> Option<redis::aio::ConnectionManager> {
105        if self.connection_manager.read().await.is_none() {
106            let result = self.client.get_connection_manager().await;
107            if let Err(e) = result {
108                tracing::error!("Failed to get connection manager: {}", e);
109                return None;
110            }
111            let mut cm = self.connection_manager.write().await;
112            *cm = result.ok();
113        }
114
115        self.connection_manager.read().await.clone()
116    }
117
118    /// Generates a Redis key for a given node using the key prefix and the node's hash.
119    ///
120    /// # Parameters
121    ///
122    /// * `node` - The node for which the key is to be generated.
123    ///
124    /// # Returns
125    ///
126    /// A `String` representing the Redis key for the node.
127    fn cache_key_for_node(&self, node: &Node) -> String {
128        format!("{}:{}", self.cache_key_prefix, node.id())
129    }
130
131    /// Generates a key for a given node to be persisted in Redis.
132    fn persist_key_for_node(&self, node: &Node) -> Result<String> {
133        if let Some(key_fn) = self.persist_key_fn {
134            key_fn(node)
135        } else {
136            let hash = node.id();
137            Ok(format!("{}:{}", node.path.to_string_lossy(), hash))
138        }
139    }
140
141    /// Generates a value for a given node to be persisted in Redis.
142    /// By default, the node is serialized as JSON.
143    /// If a custom function is provided, it is used to generate the value.
144    /// Otherwise, the node is serialized as JSON.
145    fn persist_value_for_node(&self, node: &Node) -> Result<String> {
146        if let Some(value_fn) = self.persist_value_fn {
147            value_fn(node)
148        } else {
149            Ok(serde_json::to_string(node)?)
150        }
151    }
152
153    /// Resets the cache by deleting all keys with the specified prefix.
154    /// This function is intended for testing purposes and is inefficient for production use.
155    ///
156    /// # Errors
157    ///
158    /// Panics if the keys cannot be retrieved or deleted.
159    #[allow(dead_code)]
160    async fn reset_cache(&self) {
161        if let Some(mut cm) = self.lazy_connect().await {
162            let keys: Vec<String> = redis::cmd("KEYS")
163                .arg(format!("{}:*", self.cache_key_prefix))
164                .query_async(&mut cm)
165                .await
166                .expect("Could not get keys");
167
168            for key in &keys {
169                let _: usize = redis::cmd("DEL")
170                    .arg(key)
171                    .query_async(&mut cm)
172                    .await
173                    .expect("Failed to reset cache");
174            }
175        }
176    }
177
178    /// Gets a node persisted in Redis using the GET command
179    /// Takes a node and returns a Result<Option<String>>
180    #[allow(dead_code)]
181    async fn get_node(&self, node: &Node) -> Result<Option<String>> {
182        if let Some(mut cm) = self.lazy_connect().await {
183            let key = self.persist_key_for_node(node)?;
184            let result: Option<String> = redis::cmd("GET")
185                .arg(key)
186                .query_async(&mut cm)
187                .await
188                .context("Error getting from redis")?;
189            Ok(result)
190        } else {
191            anyhow::bail!("Failed to connect to Redis")
192        }
193    }
194}
195
196// Redis CM does not implement debug
197#[allow(clippy::missing_fields_in_debug)]
198impl std::fmt::Debug for Redis {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        f.debug_struct("Redis")
201            .field("client", &self.client)
202            .finish()
203    }
204}
205
206impl Clone for Redis {
207    fn clone(&self) -> Self {
208        Self {
209            client: self.client.clone(),
210            connection_manager: RwLock::new(None),
211            cache_key_prefix: self.cache_key_prefix.clone(),
212            batch_size: self.batch_size,
213            persist_key_fn: self.persist_key_fn,
214            persist_value_fn: self.persist_value_fn,
215        }
216    }
217}