Skip to main content

nidus_cache/
lib.rs

1#![deny(missing_docs)]
2
3//! Official cache adapter for Nidus applications.
4//!
5//! This crate is installed separately from the core `nidus` facade so cache
6//! backend dependencies are only compiled by applications that choose them.
7
8use nidus_core::NidusError;
9use thiserror::Error;
10
11/// Result type used by cache adapter operations.
12pub type Result<T> = std::result::Result<T, CacheError>;
13
14/// Error returned by cache adapter operations.
15#[derive(Debug, Error)]
16pub enum CacheError {
17    /// Nidus provider registration failed.
18    #[error(transparent)]
19    Nidus(#[from] NidusError),
20}
21
22/// Cache provider configuration shared by cache backends.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct CacheConfig {
25    namespace: Option<String>,
26    time_to_live: Option<std::time::Duration>,
27    max_capacity: Option<u64>,
28}
29
30impl CacheConfig {
31    /// Creates empty cache configuration.
32    pub fn new() -> Self {
33        Self {
34            namespace: None,
35            time_to_live: None,
36            max_capacity: None,
37        }
38    }
39
40    /// Sets the namespace prefix applied to logical cache keys.
41    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
42        self.namespace = Some(namespace.into());
43        self
44    }
45
46    /// Sets the default time to live for cache entries.
47    pub fn time_to_live(mut self, time_to_live: std::time::Duration) -> Self {
48        self.time_to_live = Some(time_to_live);
49        self
50    }
51
52    /// Sets the maximum weighted entry capacity.
53    pub fn max_capacity(mut self, max_capacity: u64) -> Self {
54        self.max_capacity = Some(max_capacity);
55        self
56    }
57
58    /// Returns the configured namespace.
59    pub fn namespace_value(&self) -> Option<&str> {
60        self.namespace.as_deref()
61    }
62
63    /// Returns the configured default time to live.
64    pub fn time_to_live_value(&self) -> Option<std::time::Duration> {
65        self.time_to_live
66    }
67
68    /// Returns the configured maximum capacity.
69    pub fn max_capacity_value(&self) -> Option<u64> {
70        self.max_capacity
71    }
72}
73
74impl Default for CacheConfig {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80/// Namespaced cache key helper.
81#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct CacheKey(String);
83
84impl CacheKey {
85    /// Creates a cache key from optional namespace and logical key parts.
86    pub fn new(namespace: Option<&str>, key: impl AsRef<str>) -> Self {
87        match namespace {
88            Some(namespace) if !namespace.is_empty() => Self::namespaced(namespace, key.as_ref()),
89            _ => Self(key.as_ref().to_owned()),
90        }
91    }
92
93    fn namespaced(namespace: &str, key: &str) -> Self {
94        let mut backend_key = String::with_capacity(namespace.len() + 1 + key.len());
95        backend_key.push_str(namespace);
96        backend_key.push(':');
97        backend_key.push_str(key);
98        Self(backend_key)
99    }
100
101    /// Returns the full backend key.
102    pub fn as_str(&self) -> &str {
103        &self.0
104    }
105
106    /// Consumes the key and returns the full backend key.
107    pub fn into_string(self) -> String {
108        self.0
109    }
110}
111
112#[cfg(feature = "moka")]
113mod moka_backend {
114    #[cfg(feature = "observability")]
115    use std::time::Instant;
116
117    use nidus_core::{Container, ProviderRegistrant, Result as NidusResult};
118
119    use super::{CacheConfig, CacheKey, Result};
120
121    /// Builder for a Moka local in-memory cache provider.
122    #[derive(Clone, Debug, Default)]
123    pub struct MokaCacheBuilder {
124        config: CacheConfig,
125        #[cfg(feature = "observability")]
126        observer: Option<nidus_observability::ObservabilityAdapterObserver>,
127    }
128
129    impl MokaCacheBuilder {
130        /// Creates a Moka cache builder.
131        pub fn new() -> Self {
132            Self::default()
133        }
134
135        /// Replaces the builder config.
136        pub fn config(mut self, config: CacheConfig) -> Self {
137            self.config = config;
138            self
139        }
140
141        /// Sets the namespace prefix applied to logical cache keys.
142        pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
143            self.config = self.config.namespace(namespace);
144            self
145        }
146
147        /// Sets the default time to live for cache entries.
148        pub fn time_to_live(mut self, time_to_live: std::time::Duration) -> Self {
149            self.config = self.config.time_to_live(time_to_live);
150            self
151        }
152
153        /// Sets the maximum weighted entry capacity.
154        pub fn max_capacity(mut self, max_capacity: u64) -> Self {
155            self.config = self.config.max_capacity(max_capacity);
156            self
157        }
158
159        /// Instruments adapter-owned cache operations with Nidus observability.
160        #[cfg(feature = "observability")]
161        pub fn observability(
162            mut self,
163            observer: nidus_observability::ObservabilityAdapterObserver,
164        ) -> Self {
165            self.observer = Some(observer);
166            self
167        }
168
169        /// Builds a Moka cache provider.
170        pub fn build(self) -> MokaCacheProvider {
171            let mut builder = moka::future::Cache::builder();
172            if let Some(time_to_live) = self.config.time_to_live {
173                builder = builder.time_to_live(time_to_live);
174            }
175            if let Some(max_capacity) = self.config.max_capacity {
176                builder = builder.max_capacity(max_capacity);
177            }
178            MokaCacheProvider {
179                namespace: self.config.namespace,
180                cache: builder.build(),
181                #[cfg(feature = "observability")]
182                observer: self.observer,
183            }
184        }
185
186        /// Builds and registers a Moka cache provider as a Nidus singleton.
187        pub fn register(self, container: &mut Container) -> Result<()> {
188            container.register_singleton(self.build())?;
189            Ok(())
190        }
191    }
192
193    /// Nidus provider wrapping a Moka local in-memory cache.
194    #[derive(Clone, Debug)]
195    pub struct MokaCacheProvider {
196        namespace: Option<String>,
197        cache: moka::future::Cache<String, Vec<u8>>,
198        #[cfg(feature = "observability")]
199        observer: Option<nidus_observability::ObservabilityAdapterObserver>,
200    }
201
202    impl MokaCacheProvider {
203        /// Creates a Moka cache provider builder.
204        pub fn builder() -> MokaCacheBuilder {
205            MokaCacheBuilder::new()
206        }
207
208        /// Creates a provider from an existing Moka cache and optional namespace.
209        pub fn from_cache(
210            cache: moka::future::Cache<String, Vec<u8>>,
211            namespace: Option<String>,
212        ) -> Self {
213            Self {
214                namespace,
215                cache,
216                #[cfg(feature = "observability")]
217                observer: None,
218            }
219        }
220
221        /// Inserts a value by logical key.
222        pub async fn insert(&self, key: impl AsRef<str>, value: Vec<u8>) {
223            #[cfg(feature = "observability")]
224            let started_at = Instant::now();
225            self.cache
226                .insert(self.cache_key(key).into_string(), value)
227                .await;
228            #[cfg(feature = "observability")]
229            self.record(
230                "insert",
231                nidus_observability::OperationStatus::Success,
232                started_at,
233            );
234        }
235
236        /// Returns a value by logical key.
237        pub async fn get(&self, key: impl AsRef<str>) -> Option<Vec<u8>> {
238            #[cfg(feature = "observability")]
239            let started_at = Instant::now();
240            let key = key.as_ref();
241            let result = match self.namespace.as_deref() {
242                Some(namespace) if !namespace.is_empty() => {
243                    let key = CacheKey::namespaced(namespace, key);
244                    self.cache.get(key.as_str()).await
245                }
246                _ => self.cache.get(key).await,
247            };
248            #[cfg(feature = "observability")]
249            self.record(
250                "get",
251                nidus_observability::OperationStatus::Success,
252                started_at,
253            );
254            result
255        }
256
257        /// Invalidates a value by logical key.
258        pub async fn invalidate(&self, key: impl AsRef<str>) {
259            #[cfg(feature = "observability")]
260            let started_at = Instant::now();
261            let key = key.as_ref();
262            match self.namespace.as_deref() {
263                Some(namespace) if !namespace.is_empty() => {
264                    let key = CacheKey::namespaced(namespace, key);
265                    self.cache.invalidate(key.as_str()).await;
266                }
267                _ => self.cache.invalidate(key).await,
268            }
269            #[cfg(feature = "observability")]
270            self.record(
271                "invalidate",
272                nidus_observability::OperationStatus::Success,
273                started_at,
274            );
275        }
276
277        /// Returns direct access to the underlying Moka cache.
278        pub fn inner(&self) -> &moka::future::Cache<String, Vec<u8>> {
279            &self.cache
280        }
281
282        /// Returns the namespace used for logical keys.
283        pub fn namespace(&self) -> Option<&str> {
284            self.namespace.as_deref()
285        }
286
287        /// Returns a local health status for this in-memory provider.
288        #[cfg(feature = "health")]
289        pub fn health_status(&self) -> nidus_http::health::HealthStatus {
290            #[cfg(feature = "observability")]
291            let started_at = Instant::now();
292            #[cfg(feature = "observability")]
293            self.record(
294                "health",
295                nidus_observability::OperationStatus::Success,
296                started_at,
297            );
298            nidus_http::health::HealthStatus::up()
299        }
300
301        /// Adds this provider as a readiness check on a health registry.
302        ///
303        /// The provider is expected to be the shared instance resolved from the
304        /// Nidus container, so the method takes `Arc<Self>` and does not clone
305        /// the underlying cache directly.
306        #[cfg(feature = "health")]
307        pub fn register_ready_check(
308            self: std::sync::Arc<Self>,
309            registry: nidus_http::health::HealthRegistry,
310            name: impl Into<String>,
311        ) -> nidus_http::health::HealthRegistry {
312            registry.ready_check_sync(name, move || self.health_status())
313        }
314
315        fn cache_key(&self, key: impl AsRef<str>) -> CacheKey {
316            CacheKey::new(self.namespace.as_deref(), key)
317        }
318
319        #[cfg(feature = "observability")]
320        fn record(
321            &self,
322            operation: &'static str,
323            status: nidus_observability::OperationStatus,
324            started_at: Instant,
325        ) {
326            if let Some(observer) = &self.observer {
327                observer.record("nidus-cache", operation, status, started_at.elapsed());
328            }
329        }
330    }
331
332    impl ProviderRegistrant for MokaCacheProvider {
333        fn register_provider(container: &mut Container) -> NidusResult<()> {
334            container.register_singleton(Self::builder().build())?;
335            Ok(())
336        }
337    }
338}
339
340#[cfg(feature = "moka")]
341pub use moka_backend::{MokaCacheBuilder, MokaCacheProvider};