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() => {
89                Self(format!("{namespace}:{}", key.as_ref()))
90            }
91            _ => Self(key.as_ref().to_owned()),
92        }
93    }
94
95    /// Returns the full backend key.
96    pub fn as_str(&self) -> &str {
97        &self.0
98    }
99
100    /// Consumes the key and returns the full backend key.
101    pub fn into_string(self) -> String {
102        self.0
103    }
104}
105
106#[cfg(feature = "moka")]
107mod moka_backend {
108    #[cfg(feature = "observability")]
109    use std::time::Instant;
110
111    use nidus_core::{Container, ProviderRegistrant, Result as NidusResult};
112
113    use super::{CacheConfig, CacheKey, Result};
114
115    /// Builder for a Moka local in-memory cache provider.
116    #[derive(Clone, Debug, Default)]
117    pub struct MokaCacheBuilder {
118        config: CacheConfig,
119        #[cfg(feature = "observability")]
120        observer: Option<nidus_observability::ObservabilityAdapterObserver>,
121    }
122
123    impl MokaCacheBuilder {
124        /// Creates a Moka cache builder.
125        pub fn new() -> Self {
126            Self::default()
127        }
128
129        /// Replaces the builder config.
130        pub fn config(mut self, config: CacheConfig) -> Self {
131            self.config = config;
132            self
133        }
134
135        /// Sets the namespace prefix applied to logical cache keys.
136        pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
137            self.config = self.config.namespace(namespace);
138            self
139        }
140
141        /// Sets the default time to live for cache entries.
142        pub fn time_to_live(mut self, time_to_live: std::time::Duration) -> Self {
143            self.config = self.config.time_to_live(time_to_live);
144            self
145        }
146
147        /// Sets the maximum weighted entry capacity.
148        pub fn max_capacity(mut self, max_capacity: u64) -> Self {
149            self.config = self.config.max_capacity(max_capacity);
150            self
151        }
152
153        /// Instruments adapter-owned cache operations with Nidus observability.
154        #[cfg(feature = "observability")]
155        pub fn observability(
156            mut self,
157            observer: nidus_observability::ObservabilityAdapterObserver,
158        ) -> Self {
159            self.observer = Some(observer);
160            self
161        }
162
163        /// Builds a Moka cache provider.
164        pub fn build(self) -> MokaCacheProvider {
165            let mut builder = moka::future::Cache::builder();
166            if let Some(time_to_live) = self.config.time_to_live {
167                builder = builder.time_to_live(time_to_live);
168            }
169            if let Some(max_capacity) = self.config.max_capacity {
170                builder = builder.max_capacity(max_capacity);
171            }
172            MokaCacheProvider {
173                namespace: self.config.namespace,
174                cache: builder.build(),
175                #[cfg(feature = "observability")]
176                observer: self.observer,
177            }
178        }
179
180        /// Builds and registers a Moka cache provider as a Nidus singleton.
181        pub fn register(self, container: &mut Container) -> Result<()> {
182            container.register_singleton(self.build())?;
183            Ok(())
184        }
185    }
186
187    /// Nidus provider wrapping a Moka local in-memory cache.
188    #[derive(Clone, Debug)]
189    pub struct MokaCacheProvider {
190        namespace: Option<String>,
191        cache: moka::future::Cache<String, Vec<u8>>,
192        #[cfg(feature = "observability")]
193        observer: Option<nidus_observability::ObservabilityAdapterObserver>,
194    }
195
196    impl MokaCacheProvider {
197        /// Creates a Moka cache provider builder.
198        pub fn builder() -> MokaCacheBuilder {
199            MokaCacheBuilder::new()
200        }
201
202        /// Creates a provider from an existing Moka cache and optional namespace.
203        pub fn from_cache(
204            cache: moka::future::Cache<String, Vec<u8>>,
205            namespace: Option<String>,
206        ) -> Self {
207            Self {
208                namespace,
209                cache,
210                #[cfg(feature = "observability")]
211                observer: None,
212            }
213        }
214
215        /// Inserts a value by logical key.
216        pub async fn insert(&self, key: impl AsRef<str>, value: Vec<u8>) {
217            #[cfg(feature = "observability")]
218            let started_at = Instant::now();
219            self.cache
220                .insert(self.cache_key(key).into_string(), value)
221                .await;
222            #[cfg(feature = "observability")]
223            self.record(
224                "insert",
225                nidus_observability::OperationStatus::Success,
226                started_at,
227            );
228        }
229
230        /// Returns a value by logical key.
231        pub async fn get(&self, key: impl AsRef<str>) -> Option<Vec<u8>> {
232            #[cfg(feature = "observability")]
233            let started_at = Instant::now();
234            let result = self.cache.get(self.cache_key(key).as_str()).await;
235            #[cfg(feature = "observability")]
236            self.record(
237                "get",
238                nidus_observability::OperationStatus::Success,
239                started_at,
240            );
241            result
242        }
243
244        /// Invalidates a value by logical key.
245        pub async fn invalidate(&self, key: impl AsRef<str>) {
246            #[cfg(feature = "observability")]
247            let started_at = Instant::now();
248            self.cache.invalidate(self.cache_key(key).as_str()).await;
249            #[cfg(feature = "observability")]
250            self.record(
251                "invalidate",
252                nidus_observability::OperationStatus::Success,
253                started_at,
254            );
255        }
256
257        /// Returns direct access to the underlying Moka cache.
258        pub fn inner(&self) -> &moka::future::Cache<String, Vec<u8>> {
259            &self.cache
260        }
261
262        /// Returns the namespace used for logical keys.
263        pub fn namespace(&self) -> Option<&str> {
264            self.namespace.as_deref()
265        }
266
267        /// Returns a local health status for this in-memory provider.
268        #[cfg(feature = "health")]
269        pub fn health_status(&self) -> nidus_http::health::HealthStatus {
270            #[cfg(feature = "observability")]
271            let started_at = Instant::now();
272            #[cfg(feature = "observability")]
273            self.record(
274                "health",
275                nidus_observability::OperationStatus::Success,
276                started_at,
277            );
278            nidus_http::health::HealthStatus::up()
279        }
280
281        /// Adds this provider as a readiness check on a health registry.
282        ///
283        /// The provider is expected to be the shared instance resolved from the
284        /// Nidus container, so the method takes `Arc<Self>` and does not clone
285        /// the underlying cache directly.
286        #[cfg(feature = "health")]
287        pub fn register_ready_check(
288            self: std::sync::Arc<Self>,
289            registry: nidus_http::health::HealthRegistry,
290            name: impl Into<String>,
291        ) -> nidus_http::health::HealthRegistry {
292            registry.ready_check_sync(name, move || self.health_status())
293        }
294
295        fn cache_key(&self, key: impl AsRef<str>) -> CacheKey {
296            CacheKey::new(self.namespace.as_deref(), key)
297        }
298
299        #[cfg(feature = "observability")]
300        fn record(
301            &self,
302            operation: &'static str,
303            status: nidus_observability::OperationStatus,
304            started_at: Instant,
305        ) {
306            if let Some(observer) = &self.observer {
307                observer.record("nidus-cache", operation, status, started_at.elapsed());
308            }
309        }
310    }
311
312    impl ProviderRegistrant for MokaCacheProvider {
313        fn register_provider(container: &mut Container) -> NidusResult<()> {
314            container.register_singleton(Self::builder().build())?;
315            Ok(())
316        }
317    }
318}
319
320#[cfg(feature = "moka")]
321pub use moka_backend::{MokaCacheBuilder, MokaCacheProvider};