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'
2022-06-30 02:09:06 -04:00
module Feature
2017-05-31 17:06:01 -04:00
# 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
2022-01-12 04:15:13 -05:00
def feature_flags_available?
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
active_db_connection = ActiveRecord :: Base . connection . active? rescue false # rubocop:disable Database/MultipleDatabases
active_db_connection && Feature :: FlipperFeature . table_exists?
rescue ActiveRecord :: NoDatabaseError
false
end
2017-05-31 17:06:01 -04:00
def all
flipper . features . to_a
end
2022-05-03 20:09:14 -04:00
RecursionError = Class . new ( RuntimeError )
2017-05-31 17:06:01 -04:00
def get ( key )
2022-05-03 20:09:14 -04:00
with_feature ( key , & :itself )
2017-05-31 17:06:01 -04:00
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
2022-05-10 20:08:02 -04:00
# The default state of feature flag is read from `YAML`:
# 1. If feature flag does not have YAML it will fallback to `default_enabled: false`
# in production environment, but raise exception in development or tests.
# 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 )
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
2022-05-10 20:08:02 -04:00
Feature :: Definition . valid_usage! ( key , type : type )
2020-05-29 14:08:26 -04:00
end
2022-05-10 20:08:02 -04:00
default_enabled = Feature :: Definition . default_enabled? ( key , default_enabled_if_undefined : default_enabled_if_undefined )
2020-12-14 10:09:40 -05:00
2022-05-03 20:09:14 -04:00
feature_value = with_feature ( key ) do | feature |
feature_value = current_feature_value ( feature , thing , default_enabled : default_enabled )
end
2018-09-04 14:34:37 -04:00
2022-05-03 20:09:14 -04:00
# If not yielded, then either recursion is happening, or the database does not exist yet, so use default_enabled.
feature_value = default_enabled if feature_value . nil?
2021-11-22 10:14:13 -05:00
# 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
2022-05-10 20:08:02 -04:00
def disabled? ( key , thing = nil , type : :development , default_enabled_if_undefined : nil )
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
2022-05-10 20:08:02 -04:00
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 )
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 )
2022-07-04 23:08:33 -04:00
return_value = with_feature ( key ) { _1 . enable ( thing ) }
# rubocop:disable Gitlab/RailsLogger
Rails . logger . warn ( 'WARNING: Understand the stability and security risks of enabling in-development features with feature flags.' )
Rails . logger . warn ( 'See https://docs.gitlab.com/ee/administration/feature_flags.html#risks-when-enabling-features-still-in-development for more information.' )
# rubocop:enable Gitlab/RailsLogger
return_value
2017-06-21 10:49:51 -04:00
end
def disable ( key , thing = false )
2020-12-01 22:09:38 -05:00
log ( key : key , action : __method__ , thing : thing )
2022-05-03 20:09:14 -04:00
with_feature ( key ) { _1 . 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 )
2022-05-03 20:09:14 -04:00
with_feature ( key ) { _1 . enable_percentage_of_time ( percentage ) }
2020-06-02 08:08:33 -04:00
end
def disable_percentage_of_time ( key )
2020-12-01 22:09:38 -05:00
log ( key : key , action : __method__ )
2022-05-03 20:09:14 -04:00
with_feature ( key , & :disable_percentage_of_time )
2020-06-02 08:08:33 -04:00
end
def enable_percentage_of_actors ( key , percentage )
2020-12-01 22:09:38 -05:00
log ( key : key , action : __method__ , percentage : percentage )
2022-05-03 20:09:14 -04:00
with_feature ( key ) { _1 . enable_percentage_of_actors ( percentage ) }
2020-06-02 08:08:33 -04:00
end
def disable_percentage_of_actors ( key )
2020-12-01 22:09:38 -05:00
log ( key : key , action : __method__ )
2022-05-03 20:09:14 -04:00
with_feature ( key , & :disable_percentage_of_actors )
2020-06-02 08:08:33 -04:00
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__ )
2022-05-03 20:09:14 -04:00
with_feature ( 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 )
2021-12-14 16:13:08 -05:00
Feature :: Definition . log_states? ( key )
2021-11-22 10:14:13 -05:00
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
2022-04-22 14:09:22 -04:00
# Evaluate if `default enabled: false` or the feature has been persisted.
# `persisted_name?` 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`
def current_feature_value ( feature , thing , default_enabled : )
return true if default_enabled && ! Feature . persisted_name? ( feature . name )
feature . enabled? ( thing )
end
2022-05-03 20:09:14 -04:00
# NOTE: it is not safe to call `Flipper::Feature#enabled?` outside the block
def with_feature ( key )
feature = unsafe_get ( key )
yield feature if feature . present?
ensure
pop_recursion_stack
end
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?
flag_stack = :: Thread . current [ :feature_flag_recursion_check ] || [ ]
Thread . current [ :feature_flag_recursion_check ] = flag_stack
# Prevent more than 10 levels of recursion. This limit was chosen as a fairly
# low limit while allowing some nesting of flag evaluation. We have not seen
# this limit hit in production.
if flag_stack . size > 10
Gitlab :: ErrorTracking . track_exception ( RecursionError . new ( 'deep recursion' ) , stack : flag_stack )
return
elsif flag_stack . include? ( key )
Gitlab :: ErrorTracking . track_exception ( RecursionError . new ( 'self recursion' ) , stack : flag_stack )
return
end
flag_stack . push ( key )
flipper . feature ( key )
end
def pop_recursion_stack
flag_stack = Thread . current [ :feature_flag_recursion_check ]
flag_stack . pop if flag_stack
end
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
2022-05-19 05:09:08 -04:00
UnknowTargetError = Class . new ( StandardError )
2019-01-07 05:07:14 -05:00
attr_reader :params
def initialize ( params )
@params = params
end
def gate_specified?
2022-02-17 07:12:30 -05:00
% i ( user project group feature_group namespace ) . any? { | key | params . key? ( key ) }
2019-01-07 05:07:14 -05:00
end
def targets
2022-05-19 05:09:08 -04:00
[ feature_group , users , projects , groups , namespaces ] . flatten . 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
2022-05-19 05:09:08 -04:00
def users
2019-01-07 05:07:14 -05:00
return unless params . key? ( :user )
2022-05-19 05:09:08 -04:00
params [ :user ] . split ( ',' ) . map do | arg |
UserFinder . new ( arg ) . find_by_username || ( raise UnknowTargetError , " #{ arg } is not found! " )
end
2019-01-07 05:07:14 -05:00
end
2022-05-19 05:09:08 -04:00
def projects
2019-01-07 05:07:14 -05:00
return unless params . key? ( :project )
2022-05-19 05:09:08 -04:00
params [ :project ] . split ( ',' ) . map do | arg |
Project . find_by_full_path ( arg ) || ( raise UnknowTargetError , " #{ arg } is not found! " )
end
2019-01-07 05:07:14 -05:00
end
2019-02-07 15:27:03 -05:00
2022-05-19 05:09:08 -04:00
def groups
2019-02-07 15:27:03 -05:00
return unless params . key? ( :group )
2022-05-19 05:09:08 -04:00
params [ :group ] . split ( ',' ) . map do | arg |
Group . find_by_full_path ( arg ) || ( raise UnknowTargetError , " #{ arg } is not found! " )
end
2019-02-07 15:27:03 -05:00
end
2022-02-17 07:12:30 -05:00
2022-05-19 05:09:08 -04:00
def namespaces
2022-02-17 07:12:30 -05:00
return unless params . key? ( :namespace )
2022-05-19 05:09:08 -04:00
params [ :namespace ] . split ( ',' ) . map do | arg |
# We are interested in Group or UserNamespace
Namespace . without_project_namespaces . find_by_full_path ( arg ) || ( raise UnknowTargetError , " #{ arg } is not found! " )
end
2022-02-17 07:12:30 -05:00
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' )