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}