diff options
author | Shinya Maeda <shinya@gitlab.com> | 2019-09-02 14:55:56 +0700 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2019-09-02 16:01:30 +0700 |
commit | f8e3f088ab18df3093239be82fa01deb03161e20 (patch) | |
tree | 07d389b4c0c6f4a4f122a420b4ca77cd1d65dc32 | |
parent | 12834544776a1888db3a5d0780bdc3aa0d94e7d6 (diff) | |
download | gitlab-ce-implement-unleash-client.tar.gz |
Generalize Feature Flag system and implement unleash adapterimplement-unleash-client
This commit abstracts the current Feature implementation
and make it adoptable with any feature flag systems.
-rw-r--r-- | app/controllers/application_controller.rb | 11 | ||||
-rw-r--r-- | config/gitlab.yml.example | 6 | ||||
-rw-r--r-- | config/initializers/1_settings.rb | 8 | ||||
-rw-r--r-- | config/initializers/flipper.rb | 2 | ||||
-rw-r--r-- | lib/feature.rb | 114 | ||||
-rw-r--r-- | lib/gitlab/feature_flag/adapters/flipper.rb | 127 | ||||
-rw-r--r-- | lib/gitlab/feature_flag/adapters/unleash.rb | 78 |
7 files changed, 242 insertions, 104 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9a7859fc687..5af62576299 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -26,6 +26,7 @@ class ApplicationController < ActionController::Base before_action :require_email, unless: :devise_controller? before_action :set_usage_stats_consent_flag before_action :check_impersonation_availability + before_action :set_unleash_context around_action :set_locale around_action :set_session_storage @@ -533,6 +534,16 @@ class ApplicationController < ActionController::Base yield end end + + private + + def set_unleash_context + @unleash_context = Unleash::Context.new( + session_id: session.id, + remote_address: request.remote_ip, + user_id: session[:user_id] + ) + end end ApplicationController.prepend_if_ee('EE::ApplicationController') diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 20b1020e025..c503102813d 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -463,6 +463,12 @@ production: &base # enabled: true # primary_api_url: http://localhost:5000/ # internal address to the primary registry, will be used by GitLab to directly communicate with primary registry API + ## Feature Flag + unleash: + # enabled: false + # url: https://gitlab.com/api/v4/feature_flags/unleash/<project_id> + # app_name: gitlab.com # Environment name of your GitLab instance + # instance_id: INSTNACE_ID # # 2. GitLab CI settings diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 4160f488a7a..6e6d96d3f66 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -309,6 +309,14 @@ Gitlab.ee do end # +# Unleash +# +Settings['unleash'] ||= Settingslogic.new({}) +Settings.unleash['enabled'] = false if Settings.unleash['enabled'].nil? +Settings.unleash['app_name'] = Rails.env if Settings.unleash['app_name'].nil? +Settings.unleash['instance_id'] = ENV['UNLEASH_INSTANCE_ID'] if Settings.unleash['instance_id'].nil? + +# # External merge request diffs # Settings['external_diffs'] ||= Settingslogic.new({}) diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index 80cab7273e5..ddf9f3e8ab6 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -1 +1 @@ -Feature.register_feature_groups +Gitlab::FeatureFlag::Adapters::Flipper.register_feature_groups diff --git a/lib/feature.rb b/lib/feature.rb index 88b0d871c3a..a5da331e49f 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -1,66 +1,21 @@ # frozen_string_literal: true -require 'flipper/adapters/active_record' -require 'flipper/adapters/active_support_cache_store' - class Feature prepend_if_ee('EE::Feature') # rubocop: disable Cop/InjectEnterpriseEditionModule - # Classes to override flipper table names - class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature - # Using `self.table_name` won't work. ActiveRecord bug? - superclass.table_name = 'features' - - def self.feature_names - pluck(:key) - end - end - - class FlipperGate < Flipper::Adapters::ActiveRecord::Gate - superclass.table_name = 'feature_gates' - end + SUPPORTED_FEATURE_FLAG_ADAPTERS = %w[unleash flipper] class << self - delegate :group, to: :flipper - - def all - flipper.features.to_a - end + delegate :all, :get, :enabled?, :remove, :group, to: :adapter - def get(key) - flipper.feature(key) - end - - def persisted_names - Gitlab::SafeRequestStore[:flipper_persisted_names] ||= - begin - # We saw on GitLab.com, this database request was called 2300 - # times/s. Let's cache it for a minute to avoid that load. - Gitlab::ThreadMemoryCache.cache_backend.fetch('flipper:persisted_names', expires_in: 1.minute) do - FlipperFeature.feature_names - end + def adapter + @adapter ||= + SUPPORTED_FEATURE_FLAG_ADAPTERS.find do |type| + adapter = get_adapter(type) + break adapter if adapter.available? end end - def persisted?(feature) - # Flipper creates on-memory features when asked for a not-yet-created one. - # If we want to check if a feature has been actually set, we look for it - # on the persisted features list. - persisted_names.include?(feature.name.to_s) - end - - # use `default_enabled: true` to default the flag to being `enabled` - # unless set explicitly. The default is `disabled` - def enabled?(key, thing = nil, default_enabled: false) - feature = Feature.get(key) - - # If we're not default enabling the flag or the feature has been set, always evaluate. - # `persisted?` can potentially generate DB queries and also checks for inclusion - # in an array of feature names (177 at last count), possibly reducing performance by half. - # So we only perform the `persisted` check if `default_enabled: true` - !default_enabled || Feature.persisted?(feature) ? feature.enabled?(thing) : true - end - def disabled?(key, thing = nil, default_enabled: false) # we need to make different method calls to make it easy to mock / define expectations in test mode thing.nil? ? !enabled?(key, default_enabled: default_enabled) : !enabled?(key, thing, default_enabled: default_enabled) @@ -82,57 +37,10 @@ class Feature get(key).disable_group(group) end - def remove(key) - feature = get(key) - return unless persisted?(feature) - - feature.remove - end - - def flipper - if Gitlab::SafeRequestStore.active? - Gitlab::SafeRequestStore[:flipper] ||= build_flipper_instance - else - @flipper ||= build_flipper_instance - end - end - - def build_flipper_instance - Flipper.new(flipper_adapter).tap { |flip| flip.memoize = true } - end - - # This method is called from config/initializers/flipper.rb and can be used - # to register Flipper groups. - # See https://docs.gitlab.com/ee/development/feature_flags.html#feature-groups - def register_feature_groups - end - - def flipper_adapter - active_record_adapter = Flipper::Adapters::ActiveRecord.new( - feature_class: FlipperFeature, - gate_class: FlipperGate) - - # Redis L2 cache - redis_cache_adapter = - Flipper::Adapters::ActiveSupportCacheStore.new( - active_record_adapter, - l2_cache_backend, - expires_in: 1.hour) - - # Thread-local L1 cache: use a short timeout since we don't have a - # way to expire this cache all at once - Flipper::Adapters::ActiveSupportCacheStore.new( - redis_cache_adapter, - l1_cache_backend, - expires_in: 1.minute) - end - - def l1_cache_backend - Gitlab::ThreadMemoryCache.cache_backend - end - - def l2_cache_backend - Rails.cache + private + + def get_adapter(type) + "Gitlab::FeatureFlag::Adapters::#{type.camelize}".constantize end end diff --git a/lib/gitlab/feature_flag/adapters/flipper.rb b/lib/gitlab/feature_flag/adapters/flipper.rb new file mode 100644 index 00000000000..869e073b2fd --- /dev/null +++ b/lib/gitlab/feature_flag/adapters/flipper.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'flipper/adapters/active_record' +require 'flipper/adapters/active_support_cache_store' + +module Gitlab + module FeatureFlag + module Adapters + class Flipper + delegate :group, to: :flipper + + # Classes to override flipper table names + class FlipperFeature < ::Flipper::Adapters::ActiveRecord::Feature + # Using `self.table_name` won't work. ActiveRecord bug? + superclass.table_name = 'features' + + def self.feature_names + pluck(:key) + end + end + + class FlipperGate < ::Flipper::Adapters::ActiveRecord::Gate + superclass.table_name = 'feature_gates' + end + + class << self + def available? + true + end + + def all + flipper.features.to_a + end + + def get(key) + flipper.feature(key) + end + + # use `default_enabled: true` to default the flag to being `enabled` + # unless set explicitly. The default is `disabled` + def enabled?(key, thing = nil, default_enabled: false) + feature = get(key) + + # If we're not default enabling the flag or the feature has been set, always evaluate. + # `persisted?` can potentially generate DB queries and also checks for inclusion + # in an array of feature names (177 at last count), possibly reducing performance by half. + # So we only perform the `persisted` check if `default_enabled: true` + !default_enabled || persisted?(feature) ? feature.enabled?(thing) : true + end + + def remove(key) + feature = get(key) + return unless persisted?(feature) + + feature.remove + end + + # This method is called from config/initializers/flipper.rb and can be used + # to register Flipper groups. + # See https://docs.gitlab.com/ee/development/feature_flags.html#feature-groups + def register_feature_groups + end + + private + + def flipper + if Gitlab::SafeRequestStore.active? + Gitlab::SafeRequestStore[:flipper] ||= build_flipper_instance + else + @flipper ||= build_flipper_instance + end + end + + def persisted_names + Gitlab::SafeRequestStore[:flipper_persisted_names] ||= + begin + # We saw on GitLab.com, this database request was called 2300 + # times/s. Let's cache it for a minute to avoid that load. + Gitlab::ThreadMemoryCache.cache_backend.fetch('flipper:persisted_names', expires_in: 1.minute) do + FlipperFeature.feature_names + end + end + end + + def persisted?(feature) + # Flipper creates on-memory features when asked for a not-yet-created one. + # If we want to check if a feature has been actually set, we look for it + # on the persisted features list. + persisted_names.include?(feature.name.to_s) + end + + def build_flipper_instance + ::Flipper.new(flipper_adapter).tap { |flip| flip.memoize = true } + end + + def flipper_adapter + active_record_adapter = ::Flipper::Adapters::ActiveRecord.new( + feature_class: FlipperFeature, + gate_class: FlipperGate) + + # Redis L2 cache + redis_cache_adapter = + ::Flipper::Adapters::ActiveSupportCacheStore.new( + active_record_adapter, + l2_cache_backend, + expires_in: 1.hour) + + # Thread-local L1 cache: use a short timeout since we don't have a + # way to expire this cache all at once + ::Flipper::Adapters::ActiveSupportCacheStore.new( + redis_cache_adapter, + l1_cache_backend, + expires_in: 1.minute) + end + + def l1_cache_backend + Gitlab::ThreadMemoryCache.cache_backend + end + + def l2_cache_backend + Rails.cache + end + end + end + end + end +end diff --git a/lib/gitlab/feature_flag/adapters/unleash.rb b/lib/gitlab/feature_flag/adapters/unleash.rb new file mode 100644 index 00000000000..5e871d41996 --- /dev/null +++ b/lib/gitlab/feature_flag/adapters/unleash.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Gitlab + module FeatureFlag + module Adapters + class Unleash + class Feature + def enable(key, thing = true) + # TODO: + end + + def disable(key, thing = false) + # TODO: + end + + def enable_group(key, group) + # Not Implemented yet + end + + def disable_group(key, group) + # Not Implemented yet + end + + def remove + # TODO: + end + + # new(Unleash::ToggleFetcher.toggle_cache[key]) + end + + class << self + def available? + Gitlab.config.unleash.enabled + end + + def all + Unleash::ToggleFetcher.toggle_cache + end + + def get(key) + Feature.get(key) + end + + def enabled?(key, thing = nil, default_enabled: false) + feature = get(key) + + # If we're not default enabling the flag or the feature has been set, always evaluate. + # `persisted?` can potentially generate DB queries and also checks for inclusion + # in an array of feature names (177 at last count), possibly reducing performance by half. + # So we only perform the `persisted` check if `default_enabled: true` + !default_enabled || persisted?(feature) ? feature.enabled?(thing) : true + end + + def remove(key) + get(key).remove + end + + def configure + ::Unleash.configure do |config| + config.url = Gitlab.config.unleash.url + config.app_name = Gitlab.config.unleash.app_name + config.instance_id = Gitlab.config.unleash.instance_id + config.logger = Logger.new(STDOUT) # TODO: Structured logging + config.log_level = Logger::DEBUG + end + end + + private + + def context + context = Unleash::Context.new + context.user_id = 123 + end + end + end + end + end +end |