diff options
Diffstat (limited to 'lib')
-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 |
3 files changed, 216 insertions, 103 deletions
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 |