flipper浅析
TL;DR
feature:功能
gate:范围的类型(Boolean、Actor、Group、Percentage)
type:范围的值(true/false、id、组名、百分比)
因为判断 feature 是否启用时,需要加载该 feature 关联的所有 gate ,所以不适合一个 feature 关联大量 Actor/Group
开始探究源码
看看 Flipper.enabled? 方法到底会干些什么
[1] pry(main)> Flipper.enabled? :search
Flipper::Adapters::ActiveRecord::Gate Load (0.1ms) SELECT "flipper_gates".* FROM "flipper_gates" WHERE "flipper_gates"."feature_key" = ? [["feature_key", "search"]]
=> false
[2] pry(main)> $ Flipper.enabled?
From: /home/z/datahouse/asdf/installs/ruby/2.7.5/lib/ruby/2.7.0/forwardable.rb:226:
Owner: Flipper
Visibility: public
Signature: enabled?(*args, &block)
Number of lines: 5
def #{ali}(*args, &block)
#{pre}
begin
#{accessor}
end#{method_call}
[3] pry(main)>
因为同时使用了 flipper-active_record,所以开关记录在了数据库了。
而 Flipper.enabled? 方法的定义来自 forwardable,不好查看。于是跟踪一下enabled?方法的调用栈
binding.trace_tree(htmp: 'flipper_enabled_quiz'){ Flipper.enabled? :search }
得:
![[flipper-enabled-quiz-trace-20220525.html]]
概览如此

然后产生一个 Flipper::Feature.new(name, @adapter, instrumenter: instrumenter),并调用 Flipper::Feature#enabled?
module Flipper
class DSL
def enabled?(name, *args)
feature(name).enabled?(*args)
end
def feature(name)
# ...
@memoized_features[name.to_sym] ||= Feature.new(name, @adapter, instrumenter: instrumenter)
end
end
end
而 Flipper::Feature#enabled? 源码如下
# flipper-0.21.0/lib/flipper/feature.rb
module Flipper
class Feature
def enabled?(thing = nil)
instrument(:enabled?) do |payload|
values = gate_values
thing = gate(:actor).wrap(thing) unless thing.nil?
payload[:thing] = thing
context = FeatureCheckContext.new(
feature_name: @name,
values: values,
thing: thing
)
if open_gate = gates.detect { |gate| gate.open?(context) }
payload[:gate_name] = open_gate.name
true
else
false
end
end
end
end
end
看起来有点复杂,或者先看看 Flipper.enable 吧
开闭功能
Flipper.enable 经过 DSL 最终会去到 Flipper::Feature#enable 源码如下。而其他 enable_xxx 都是想将“范围”包装成 Type::XXX ,然后传给 enable
def enable(thing = true)
instrument(:enable) do |payload|
adapter.add self
gate = gate_for(thing)
wrapped_thing = gate.wrap(thing)
payload[:gate_name] = gate.name
payload[:thing] = wrapped_thing
adapter.enable self, gate, wrapped_thing
end
end
def enable_actor(actor)
enable Types::Actor.wrap(actor)
end
def enable_group(group)
enable Types::Group.wrap(group)
end
def enable_percentage_of_time(percentage)
enable Types::PercentageOfTime.wrap(percentage)
end
def enable_percentage_of_actors(percentage)
enable Types::PercentageOfActors.wrap(percentage)
end
Flipper::Feature#enable 内部会先根据 Type::XXX 找到对应的 Gates::XXX 。
(这里 @gates 数组的顺序似乎固定如此,否则影响 detect )
def gates
@gates ||= [
Gates::Boolean.new,
Gates::Actor.new,
Gates::PercentageOfActors.new,
Gates::PercentageOfTime.new,
Gates::Group.new,
]
end
def gate_for(thing)
gates.detect { |gate| gate.protects?(thing) } || raise(GateNotFound, thing)
end
然后 gate.wrap(thing) 返回的是 Types::XXX 。对于 enable 方法来说,这步就是把 true 转成 Types::Boolean;而对于 enable_xxx 来说,返回的就是 thing
最后,会用 adapter 保存设置。如果用的是 flipper-active_record,则保存逻辑如下
# flipper-active_record-0.24.1/lib/flipper/adapters/active_record.rb
def set(feature, gate, thing, options = {})
clear_feature = options.fetch(:clear, false)
@gate_class.transaction do
clear(feature) if clear_feature
@gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
begin
@gate_class.create! do |g|
g.feature_key = feature.key
g.key = gate.key
g.value = thing.value.to_s
end
rescue ::ActiveRecord::RecordNotUnique
end
end
nil
end
关闭功能同理,如果用的是 flipper-active_record,则会清除数据库中的配置
检查功能
源码如下
def enabled?(thing = nil)
instrument(:enabled?) do |payload|
values = gate_values
thing = gate(:actor).wrap(thing) unless thing.nil?
payload[:thing] = thing
context = FeatureCheckContext.new(
feature_name: @name,
values: values,
thing: thing
)
if open_gate = gates.detect { |gate| gate.open?(context) }
payload[:gate_name] = open_gate.name
true
else
false
end
end
end
首先会通过 gate_values,间接调用 adapter 获取 feature 关联的各种 gate
然后 gates.detect 寻找是否有某类 gate 符合条件
各种范围类型(gate)的判断逻辑如下
# lib/flipper/gates/boolean.rb
def open?(context)
context.values[key]
end
# lib/flipper/gates/actor.rb
# 会加载出这个feature关联的所有flipper_id
def open?(context)
value = context.values[key]
if context.thing.nil?
false
else
if protects?(context.thing)
actor = wrap(context.thing)
enabled_actor_ids = value
enabled_actor_ids.include?(actor.value)
else
false
end
end
end
# lib/flipper/gates/group.rb
# 会加载出这个feature关联的所有组名,然后利用组名关联的注册block去验证
def open?(context)
value = context.values[key]
if context.thing.nil?
false
else
value.any? do |name|
group = Flipper.group(name)
group.match?(context.thing, context)
end
end
end
# lib/flipper/gates/percentage_of_time.rb
# 使用随机数
def open?(context)
value = context.values[key]
rand < (value / 100.0)
end
# lib/flipper/gates/percentage_of_actors.rb
# 计算flipper_id哈希值。随着百分比增长,原本不生效的flipper_id会变成生效
def open?(context)
percentage = context.values[key]
if Types::Actor.wrappable?(context.thing)
actor = Types::Actor.wrap(context.thing)
id = "#{context.feature_name}#{actor.value}"
# this is to support up to 3 decimal places in percentages
scaling_factor = 1_000
Zlib.crc32(id) % (100 * scaling_factor) < percentage * scaling_factor
else
false
end
end
适配器
flipper-active_record 适配器的注入如下
# flipper-active_record-0.21.0/lib/flipper-active_record.rb
ActiveSupport.on_load(:active_record) do
require 'flipper/adapters/active_record'
end
# flipper-active_record-0.21.0/lib/flipper/adapters/active_record.rb
Flipper.configure do |config|
config.adapter { Flipper::Adapters::ActiveRecord.new }
end