Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/feature.rb')
-rw-r--r--lib/feature.rb107
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