Rollouts, rules and groups
A flag's settings are changed through the management functions in
howdy/flags, and the same operations are offered by the
admin and the command line. Every change is
recorded in the flag's history under the name given as by, and can be
undone. These operations are privileged: authorize the caller first.
The kill switch
The kill switch turns a flag off for everyone at once, whatever its rules
and rollout say, and revive lets those apply again:
flags.kill(features, "new_checkout", by: "mike")
flags.revive(features, "new_checkout", by: "mike")
Keep a finished feature behind a flag that is on_by_default for a while
after release, so it can be killed without a deployment.
Rollouts
Every user has a position from 0 to 99.99% for each flag, worked out from a SHA-256 of the flag's key and their id: nothing is stored per user, and the position never changes. A rollout of 20% is on for everyone below 20%, so:
- raising a rollout only adds people;
- pausing is leaving it where it is;
- rolling back from 20% to 5% removes exactly the people added after 5%, and the first 5% keep it;
- each flag picks a different first 5%, because the key is hashed too.
Rollouts are in hundredths of a percent: flags.percent(25) is 2500,
and flags.percent_to_string(1250) is 12.5%.
flags.set_rollout(features, "new_checkout", to: flags.percent(5), by: "mike")
A rollout is by user unless set_bucketing makes it by organization.
Then everyone in an organization is in or out together, so colleagues see
the same app:
flags.set_bucketing(features, "new_checkout", to: flags.ByOrganization, by: "mike")
Someone without the id the rollout is by only gets the flag at 100%.
flags.position(key, target) gives a position, so anything else that
works out who is in a rollout agrees with the app.
Rolling back changes which code runs, not the data the new code already wrote: the old path has to cope with it.
Ramps
A ramp raises the rollout through steps on a schedule, such as 1%, 5%, 25%, 50% and 100% an hour apart:
flags.start_ramp(
features,
"new_checkout",
steps: [100, 500, 2500, 5000, 10_000],
every: 3600,
by: "mike",
)
Every running node checks for due steps at its regular check; each flag's
row is locked while a step is taken, so a step is taken once however many
nodes see it due. Killing the flag or changing the rollout by hand pauses
the ramp, as does pause_ramp; resume_ramp carries on from the next
step above the current rollout, and cancel_ramp forgets it.
Groups and rules
A group is a named set of users and organizations, such as staff or beta
testers. Rules allow or block a flag for a user, an organization or a
group, named by a Target: flags.User(id), flags.Organization(id) or
flags.Group(name).
flags.create_group(features, "beta", description: "Beta testers", by: "mike")
flags.add_member(features, "beta", flags.Organization("acme"), by: "mike")
flags.allow(features, "new_checkout", flags.Group("beta"), by: "mike")
flags.block(features, "new_checkout", flags.User("42"), by: "mike")
flags.unlist(features, "new_checkout", flags.User("42"), by: "mike")
A block rule wins over an allow rule, and both win over the rollout. A
group that a rule names cannot be deleted: remove the rule first, so no
flag changes without a record in its history. flags.groups lists the
groups and their members.
History and undo
Every change records who made it, when, a summary such as
Rollout 5% → 20%, and the setting before and after. flags.history
lists them, newest first, for one flag or all of them:
let assert Ok(changes) = flags.history(features, flag: Some("new_checkout"), limit: 20)
flags.undo(features, change: id, by:) puts the flag back as it was
before a change, recorded as a change of its own, with any ramp paused.
flags.forget deletes what is stored for a flag, putting it back to its
default; it also tidies away keys the code no longer registers.
flags.setting(features, key) and flags.settings(features) read what is
stored, as a Setting with the kill switch, rollout, bucketing, rules and
ramp.
