pub struct Flags { /* private fields */ }Expand description
Every flag the application knows about, and the store behind them.
Cheap to clone — the definitions and the store are behind Arc — so it goes
into application state once and every request gets a handle to the same one.
§Precedence
Five things can have an opinion about one flag, and the question this type exists to answer is “why is this on for this user?”. So the answer is one short list, applied in order:
flags.offin configuration — off, for everybody, full stop. This is the incident switch and nothing below it can reopen the flag.- A stored override of
false, for this scope or globally — off. - A stored override of
true, for this scope or globally — on. flags.onin configuration — on.- The resolver, if the flag was defined.
- Otherwise off. A flag nobody defined is not a flag.
Two of those deserve their reasons written down.
Off beats on among stored overrides, at every level, and a global off
therefore beats a per-scope on. This is the same rule
rustlavel_rbac applies to a deny, for
the same reason: an off switch that something else can quietly overrule is
not a switch, and the moment somebody reaches for one is the moment they
cannot afford to audit what else might be set. The cost is real and worth
knowing — you cannot exempt one customer from a global off, you have to
clear the global off first — and it is the cheaper half of the trade.
Configuration outranks the store in both directions. flags.off is read
from the environment, which means it survives a database being down, a
store being unreachable, and a cache that has not caught up. That is exactly
the situation you reach for it in.
Implementations§
Source§impl Flags
impl Flags
Sourcepub fn from_config(config: &Config) -> Self
pub fn from_config(config: &Config) -> Self
An empty registry, with flags.on and flags.off already applied.
Define the flags on top of it: Flags::from_config(&config).define(...).
Sourcepub fn configured(self, config: &Config) -> Self
pub fn configured(self, config: &Config) -> Self
Read flags.on and flags.off and add them to what is already forced.
Both are lists, which in .env means the comma-separated spelling —
FLAGS_OFF=new-checkout,beta-search — and in a configuration file may
be a JSON array. Config::list accepts either.
It adds: calling it twice, or calling it after Flags::force_off,
leaves both sets of names forced. There is deliberately no way to
un-force a name from configuration, because the only reason to want one
is to undo an incident switch, and undoing an incident switch should
take an edit to the thing that set it.
A name in both lists is off. It has to be — see the precedence chain on
Flags — and a deployment that has contradicted itself is one where
the safe reading of the contradiction is the closed one.
The environment is read directly when the configuration key is absent,
which is what makes the claim above true. flags.off is described as
the switch that survives a database being down; it would not survive an
application with no config/flags.json in it, and an application that
added this crate by hand has no reason to have one. So FLAGS_OFF in
the environment works whether or not anything maps it into config.
Sourcepub fn store(self, store: impl FlagStore) -> Self
pub fn store(self, store: impl FlagStore) -> Self
Put a store behind the flags: a database table, Redis, anything that
implements FlagStore.
Sourcepub fn with_store(self, store: Arc<dyn FlagStore>) -> Self
pub fn with_store(self, store: Arc<dyn FlagStore>) -> Self
Use a store somebody else already built and holds a handle to.
Sourcepub fn define<F, Fut, A>(self, name: impl Into<String>, resolver: F) -> Self
pub fn define<F, Fut, A>(self, name: impl Into<String>, resolver: F) -> Self
Define a flag: a name, and code that decides it for a scope.
The resolver is given the Scope by value and may do anything an
async function may do, including go to the database. It is called at
most once per scope inside a ScopedFlags batch, so a slow resolver
costs its slowness once per request rather than once per question.
let flags = Flags::new()
.define("new-checkout", |scope: Scope| async move { scope.id().ends_with('7') })
.define("beta-search", |_| async { false });Defining a name twice replaces the first definition rather than
complaining. That is what makes a test able to say “this application,
but with new-checkout on” in one line.
Sourcepub fn rollout(self, name: impl Into<String>, percent: u8) -> Self
pub fn rollout(self, name: impl Into<String>, percent: u8) -> Self
Define a flag as a stable percentage rollout.
Shorthand for a resolver calling rollout::in_rollout; read that
module for why the bucket is hashed rather than drawn, and for what
widening a rollout does and does not do to the people already in it.
Checked against Scope::none a rollout is not a percentage of
anything — there is one scope, so it lands in one bucket and stays
there, on or off for the whole installation. Percentages need a subject.
Sourcepub fn force_on<I, S>(self, names: I) -> Self
pub fn force_on<I, S>(self, names: I) -> Self
Force these flags on, below any stored override. See Flags::configured.
Sourcepub fn force_off<I, S>(self, names: I) -> Self
pub fn force_off<I, S>(self, names: I) -> Self
Force these flags off, above everything. See Flags::configured.
Sourcepub fn defined(&self) -> Vec<&str>
pub fn defined(&self) -> Vec<&str>
Every defined flag’s name, sorted. Does not include names that only exist as a forced value or a stored override.
Sourcepub fn is_defined(&self, flag: &str) -> bool
pub fn is_defined(&self, flag: &str) -> bool
Whether a resolver was registered under this name.
Worth checking in a boot-time assertion: an undefined flag is off, and a misspelled one is indistinguishable from a feature that never shipped.
Sourcepub fn store_handle(&self) -> &Arc<dyn FlagStore> ⓘ
pub fn store_handle(&self) -> &Arc<dyn FlagStore> ⓘ
The store behind the flags, for an admin route that writes overrides.
Sourcepub async fn active(&self, flag: &str, scope: &Scope) -> Result<bool>
pub async fn active(&self, flag: &str, scope: &Scope) -> Result<bool>
Is this flag on for this scope?
Result, and not bool, deliberately. “I could not find out” is a
third answer — a store that is down, a resolver whose query failed — and
collapsing it into false would silently turn a feature off for
everybody the moment something unrelated broke, which is exactly the
kind of outage nobody thinks to look for.
This call does not remember anything. Use Flags::for_scope when a
request asks more than one question.
Sourcepub async fn inactive(&self, flag: &str, scope: &Scope) -> Result<bool>
pub async fn inactive(&self, flag: &str, scope: &Scope) -> Result<bool>
The negation, for the reading that comes out shorter.
Sourcepub fn for_scope(&self, scope: Scope) -> ScopedFlags
pub fn for_scope(&self, scope: Scope) -> ScopedFlags
A view fixed on one scope, which remembers what it has already resolved.
This is what a request should hold. Ten checks against one
ScopedFlags run each resolver at most once; ten checks against
Flags::active run each of them ten times.
Sourcepub async fn activate_for(&self, flag: &str, scope: &Scope) -> Result<()>
pub async fn activate_for(&self, flag: &str, scope: &Scope) -> Result<()>
Turn a flag on for one scope, overriding whatever the resolver says.
Sourcepub async fn deactivate_for(&self, flag: &str, scope: &Scope) -> Result<()>
pub async fn deactivate_for(&self, flag: &str, scope: &Scope) -> Result<()>
Turn a flag off for one scope, overriding whatever the resolver says.
Sourcepub async fn forget_for(&self, flag: &str, scope: &Scope) -> Result<()>
pub async fn forget_for(&self, flag: &str, scope: &Scope) -> Result<()>
Drop one scope’s override and let the resolver decide again.
Sourcepub async fn activate(&self, flag: &str) -> Result<()>
pub async fn activate(&self, flag: &str) -> Result<()>
Turn a flag on for everybody who has no override of their own.
Sourcepub async fn deactivate(&self, flag: &str) -> Result<()>
pub async fn deactivate(&self, flag: &str) -> Result<()>
Turn a flag off for everybody, including scopes that have an override
of their own saying otherwise — see the precedence chain on Flags.