ferron_common/util/
module_cache.rs

1use std::hash::Hasher;
2use std::sync::Arc;
3use std::{collections::HashMap, error::Error};
4
5use crate::config::{ServerConfiguration, ServerConfigurationEntries};
6
7/// A highly optimized cache that stores modules according to server configuration
8pub struct ModuleCache<T> {
9  // Use a single HashMap for O(1) average-case lookups
10  inner: HashMap<CacheKey, Arc<T>>,
11  properties: Box<[&'static str]>, // Box<[T]> is more memory efficient than Vec<T>
12}
13
14// Optimized cache key that implements fast hashing and comparison
15#[derive(Clone, PartialEq, Eq)]
16struct CacheKey {
17  // Pre-sorted entries for consistent hashing
18  entries: Box<[(String, Option<ServerConfigurationEntries>)]>,
19}
20
21impl std::hash::Hash for CacheKey {
22  fn hash<H: Hasher>(&self, state: &mut H) {
23    self.entries.hash(state);
24  }
25}
26
27impl CacheKey {
28  fn new(config: &ServerConfiguration, properties: &[&'static str]) -> Self {
29    let mut entries: Vec<_> = properties
30      .iter()
31      .map(|&prop| (prop.to_string(), config.entries.get(prop).cloned()))
32      .collect();
33
34    // Sort for consistent cache keys regardless of property order
35    entries.sort_by(|a, b| a.0.cmp(&b.0));
36
37    Self {
38      entries: entries.into_boxed_slice(),
39    }
40  }
41}
42
43#[allow(dead_code)]
44impl<T> ModuleCache<T> {
45  /// Creates a cache that stores modules per specific properties
46  pub fn new(properties: Vec<&'static str>) -> Self {
47    Self {
48      inner: HashMap::with_capacity(16), // Pre-allocate reasonable capacity
49      properties: properties.into_boxed_slice(),
50    }
51  }
52
53  /// Creates a cache with custom initial capacity
54  pub fn with_capacity(properties: Vec<&'static str>, capacity: usize) -> Self {
55    Self {
56      inner: HashMap::with_capacity(capacity),
57      properties: properties.into_boxed_slice(),
58    }
59  }
60
61  /// Obtains a module from cache, initializing if not present
62  /// This is now O(1) average case instead of O(n)
63  pub fn get_or_init<F, E>(&mut self, config: &ServerConfiguration, init_fn: F) -> Result<Arc<T>, E>
64  where
65    F: FnOnce(&ServerConfiguration) -> Result<Arc<T>, E>,
66    E: From<Box<dyn Error + Send + Sync>>,
67  {
68    let cache_key = CacheKey::new(config, &self.properties);
69
70    // Fast path: check if already cached
71    if let Some(cached_value) = self.inner.get(&cache_key) {
72      return Ok(cached_value.clone());
73    }
74
75    // Slow path: initialize and cache
76    let new_value = init_fn(config)?;
77    self.inner.insert(cache_key, new_value.clone());
78    Ok(new_value)
79  }
80
81  /// Non-mutable variant that only retrieves from cache
82  pub fn get(&self, config: &ServerConfiguration) -> Option<Arc<T>> {
83    let cache_key = CacheKey::new(config, &self.properties);
84    self.inner.get(&cache_key).cloned()
85  }
86
87  /// Clear the cache
88  pub fn clear(&mut self) {
89    self.inner.clear();
90  }
91
92  /// Get current cache size
93  pub fn len(&self) -> usize {
94    self.inner.len()
95  }
96
97  /// Check if cache is empty
98  pub fn is_empty(&self) -> bool {
99    self.inner.is_empty()
100  }
101
102  /// Reserve capacity for additional entries
103  pub fn reserve(&mut self, additional: usize) {
104    self.inner.reserve(additional);
105  }
106
107  /// Gets a module from cache, or creates one with the fallback function without caching
108  pub fn get_or<F, E>(&self, config: &ServerConfiguration, fallback_fn: F) -> Result<Arc<T>, E>
109  where
110    F: FnOnce(&ServerConfiguration) -> Result<Arc<T>, E>,
111  {
112    let cache_key = CacheKey::new(config, &self.properties);
113
114    // Check if already cached
115    if let Some(cached_value) = self.inner.get(&cache_key) {
116      return Ok(cached_value.clone());
117    }
118
119    // Not cached, use fallback function (but don't cache the result)
120    fallback_fn(config)
121  }
122}
123
124// Implement Default for convenience
125impl<T> Default for ModuleCache<T> {
126  fn default() -> Self {
127    Self::new(Vec::new())
128  }
129}
130
131#[cfg(test)]
132mod test {
133  use crate::{
134    config::{ServerConfigurationEntry, ServerConfigurationFilters, ServerConfigurationValue},
135    observability::ObservabilityBackendChannels,
136  };
137
138  use super::*;
139
140  #[test]
141  fn module_loading_test() {
142    let module = 1;
143
144    let cache = ModuleCache::new(vec!["property"]);
145
146    let mut config_entries = HashMap::new();
147    config_entries.insert(
148      "property".to_string(),
149      ServerConfigurationEntries {
150        inner: vec![ServerConfigurationEntry {
151          values: vec![ServerConfigurationValue::String("something".to_string())],
152          props: HashMap::new(),
153        }],
154      },
155    );
156    let config = ServerConfiguration {
157      entries: config_entries,
158      filters: ServerConfigurationFilters {
159        is_host: true,
160        hostname: None,
161        ip: None,
162        port: None,
163        condition: None,
164        error_handler_status: None,
165      },
166      modules: vec![],
167      observability: ObservabilityBackendChannels::new(),
168    };
169
170    let mut config2_entries = HashMap::new();
171    config2_entries.insert(
172      "property".to_string(),
173      ServerConfigurationEntries {
174        inner: vec![ServerConfigurationEntry {
175          values: vec![ServerConfigurationValue::String("something".to_string())],
176          props: HashMap::new(),
177        }],
178      },
179    );
180    config2_entries.insert(
181      "ignore".to_string(),
182      ServerConfigurationEntries {
183        inner: vec![ServerConfigurationEntry {
184          values: vec![ServerConfigurationValue::String("something else".to_string())],
185          props: HashMap::new(),
186        }],
187      },
188    );
189    let config2 = ServerConfiguration {
190      entries: config2_entries,
191      filters: ServerConfigurationFilters {
192        is_host: true,
193        hostname: None,
194        ip: None,
195        port: Some(80),
196        condition: None,
197        error_handler_status: None,
198      },
199      modules: vec![],
200      observability: ObservabilityBackendChannels::new(),
201    };
202
203    assert_eq!(
204      cache
205        .get_or::<_, Box<dyn std::error::Error + Send + Sync>>(&config, |_config| Ok(Arc::new(module)))
206        .unwrap(),
207      Arc::new(module)
208    );
209
210    assert_eq!(
211      cache
212        .get_or::<_, Box<dyn std::error::Error + Send + Sync>>(&config2, |_config| Ok(Arc::new(module)))
213        .unwrap(),
214      Arc::new(module)
215    );
216  }
217
218  #[test]
219  fn should_cache_the_module() {
220    let module = 1;
221    let module2 = 2;
222
223    let mut cache = ModuleCache::new(vec!["property"]);
224
225    let mut config_entries = HashMap::new();
226    config_entries.insert(
227      "property".to_string(),
228      ServerConfigurationEntries {
229        inner: vec![ServerConfigurationEntry {
230          values: vec![ServerConfigurationValue::String("something".to_string())],
231          props: HashMap::new(),
232        }],
233      },
234    );
235    let config = ServerConfiguration {
236      entries: config_entries,
237      filters: ServerConfigurationFilters {
238        is_host: true,
239        hostname: None,
240        ip: None,
241        port: None,
242        condition: None,
243        error_handler_status: None,
244      },
245      modules: vec![],
246      observability: ObservabilityBackendChannels::new(),
247    };
248
249    let mut config2_entries = HashMap::new();
250    config2_entries.insert(
251      "property".to_string(),
252      ServerConfigurationEntries {
253        inner: vec![ServerConfigurationEntry {
254          values: vec![ServerConfigurationValue::String("something".to_string())],
255          props: HashMap::new(),
256        }],
257      },
258    );
259    config2_entries.insert(
260      "ignore".to_string(),
261      ServerConfigurationEntries {
262        inner: vec![ServerConfigurationEntry {
263          values: vec![ServerConfigurationValue::String("something else".to_string())],
264          props: HashMap::new(),
265        }],
266      },
267    );
268    let config2 = ServerConfiguration {
269      entries: config2_entries,
270      filters: ServerConfigurationFilters {
271        is_host: true,
272        hostname: None,
273        ip: None,
274        port: Some(80),
275        condition: None,
276        error_handler_status: None,
277      },
278      modules: vec![],
279      observability: ObservabilityBackendChannels::new(),
280    };
281
282    // Should initialize a module
283    assert_eq!(
284      cache
285        .get_or_init::<_, Box<dyn std::error::Error + Send + Sync>>(&config, |_config| Ok(Arc::new(module)))
286        .unwrap(),
287      Arc::new(module)
288    );
289
290    // Should obtain cached module (not initialize module2)
291    assert_eq!(
292      cache
293        .get_or_init::<_, Box<dyn std::error::Error + Send + Sync>>(&config2, |_config| Ok(Arc::new(module2)))
294        .unwrap(),
295      Arc::new(module)
296    );
297  }
298
299  #[test]
300  fn test_cache_operations() {
301    let mut cache = ModuleCache::with_capacity(vec!["test_prop"], 10);
302
303    let config = ServerConfiguration {
304      entries: HashMap::new(),
305      filters: ServerConfigurationFilters {
306        is_host: true,
307        hostname: None,
308        ip: None,
309        port: None,
310        condition: None,
311        error_handler_status: None,
312      },
313      modules: vec![],
314      observability: ObservabilityBackendChannels::new(),
315    };
316
317    assert!(cache.is_empty());
318    assert_eq!(cache.len(), 0);
319
320    // Use Box<dyn Error + Send + Sync> directly
321    let value = cache
322      .get_or_init::<_, Box<dyn std::error::Error + Send + Sync>>(&config, |_| Ok(Arc::new(42)))
323      .unwrap();
324    assert_eq!(*value, 42);
325    assert_eq!(cache.len(), 1);
326
327    // Test direct get
328    let cached = cache.get(&config).unwrap();
329    assert_eq!(*cached, 42);
330
331    cache.clear();
332    assert!(cache.is_empty());
333  }
334}