Skip to main content

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}