rainier_cache/lib.rs
1//! # rainier-cache
2//!
3//! A [`Cache`] port and its drivers — the cache surface an MVC framework
4//! ships, with the 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;
88#[cfg(feature = "cloudflare-kv")]
89pub mod kv;
90pub mod lock;
91pub mod memory;
92pub mod prefixed;
93pub mod rate_limit;
94
95#[cfg(feature = "dynamodb")]
96pub mod dynamodb;
97#[cfg(feature = "memcached")]
98pub mod memcached;
99#[cfg(feature = "redis-driver")]
100pub mod redis;
101
102pub use cache::{Cache, CacheExt};
103pub use driver::CacheDriver;
104#[cfg(feature = "cloudflare-kv")]
105pub use kv::{KvCache, KvTransport, MIN_TTL as KV_MIN_TTL};
106pub use lock::{Lock, LockGuard, LockManager};
107pub use memory::MemoryCache;
108pub use prefixed::PrefixedCache;
109pub use rate_limit::CacheRateLimiter;
110
111#[cfg(feature = "dynamodb")]
112pub use dynamodb::DynamoDbCache;
113#[cfg(feature = "memcached")]
114pub use memcached::MemcachedCache;
115#[cfg(feature = "redis-driver")]
116pub use redis::RedisCache;
117
118use std::sync::Arc;
119
120/// The application's cache, as one container-storable value.
121///
122/// A newtype over the port rather than binding `Arc<dyn Cache>` directly, so
123/// swapping a driver does not change the type every call site names — the same
124/// shape as `rainier-framework`'s `Views`.
125#[derive(Clone)]
126pub struct CacheManager {
127 store: Arc<dyn Cache>,
128}
129
130impl CacheManager {
131 /// Wrap a cache.
132 pub fn new(store: Arc<dyn Cache>) -> Self {
133 Self { store }
134 }
135
136 /// An in-process cache — the default, and right for development.
137 pub fn memory() -> Self {
138 Self::new(Arc::new(MemoryCache::new()))
139 }
140
141 /// The cache underneath.
142 pub fn store(&self) -> &Arc<dyn Cache> {
143 &self.store
144 }
145
146 /// The driver's name — `"memory"`, `"redis"`, `"redis-cluster"`,
147 /// `"memcached"`.
148 pub fn driver(&self) -> &str {
149 self.store.name()
150 }
151}
152
153impl std::ops::Deref for CacheManager {
154 type Target = Arc<dyn Cache>;
155
156 /// So `Cache::instance().get(..)` works without naming `store()`.
157 fn deref(&self) -> &Self::Target {
158 &self.store
159 }
160}
161
162impl std::fmt::Debug for CacheManager {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.debug_struct("CacheManager").field("driver", &self.driver()).finish()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use std::time::Duration;
172
173 #[tokio::test]
174 async fn the_manager_delegates_to_its_store() {
175 let cache = CacheManager::memory();
176
177 cache.put_string("k", "v", None).await.unwrap();
178 assert_eq!(cache.get_string("k").await.unwrap().as_deref(), Some("v"));
179 assert_eq!(cache.driver(), "memory");
180 }
181
182 #[tokio::test]
183 async fn the_manager_reaches_the_typed_helpers_through_deref() {
184 let cache = CacheManager::memory();
185
186 cache.put_json("n", &vec![1u8, 2, 3], Some(Duration::from_secs(30))).await.unwrap();
187 assert_eq!(cache.get_json::<Vec<u8>>("n").await.unwrap(), Some(vec![1, 2, 3]));
188 }
189
190 #[tokio::test]
191 async fn a_prefixed_cache_can_be_the_manager() {
192 let shared: Arc<dyn Cache> = Arc::new(MemoryCache::new());
193 let cache = CacheManager::new(Arc::new(PrefixedCache::new(shared, "app")));
194
195 cache.put_string("k", "v", None).await.unwrap();
196 assert_eq!(cache.get_string("k").await.unwrap().as_deref(), Some("v"));
197 assert_eq!(cache.driver(), "memory", "the prefix is not a driver");
198 }
199}