rainier_cache/driver.rs
1//! Which cache store to build — [`CacheDriver`].
2
3use rainier_support::setting_enum;
4
5setting_enum! {
6 /// Where cached values live.
7 ///
8 /// The enum names every store this crate knows how to build, whether or not
9 /// the cargo feature behind it is on. That is deliberate: `CACHE_DRIVER=redis`
10 /// in a build without the `redis-driver` feature is a *build* mistake, and
11 /// it deserves a message saying so — not the same "unknown driver" message a
12 /// typo gets.
13 ///
14 /// ```
15 /// use rainier_cache::CacheDriver;
16 /// use rainier_support::Setting;
17 ///
18 /// assert_eq!(CacheDriver::parse("redis-cluster").unwrap(), CacheDriver::RedisCluster);
19 /// assert!(CacheDriver::parse("redys").is_err());
20 /// ```
21 pub enum CacheDriver: "cache driver" {
22 /// This process only. Fast, and shares nothing between instances.
23 ///
24 /// The default because it needs no server, and the right choice for
25 /// tests and single-process development. Anything cached **for
26 /// correctness** rather than for speed is wrong in it — a rate-limit
27 /// counter becomes `N ×` its limit across `N` instances, and a lock is
28 /// not a lock.
29 #[default]
30 Memory = "memory",
31
32 /// One Redis server, or a Sentinel-fronted primary.
33 Redis = "redis",
34
35 /// A sharded Redis Cluster. Keys distribute across the seed nodes.
36 RedisCluster = "redis-cluster",
37
38 /// Memcached over its text protocol.
39 Memcached = "memcached",
40
41 /// DynamoDB, with TTL doing the expiry. No server to run.
42 DynamoDb = "dynamodb",
43
44 /// Cloudflare Workers KV — the edge key-value store.
45 ///
46 /// **Read-heavy and eventually consistent.** A write reaches other
47 /// edges in roughly a minute, and there is no compare-and-set at all,
48 /// so it cannot hold a lock or count anything that has to be exact.
49 /// Right for a configuration blob or a feature-flag set; wrong for a
50 /// session, a rate limit or `on_one_server`.
51 ///
52 /// See [`KvCache`](crate::kv::KvCache), which says the same thing at
53 /// more length and is worth reading before choosing this.
54 Kv = "kv",
55 }
56}
57
58impl CacheDriver {
59 /// Whether this driver is shared between instances of the application.
60 ///
61 /// The question a production checklist is really asking. A rate limiter, a
62 /// lock, or a cached authorisation decision needs `true` here; a memoised
63 /// computation does not.
64 pub fn is_shared(&self) -> bool {
65 !matches!(self, Self::Memory)
66 }
67
68 /// Whether this driver can hold a lock.
69 ///
70 /// Shared and lock-capable are **different questions**, and Workers KV is
71 /// the reason: it is visible to every replica on earth and has no
72 /// compare-and-set, so two callers both win the `add` a lock is built
73 /// from and both believe they hold it.
74 ///
75 /// The check that matters is
76 /// [`LockManager::is_shared`](crate::LockManager::is_shared), which asks
77 /// the built store rather than the configured name; this answers the same
78 /// question about a driver nobody has built yet, for a boot-time
79 /// configuration check.
80 pub fn can_hold_a_lock(&self) -> bool {
81 self.is_shared() && !matches!(self, Self::Kv)
82 }
83
84 /// The cargo feature that has to be on for this driver to be built.
85 ///
86 /// `None` for the ones that are always available.
87 pub fn feature(&self) -> Option<&'static str> {
88 match self {
89 Self::Memory => None,
90 Self::Redis => Some("redis-driver"),
91 Self::RedisCluster => Some("redis-cluster"),
92 Self::Memcached => Some("memcached"),
93 Self::DynamoDb => Some("dynamodb"),
94 Self::Kv => Some("cloudflare-kv"),
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use rainier_support::Setting;
103
104 #[test]
105 fn only_memory_is_per_process() {
106 assert!(!CacheDriver::Memory.is_shared());
107 for driver in CacheDriver::ALL.iter().filter(|d| **d != CacheDriver::Memory) {
108 assert!(driver.is_shared(), "{driver} should be shared");
109 }
110 }
111
112 #[test]
113 fn every_driver_but_memory_names_a_feature() {
114 // The message "enable the `redis-driver` feature" is only possible if
115 // the driver knows which feature that is.
116 assert_eq!(CacheDriver::Memory.feature(), None);
117 for driver in CacheDriver::ALL.iter().filter(|d| **d != CacheDriver::Memory) {
118 assert!(driver.feature().is_some(), "{driver} should name its feature");
119 }
120 }
121
122 #[test]
123 fn the_wire_spellings_are_what_a_dotenv_would_hold() {
124 assert_eq!(CacheDriver::RedisCluster.as_str(), "redis-cluster");
125 assert_eq!(CacheDriver::DynamoDb.as_str(), "dynamodb");
126 }
127
128 #[test]
129 fn shared_and_lock_capable_are_different_questions() {
130 // The whole reason `can_hold_a_lock` exists. KV is visible to every
131 // replica on earth and still cannot hold a lock.
132 assert!(CacheDriver::Kv.is_shared());
133 assert!(!CacheDriver::Kv.can_hold_a_lock());
134
135 // And the ones that can.
136 for driver in [CacheDriver::Redis, CacheDriver::RedisCluster, CacheDriver::Memcached] {
137 assert!(driver.can_hold_a_lock(), "{driver}");
138 }
139
140 // Memory is neither, for the ordinary reason.
141 assert!(!CacheDriver::Memory.can_hold_a_lock());
142 }
143
144 #[test]
145 fn every_driver_names_the_feature_it_needs() {
146 // Which is what lets the "not built with that" error say which line to
147 // add to Cargo.toml.
148 for driver in CacheDriver::ALL {
149 if *driver != CacheDriver::Memory {
150 assert!(driver.feature().is_some(), "{driver}");
151 }
152 }
153 }
154}