Feature Toggles

什麼是 feature toggle?

若一個功能要成功上線需有:


一般開發方式大致分成下面幾種:

One branch (最簡單的方式一定是通通在 master!)

請在自己 side_project 上這樣做

只在 master 上開發,程式必須等到功能開發完全才能 release,中途 release 會影響到現有、或別人做的功能。

開發複雜度小很多,但真的沒有公司這樣用。


Feature Branch

為了協作開發,開發 A 功能的時候也能同時開發 B 功能,也不會影響到其他 release 的東西。


開另一支分支,全部完成再 release,中途 release master 也不會影響到現有功能,但隨著功能的開發時間拉長,merge 回 master 的時候衝突可能很多,code_review 也會很大包,也有可能有重工的狀況。


trunk_base_flow

降低每個 commit 粒度,branch 的週期,merge 的時候更 easy

merge 回 master 時不會一次要解很多衝突,code_review 方便,持續整合,加快迭代。

Q:但這樣不就把未完成的功能一起 release 出去了嗎?
A:所以我們搭配 Feature Toggles/Flags ,來隱藏尚未完成的功能!

rails 的 Feature Toggles

Unleash

👆官方文件,請點標題

文件詳細,但複雜度也很高,要另架一個 server,自己串接 API

Flipper and Rollout

👆官方文件,請點標題

rollout 是仰賴 redis 去做開關的控制,而 Flipper 是可以指定你要用什麼方式去存取開關的資料。
基本做法都差不多,也都有另外的 ui 可以用,甚至可以指定誰可以執行、百分比釋出

ps. Flipper 支援 rollout,也就是你可以用 Flipper 把 Rollout 包起來

開關有分兩種,一種就是很簡單的開跟關,另一種是可以依照百分比釋出的。

實作(以 active_record 為例)

install

1
2
3
# In Your Gemfile.rb
gem 'flipper'
gem 'flipper-active_record'

請記得 bundle,接著使用rails g flipper:active_record

這會幫你 create 兩個 table

  1. flipper_features
  2. flipper_gates
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class CreateFlipperTables < ActiveRecord::Migration[6.1]
    def self.up
    create_table :flipper_features do |t|
    t.string :key, null: false # feature 的名稱
    t.timestamps null: false
    end
    add_index :flipper_features, :key, unique: true

    create_table :flipper_gates do |t|
    t.string :feature_key, null: false # feature 的名稱
    t.string :key, null: false # 控制開關的 type
    t.string :value # 隨著 type 不一樣有不一樣的值
    t.timestamps null: false
    end
    add_index :flipper_gates, [:feature_key, :key, :value], unique: true
    end

    def self.down
    drop_table :flipper_gates
    drop_table :flipper_features
    end
    end
    記得 migration!

    flipper_features 只記有哪些開關,flipper_gates 記你要怎麼控制這個開關

到這邊其實已經可以直接在 console 使用 feature_toggle 的功能了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# rails console
Flipper.enable(:search) # 會幫你直接加入資料
# 可以使用下方來查看你的開關。
pp Flipper::Adapters::ActiveRecord::Feature.all
# [#<Flipper::Adapters::ActiveRecord::Feature:0x00007f97e8f05cd0
# id: 1,
# key: "search",
# created_at: Sat, 31 Jul 2021 06:20:03.158525000 UTC +00:00,
# updated_at: Sat, 31 Jul 2021 06:20:03.158525000 UTC +00:00>]

# 下方可以看到你控制開關的方式(要 enable才會 create 資料,disable 就刪掉資料)
pp Flipper::Adapters::ActiveRecord::Gate.all
# [#<Flipper::Adapters::ActiveRecord::Gate:0x00007f97e8e6e920
# id: 1,
# feature_key: [FILTERED],
# key: "boolean",
# value: "true",
# created_at: Sat, 31 Jul 2021 06:26:55.841467000 UTC +00:00,
# updated_at: Sat, 31 Jul 2021 06:26:55.841467000 UTC +00:00>]

不過要在 controller 中調用 Flipper 的方法,必須先 initialize,或者直接建立一份檔案,放在 lib/

1
2
3
4
5
6
7
8
9
# config/initializers/flipper/feature.rb

# initializer
Flipper.configure do |config|
config.adapter { Flipper::Adapters::ActiveRecord.new }
end

# 初始化的時候就設定 group
Flipper.register(:admins) { |thing| thing.admin? }

UI

1
2
# In Your Gemfile.rb
gem 'flipper-ui'
  • 記得 bundle,接著設定你的 router!

    1
    2
    3
    4
    # config/routes.rb
    YourRailsApp::Application.routes.draw do
    mount Flipper::UI.app(Flipper) => '/flipper'
    end
  • 若要依賴 devise 設定權限,example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # initializers/admin_access.rb

    class CanAccessFlipperUI
    def self.matches?(request)
    current_user = request.env['warden'].user
    current_user.present? && current_user.respond_to?(:admin?) && current_user.admin?
    end
    end

    # config/routes.rb

    constraints CanAccessFlipperUI do
    mount Flipper::UI.app(Flipper) => '/flipper'
    end
  • 或是直接使用 devise_group 做輔助

    1
    2
    3
    4
    5
    devise_scope :admin do
    authenticated :admin do
    mount Flipper::UI.app(Flipper) => '/flipper'
    end
    end
  • UI 的部分可以只能稍微客製化,彈性不大

    1
    2
    3
    4
    5
    6
    7
    # config/initializers/flipper_ui_config.rb
    require 'flipper/ui'

    Flipper::UI.configure do |config|
    config.banner_text = 'Production Environment'
    config.banner_class = 'danger'
    end

    由於沒有生出任何的檔案讓你做修改,所以要客製化,只能在 config/initializers/ 裡新增檔案,在檔案裡 require flipper/** 的相關檔案才能進行設定。也就是改一次就要關一次 server 喔

Flipper 控制開關的方法 Gates

flipper_gates 的 key

Boolean

  • 只有開跟關的選項
    1
    2
    Flipper.enable(:search)
    Flipper.enabled?(:search) # true

    group

  • 先製作好 group
    • 使用 Flipper.register(:admins) { |thing| thing.admin? }所以你的 thing 要有 admin? 這個方法可以用
    • 就可以直接使用 Flipper.enabled?(:feature_name, current_user) 去做判斷。
      1
      2
      3
      4
      Flipper.register(:admins) { |thing| thing.admin? }

      Flipper.enabled?(:feature_name, admin_user) # true
      Flipper.enabled?(:feature_name, non_admin_user) #false

actor

  • 直接使用 flipper_id 指定誰(ex. usercompany)可以過開關,請記得一定要有 flipper_id 這個方法可以用,因為 gates 是認這個!
    1
    2
    # model/user.rb
    alias_method :flipper_id, :id

    實作起來的實際狀況:
    就算沒有設定flipper_id 這個方法,預設好像也是抓 id,但 value 那一格存的會是 User;1,有設定的話就會是你設定的 value

百分比模式

percentage_of_actors

  • 設定為整個系統中,多少百分比 的 actors 打開
  • 不過要這樣用的話一定要加 alias_method :flipper_id, :id,在你要控制的 model 裡
  • 每個功能應該要只有一種 actor
    1
    2
    3
    Flipper.enable :feature_name, Flipper.actors(10)
    # or
    Flipper.enable_percentage_of_actors :feature_name, 10

percentage_of_time

  • 用機率的方式決定是否開啟功能。
    1
    2
    3
    4
    5
    # 不論是否有給第二個參數,皆有 25% 機率為 true
    Flipper.enable_percentage_of_time(:new_feature, 25)

    Flipper.enabled?(:new_feature) # 25% 機率為 true
    Flipper.enabled?(:new_feature, user)# 25% 機率為 true

總結

feature_toggle 雖然好用,但請記得:

  1. toggle 太多,會不好維護 => 你可能要維護兩版。
  2. toggle 的功能粒度太大,但能控制的細節是有限的,請維持適中的粒度大小
  3. 每個 toggle 的功能,盡量不要耦合到其他 toggle 的功能,請別搞死自己XD”
  4. 還是會有搞爆 production 的風險啊!

規範可參考 gitLab 的 使用原則

參考文章

  1. Feature Flag 功能釋出控制
  2. Feature Toggle 應用常見問題
  3. Flipper Cloud