2018-10-06 19:10:08 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2018-04-19 12:53:07 -04:00
|
|
|
require 'flipper/adapters/active_record'
|
|
|
|
require 'flipper/adapters/active_support_cache_store'
|
|
|
|
|
2017-05-31 17:06:01 -04:00
|
|
|
class Feature
|
|
|
|
# Classes to override flipper table names
|
|
|
|
class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature
|
2021-11-15 10:10:57 -05:00
|
|
|
include DatabaseReflection
|
|
|
|
|
2017-05-31 17:06:01 -04:00
|
|
|
# Using `self.table_name` won't work. ActiveRecord bug?
|
|
|
|
superclass.table_name = 'features'
|
2017-10-30 11:10:31 -04:00
|
|
|
|
|
|
|
def self.feature_names
|
|
|
|
pluck(:key)
|
|
|
|
end
|
2017-05-31 17:06:01 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
class FlipperGate < Flipper::Adapters::ActiveRecord::Gate
|
|
|
|
superclass.table_name = 'feature_gates'
|
|
|
|
end
|
|
|
|
|
2021-06-07 08:10:00 -04:00
|
|
|
# To enable EE overrides
|
|
|
|
class ActiveSupportCacheStoreAdapter < Flipper::Adapters::ActiveSupportCacheStore
|
|
|
|
end
|
|
|
|
|
2020-05-29 14:08:26 -04:00
|
|
|
InvalidFeatureFlagError = Class.new(Exception) # rubocop:disable Lint/InheritException
|
|
|
|
|
2017-05-31 17:06:01 -04:00
|
|
|
class << self
|
2017-06-21 10:49:51 -04:00
|
|
|
delegate :group, to: :flipper
|
|
|
|
|
2017-05-31 17:06:01 -04:00
|
|
|
def all
|
|
|
|
flipper.features.to_a
|
|
|
|
end
|
|
|
|
|
|
|
|
def get(key)
|
|
|
|
flipper.feature(key)
|
|
|
|
end
|
|
|
|
|
2017-10-30 11:10:31 -04:00
|
|
|
def persisted_names
|
2021-11-15 10:10:57 -05:00
|
|
|
return [] unless ApplicationRecord.database.exists?
|
2020-02-12 07:09:01 -05:00
|
|
|
|
2020-06-30 08:08:57 -04:00
|
|
|
# This loads names of all stored feature flags
|
|
|
|
# and returns a stable Set in the following order:
|
|
|
|
# - Memoized: using Gitlab::SafeRequestStore or @flipper
|
|
|
|
# - L1: using Process cache
|
|
|
|
# - L2: using Redis cache
|
|
|
|
# - DB: using a single SQL query
|
|
|
|
flipper.adapter.features
|
2017-10-30 11:10:31 -04:00
|
|
|
end
|
|
|
|
|
2020-05-29 14:08:26 -04:00
|
|
|
def persisted_name?(feature_name)
|
2017-05-31 17:06:01 -04:00
|
|
|
# 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.
|
2020-05-29 14:08:26 -04:00
|
|
|
persisted_names.include?(feature_name.to_s)
|
2017-05-31 17:06:01 -04:00
|
|
|
end
|
|
|
|
|
2018-09-05 09:14:16 -04:00
|
|
|
# use `default_enabled: true` to default the flag to being `enabled`
|
|
|
|
# unless set explicitly. The default is `disabled`
|
2021-08-03 11:10:03 -04:00
|
|
|
# TODO: remove the `default_enabled:` and read it from the `definition_yaml`
|
2021-03-29 11:09:30 -04:00
|
|
|
# check: https://gitlab.com/gitlab-org/gitlab/-/issues/30228
|
2020-07-02 05:09:00 -04:00
|
|
|
def enabled?(key, thing = nil, type: :development, default_enabled: false)
|
2020-05-29 14:08:26 -04:00
|
|
|
if check_feature_flags_definition?
|
|
|
|
if thing && !thing.respond_to?(:flipper_id)
|
|
|
|
raise InvalidFeatureFlagError,
|
|
|
|
"The thing '#{thing.class.name}' for feature flag '#{key}' needs to include `FeatureGate` or implement `flipper_id`"
|
|
|
|
end
|
2020-07-02 05:09:00 -04:00
|
|
|
|
2021-03-29 11:09:30 -04:00
|
|
|
Feature::Definition.valid_usage!(key, type: type, default_enabled: default_enabled)
|
2020-05-29 14:08:26 -04:00
|
|
|
end
|
|
|
|
|
2021-03-29 11:09:30 -04:00
|
|
|
# If `default_enabled: :yaml` we fetch the value from the YAML definition instead.
|
|
|
|
default_enabled = Feature::Definition.default_enabled?(key) if default_enabled == :yaml
|
2020-12-14 10:09:40 -05:00
|
|
|
|
2020-01-10 07:07:47 -05:00
|
|
|
# During setup the database does not exist yet. So we haven't stored a value
|
|
|
|
# for the feature yet and return the default.
|
2021-11-15 10:10:57 -05:00
|
|
|
return default_enabled unless ApplicationRecord.database.exists?
|
2020-01-10 07:07:47 -05:00
|
|
|
|
2020-06-02 08:08:33 -04:00
|
|
|
feature = get(key)
|
2018-09-04 14:34:37 -04:00
|
|
|
|
2018-09-05 09:14:16 -04:00
|
|
|
# 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`
|
2021-11-22 10:14:13 -05:00
|
|
|
feature_value = !default_enabled || Feature.persisted_name?(feature.name) ? feature.enabled?(thing) : true
|
|
|
|
|
|
|
|
# If we don't filter out this flag here we will enter an infinite loop
|
|
|
|
log_feature_flag_state(key, feature_value) if log_feature_flag_states?(key)
|
|
|
|
|
|
|
|
feature_value
|
2017-06-21 10:49:51 -04:00
|
|
|
end
|
|
|
|
|
2020-07-02 05:09:00 -04:00
|
|
|
def disabled?(key, thing = nil, type: :development, default_enabled: false)
|
2018-08-20 14:52:56 -04:00
|
|
|
# we need to make different method calls to make it easy to mock / define expectations in test mode
|
2020-07-02 05:09:00 -04:00
|
|
|
thing.nil? ? !enabled?(key, type: type, default_enabled: default_enabled) : !enabled?(key, thing, type: type, default_enabled: default_enabled)
|
Add repository languages for projects
Our friends at GitHub show the programming languages for a long time,
and inspired by that this commit means to create about the same
functionality.
Language detection is done through Linguist, as before, where the
difference is that we cache the result in the database. Also, Gitaly can
incrementaly scan a repository. This is done through a shell out, which
creates overhead of about 3s each run. For now this won't be improved.
Scans are triggered by pushed to the default branch, usually `master`.
However, one exception to this rule the charts page. If we're requesting
this expensive data anyway, we just cache it in the database.
Edge cases where there is no repository, or its empty are caught in the
Repository model. This makes use of Redis caching, which is probably
already loaded.
The added model is called RepositoryLanguage, which will make it harder
if/when GitLab supports multiple repositories per project. However, for
now I think this shouldn't be a concern. Also, Language could be
confused with the i18n languages and felt like the current name was
suiteable too.
Design of the Project#Show page is done with help from @dimitrieh. This
change is not visible to the end user unless detections are done.
2018-06-06 07:10:59 -04:00
|
|
|
end
|
|
|
|
|
2017-06-21 10:49:51 -04:00
|
|
|
def enable(key, thing = true)
|
2020-12-01 22:09:38 -05:00
|
|
|
log(key: key, action: __method__, thing: thing)
|
2017-06-21 10:49:51 -04:00
|
|
|
get(key).enable(thing)
|
|
|
|
end
|
|
|
|
|
|
|
|
def disable(key, thing = false)
|
2020-12-01 22:09:38 -05:00
|
|
|
log(key: key, action: __method__, thing: thing)
|
2017-06-21 10:49:51 -04:00
|
|
|
get(key).disable(thing)
|
2017-05-12 12:44:03 -04:00
|
|
|
end
|
|
|
|
|
2020-06-02 08:08:33 -04:00
|
|
|
def enable_percentage_of_time(key, percentage)
|
2020-12-01 22:09:38 -05:00
|
|
|
log(key: key, action: __method__, percentage: percentage)
|
2020-06-02 08:08:33 -04:00
|
|
|
get(key).enable_percentage_of_time(percentage)
|
|
|
|
end
|
|
|
|
|
|
|
|
def disable_percentage_of_time(key)
|
2020-12-01 22:09:38 -05:00
|
|
|
log(key: key, action: __method__)
|
2020-06-02 08:08:33 -04:00
|
|
|
get(key).disable_percentage_of_time
|
|
|
|
end
|
|
|
|
|
|
|
|
def enable_percentage_of_actors(key, percentage)
|
2020-12-01 22:09:38 -05:00
|
|
|
log(key: key, action: __method__, percentage: percentage)
|
2020-06-02 08:08:33 -04:00
|
|
|
get(key).enable_percentage_of_actors(percentage)
|
|
|
|
end
|
|
|
|
|
|
|
|
def disable_percentage_of_actors(key)
|
2020-12-01 22:09:38 -05:00
|
|
|
log(key: key, action: __method__)
|
2020-06-02 08:08:33 -04:00
|
|
|
get(key).disable_percentage_of_actors
|
|
|
|
end
|
|
|
|
|
2019-07-30 20:41:11 -04:00
|
|
|
def remove(key)
|
2020-05-29 14:08:26 -04:00
|
|
|
return unless persisted_name?(key)
|
2019-07-30 20:41:11 -04:00
|
|
|
|
2020-12-01 22:09:38 -05:00
|
|
|
log(key: key, action: __method__)
|
2020-05-29 14:08:26 -04:00
|
|
|
get(key).remove
|
2019-07-30 20:41:11 -04:00
|
|
|
end
|
|
|
|
|
2020-06-02 08:08:33 -04:00
|
|
|
def reset
|
|
|
|
Gitlab::SafeRequestStore.delete(:flipper) if Gitlab::SafeRequestStore.active?
|
|
|
|
@flipper = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
# This method is called from config/initializers/flipper.rb and can be used
|
|
|
|
# to register Flipper groups.
|
2021-02-10 10:11:19 -05:00
|
|
|
# See https://docs.gitlab.com/ee/development/feature_flags/index.html
|
2020-06-02 08:08:33 -04:00
|
|
|
def register_feature_groups
|
|
|
|
end
|
|
|
|
|
2020-07-02 05:09:00 -04:00
|
|
|
def register_definitions
|
2020-09-21 17:09:27 -04:00
|
|
|
Feature::Definition.reload!
|
2020-07-02 05:09:00 -04:00
|
|
|
end
|
|
|
|
|
2020-09-01 08:11:01 -04:00
|
|
|
def register_hot_reloader
|
|
|
|
return unless check_feature_flags_definition?
|
|
|
|
|
|
|
|
Feature::Definition.register_hot_reloader!
|
|
|
|
end
|
|
|
|
|
2020-12-01 22:09:38 -05:00
|
|
|
def logger
|
|
|
|
@logger ||= Feature::Logger.build
|
|
|
|
end
|
|
|
|
|
2021-11-22 10:14:13 -05:00
|
|
|
def log_feature_flag_states?(key)
|
|
|
|
key != :feature_flag_state_logs && Feature.enabled?(:feature_flag_state_logs, type: :ops)
|
|
|
|
end
|
|
|
|
|
|
|
|
def log_feature_flag_state(key, feature_value)
|
|
|
|
logged_states[key] ||= feature_value
|
|
|
|
end
|
|
|
|
|
|
|
|
def logged_states
|
|
|
|
RequestStore.fetch(:feature_flag_events) { {} }
|
|
|
|
end
|
|
|
|
|
2020-06-02 08:08:33 -04:00
|
|
|
private
|
|
|
|
|
2017-05-31 17:06:01 -04:00
|
|
|
def flipper
|
2018-10-05 10:20:19 -04:00
|
|
|
if Gitlab::SafeRequestStore.active?
|
2021-10-21 11:12:54 -04:00
|
|
|
Gitlab::SafeRequestStore[:flipper] ||= build_flipper_instance(memoize: true)
|
2018-10-05 10:20:19 -04:00
|
|
|
else
|
|
|
|
@flipper ||= build_flipper_instance
|
|
|
|
end
|
2018-05-31 12:12:48 -04:00
|
|
|
end
|
|
|
|
|
2021-10-21 11:12:54 -04:00
|
|
|
def build_flipper_instance(memoize: false)
|
2018-04-19 12:53:07 -04:00
|
|
|
active_record_adapter = Flipper::Adapters::ActiveRecord.new(
|
|
|
|
feature_class: FlipperFeature,
|
|
|
|
gate_class: FlipperGate)
|
|
|
|
|
2019-07-02 14:19:30 -04:00
|
|
|
# Redis L2 cache
|
|
|
|
redis_cache_adapter =
|
2020-09-14 14:09:48 -04:00
|
|
|
ActiveSupportCacheStoreAdapter.new(
|
2019-07-02 14:19:30 -04:00
|
|
|
active_record_adapter,
|
|
|
|
l2_cache_backend,
|
2021-06-07 08:10:00 -04:00
|
|
|
expires_in: 1.hour,
|
|
|
|
write_through: true)
|
2019-07-02 14:19:30 -04:00
|
|
|
|
|
|
|
# Thread-local L1 cache: use a short timeout since we don't have a
|
|
|
|
# way to expire this cache all at once
|
2020-05-29 14:08:26 -04:00
|
|
|
flipper_adapter = Flipper::Adapters::ActiveSupportCacheStore.new(
|
2019-07-02 14:19:30 -04:00
|
|
|
redis_cache_adapter,
|
|
|
|
l1_cache_backend,
|
|
|
|
expires_in: 1.minute)
|
2020-05-29 14:08:26 -04:00
|
|
|
|
|
|
|
Flipper.new(flipper_adapter).tap do |flip|
|
2021-10-21 11:12:54 -04:00
|
|
|
flip.memoize = memoize
|
2020-05-29 14:08:26 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_feature_flags_definition?
|
|
|
|
# We want to check feature flags usage only when
|
|
|
|
# running in development or test environment
|
|
|
|
Gitlab.dev_or_test_env?
|
|
|
|
end
|
|
|
|
|
2019-07-02 14:19:30 -04:00
|
|
|
def l1_cache_backend
|
2020-05-06 17:10:00 -04:00
|
|
|
Gitlab::ProcessMemoryCache.cache_backend
|
2019-07-02 14:19:30 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def l2_cache_backend
|
|
|
|
Rails.cache
|
2018-04-19 12:53:07 -04:00
|
|
|
end
|
2020-12-01 22:09:38 -05:00
|
|
|
|
|
|
|
def log(key:, action:, **extra)
|
|
|
|
extra ||= {}
|
|
|
|
extra = extra.transform_keys { |k| "extra.#{k}" }
|
|
|
|
extra = extra.transform_values { |v| v.respond_to?(:flipper_id) ? v.flipper_id : v }
|
|
|
|
extra = extra.transform_values(&:to_s)
|
|
|
|
logger.info(key: key, action: action, **extra)
|
|
|
|
end
|
2017-05-31 17:06:01 -04:00
|
|
|
end
|
2019-01-07 05:07:14 -05:00
|
|
|
|
|
|
|
class Target
|
|
|
|
attr_reader :params
|
|
|
|
|
|
|
|
def initialize(params)
|
|
|
|
@params = params
|
|
|
|
end
|
|
|
|
|
|
|
|
def gate_specified?
|
2019-02-07 15:27:03 -05:00
|
|
|
%i(user project group feature_group).any? { |key| params.key?(key) }
|
2019-01-07 05:07:14 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def targets
|
2019-02-07 15:27:03 -05:00
|
|
|
[feature_group, user, project, group].compact
|
2019-01-07 05:07:14 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
|
|
|
def feature_group
|
|
|
|
return unless params.key?(:feature_group)
|
|
|
|
|
|
|
|
Feature.group(params[:feature_group])
|
|
|
|
end
|
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
|
|
|
|
|
|
|
def user
|
|
|
|
return unless params.key?(:user)
|
|
|
|
|
|
|
|
UserFinder.new(params[:user]).find_by_username!
|
|
|
|
end
|
|
|
|
|
|
|
|
def project
|
|
|
|
return unless params.key?(:project)
|
|
|
|
|
|
|
|
Project.find_by_full_path(params[:project])
|
|
|
|
end
|
2019-02-07 15:27:03 -05:00
|
|
|
|
|
|
|
def group
|
|
|
|
return unless params.key?(:group)
|
|
|
|
|
|
|
|
Group.find_by_full_path(params[:group])
|
|
|
|
end
|
2019-01-07 05:07:14 -05:00
|
|
|
end
|
2017-05-31 17:06:01 -04:00
|
|
|
end
|
2020-05-24 08:08:20 -04:00
|
|
|
|
2021-05-11 17:10:21 -04:00
|
|
|
Feature::ActiveSupportCacheStoreAdapter.prepend_mod_with('Feature::ActiveSupportCacheStoreAdapter')
|