sentinel_common/
registry.rs

1//! Generic registry for thread-safe storage of named components.
2//!
3//! This module provides a `Registry<T>` type that wraps the common
4//! `Arc<RwLock<HashMap<String, Arc<T>>>>` pattern used throughout Sentinel.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use sentinel_common::Registry;
10//!
11//! let registry: Registry<MyService> = Registry::new();
12//!
13//! // Insert a component
14//! registry.insert("service-1", Arc::new(MyService::new())).await;
15//!
16//! // Get a component
17//! if let Some(service) = registry.get("service-1").await {
18//!     service.do_something();
19//! }
20//! ```
21
22use std::collections::HashMap;
23use std::sync::Arc;
24use tokio::sync::RwLock;
25
26/// A thread-safe registry for named components.
27///
28/// Provides concurrent read access with exclusive write access,
29/// suitable for storing configuration-driven components that are
30/// read frequently but updated rarely (e.g., during config reload).
31#[derive(Debug)]
32pub struct Registry<T> {
33    items: Arc<RwLock<HashMap<String, Arc<T>>>>,
34}
35
36impl<T> Registry<T> {
37    /// Create a new empty registry.
38    pub fn new() -> Self {
39        Self {
40            items: Arc::new(RwLock::new(HashMap::new())),
41        }
42    }
43
44    /// Create a registry with pre-allocated capacity.
45    pub fn with_capacity(capacity: usize) -> Self {
46        Self {
47            items: Arc::new(RwLock::new(HashMap::with_capacity(capacity))),
48        }
49    }
50
51    /// Create a registry from an existing HashMap.
52    pub fn from_map(map: HashMap<String, Arc<T>>) -> Self {
53        Self {
54            items: Arc::new(RwLock::new(map)),
55        }
56    }
57
58    /// Get a component by ID.
59    pub async fn get(&self, id: &str) -> Option<Arc<T>> {
60        self.items.read().await.get(id).cloned()
61    }
62
63    /// Check if a component exists.
64    pub async fn contains(&self, id: &str) -> bool {
65        self.items.read().await.contains_key(id)
66    }
67
68    /// Insert a component, returning the previous value if any.
69    pub async fn insert(&self, id: impl Into<String>, item: Arc<T>) -> Option<Arc<T>> {
70        self.items.write().await.insert(id.into(), item)
71    }
72
73    /// Remove a component by ID.
74    pub async fn remove(&self, id: &str) -> Option<Arc<T>> {
75        self.items.write().await.remove(id)
76    }
77
78    /// Get all component IDs.
79    pub async fn keys(&self) -> Vec<String> {
80        self.items.read().await.keys().cloned().collect()
81    }
82
83    /// Get the number of components.
84    pub async fn len(&self) -> usize {
85        self.items.read().await.len()
86    }
87
88    /// Check if the registry is empty.
89    pub async fn is_empty(&self) -> bool {
90        self.items.read().await.is_empty()
91    }
92
93    /// Clear all components.
94    pub async fn clear(&self) {
95        self.items.write().await.clear()
96    }
97
98    /// Replace all items atomically, returning the old map.
99    pub async fn replace(&self, new_items: HashMap<String, Arc<T>>) -> HashMap<String, Arc<T>> {
100        let mut guard = self.items.write().await;
101        std::mem::replace(&mut *guard, new_items)
102    }
103
104    /// Get a snapshot of all items.
105    pub async fn snapshot(&self) -> HashMap<String, Arc<T>> {
106        self.items.read().await.clone()
107    }
108
109    /// Execute a function while holding the read lock.
110    ///
111    /// Useful for operations that need to access multiple items atomically.
112    pub async fn with_read<F, R>(&self, f: F) -> R
113    where
114        F: FnOnce(&HashMap<String, Arc<T>>) -> R,
115    {
116        f(&*self.items.read().await)
117    }
118
119    /// Execute a function while holding the write lock.
120    ///
121    /// Useful for operations that need to modify multiple items atomically.
122    pub async fn with_write<F, R>(&self, f: F) -> R
123    where
124        F: FnOnce(&mut HashMap<String, Arc<T>>) -> R,
125    {
126        f(&mut *self.items.write().await)
127    }
128}
129
130impl<T> Default for Registry<T> {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl<T> Clone for Registry<T> {
137    fn clone(&self) -> Self {
138        Self {
139            items: Arc::clone(&self.items),
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[tokio::test]
149    async fn test_registry_basic_operations() {
150        let registry: Registry<String> = Registry::new();
151
152        // Insert
153        registry
154            .insert("key1", Arc::new("value1".to_string()))
155            .await;
156        registry
157            .insert("key2", Arc::new("value2".to_string()))
158            .await;
159
160        // Get
161        assert_eq!(
162            registry.get("key1").await.as_deref().map(|s| s.as_str()),
163            Some("value1")
164        );
165        assert!(registry.get("nonexistent").await.is_none());
166
167        // Contains
168        assert!(registry.contains("key1").await);
169        assert!(!registry.contains("nonexistent").await);
170
171        // Len
172        assert_eq!(registry.len().await, 2);
173
174        // Keys
175        let keys = registry.keys().await;
176        assert!(keys.contains(&"key1".to_string()));
177        assert!(keys.contains(&"key2".to_string()));
178
179        // Remove
180        let removed = registry.remove("key1").await;
181        assert!(removed.is_some());
182        assert!(!registry.contains("key1").await);
183        assert_eq!(registry.len().await, 1);
184
185        // Clear
186        registry.clear().await;
187        assert!(registry.is_empty().await);
188    }
189
190    #[tokio::test]
191    async fn test_registry_replace() {
192        let registry: Registry<i32> = Registry::new();
193        registry.insert("a", Arc::new(1)).await;
194        registry.insert("b", Arc::new(2)).await;
195
196        let mut new_items = HashMap::new();
197        new_items.insert("c".to_string(), Arc::new(3));
198        new_items.insert("d".to_string(), Arc::new(4));
199
200        let old = registry.replace(new_items).await;
201
202        assert!(old.contains_key("a"));
203        assert!(old.contains_key("b"));
204        assert!(!registry.contains("a").await);
205        assert!(registry.contains("c").await);
206        assert!(registry.contains("d").await);
207    }
208
209    #[tokio::test]
210    async fn test_registry_with_read() {
211        let registry: Registry<i32> = Registry::new();
212        registry.insert("a", Arc::new(1)).await;
213        registry.insert("b", Arc::new(2)).await;
214
215        let sum = registry
216            .with_read(|items| items.values().map(|v| **v).sum::<i32>())
217            .await;
218
219        assert_eq!(sum, 3);
220    }
221
222    #[tokio::test]
223    async fn test_registry_clone_shares_data() {
224        let registry1: Registry<String> = Registry::new();
225        let registry2 = registry1.clone();
226
227        registry1.insert("key", Arc::new("value".to_string())).await;
228
229        // Clone should see the same data
230        assert!(registry2.contains("key").await);
231    }
232}