This is how percentages work in Flipper

Flipper is a Ruby library that implements feature flags. We use it to turn features on and off via configuration data that our application reads at runtime, meaning we can enable and disable features without modifying our actual Ruby code. Flipper has the concept of a Gate to determine whether or not a feature is enabled – for example, the simplest Gate is a boolean that globally determines whether a feature is turned on/off for the whole application. However, Flipper also lets us turn a feature on/off for a certain percentage of actors. Within this blog post, I’ll treat a “user” and “actor” as being interchangeable concepts.

If we want to gradually roll out a new feature to our application’s users, we may start by only enabling a Flipper Gate for 1% of users – then gradually increase that percentage to 2%, then 3%, and so on until we reach 100%.

This raises multiple questions, though:

  • Which 1% of users do we enable this feature for? Are these users selected randomly or deterministically?
  • When we increase that percentage from 1% to 2%, are the users from the 1% cohort a subset of the 2% cohort? We’d hope that Flipper is designed this way, since we don’t want a user to see a new feature in our application toggled on/off during a gradual rollout.
  • Are we introducing additional state to a user when a feature is enabled for them?

Let’s find out by reading the percentage_of_actors.rb file in the Flipper source code. open? looks like a promising method to read in that file:

We can avoid diving deeply into how Types::Actor.wrappable? works, since it’s an internal implementation detail of Flipper that is outside of the scope of this blog post. We can assume that actor.value returns the unique flipper_id that we see examples of in Flipper’s documentation. For example, if we’re dealing with a feature flag called foo_enabled for a user with an ID of 123, actor.value should be "User;#123", meaning the local variable id in the above snippet is "foo_enabledUser;123".

We then calculate the CRC32 of that id. CRC32 is an algorithm designed to check data integrity, but it works pretty well as a hash function if we’re not using it in a security-related context. This means that the output of CRC32 is pseudorandom and fairly uniformly distributed, even though it’s calculated deterministically. So, it should look like the users who have a feature enabled were selected at random, even though their eligibility is deterministic.

The inequality below (taken from the code snippet) will tell us whether or not cohorts for smaller percentages will be subsets of cohorts for larger percentages:

Zlib.crc32(id) % (100 * scaling_factor) < percentage * scaling_factor

percentage is the variable that we defined to be strictly increasing from 1% to 100%. scaling_factor is hardcoded to 1000, meaning percentage * scaling_factor is strictly increasing as well. Ruby’s implementation of CRC32 also interprets its output as an unsigned (ie. positive) integer. This means that all of the IDs for whom the inequality held true with a given percentage will also hold true for a larger percentage.

Let’s use an example: if percentage is 1, then any users where Zlib.crc32(id) % (100 * scaling_factor) is less than 1000 will have the foo_enabled feature turned on for them. Roughly 99% of our users will have a value of over 1000 in this check, so this feature will be turned off for 99% of users. However, for user 24 (ie. with id value set to "foo_enabledUser;24“, the CRC of their ID is 278800337, and 278800337 % (100*scaling_factor) returns 337.

The key point here is that this will always be 337 for user 24, regardless of what our percentage is set to! 337 is less than 1000, and if we strictly increase percentage to values such as 2 and 3, this inequality also holds true for their corresponding values for percentage * scaling_factor (such as 2000 and 3000).

To conclude:

  • We can expect our users to be sampled more-or-less at random by Flipper, even though their eligibility for a feature is calculated deterministically via CRC32. This is because CRC32 can be used as a hash function in this context.
  • If we’re strictly increasing our percentage of users enabled, then all users from the cohort of smaller percentages will also be included in the cohorts of larger percentages too.
  • Calculating a user’s eligibility for a feature via CRC32 doesn’t add additional state to the user.

Start your journey towards writing better software, and watch this space for new content.