Skip to main content

rustlavel_flags/
store.rs

1//! Where an answer is written down so it outlives the process.
2//!
3//! A resolver is code, and code only changes with a deploy. The store is the
4//! other half: somewhere an operator can put an answer *now* — this customer
5//! gets the new checkout today, that flag goes off while we work out what broke
6//! — and have it survive a restart and be seen by every process behind the load
7//! balancer.
8//!
9//! An entry is an **override**, not a cache. That distinction runs through the
10//! whole crate: a stored value is a decision somebody made, so it beats the
11//! resolver rather than standing in for it, and nothing expires it. See
12//! [`Flags`](crate::Flags) for the precedence chain it sits in.
13//!
14//! Only the in-memory implementation ships here. A database-backed store is a
15//! dozen lines against this trait, and it is not written for you because the
16//! table belongs to the application: its name, its columns, and above all which
17//! migration owns it are decisions this crate should not be making.
18
19use crate::scope::Scope;
20use rustlavel_core::Result;
21use std::collections::HashMap;
22use std::future::Future;
23use std::pin::Pin;
24use std::sync::{Arc, RwLock};
25
26/// A boxed future borrowed from the store and its arguments.
27///
28/// Mirrors [`rustlavel_cache::store::BoxFuture`] and exists for the same
29/// reason: [`FlagStore`] has to be dyn-compatible, because an application
30/// chooses its store at boot and everything downstream holds an
31/// `Arc<dyn FlagStore>`. `async fn` in a trait is not dyn-compatible, so the
32/// methods return this instead.
33///
34/// [`rustlavel_cache::store::BoxFuture`]: https://docs.rs/rustlavel-cache
35pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
36
37/// Somewhere overrides are kept.
38///
39/// Every method is keyed by a `(flag, scope)` pair, and the store is free to
40/// key its rows however it likes as long as [`Scope::key`] distinctions are
41/// preserved — a user and a tenant with the same name are two subjects.
42pub trait FlagStore: Send + Sync + 'static {
43    /// The store's name, for error messages and boot logs.
44    fn name(&self) -> &'static str;
45
46    /// The override for one flag and scope: `None` when nobody has set one.
47    ///
48    /// `None` and `Some(false)` are emphatically not the same answer. `None`
49    /// means "no opinion, ask the resolver"; `Some(false)` means "somebody
50    /// turned this off". A store that collapses the two turns every override
51    /// into a suggestion.
52    fn get<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<Option<bool>>>;
53
54    /// Write an override, replacing any that was there.
55    fn set<'a>(&'a self, flag: &'a str, scope: &'a Scope, value: bool)
56    -> BoxFuture<'a, Result<()>>;
57
58    /// Remove an override, putting the flag back in the resolver's hands.
59    ///
60    /// Not an error when there was nothing to remove: an operator clearing an
61    /// override they are not sure they set wants it gone either way.
62    fn forget<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<()>>;
63
64    /// Remove every override this store holds.
65    ///
66    /// Worth being careful with. It does not "reset the flags" — it deletes
67    /// every decision an operator ever recorded, including the ones holding a
68    /// feature off, and the next request resolves them all from scratch.
69    fn flush(&self) -> BoxFuture<'_, Result<()>>;
70}
71
72/// Overrides in a map, for a single process.
73///
74/// Cheap to clone, and clones share one map, so the store can be handed to a
75/// [`Flags`](crate::Flags) and still be written to from an admin route.
76///
77/// The limitation is the obvious one and it is worth saying out loud: **an
78/// override set here reaches this process and no other.** On one machine that
79/// is everything; behind a load balancer it means an operator turning a flag
80/// off has turned it off for a third of the traffic. This is the right store
81/// for a single-process application, for tests, and for a default that works
82/// before anybody has chosen a database — and the wrong one for an incident
83/// switch across a fleet.
84#[derive(Clone, Default)]
85pub struct MemoryStore {
86    entries: Arc<RwLock<HashMap<(String, String), bool>>>,
87}
88
89impl MemoryStore {
90    pub fn new() -> Self {
91        MemoryStore::default()
92    }
93
94    /// How many overrides are stored. Mostly for tests and a status page.
95    pub fn len(&self) -> usize {
96        self.read().len()
97    }
98
99    pub fn is_empty(&self) -> bool {
100        self.len() == 0
101    }
102
103    /// A poisoned lock means a thread panicked while holding it. The map is
104    /// still a map — nothing here can leave it half-written — so the entries
105    /// are recovered rather than propagating the panic into every later
106    /// request.
107    fn read(&self) -> std::sync::RwLockReadGuard<'_, HashMap<(String, String), bool>> {
108        self.entries.read().unwrap_or_else(|poisoned| poisoned.into_inner())
109    }
110
111    fn write(&self) -> std::sync::RwLockWriteGuard<'_, HashMap<(String, String), bool>> {
112        self.entries.write().unwrap_or_else(|poisoned| poisoned.into_inner())
113    }
114}
115
116impl FlagStore for MemoryStore {
117    fn name(&self) -> &'static str {
118        "memory"
119    }
120
121    fn get<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<Option<bool>>> {
122        Box::pin(async move {
123            let key = (flag.to_string(), scope.key());
124            Ok(self.read().get(&key).copied())
125        })
126    }
127
128    fn set<'a>(
129        &'a self,
130        flag: &'a str,
131        scope: &'a Scope,
132        value: bool,
133    ) -> BoxFuture<'a, Result<()>> {
134        Box::pin(async move {
135            self.write().insert((flag.to_string(), scope.key()), value);
136            Ok(())
137        })
138    }
139
140    fn forget<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<()>> {
141        Box::pin(async move {
142            self.write().remove(&(flag.to_string(), scope.key()));
143            Ok(())
144        })
145    }
146
147    fn flush(&self) -> BoxFuture<'_, Result<()>> {
148        Box::pin(async move {
149            self.write().clear();
150            Ok(())
151        })
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[tokio::test]
160    async fn an_override_round_trips() {
161        let store = MemoryStore::new();
162        let scope = Scope::user_id(41);
163
164        assert_eq!(store.get("new-checkout", &scope).await.unwrap(), None);
165
166        store.set("new-checkout", &scope, true).await.unwrap();
167        assert_eq!(store.get("new-checkout", &scope).await.unwrap(), Some(true));
168
169        store.set("new-checkout", &scope, false).await.unwrap();
170        assert_eq!(store.get("new-checkout", &scope).await.unwrap(), Some(false));
171
172        store.forget("new-checkout", &scope).await.unwrap();
173        assert_eq!(store.get("new-checkout", &scope).await.unwrap(), None);
174    }
175
176    #[tokio::test]
177    async fn a_stored_false_is_not_the_same_answer_as_nothing_stored() {
178        let store = MemoryStore::new();
179        let scope = Scope::user_id(7);
180
181        store.set("beta-search", &scope, false).await.unwrap();
182
183        // The distinction the whole precedence chain rests on.
184        assert_eq!(store.get("beta-search", &scope).await.unwrap(), Some(false));
185        assert_eq!(store.get("never-set", &scope).await.unwrap(), None);
186    }
187
188    #[tokio::test]
189    async fn scopes_and_flags_are_kept_apart() {
190        let store = MemoryStore::new();
191
192        store.set("new-checkout", &Scope::user("acme"), true).await.unwrap();
193
194        assert_eq!(store.get("new-checkout", &Scope::tenant("acme")).await.unwrap(), None);
195        assert_eq!(store.get("new-checkout", &Scope::user("other")).await.unwrap(), None);
196        assert_eq!(store.get("beta-search", &Scope::user("acme")).await.unwrap(), None);
197        assert_eq!(store.get("new-checkout", &Scope::none()).await.unwrap(), None);
198    }
199
200    #[tokio::test]
201    async fn forgetting_something_that_was_never_there_is_not_an_error() {
202        let store = MemoryStore::new();
203        store.forget("never-set", &Scope::none()).await.unwrap();
204    }
205
206    #[tokio::test]
207    async fn flush_empties_the_store() {
208        let store = MemoryStore::new();
209        store.set("a", &Scope::user_id(1), true).await.unwrap();
210        store.set("b", &Scope::none(), false).await.unwrap();
211        assert_eq!(store.len(), 2);
212
213        store.flush().await.unwrap();
214
215        assert!(store.is_empty());
216        assert_eq!(store.get("a", &Scope::user_id(1)).await.unwrap(), None);
217    }
218
219    #[tokio::test]
220    async fn a_clone_shares_the_same_entries() {
221        // What makes it usable as application state: the handle a route writes
222        // through and the handle `Flags` reads through are the same map.
223        let store = MemoryStore::new();
224        let handle = store.clone();
225
226        handle.set("new-checkout", &Scope::none(), true).await.unwrap();
227
228        assert_eq!(store.get("new-checkout", &Scope::none()).await.unwrap(), Some(true));
229        assert_eq!(store.name(), "memory");
230    }
231
232    #[tokio::test]
233    async fn it_works_through_a_trait_object() {
234        let store: Arc<dyn FlagStore> = Arc::new(MemoryStore::new());
235
236        store.set("new-checkout", &Scope::user_id(41), true).await.unwrap();
237
238        assert_eq!(store.get("new-checkout", &Scope::user_id(41)).await.unwrap(), Some(true));
239    }
240}