Skip to main content

rainier_cache/
lib.rs

1//! # rainier-cache
2//!
3//! A [`Cache`] port and its drivers — Laravel's `Cache`, with the same shape
4//! and the same drivers you would expect.
5//!
6//! ```
7//! use rainier_cache::{Cache, CacheExt, MemoryCache};
8//! use std::time::Duration;
9//!
10//! # #[tokio::main] async fn main() -> rainier_support::Result<()> {
11//! let cache = MemoryCache::new();
12//!
13//! cache.put_string("greeting", "hello", Some(Duration::from_secs(60))).await?;
14//! assert_eq!(cache.get_string("greeting").await?.as_deref(), Some("hello"));
15//!
16//! // A counter, incremented atomically.
17//! assert_eq!(cache.increment("hits", 1).await?, 1);
18//! # Ok(()) }
19//! ```
20//!
21//! ## Drivers
22//!
23//! | Driver | Feature | Shared between instances |
24//! |---|---|---|
25//! | [`MemoryCache`] | — | no |
26//! | `RedisCache` | `redis-driver` | yes |
27//! | `RedisCache` on a cluster | `redis-cluster` | yes, sharded |
28//! | `MemcachedCache` | `memcached` | yes |
29//! | `DynamoDbCache` | `dynamodb` | yes, and no server to run |
30//!
31//! The transports live in [`rainier-drivers`](https://docs.rs/rainier-drivers)
32//! rather than here, because Redis is wanted by the queue too and an
33//! application should configure it once.
34//!
35//! Needs the matching feature, so this one is not compiled as a doctest:
36//!
37//! ```ignore
38//! // One server…
39//! let cache = RedisCache::connect(RedisConnector::open("redis://127.0.0.1/")?).await?;
40//!
41//! // …or a sharded cluster, with no change to anything downstream.
42//! let cache = RedisCache::connect(
43//!     RedisConnector::open_cluster(["redis://10.0.0.1:6379", "redis://10.0.0.2:6379"])?,
44//! ).await?;
45//! ```
46//!
47//! ## A miss is not a failure
48//!
49//! [`get`](Cache::get) returns `Ok(None)` for an absent key and `Err` only when
50//! the cache could not be reached. Conflating the two is how a cache outage
51//! becomes an application outage — and a cache is the one dependency an
52//! application should be able to lose.
53//!
54//! Driver errors are [`ServiceUnavailable`](rainier_support::ErrorKind::ServiceUnavailable),
55//! so they land in the right bucket on a dashboard rather than looking like
56//! bugs.
57//!
58//! ## Namespace a shared cache
59//!
60//! Two applications caching `user:1` on one Redis database read each other's
61//! values, and the symptom is a user seeing another application's data.
62//! [`PrefixedCache`] fixes it, and makes "flush mine" refuse rather than empty
63//! the server:
64//!
65//! ```
66//! # use rainier_cache::{Cache, MemoryCache, PrefixedCache};
67//! # use std::sync::Arc;
68//! let shared: Arc<dyn Cache> = Arc::new(MemoryCache::new());
69//! let mine = PrefixedCache::new(shared, "billing");
70//! ```
71//!
72//! ## What not to cache in memory
73//!
74//! A [`MemoryCache`] is per-process, so anything cached **for correctness**
75//! rather than for speed is wrong in it: a rate-limit counter becomes `N ×
76//! limit` across `N` instances, and a lock is not a lock.
77//!
78//! [`Cache::add`] and [`Cache::forget_if`] are atomic on every driver, so a
79//! [`Lock`] over a shared one is a real lock. Over a `MemoryCache` it is a real
80//! lock *within this process* and nothing at all between two —
81//! [`LockManager::is_shared`] is the check worth making at boot.
82
83#![deny(missing_docs)]
84#![deny(rustdoc::broken_intra_doc_links)]
85
86pub mod cache;
87pub mod driver;
88pub mod lock;
89pub mod memory;
90pub mod prefixed;
91
92#[cfg(feature = "dynamodb")]
93pub mod dynamodb;
94#[cfg(feature = "memcached")]
95pub mod memcached;
96#[cfg(feature = "redis-driver")]
97pub mod redis;
98
99pub use cache::{Cache, CacheExt};
100pub use driver::CacheDriver;
101pub use lock::{Lock, LockGuard, LockManager};
102pub use memory::MemoryCache;
103pub use prefixed::PrefixedCache;
104
105#[cfg(feature = "dynamodb")]
106pub use dynamodb::DynamoDbCache;
107#[cfg(feature = "memcached")]
108pub use memcached::MemcachedCache;
109#[cfg(feature = "redis-driver")]
110pub use redis::RedisCache;
111
112use std::sync::Arc;
113
114/// The application's cache, as one container-storable value.
115///
116/// A newtype over the port rather than binding `Arc<dyn Cache>` directly, so
117/// swapping a driver does not change the type every call site names — the same
118/// shape as `rainier-framework`'s `Views`.
119#[derive(Clone)]
120pub struct CacheManager {
121    store: Arc<dyn Cache>,
122}
123
124impl CacheManager {
125    /// Wrap a cache.
126    pub fn new(store: Arc<dyn Cache>) -> Self {
127        Self { store }
128    }
129
130    /// An in-process cache — the default, and right for development.
131    pub fn memory() -> Self {
132        Self::new(Arc::new(MemoryCache::new()))
133    }
134
135    /// The cache underneath.
136    pub fn store(&self) -> &Arc<dyn Cache> {
137        &self.store
138    }
139
140    /// The driver's name — `"memory"`, `"redis"`, `"redis-cluster"`,
141    /// `"memcached"`.
142    pub fn driver(&self) -> &str {
143        self.store.name()
144    }
145}
146
147impl std::ops::Deref for CacheManager {
148    type Target = Arc<dyn Cache>;
149
150    /// So `Cache::instance().get(..)` works without naming `store()`.
151    fn deref(&self) -> &Self::Target {
152        &self.store
153    }
154}
155
156impl std::fmt::Debug for CacheManager {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        f.debug_struct("CacheManager").field("driver", &self.driver()).finish()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use std::time::Duration;
166
167    #[tokio::test]
168    async fn the_manager_delegates_to_its_store() {
169        let cache = CacheManager::memory();
170
171        cache.put_string("k", "v", None).await.unwrap();
172        assert_eq!(cache.get_string("k").await.unwrap().as_deref(), Some("v"));
173        assert_eq!(cache.driver(), "memory");
174    }
175
176    #[tokio::test]
177    async fn the_manager_reaches_the_typed_helpers_through_deref() {
178        let cache = CacheManager::memory();
179
180        cache.put_json("n", &vec![1u8, 2, 3], Some(Duration::from_secs(30))).await.unwrap();
181        assert_eq!(cache.get_json::<Vec<u8>>("n").await.unwrap(), Some(vec![1, 2, 3]));
182    }
183
184    #[tokio::test]
185    async fn a_prefixed_cache_can_be_the_manager() {
186        let shared: Arc<dyn Cache> = Arc::new(MemoryCache::new());
187        let cache = CacheManager::new(Arc::new(PrefixedCache::new(shared, "app")));
188
189        cache.put_string("k", "v", None).await.unwrap();
190        assert_eq!(cache.get_string("k").await.unwrap().as_deref(), Some("v"));
191        assert_eq!(cache.driver(), "memory", "the prefix is not a driver");
192    }
193}