Skip to main content

Flags

Struct Flags 

Source
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:

  1. flags.off in configuration — off, for everybody, full stop. This is the incident switch and nothing below it can reopen the flag.
  2. A stored override of false, for this scope or globally — off.
  3. A stored override of true, for this scope or globally — on.
  4. flags.on in configuration — on.
  5. The resolver, if the flag was defined.
  6. 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

Source

pub fn new() -> Self

An empty registry over an in-memory store.

Source

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(...).

Source

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.

Source

pub fn store(self, store: impl FlagStore) -> Self

Put a store behind the flags: a database table, Redis, anything that implements FlagStore.

Source

pub fn with_store(self, store: Arc<dyn FlagStore>) -> Self

Use a store somebody else already built and holds a handle to.

Source

pub fn define<F, Fut, A>(self, name: impl Into<String>, resolver: F) -> Self
where F: Fn(Scope) -> Fut + Send + Sync + 'static, Fut: Future<Output = A> + Send + 'static, A: FlagAnswer,

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.

Source

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.

Source

pub fn force_on<I, S>(self, names: I) -> Self
where I: IntoIterator<Item = S>, S: Into<String>,

Force these flags on, below any stored override. See Flags::configured.

Source

pub fn force_off<I, S>(self, names: I) -> Self
where I: IntoIterator<Item = S>, S: Into<String>,

Force these flags off, above everything. See Flags::configured.

Source

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.

Source

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.

Source

pub fn store_handle(&self) -> &Arc<dyn FlagStore> ⓘ

The store behind the flags, for an admin route that writes overrides.

Source

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.

Source

pub async fn inactive(&self, flag: &str, scope: &Scope) -> Result<bool>

The negation, for the reading that comes out shorter.

Source

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.

Source

pub async fn activate_for(&self, flag: &str, scope: &Scope) -> Result<()>

Turn a flag on for one scope, overriding whatever the resolver says.

Source

pub async fn deactivate_for(&self, flag: &str, scope: &Scope) -> Result<()>

Turn a flag off for one scope, overriding whatever the resolver says.

Source

pub async fn forget_for(&self, flag: &str, scope: &Scope) -> Result<()>

Drop one scope’s override and let the resolver decide again.

Source

pub async fn activate(&self, flag: &str) -> Result<()>

Turn a flag on for everybody who has no override of their own.

Source

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.

Source

pub async fn forget(&self, flag: &str) -> Result<()>

Drop the global override. Per-scope overrides are left alone.

Source

pub async fn purge(&self) -> Result<()>

Drop every override in the store, for every flag and every scope.

Trait Implementations§

Source§

impl Clone for Flags

Source§

fn clone(&self) -> Flags

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Flags

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

The names and the forced lists. Resolvers are closures and have nothing to show, but which flags exist is the thing you actually want in a log line when a check came back with an answer you did not expect.

Source§

impl Default for Flags

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl !RefUnwindSafe for Flags

§

impl !UnwindSafe for Flags

§

impl Freeze for Flags

§

impl Send for Flags

§

impl Sync for Flags

§

impl Unpin for Flags

§

impl UnsafeUnpin for Flags

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.