diff options
Diffstat (limited to 'lib/feature.rb')
-rw-r--r-- | lib/feature.rb | 107 |
1 files changed, 90 insertions, 17 deletions
diff --git a/lib/feature.rb b/lib/feature.rb index 7df692ec552..4b3ebab6dce 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -4,6 +4,33 @@ require 'flipper/adapters/active_record' require 'flipper/adapters/active_support_cache_store' module Feature + module BypassLoadBalancer + FLAG = 'FEATURE_FLAGS_BYPASS_LOAD_BALANCER' + class FlipperRecord < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord -- This class perfectly replaces + # Flipper::Adapters::ActiveRecord::Model, which inherits ActiveRecord::Base + include DatabaseReflection + self.abstract_class = true + + # Bypass the load balancer by restoring the default behavior of `connection` + # before the load balancer patches ActiveRecord::Base + def self.connection + retrieve_connection + end + end + + class FlipperFeature < FlipperRecord + self.table_name = 'features' + end + + class FlipperGate < FlipperRecord + self.table_name = 'feature_gates' + end + + def self.enabled? + Gitlab::Utils.to_boolean(ENV[FLAG], default: false) + end + end + # Classes to override flipper table names class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature include DatabaseReflection @@ -33,14 +60,19 @@ module Feature # Generates the same flipper_id when in a request # If not in a request, it generates a unique flipper_id every time class FlipperRequest - def id + def flipper_id Gitlab::SafeRequestStore.fetch("flipper_request_id") do - SecureRandom.uuid + "FlipperRequest:#{SecureRandom.uuid}".freeze end end + end - def flipper_id - "FlipperRequest:#{id}" + # Generates a unique flipper_id for the current GitLab instance. + class FlipperGitlabInstance + attr_reader :flipper_id + + def initialize + @flipper_id = "FlipperGitlabInstance:#{::Gitlab.config.gitlab.host}".freeze end end @@ -65,7 +97,8 @@ module Feature end def persisted_names - return [] unless ApplicationRecord.database.exists? + model = BypassLoadBalancer.enabled? ? BypassLoadBalancer::FlipperRecord : ApplicationRecord + return [] unless model.database.exists? # This loads names of all stored feature flags # and returns a stable Set in the following order: @@ -89,14 +122,9 @@ module Feature # 2. The `default_enabled_if_undefined:` is tech debt related to Gitaly flags # and should not be used outside of Gitaly's `lib/feature/gitaly.rb` def enabled?(key, thing = nil, type: :development, default_enabled_if_undefined: nil) - if check_feature_flags_definition? - if thing && !thing.respond_to?(:flipper_id) && !thing.is_a?(Flipper::Types::Group) - raise InvalidFeatureFlagError, - "The thing '#{thing.class.name}' for feature flag '#{key}' needs to include `FeatureGate` or implement `flipper_id`" - end + thing = sanitized_thing(thing) - Feature::Definition.valid_usage!(key, type: type) - end + check_feature_flags_definition!(key, thing, type) default_enabled = Feature::Definition.default_enabled?(key, default_enabled_if_undefined: default_enabled_if_undefined) feature_value = current_feature_value(key, thing, default_enabled: default_enabled) @@ -111,11 +139,15 @@ module Feature end def disabled?(key, thing = nil, type: :development, default_enabled_if_undefined: nil) + thing = sanitized_thing(thing) + # we need to make different method calls to make it easy to mock / define expectations in test mode thing.nil? ? !enabled?(key, type: type, default_enabled_if_undefined: default_enabled_if_undefined) : !enabled?(key, thing, type: type, default_enabled_if_undefined: default_enabled_if_undefined) end def enable(key, thing = true) + thing = sanitized_thing(thing) + log(key: key, action: __method__, thing: thing) return_value = with_feature(key) { _1.enable(thing) } @@ -129,12 +161,16 @@ module Feature end def disable(key, thing = false) + thing = sanitized_thing(thing) + log(key: key, action: __method__, thing: thing) with_feature(key) { _1.disable(thing) } end def opted_out?(key, thing) + thing = sanitized_thing(thing) + return false unless thing.respond_to?(:flipper_id) # Ignore Feature::Types::Group return false unless persisted_name?(key) @@ -144,6 +180,8 @@ module Feature end def opt_out(key, thing) + thing = sanitized_thing(thing) + return unless thing.respond_to?(:flipper_id) # Ignore Feature::Types::Group log(key: key, action: __method__, thing: thing) @@ -153,6 +191,8 @@ module Feature end def remove_opt_out(key, thing) + thing = sanitized_thing(thing) + return unless thing.respond_to?(:flipper_id) # Ignore Feature::Types::Group return unless persisted_name?(key) @@ -228,6 +268,10 @@ module Feature end end + def gitlab_instance + @flipper_gitlab_instance ||= FlipperGitlabInstance.new + end + def logger @logger ||= Feature::Logger.build end @@ -246,6 +290,17 @@ module Feature private + def sanitized_thing(thing) + case thing + when :instance + gitlab_instance + when :request, :current_request + current_request + else + thing + end + end + # Compute if thing is enabled, taking opt-out overrides into account # Evaluate if `default enabled: false` or the feature has been persisted. # `persisted_name?` can potentially generate DB queries and also checks for inclusion @@ -279,7 +334,9 @@ module Feature def unsafe_get(key) # During setup the database does not exist yet. So we haven't stored a value # for the feature yet and return the default. - return unless ApplicationRecord.database.exists? + + model = BypassLoadBalancer.enabled? ? BypassLoadBalancer::FlipperRecord : ApplicationRecord + return unless model.database.exists? flag_stack = ::Thread.current[:feature_flag_recursion_check] || [] Thread.current[:feature_flag_recursion_check] = flag_stack @@ -313,10 +370,15 @@ module Feature end def build_flipper_instance(memoize: false) - active_record_adapter = Flipper::Adapters::ActiveRecord.new( - feature_class: FlipperFeature, - gate_class: FlipperGate) - + active_record_adapter = if BypassLoadBalancer.enabled? + Flipper::Adapters::ActiveRecord.new( + feature_class: BypassLoadBalancer::FlipperFeature, + gate_class: BypassLoadBalancer::FlipperGate) + else + Flipper::Adapters::ActiveRecord.new( + feature_class: FlipperFeature, + gate_class: FlipperGate) + end # Redis L2 cache redis_cache_adapter = ActiveSupportCacheStoreAdapter.new( @@ -343,6 +405,17 @@ module Feature Gitlab.dev_or_test_env? end + def check_feature_flags_definition!(key, thing, type) + return unless check_feature_flags_definition? + + if thing && !thing.respond_to?(:flipper_id) && !thing.is_a?(Flipper::Types::Group) + raise InvalidFeatureFlagError, + "The thing '#{thing.class.name}' for feature flag '#{key}' needs to include `FeatureGate` or implement `flipper_id`" + end + + Feature::Definition.valid_usage!(key, type: type) + end + def l1_cache_backend Gitlab::ProcessMemoryCache.cache_backend end |