Feature flags
Three environments compared as sets of flags - who is ahead, and what a promotion would turn on and off - and flags rolled out to a share of users by a bucket each user keeps, so a larger share only adds users; allow and deny lists; and the bucket against a new draw at every check.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Feature flags in three environments, compared as sets. Flags can also be rolled out to a share of users, each user keeping one bucket per flag.
main.eml- the menu, the environments table, one user's flags, a change of share, bucket against a new draw, and the allow and deny listsflags.eml- the known flags, set questions on lists, the bucket hash, the decision for one user, and counting over many users
How each part works:
- Environments, as in the corpus case
feature-flag-rollout: whether one environment already has every flag of another is a subset question, and what promoting it would change is a difference. Two environments can each have a flag the other lacks, and then neither is ahead. Flags are always listed in one fixed order. - A user's bucket for a flag is 0 to 9999. It comes from a hash of the flag's name, a colon and the user's id: each character is folded in and the result cubed, modulo the prime 4294967291, and the last step is modulo 10000. Cubing is one-to-one modulo that prime, and it is not linear. With a plain polynomial hash, one user's buckets for two flags were correlated (0.32), so a user early in one rollout tended to be early in the next.
- The decision: the deny list first, then the allow list, then the user is in when the bucket is under the share (in hundredths of a percent). Because the bucket never changes, raising the share only adds users and lowering it only removes them.
- Against a new draw at every check, the share comes out about right too, but users flip from one check to the next.
What is checked: menu choices 1 to 6; user ids of up to 20 letters, digits and - _ . @; a flag that is rolled out; a share from 0 to 100% with at most two decimals; allow or deny. An empty answer cancels.
Sessions: sessions/basic.in gives: - the environments: dev is ahead of staging, while staging and prod, and dev and prod, each have a flag the other lacks; - the flags of ana (on by the allow list, off by the deny list), user42 and ops-bot; - new-checkout raised from 10% to 20% and 50% with nobody leaving, then lowered to 5% with nobody joining; - search-v2 by bucket against a new draw: 0 users flip against 411; - user42 added to search-v2's allow list, and their flags again.
sessions/bad-input.in gives: - menu choices 0 and x; - a cancel, a user id too long, and one with a !; - shares: a cancel, a flag that is not rolled out, a cancel, 101, abc and 12.345; - a cancel for bucket or new draw; - lists: neither allow nor deny, two cancels, and a user who is already in the list.
Built on the verified corpus case feature-flag-rollout (being ready to promote is a subset question, and what would change is a difference).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Feature flags ==
Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 0
Pick a number from 1 to 6.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> x
Pick a number from 1 to 6.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 2
user id (letters, digits, - _ . @)>
Cancelled.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 2
user id (letters, digits, - _ . @)> a very long user identifier
Keep a user id to 20 characters.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 2
user id (letters, digits, - _ . @)> bad id!
A user id has letters, digits and - _ . @ only.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)>
Cancelled.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> fancy
Pick a flag that is rolled out.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> search-v2
new share in percent (0 to 100, two decimals at most)>
Cancelled.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> search-v2
new share in percent (0 to 100, two decimals at most)> 101
Type a share from 0 to 100, at most two decimals.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> search-v2
new share in percent (0 to 100, two decimals at most)> abc
Type a share from 0 to 100, at most two decimals.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> search-v2
new share in percent (0 to 100, two decimals at most)> 12.345
Type a share from 0 to 100, at most two decimals.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 4
flag (new-checkout, dark-mode, search-v2)>
Cancelled.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> dark-mode
allow or deny> maybe
Type allow or deny.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> dark-mode
allow or deny>
Cancelled.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> dark-mode
allow or deny> deny
user id (letters, digits, - _ . @)>
Cancelled.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> new-checkout
allow or deny> allow
user id (letters, digits, - _ . @)> ana
ana is already in the allow list of new-checkout.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 6
Bye.
What was typed (41 lines)
0
x
2
2
a very long user identifier
2
bad id!
3
3
fancy
3
search-v2
3
search-v2
101
3
search-v2
abc
3
search-v2
12.345
4
5
dark-mode
maybe
5
dark-mode
5
dark-mode
deny
5
new-checkout
allow
ana
6
basic
interpreter: byte-equal== Feature flags ==
Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 1
environment flags
dev new-checkout, dark-mode, search-v2, beta-banner
staging new-checkout, dark-mode
prod dark-mode, legacy-export
Promoting dev to staging would turn on search-v2, beta-banner and turn off (none).
dev has every flag of staging and more.
Promoting staging to prod would turn on new-checkout and turn off legacy-export.
Neither has every flag of the other, so neither is ahead: there is only the difference.
Promoting dev to prod would turn on new-checkout, search-v2, beta-banner and turn off legacy-export.
Neither has every flag of the other, so neither is ahead: there is only the difference.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 2
user id (letters, digits, - _ . @)> ana
new-checkout on in the allow list (10% rolled out)
dark-mode on bucket 2435 is under 10000 (100% rolled out)
search-v2 off in the deny list (25% rolled out)
The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 2
user id (letters, digits, - _ . @)> user42
new-checkout off bucket 5396 is not under 1000 (10% rolled out)
dark-mode on bucket 5395 is under 10000 (100% rolled out)
search-v2 off bucket 7645 is not under 2500 (25% rolled out)
The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 2
user id (letters, digits, - _ . @)> ops-bot
new-checkout off in the deny list (10% rolled out)
dark-mode on bucket 7177 is under 10000 (100% rolled out)
search-v2 off bucket 8997 is not under 2500 (25% rolled out)
The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> new-checkout
new share in percent (0 to 100, two decimals at most)> 20
new-checkout from 10% to 20%, over 1000 users:
on before 76, on after 178: 76 stayed on, 102 joined, 0 left.
Raising the share only adds users: everyone whose bucket was under the old share is under the new one.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> new-checkout
new share in percent (0 to 100, two decimals at most)> 50
new-checkout from 20% to 50%, over 1000 users:
on before 178, on after 499: 178 stayed on, 321 joined, 0 left.
Raising the share only adds users: everyone whose bucket was under the old share is under the new one.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 3
flag (new-checkout, dark-mode, search-v2)> new-checkout
new share in percent (0 to 100, two decimals at most)> 5
new-checkout from 50% to 5%, over 1000 users:
on before 499, on after 38: 38 stayed on, 0 joined, 461 left.
Lowering the share only removes users, those whose buckets lie between the new share and the old.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 4
flag (new-checkout, dark-mode, search-v2)> search-v2
search-v2 at 25%, each of 1000 users checked twice:
by bucket: 218 on at the first check, 218 at the second, 0 users flipped
a new draw each time: 257 on at the first check, 268 at the second, 411 users flipped
The share comes out about right either way; only the bucket keeps each user's answer the same from one check to the next.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 5
flag (new-checkout, dark-mode, search-v2)> search-v2
allow or deny> allow
user id (letters, digits, - _ . @)> user42
user42 added to the allow list of search-v2: user42.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 2
user id (letters, digits, - _ . @)> user42
new-checkout off bucket 5396 is not under 500 (5% rolled out)
dark-mode on bucket 5395 is under 10000 (100% rolled out)
search-v2 on in the allow list (25% rolled out)
The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.
1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit
choice> 6
Bye.
What was typed (25 lines)
1
2
ana
2
user42
2
ops-bot
3
new-checkout
20
3
new-checkout
50
3
new-checkout
5
4
search-v2
5
search-v2
allow
user42
2
user42
6
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P108 feature flags: environments compared as sets of flags, one user's
# flags and why, a rollout raised or lowered, and stable buckets against a
# new draw at every check.
import flags
def plain(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def pad(s, n):
while len(s) < n:
s + " " => s
return s
def listed(xs):
if len(xs) == 0:
return "(none)"
"" => out
for x in xs:
if out != "":
out + ", " => out
out + x => out
return out
def percent(bp):
# hundredths of a percent as a percent
bp % 100 => f
str(int((bp - f) / 100)) => s
if f == 0:
return s + "%"
if f < 10:
return s + ".0" + str(f) + "%"
return s + "." + str(f) + "%"
def ask_user():
plain(input("user id (letters, digits, - _ . @)> ")) => u
if u == "":
return ""
if len(u) > 20:
"Keep a user id to 20 characters." ^0
return "!"
for c in u:
if not ((c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or (c >= "0" and c <= "9") or c == "-" or c == "_" or c == "." or c == "@"):
"A user id has letters, digits and - _ . @ only." ^0
return "!"
return u
def ask_flag(rules):
plain(input("flag (" + listed(rule_names(rules)) + ")> ")) => f
if f == "":
return ""
if not (f in rules):
"Pick a flag that is rolled out." ^0
return "!"
return f
def rule_names(rules):
[] => out
for f in flags.known():
if f in rules:
out + [f] => out
return out
def environments(envs):
" environment flags" ^0
for e in ["dev", "staging", "prod"]:
(" " + pad(e, 13) + listed(flags.in_order(envs[e]))) ^0
for pair in [["dev", "staging"], ["staging", "prod"], ["dev", "prod"]]:
envs[pair[0]] => a
envs[pair[1]] => b
("Promoting " + pair[0] + " to " + pair[1] + " would turn on " + listed(flags.minus(a, b)) + " and turn off " + listed(flags.minus(b, a)) + ".") ^0
if flags.subset(a, b) and flags.subset(b, a):
(" The two have the same flags.") ^0
elif flags.subset(a, b):
(" " + pair[1] + " already has every flag of " + pair[0] + ".") ^0
elif flags.subset(b, a):
(" " + pair[0] + " has every flag of " + pair[1] + " and more.") ^0
else:
(" Neither has every flag of the other, so neither is ahead: there is only the difference.") ^0
def one_user(rules):
ask_user() => u
if u == "":
"Cancelled." ^0
return
if u == "!":
return
for f in rule_names(rules):
flags.decide(rules[f], f, u) => d
"off" => state
if d[0]:
"on " => state
(" " + pad(f, 14) + state + " " + d[1] + " (" + percent(rules[f][0]) + " rolled out)") ^0
"The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets." ^0
def roll(rules, people):
ask_flag(rules) => f
if f == "":
"Cancelled." ^0
return rules
if f == "!":
return rules
plain(input("new share in percent (0 to 100, two decimals at most)> ")) => t
if t == "":
"Cancelled." ^0
return rules
-1 => bp
"" => whole
"" => frac
False => dot
True => ok
for c in t:
if c == "." and not dot:
True => dot
elif c >= "0" and c <= "9":
if dot:
frac + c => frac
else:
whole + c => whole
else:
False => ok
if ok and whole != "" and len(whole) <= 3 and len(frac) <= 2 and not (dot and frac == ""):
while len(frac) < 2:
frac + "0" => frac
int(whole) * 100 + int(frac) => bp
if bp < 0 or bp > 10000:
"Type a share from 0 to 100, at most two decimals." ^0
return rules
rules[f] => old
[bp, old[1], old[2]] => new
flags.change(old, new, f, people) => c
(f + " from " + percent(old[0]) + " to " + percent(bp) + ", over " + str(len(people)) + " users:") ^0
(" on before " + str(c[0]) + ", on after " + str(c[1]) + ": " + str(c[2]) + " stayed on, " + str(c[3]) + " joined, " + str(c[4]) + " left.") ^0
if bp >= old[0]:
"Raising the share only adds users: everyone whose bucket was under the old share is under the new one." ^0
else:
"Lowering the share only removes users, those whose buckets lie between the new share and the old." ^0
new => rules[f]
return rules
def stable_vs_drawn(rules, people):
ask_flag(rules) => f
if f == "":
"Cancelled." ^0
return
if f == "!":
return
rules[f] => r
flags.count_on(r, f, people) => n
flags.rerolled(r[0], people, 2026) => d
(f + " at " + percent(r[0]) + ", each of " + str(len(people)) + " users checked twice:") ^0
(" by bucket: " + str(n) + " on at the first check, " + str(n) + " at the second, 0 users flipped") ^0
(" a new draw each time: " + str(d[0]) + " on at the first check, " + str(d[1]) + " at the second, " + str(d[2]) + " users flipped") ^0
"The share comes out about right either way; only the bucket keeps each user's answer the same from one check to the next." ^0
def lists(rules):
ask_flag(rules) => f
if f == "":
"Cancelled." ^0
return rules
if f == "!":
return rules
plain(input("allow or deny> ")) => k
if k != "allow" and k != "deny":
if k == "":
"Cancelled." ^0
else:
"Type allow or deny." ^0
return rules
ask_user() => u
if u == "":
"Cancelled." ^0
return rules
if u == "!":
return rules
rules[f] => r
1 => slot
if k == "deny":
2 => slot
if flags.has(r[slot], u):
(u + " is already in the " + k + " list of " + f + ".") ^0
return rules
r[slot] + [u] => r[slot]
r => rules[f]
(u + " added to the " + k + " list of " + f + ": " + listed(r[slot]) + ".") ^0
return rules
{"dev": ["new-checkout", "dark-mode", "search-v2", "beta-banner"], "staging": ["dark-mode", "new-checkout"], "prod": ["dark-mode", "legacy-export"]} => envs
{"new-checkout": [1000, ["ana"], ["ops-bot"]], "dark-mode": [10000, [], []], "search-v2": [2500, [], ["ana"]]} => rules
flags.users(1000) => people
"== Feature flags ==" ^0
"Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps." ^0
True => running
while running:
"" ^0
"1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
environments(envs)
elif choice == "2":
one_user(rules)
elif choice == "3":
roll(rules, people) => rules
elif choice == "4":
stable_vs_drawn(rules, people)
elif choice == "5":
lists(rules) => rules
elif choice == "6":
False => running
else:
"Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import flags
def plain(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def pad(s, n):
while len(s) < n:
s = s + " "
return s
def listed(xs):
if len(xs) == 0:
return "(none)"
out = ""
for x in xs:
if out != "":
out = out + ", "
out = out + x
return out
def percent(bp):
f = bp % 100
s = str(int((bp - f) / 100))
if f == 0:
return s + "%"
if f < 10:
return s + ".0" + str(f) + "%"
return s + "." + str(f) + "%"
def ask_user():
u = plain(input("user id (letters, digits, - _ . @)> "))
if u == "":
return ""
if len(u) > 20:
print("Keep a user id to 20 characters.")
return "!"
for c in u:
if not (c >= "a" and c <= "z" or c >= "A" and c <= "Z" or c >= "0" and c <= "9" or c == "-" or c == "_" or c == "." or c == "@"):
print("A user id has letters, digits and - _ . @ only.")
return "!"
return u
def ask_flag(rules):
f = plain(input("flag (" + listed(rule_names(rules)) + ")> "))
if f == "":
return ""
if not f in rules:
print("Pick a flag that is rolled out.")
return "!"
return f
def rule_names(rules):
out = []
for f in flags.known():
if f in rules:
out = out + [f]
return out
def environments(envs):
print(" environment flags")
for e in ["dev", "staging", "prod"]:
print(" " + pad(e, 13) + listed(flags.in_order(envs[e])))
for pair in [["dev", "staging"], ["staging", "prod"], ["dev", "prod"]]:
a = envs[pair[0]]
b = envs[pair[1]]
print("Promoting " + pair[0] + " to " + pair[1] + " would turn on " + listed(flags.minus(a, b)) + " and turn off " + listed(flags.minus(b, a)) + ".")
if flags.subset(a, b) and flags.subset(b, a):
print(" The two have the same flags.")
elif flags.subset(a, b):
print(" " + pair[1] + " already has every flag of " + pair[0] + ".")
elif flags.subset(b, a):
print(" " + pair[0] + " has every flag of " + pair[1] + " and more.")
else:
print(" Neither has every flag of the other, so neither is ahead: there is only the difference.")
def one_user(rules):
u = ask_user()
if u == "":
print("Cancelled.")
return
if u == "!":
return
for f in rule_names(rules):
d = flags.decide(rules[f], f, u)
state = "off"
if d[0]:
state = "on "
print(" " + pad(f, 14) + state + " " + d[1] + " (" + percent(rules[f][0]) + " rolled out)")
print("The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.")
def roll(rules, people):
f = ask_flag(rules)
if f == "":
print("Cancelled.")
return rules
if f == "!":
return rules
t = plain(input("new share in percent (0 to 100, two decimals at most)> "))
if t == "":
print("Cancelled.")
return rules
bp = -1
whole = ""
frac = ""
dot = False
ok = True
for c in t:
if c == "." and not dot:
dot = True
elif c >= "0" and c <= "9":
if dot:
frac = frac + c
else:
whole = whole + c
else:
ok = False
if ok and whole != "" and len(whole) <= 3 and len(frac) <= 2 and not (dot and frac == ""):
while len(frac) < 2:
frac = frac + "0"
bp = int(whole) * 100 + int(frac)
if bp < 0 or bp > 10000:
print("Type a share from 0 to 100, at most two decimals.")
return rules
old = rules[f]
new = [bp, old[1], old[2]]
c = flags.change(old, new, f, people)
print(f + " from " + percent(old[0]) + " to " + percent(bp) + ", over " + str(len(people)) + " users:")
print(" on before " + str(c[0]) + ", on after " + str(c[1]) + ": " + str(c[2]) + " stayed on, " + str(c[3]) + " joined, " + str(c[4]) + " left.")
if bp >= old[0]:
print("Raising the share only adds users: everyone whose bucket was under the old share is under the new one.")
else:
print("Lowering the share only removes users, those whose buckets lie between the new share and the old.")
rules[f] = new
return rules
def stable_vs_drawn(rules, people):
f = ask_flag(rules)
if f == "":
print("Cancelled.")
return
if f == "!":
return
r = rules[f]
n = flags.count_on(r, f, people)
d = flags.rerolled(r[0], people, 2026)
print(f + " at " + percent(r[0]) + ", each of " + str(len(people)) + " users checked twice:")
print(" by bucket: " + str(n) + " on at the first check, " + str(n) + " at the second, 0 users flipped")
print(" a new draw each time: " + str(d[0]) + " on at the first check, " + str(d[1]) + " at the second, " + str(d[2]) + " users flipped")
print("The share comes out about right either way; only the bucket keeps each user's answer the same from one check to the next.")
def lists(rules):
f = ask_flag(rules)
if f == "":
print("Cancelled.")
return rules
if f == "!":
return rules
k = plain(input("allow or deny> "))
if k != "allow" and k != "deny":
if k == "":
print("Cancelled.")
else:
print("Type allow or deny.")
return rules
u = ask_user()
if u == "":
print("Cancelled.")
return rules
if u == "!":
return rules
r = rules[f]
slot = 1
if k == "deny":
slot = 2
if flags.has(r[slot], u):
print(u + " is already in the " + k + " list of " + f + ".")
return rules
r[slot] = r[slot] + [u]
rules[f] = r
print(u + " added to the " + k + " list of " + f + ": " + listed(r[slot]) + ".")
return rules
envs = {"dev": ["new-checkout", "dark-mode", "search-v2", "beta-banner"], "staging": ["dark-mode", "new-checkout"], "prod": ["dark-mode", "legacy-export"]}
rules = {"new-checkout": [1000, ["ana"], ["ops-bot"]], "dark-mode": [10000, [], []], "search-v2": [2500, [], ["ana"]]}
people = flags.users(1000)
print("== Feature flags ==")
print("Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.")
running = True
while running:
print("")
print("1) environments 2) one user 3) change a share 4) bucket or new draw 5) allow or deny 6) quit")
choice = plain(input("choice> "))
if choice == "1":
environments(envs)
elif choice == "2":
one_user(rules)
elif choice == "3":
rules = roll(rules, people)
elif choice == "4":
stable_vs_drawn(rules, people)
elif choice == "5":
rules = lists(rules)
elif choice == "6":
running = False
else:
print("Pick a number from 1 to 6.")
print("Bye.")
flags.eml
eml# P108 feature flags - as in the corpus case feature-flag-rollout, whether
# one environment already has every flag of another is a subset question,
# and what promoting it would change is a difference. A flag can also be
# rolled out to a share of users: each user falls in a bucket from 0 to
# 9999 by a hash of the flag's name and the user's id, the same bucket
# every time, so raising the share only ever adds users. An allow list
# turns the flag on for a user and a deny list turns it off, the deny list
# first.
def known():
# every flag, in the order reports list them
return ["new-checkout", "dark-mode", "search-v2", "beta-banner", "legacy-export"]
def has(xs, x):
for y in xs:
if y == x:
return True
return False
def subset(a, b):
# every flag of a is in b
for x in a:
if not has(b, x):
return False
return True
def minus(a, b):
# the flags of a not in b, in the known order
[] => out
for x in known():
if has(a, x) and not has(b, x):
out + [x] => out
return out
def in_order(a):
[] => out
for x in known():
if has(a, x):
out + [x] => out
return out
def codes():
# every character a flag name or a user id can hold, numbered from 1
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.@:" => chars
{} => out
for i in [0:len(chars) - 1]:
i + 1 => out[chars[i]]
return out
codes() => code_table
def bucket(flag, user):
# 0 to 9999: each character of flag:user is folded in and the result
# cubed, modulo the prime P = 4294967291, then taken modulo 10000.
# Cubing is a one-to-one map modulo P (3 does not divide P - 1) and is
# not linear: a plain polynomial hash gives one user's buckets for two
# flags a correlation of 0.32, so a user early in one rollout would
# tend to be early in the next.
flag + ":" + user => s
0 => h
for c in s:
(h * 257 + code_table[c]) % 4294967291 => t
t * t % 4294967291 * t % 4294967291 => h
return h % 10000
def decide(rule, flag, user):
# rule is [share in hundredths of a percent, allow list, deny list];
# [on, why]
if has(rule[2], user):
return [False, "in the deny list"]
if has(rule[1], user):
return [True, "in the allow list"]
bucket(flag, user) => b
if b < rule[0]:
return [True, "bucket " + str(b) + " is under " + str(rule[0])]
return [False, "bucket " + str(b) + " is not under " + str(rule[0])]
def users(n):
[] => out
for i in [1:n]:
out + ["user" + str(i)] => out
return out
def count_on(rule, flag, people):
0 => n
for u in people:
if decide(rule, flag, u)[0]:
n + 1 => n
return n
def change(old, new, flag, people):
# [on before, on after, stayed on, joined, left]
0 => before
0 => after
0 => stayed
0 => joined
0 => left
for u in people:
decide(old, flag, u)[0] => a
decide(new, flag, u)[0] => b
if a:
before + 1 => before
if b:
after + 1 => after
if a and b:
stayed + 1 => stayed
if b and not a:
joined + 1 => joined
if a and not b:
left + 1 => left
return [before, after, stayed, joined, left]
def rerolled(share, people, seed):
# every check draws a new number from 0 to 9999 instead of the bucket:
# [on at the first check, on at the second, users who flipped]
[seed] => x
0 => first
0 => second
0 => flipped
for u in people:
(1664525 * x[0] + 1013904223) % 4294967296 => x[0]
(int((x[0] - x[0] % 65536) / 65536) % 10000 < share) => a
(1664525 * x[0] + 1013904223) % 4294967296 => x[0]
(int((x[0] - x[0] % 65536) / 65536) % 10000 < share) => b
if a:
first + 1 => first
if b:
second + 1 => second
if a != b:
flipped + 1 => flipped
return [first, second, flipped]
Python projection (flags.py)
def known():
return ["new-checkout", "dark-mode", "search-v2", "beta-banner", "legacy-export"]
def has(xs, x):
for y in xs:
if y == x:
return True
return False
def subset(a, b):
for x in a:
if not has(b, x):
return False
return True
def minus(a, b):
out = []
for x in known():
if has(a, x) and not has(b, x):
out = out + [x]
return out
def in_order(a):
out = []
for x in known():
if has(a, x):
out = out + [x]
return out
def codes():
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.@:"
out = {}
for i in range(0, len(chars)):
out[chars[i]] = i + 1
return out
code_table = codes()
def bucket(flag, user):
s = flag + ":" + user
h = 0
for c in s:
t = (h * 257 + code_table[c]) % 4294967291
h = t * t % 4294967291 * t % 4294967291
return h % 10000
def decide(rule, flag, user):
if has(rule[2], user):
return [False, "in the deny list"]
if has(rule[1], user):
return [True, "in the allow list"]
b = bucket(flag, user)
if b < rule[0]:
return [True, "bucket " + str(b) + " is under " + str(rule[0])]
return [False, "bucket " + str(b) + " is not under " + str(rule[0])]
def users(n):
out = []
for i in range(1, n+1):
out = out + ["user" + str(i)]
return out
def count_on(rule, flag, people):
n = 0
for u in people:
if decide(rule, flag, u)[0]:
n = n + 1
return n
def change(old, new, flag, people):
before = 0
after = 0
stayed = 0
joined = 0
left = 0
for u in people:
a = decide(old, flag, u)[0]
b = decide(new, flag, u)[0]
if a:
before = before + 1
if b:
after = after + 1
if a and b:
stayed = stayed + 1
if b and not a:
joined = joined + 1
if a and not b:
left = left + 1
return [before, after, stayed, joined, left]
def rerolled(share, people, seed):
x = [seed]
first = 0
second = 0
flipped = 0
for u in people:
x[0] = (1664525 * x[0] + 1013904223) % 4294967296
a = int((x[0] - x[0] % 65536) / 65536) % 10000 < share
x[0] = (1664525 * x[0] + 1013904223) % 4294967296
b = int((x[0] - x[0] % 65536) / 65536) % 10000 < share
if a:
first = first + 1
if b:
second = second + 1
if a != b:
flipped = flipped + 1
return [first, second, flipped]