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}