Class: Feature

Inherits:
Object
  • Object
show all
Defined in:
lib/feature/shared.rb,
lib/feature.rb,
lib/feature/gitaly.rb,
lib/feature/logger.rb,
lib/feature/definition.rb

Overview

This file can contain only simple constructs as it is shared between:

  1. `Pure Ruby`: `bin/feature-flag`

  2. `GitLab Rails`: `lib/feature/definition.rb`

Defined Under Namespace

Modules: Shared Classes: ActiveSupportCacheStoreAdapter, Definition, FlipperFeature, FlipperGate, Gitaly, Logger, Target

Constant Summary collapse

InvalidFeatureFlagError =

rubocop:disable Lint/InheritException

Class.new(Exception)
RecursionError =
Class.new(RuntimeError)

Class Method Summary collapse

Class Method Details

.allObject


41
42
43
# File 'lib/feature.rb', line 41

def all
  flipper.features.to_a
end

.disable(key, thing = false) ⇒ Object


109
110
111
112
# File 'lib/feature.rb', line 109

def disable(key, thing = false)
  log(key: key, action: __method__, thing: thing)
  with_feature(key) { _1.disable(thing) }
end

.disable_percentage_of_actors(key) ⇒ Object


129
130
131
132
# File 'lib/feature.rb', line 129

def disable_percentage_of_actors(key)
  log(key: key, action: __method__)
  with_feature(key, &:disable_percentage_of_actors)
end

.disable_percentage_of_time(key) ⇒ Object


119
120
121
122
# File 'lib/feature.rb', line 119

def disable_percentage_of_time(key)
  log(key: key, action: __method__)
  with_feature(key, &:disable_percentage_of_time)
end

.disabled?(key, thing = nil, type: :development, default_enabled: :yaml) ⇒ Boolean

Returns:

  • (Boolean)

99
100
101
102
# File 'lib/feature.rb', line 99

def disabled?(key, thing = nil, type: :development, default_enabled: :yaml)
  # 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: default_enabled) : !enabled?(key, thing, type: type, default_enabled: default_enabled)
end

.enable(key, thing = true) ⇒ Object


104
105
106
107
# File 'lib/feature.rb', line 104

def enable(key, thing = true)
  log(key: key, action: __method__, thing: thing)
  with_feature(key) { _1.enable(thing) }
end

.enable_percentage_of_actors(key, percentage) ⇒ Object


124
125
126
127
# File 'lib/feature.rb', line 124

def enable_percentage_of_actors(key, percentage)
  log(key: key, action: __method__, percentage: percentage)
  with_feature(key) { _1.enable_percentage_of_actors(percentage) }
end

.enable_percentage_of_time(key, percentage) ⇒ Object


114
115
116
117
# File 'lib/feature.rb', line 114

def enable_percentage_of_time(key, percentage)
  log(key: key, action: __method__, percentage: percentage)
  with_feature(key) { _1.enable_percentage_of_time(percentage) }
end

.enabled?(key, thing = nil, type: :development, default_enabled: :yaml) ⇒ Boolean

The default state of feature flag is read from `YAML` If feature flag does not have YAML it will fallback to `default_enabled: false` in production environment, but raise exception in development or tests

Returns:

  • (Boolean)

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/feature.rb', line 73

def enabled?(key, thing = nil, type: :development, default_enabled: :yaml)
  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

    Feature::Definition.valid_usage!(key, type: type, default_enabled: default_enabled)
  end

  # If `default_enabled: :yaml` we fetch the value from the YAML definition instead.
  default_enabled = Feature::Definition.default_enabled?(key) if default_enabled == :yaml

  feature_value = with_feature(key) do |feature|
    feature_value = current_feature_value(feature, thing, default_enabled: default_enabled)
  end

  # 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?

  # 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
end

.feature_flags_available?Boolean

Returns:

  • (Boolean)

32
33
34
35
36
37
38
39
# File 'lib/feature.rb', line 32

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

.get(key) ⇒ Object


47
48
49
# File 'lib/feature.rb', line 47

def get(key)
  with_feature(key, &:itself)
end

.log_feature_flag_state(key, feature_value) ⇒ Object


170
171
172
# File 'lib/feature.rb', line 170

def log_feature_flag_state(key, feature_value)
  logged_states[key] ||= feature_value
end

.log_feature_flag_states?(key) ⇒ Boolean

Returns:

  • (Boolean)

166
167
168
# File 'lib/feature.rb', line 166

def log_feature_flag_states?(key)
  Feature::Definition.log_states?(key)
end

.logged_statesObject


174
175
176
# File 'lib/feature.rb', line 174

def logged_states
  RequestStore.fetch(:feature_flag_events) { {} }
end

.loggerObject


162
163
164
# File 'lib/feature.rb', line 162

def logger
  @logger ||= Feature::Logger.build
end

.persisted_name?(feature_name) ⇒ Boolean

Returns:

  • (Boolean)

63
64
65
66
67
68
# File 'lib/feature.rb', line 63

def persisted_name?(feature_name)
  # 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

.persisted_namesObject


51
52
53
54
55
56
57
58
59
60
61
# File 'lib/feature.rb', line 51

def persisted_names
  return [] unless ApplicationRecord.database.exists?

  # 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
end

.register_definitionsObject


152
153
154
# File 'lib/feature.rb', line 152

def register_definitions
  Feature::Definition.reload!
end

.register_feature_groupsObject

This method is called from config/initializers/flipper.rb and can be used to register Flipper groups. See docs.gitlab.com/ee/development/feature_flags/index.html


149
150
# File 'lib/feature.rb', line 149

def register_feature_groups
end

.register_hot_reloaderObject


156
157
158
159
160
# File 'lib/feature.rb', line 156

def register_hot_reloader
  return unless check_feature_flags_definition?

  Feature::Definition.register_hot_reloader!
end

.remove(key) ⇒ Object


134
135
136
137
138
139
# File 'lib/feature.rb', line 134

def remove(key)
  return unless persisted_name?(key)

  log(key: key, action: __method__)
  with_feature(key, &:remove)
end

.resetObject


141
142
143
144
# File 'lib/feature.rb', line 141

def reset
  Gitlab::SafeRequestStore.delete(:flipper) if Gitlab::SafeRequestStore.active?
  @flipper = nil
end