flipper 0.22.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.codeclimate.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +26 -20
- data/.github/workflows/examples.yml +62 -0
- data/.rspec +1 -0
- data/.tool-versions +1 -0
- data/Changelog.md +152 -3
- data/Dockerfile +1 -1
- data/Gemfile +9 -6
- data/README.md +15 -67
- data/Rakefile +4 -2
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_multiple_actors_ips.rb +20 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +19 -0
- data/docker-compose.yml +37 -34
- data/docs/README.md +1 -0
- data/docs/images/banner.jpg +0 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- data/examples/dsl.rb +3 -3
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/instrumentation.rb +1 -0
- data/examples/instrumentation_last_accessed_at.rb +38 -0
- data/flipper.gemspec +2 -2
- data/lib/flipper/actor.rb +4 -0
- data/lib/flipper/adapter.rb +23 -7
- data/lib/flipper/adapters/dual_write.rb +10 -16
- data/lib/flipper/adapters/failover.rb +83 -0
- data/lib/flipper/adapters/failsafe.rb +76 -0
- data/lib/flipper/adapters/http/client.rb +18 -12
- data/lib/flipper/adapters/http/error.rb +19 -1
- data/lib/flipper/adapters/http.rb +14 -4
- data/lib/flipper/adapters/instrumented.rb +25 -2
- data/lib/flipper/adapters/memoizable.rb +27 -18
- data/lib/flipper/adapters/memory.rb +56 -39
- data/lib/flipper/adapters/operation_logger.rb +16 -3
- data/lib/flipper/adapters/poll/poller.rb +2 -0
- data/lib/flipper/adapters/poll.rb +39 -0
- data/lib/flipper/adapters/pstore.rb +2 -5
- data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -6
- data/lib/flipper/adapters/sync/synchronizer.rb +2 -1
- data/lib/flipper/adapters/sync.rb +9 -15
- data/lib/flipper/dsl.rb +9 -11
- data/lib/flipper/errors.rb +3 -20
- data/lib/flipper/export.rb +26 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- data/lib/flipper/feature.rb +32 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +12 -11
- data/lib/flipper/gate_values.rb +0 -16
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/group.rb +5 -7
- data/lib/flipper/gates/percentage_of_actors.rb +10 -13
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/identifier.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +7 -3
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/instrumenters/memory.rb +6 -2
- data/lib/flipper/metadata.rb +1 -1
- data/lib/flipper/middleware/memoizer.rb +2 -12
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/railtie.rb +23 -22
- data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
- data/lib/flipper/test/shared_adapter_test.rb +24 -0
- data/lib/flipper/typecast.rb +28 -15
- data/lib/flipper/types/actor.rb +19 -13
- data/lib/flipper/types/group.rb +12 -5
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +6 -4
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/actor_spec.rb +10 -2
- data/spec/flipper/adapter_spec.rb +29 -4
- data/spec/flipper/adapters/dual_write_spec.rb +0 -2
- data/spec/flipper/adapters/failover_spec.rb +129 -0
- data/spec/flipper/adapters/failsafe_spec.rb +58 -0
- data/spec/flipper/adapters/http_spec.rb +64 -6
- data/spec/flipper/adapters/instrumented_spec.rb +28 -12
- data/spec/flipper/adapters/memoizable_spec.rb +30 -12
- data/spec/flipper/adapters/memory_spec.rb +3 -4
- data/spec/flipper/adapters/operation_logger_spec.rb +29 -12
- data/spec/flipper/adapters/pstore_spec.rb +0 -2
- data/spec/flipper/adapters/read_only_spec.rb +0 -1
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync_spec.rb +0 -2
- data/spec/flipper/configuration_spec.rb +0 -1
- data/spec/flipper/dsl_spec.rb +38 -12
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- data/spec/flipper/feature_check_context_spec.rb +17 -19
- data/spec/flipper/feature_spec.rb +76 -33
- data/spec/flipper/gate_spec.rb +0 -2
- data/spec/flipper/gate_values_spec.rb +2 -34
- data/spec/flipper/gates/actor_spec.rb +0 -2
- data/spec/flipper/gates/boolean_spec.rb +1 -3
- data/spec/flipper/gates/group_spec.rb +2 -5
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
- data/spec/flipper/identifier_spec.rb +0 -1
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -1
- data/spec/flipper/instrumenters/memory_spec.rb +18 -1
- data/spec/flipper/instrumenters/noop_spec.rb +14 -8
- data/spec/flipper/middleware/memoizer_spec.rb +0 -23
- data/spec/flipper/middleware/setup_env_spec.rb +0 -2
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/railtie_spec.rb +73 -33
- data/spec/flipper/registry_spec.rb +0 -1
- data/spec/flipper/typecast_spec.rb +82 -4
- data/spec/flipper/types/actor_spec.rb +45 -46
- data/spec/flipper/types/boolean_spec.rb +0 -1
- data/spec/flipper/types/group_spec.rb +2 -3
- data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
- data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
- data/spec/flipper/types/percentage_spec.rb +0 -1
- data/spec/flipper_integration_spec.rb +62 -51
- data/spec/flipper_spec.rb +7 -2
- data/spec/{helper.rb → spec_helper.rb} +4 -2
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +2 -6
- metadata +63 -19
- data/docs/Adapters.md +0 -124
- data/docs/Caveats.md +0 -4
- data/docs/Gates.md +0 -167
- data/docs/Instrumentation.md +0 -27
- data/docs/Optimization.md +0 -137
- data/docs/api/README.md +0 -884
- data/docs/http/README.md +0 -36
- data/docs/read-only/README.md +0 -24
data/lib/flipper/feature.rb
CHANGED
|
@@ -96,18 +96,19 @@ module Flipper
|
|
|
96
96
|
instrument(:clear) { adapter.clear(self) }
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
# Public: Check if a feature is enabled for
|
|
99
|
+
# Public: Check if a feature is enabled for zero or more actors.
|
|
100
100
|
#
|
|
101
101
|
# Returns true if enabled, false if not.
|
|
102
|
-
def enabled?(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
def enabled?(*actors)
|
|
103
|
+
actors = actors.flatten.compact.map { |actor| Types::Actor.wrap(actor) }
|
|
104
|
+
actors = nil if actors.empty?
|
|
105
|
+
|
|
106
|
+
# thing is left for backwards compatibility
|
|
107
|
+
instrument(:enabled?, thing: actors&.first, actors: actors) do |payload|
|
|
107
108
|
context = FeatureCheckContext.new(
|
|
108
109
|
feature_name: @name,
|
|
109
|
-
values:
|
|
110
|
-
|
|
110
|
+
values: gate_values,
|
|
111
|
+
actors: actors
|
|
111
112
|
)
|
|
112
113
|
|
|
113
114
|
if open_gate = gates.detect { |gate| gate.open?(context) }
|
|
@@ -207,7 +208,7 @@ module Flipper
|
|
|
207
208
|
|
|
208
209
|
if values.boolean || values.percentage_of_time == 100
|
|
209
210
|
:on
|
|
210
|
-
elsif non_boolean_gates.detect { |gate| gate.enabled?(values
|
|
211
|
+
elsif non_boolean_gates.detect { |gate| gate.enabled?(values.send(gate.key)) }
|
|
211
212
|
:conditional
|
|
212
213
|
else
|
|
213
214
|
:off
|
|
@@ -232,7 +233,8 @@ module Flipper
|
|
|
232
233
|
|
|
233
234
|
# Public: Returns the raw gate values stored by the adapter.
|
|
234
235
|
def gate_values
|
|
235
|
-
|
|
236
|
+
adapter_values = adapter.get(self)
|
|
237
|
+
GateValues.new(adapter_values)
|
|
236
238
|
end
|
|
237
239
|
|
|
238
240
|
# Public: Get groups enabled for this feature.
|
|
@@ -290,7 +292,7 @@ module Flipper
|
|
|
290
292
|
# Returns an Array of Flipper::Gate instances.
|
|
291
293
|
def enabled_gates
|
|
292
294
|
values = gate_values
|
|
293
|
-
gates.select { |gate| gate.enabled?(values
|
|
295
|
+
gates.select { |gate| gate.enabled?(values.send(gate.key)) }
|
|
294
296
|
end
|
|
295
297
|
|
|
296
298
|
# Public: Get the names of the enabled gates.
|
|
@@ -339,37 +341,41 @@ module Flipper
|
|
|
339
341
|
#
|
|
340
342
|
# Returns an array of gates
|
|
341
343
|
def gates
|
|
342
|
-
@gates ||=
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
Gates::
|
|
348
|
-
|
|
344
|
+
@gates ||= gates_hash.values.freeze
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def gates_hash
|
|
348
|
+
@gates_hash ||= {
|
|
349
|
+
boolean: Gates::Boolean.new,
|
|
350
|
+
actor: Gates::Actor.new,
|
|
351
|
+
percentage_of_actors: Gates::PercentageOfActors.new,
|
|
352
|
+
percentage_of_time: Gates::PercentageOfTime.new,
|
|
353
|
+
group: Gates::Group.new,
|
|
354
|
+
}.freeze
|
|
349
355
|
end
|
|
350
356
|
|
|
351
357
|
# Public: Find a gate by name.
|
|
352
358
|
#
|
|
353
359
|
# Returns a Flipper::Gate if found, nil if not.
|
|
354
360
|
def gate(name)
|
|
355
|
-
|
|
361
|
+
gates_hash[name.to_sym]
|
|
356
362
|
end
|
|
357
363
|
|
|
358
|
-
# Public: Find the gate that protects
|
|
364
|
+
# Public: Find the gate that protects an actor.
|
|
359
365
|
#
|
|
360
|
-
#
|
|
366
|
+
# actor - The object for which you would like to find a gate
|
|
361
367
|
#
|
|
362
368
|
# Returns a Flipper::Gate.
|
|
363
|
-
# Raises Flipper::GateNotFound if no gate found for
|
|
364
|
-
def gate_for(
|
|
365
|
-
gates.detect { |gate| gate.protects?(
|
|
369
|
+
# Raises Flipper::GateNotFound if no gate found for actor
|
|
370
|
+
def gate_for(actor)
|
|
371
|
+
gates.detect { |gate| gate.protects?(actor) } || raise(GateNotFound, actor)
|
|
366
372
|
end
|
|
367
373
|
|
|
368
374
|
private
|
|
369
375
|
|
|
370
376
|
# Private: Instrument a feature operation.
|
|
371
|
-
def instrument(operation)
|
|
372
|
-
@instrumenter.instrument(InstrumentationName) do |payload|
|
|
377
|
+
def instrument(operation, initial_payload = {})
|
|
378
|
+
@instrumenter.instrument(InstrumentationName, initial_payload) do |payload|
|
|
373
379
|
payload[:feature_name] = name
|
|
374
380
|
payload[:operation] = operation
|
|
375
381
|
payload[:result] = yield(payload) if block_given?
|
|
@@ -7,13 +7,17 @@ module Flipper
|
|
|
7
7
|
# gates for the feature.
|
|
8
8
|
attr_reader :values
|
|
9
9
|
|
|
10
|
-
# Public: The
|
|
11
|
-
attr_reader :
|
|
10
|
+
# Public: The actors we want to know if a feature is enabled for.
|
|
11
|
+
attr_reader :actors
|
|
12
12
|
|
|
13
|
-
def initialize(
|
|
14
|
-
@feature_name =
|
|
15
|
-
@values =
|
|
16
|
-
@
|
|
13
|
+
def initialize(feature_name:, values:, actors:)
|
|
14
|
+
@feature_name = feature_name
|
|
15
|
+
@values = values
|
|
16
|
+
@actors = actors
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def actors?
|
|
20
|
+
!@actors.nil? && !@actors.empty?
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
# Public: Convenience method for groups value like Feature has.
|
data/lib/flipper/gate.rb
CHANGED
|
@@ -18,28 +18,29 @@ module Flipper
|
|
|
18
18
|
raise 'Not implemented'
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def enabled?(
|
|
21
|
+
def enabled?(value)
|
|
22
22
|
raise 'Not implemented'
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
# Internal: Check if a gate is open for
|
|
25
|
+
# Internal: Check if a gate is open for one or more actors. Implemented
|
|
26
|
+
# in subclass.
|
|
26
27
|
#
|
|
27
|
-
# Returns true if gate open for
|
|
28
|
-
def open?(
|
|
28
|
+
# Returns true if gate open for any actor, false if not.
|
|
29
|
+
def open?(actors, value, options = {})
|
|
29
30
|
false
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
# Internal: Check if a gate is protects
|
|
33
|
+
# Internal: Check if a gate is protects an actor. Implemented in subclass.
|
|
33
34
|
#
|
|
34
|
-
# Returns true if gate protects
|
|
35
|
-
def protects?(
|
|
35
|
+
# Returns true if gate protects actor, false if not.
|
|
36
|
+
def protects?(actor)
|
|
36
37
|
false
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
# Internal: Allows gate to wrap
|
|
40
|
-
# types so adapters always get
|
|
41
|
-
def wrap(
|
|
42
|
-
|
|
40
|
+
# Internal: Allows gate to wrap actor using one of the supported flipper
|
|
41
|
+
# types so adapters always get someactor that responds to value.
|
|
42
|
+
def wrap(actor)
|
|
43
|
+
actor
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
# Public: Pretty string version for debugging.
|
data/lib/flipper/gate_values.rb
CHANGED
|
@@ -3,16 +3,6 @@ require 'flipper/typecast'
|
|
|
3
3
|
|
|
4
4
|
module Flipper
|
|
5
5
|
class GateValues
|
|
6
|
-
# Private: Array of instance variables that are readable through the []
|
|
7
|
-
# instance method.
|
|
8
|
-
LegitIvars = {
|
|
9
|
-
'boolean' => '@boolean',
|
|
10
|
-
'actors' => '@actors',
|
|
11
|
-
'groups' => '@groups',
|
|
12
|
-
'percentage_of_time' => '@percentage_of_time',
|
|
13
|
-
'percentage_of_actors' => '@percentage_of_actors',
|
|
14
|
-
}.freeze
|
|
15
|
-
|
|
16
6
|
attr_reader :boolean
|
|
17
7
|
attr_reader :actors
|
|
18
8
|
attr_reader :groups
|
|
@@ -27,12 +17,6 @@ module Flipper
|
|
|
27
17
|
@percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time])
|
|
28
18
|
end
|
|
29
19
|
|
|
30
|
-
def [](key)
|
|
31
|
-
if ivar = LegitIvars[key.to_s]
|
|
32
|
-
instance_variable_get(ivar)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
20
|
def eql?(other)
|
|
37
21
|
self.class.eql?(other.class) &&
|
|
38
22
|
boolean == other.boolean &&
|
data/lib/flipper/gates/actor.rb
CHANGED
|
@@ -19,30 +19,23 @@ module Flipper
|
|
|
19
19
|
!value.empty?
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# Internal: Checks if the gate is open for
|
|
22
|
+
# Internal: Checks if the gate is open for an actor.
|
|
23
23
|
#
|
|
24
|
-
# Returns true if gate open for
|
|
24
|
+
# Returns true if gate open for actor, false if not.
|
|
25
25
|
def open?(context)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if protects?(context.thing)
|
|
31
|
-
actor = wrap(context.thing)
|
|
32
|
-
enabled_actor_ids = value
|
|
33
|
-
enabled_actor_ids.include?(actor.value)
|
|
34
|
-
else
|
|
35
|
-
false
|
|
36
|
-
end
|
|
26
|
+
return false unless context.actors?
|
|
27
|
+
|
|
28
|
+
context.actors.any? do |actor|
|
|
29
|
+
context.values.actors.include?(actor.value)
|
|
37
30
|
end
|
|
38
31
|
end
|
|
39
32
|
|
|
40
|
-
def wrap(
|
|
41
|
-
Types::Actor.wrap(
|
|
33
|
+
def wrap(actor)
|
|
34
|
+
Types::Actor.wrap(actor)
|
|
42
35
|
end
|
|
43
36
|
|
|
44
|
-
def protects?(
|
|
45
|
-
Types::Actor.wrappable?(
|
|
37
|
+
def protects?(actor)
|
|
38
|
+
Types::Actor.wrappable?(actor)
|
|
46
39
|
end
|
|
47
40
|
end
|
|
48
41
|
end
|
data/lib/flipper/gates/group.rb
CHANGED
|
@@ -23,13 +23,11 @@ module Flipper
|
|
|
23
23
|
#
|
|
24
24
|
# Returns true if gate open for thing, false if not.
|
|
25
25
|
def open?(context)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
group = Flipper.group(name)
|
|
32
|
-
group.match?(context.thing, context)
|
|
26
|
+
return false unless context.actors?
|
|
27
|
+
|
|
28
|
+
context.values.groups.any? do |name|
|
|
29
|
+
context.actors.any? do |actor|
|
|
30
|
+
Flipper.group(name).match?(actor, context)
|
|
33
31
|
end
|
|
34
32
|
end
|
|
35
33
|
end
|
|
@@ -21,21 +21,18 @@ module Flipper
|
|
|
21
21
|
value > 0
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
#
|
|
24
|
+
# Private: this constant is used to support up to 3 decimal places
|
|
25
|
+
# in percentages.
|
|
26
|
+
SCALING_FACTOR = 1_000
|
|
27
|
+
private_constant :SCALING_FACTOR
|
|
28
|
+
|
|
29
|
+
# Internal: Checks if the gate is open for one or more actors.
|
|
25
30
|
#
|
|
26
|
-
# Returns true if gate open for
|
|
31
|
+
# Returns true if gate open for any actors, false if not.
|
|
27
32
|
def open?(context)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
actor = Types::Actor.wrap(context.thing)
|
|
32
|
-
id = "#{context.feature_name}#{actor.value}"
|
|
33
|
-
# this is to support up to 3 decimal places in percentages
|
|
34
|
-
scaling_factor = 1_000
|
|
35
|
-
Zlib.crc32(id) % (100 * scaling_factor) < percentage * scaling_factor
|
|
36
|
-
else
|
|
37
|
-
false
|
|
38
|
-
end
|
|
33
|
+
return false unless context.actors?
|
|
34
|
+
id = "#{context.feature_name}#{context.actors.map(&:value).sort.join}"
|
|
35
|
+
Zlib.crc32(id) % (100 * SCALING_FACTOR) < context.values.percentage_of_actors * SCALING_FACTOR
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
def protects?(thing)
|
data/lib/flipper/identifier.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Flipper
|
|
|
10
10
|
# Example Output
|
|
11
11
|
#
|
|
12
12
|
# flipper[:search].enabled?(user)
|
|
13
|
-
# # Flipper feature(search) enabled? false (1.2ms) [
|
|
13
|
+
# # Flipper feature(search) enabled? false (1.2ms) [ actors=... ]
|
|
14
14
|
#
|
|
15
15
|
# Returns nothing.
|
|
16
16
|
def feature_operation(event)
|
|
@@ -20,10 +20,14 @@ module Flipper
|
|
|
20
20
|
gate_name = event.payload[:gate_name]
|
|
21
21
|
operation = event.payload[:operation]
|
|
22
22
|
result = event.payload[:result]
|
|
23
|
-
thing = event.payload[:thing]
|
|
24
23
|
|
|
25
24
|
description = "Flipper feature(#{feature_name}) #{operation} #{result.inspect}"
|
|
26
|
-
|
|
25
|
+
|
|
26
|
+
details = if event.payload.key?(:actors)
|
|
27
|
+
"actors=#{event.payload[:actors].inspect}"
|
|
28
|
+
else
|
|
29
|
+
"thing=#{event.payload[:thing].inspect}"
|
|
30
|
+
end
|
|
27
31
|
|
|
28
32
|
details += " gate_name=#{gate_name}" unless gate_name.nil?
|
|
29
33
|
|
|
@@ -45,7 +45,6 @@ module Flipper
|
|
|
45
45
|
gate_name = @payload[:gate_name]
|
|
46
46
|
operation = strip_trailing_question_mark(@payload[:operation])
|
|
47
47
|
result = @payload[:result]
|
|
48
|
-
thing = @payload[:thing]
|
|
49
48
|
|
|
50
49
|
update_timer "flipper.feature_operation.#{operation}"
|
|
51
50
|
|
|
@@ -72,6 +71,14 @@ module Flipper
|
|
|
72
71
|
update_timer "flipper.adapter.#{adapter_name}.#{operation}"
|
|
73
72
|
end
|
|
74
73
|
|
|
74
|
+
def update_poller_metrics
|
|
75
|
+
# noop
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def update_synchronizer_call_metrics
|
|
79
|
+
# noop
|
|
80
|
+
end
|
|
81
|
+
|
|
75
82
|
QUESTION_MARK = '?'.freeze
|
|
76
83
|
|
|
77
84
|
# Private
|
|
@@ -17,9 +17,13 @@ module Flipper
|
|
|
17
17
|
# block rather than the one passed to #instrument.
|
|
18
18
|
payload = payload.dup
|
|
19
19
|
|
|
20
|
-
result =
|
|
20
|
+
result = yield payload if block_given?
|
|
21
|
+
rescue Exception => e
|
|
22
|
+
payload[:exception] = [e.class.name, e.message]
|
|
23
|
+
payload[:exception_object] = e
|
|
24
|
+
raise e
|
|
25
|
+
ensure
|
|
21
26
|
@events << Event.new(name, payload, result)
|
|
22
|
-
result
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def events_by_name(name)
|
data/lib/flipper/metadata.rb
CHANGED
|
@@ -25,11 +25,6 @@ module Flipper
|
|
|
25
25
|
raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
if opts[:preload_all]
|
|
29
|
-
warn "Flipper::Middleware::Memoizer: `preload_all` is deprecated, use `preload: true`"
|
|
30
|
-
opts[:preload] = true
|
|
31
|
-
end
|
|
32
|
-
|
|
33
28
|
@app = app
|
|
34
29
|
@opts = opts
|
|
35
30
|
@env_key = opts.fetch(:env_key, 'flipper')
|
|
@@ -74,14 +69,9 @@ module Flipper
|
|
|
74
69
|
when Array then flipper.preload(@opts[:preload])
|
|
75
70
|
end
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
response[2] = Rack::BodyProxy.new(response[2]) do
|
|
79
|
-
flipper.memoize = false
|
|
80
|
-
end
|
|
81
|
-
reset_on_body_close = true
|
|
82
|
-
response
|
|
72
|
+
@app.call(env)
|
|
83
73
|
ensure
|
|
84
|
-
flipper.memoize = false if flipper
|
|
74
|
+
flipper.memoize = false if flipper
|
|
85
75
|
end
|
|
86
76
|
end
|
|
87
77
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
require 'concurrent/utility/monotonic_time'
|
|
3
|
+
require 'concurrent/map'
|
|
4
|
+
require 'concurrent/atomic/atomic_fixnum'
|
|
5
|
+
|
|
6
|
+
module Flipper
|
|
7
|
+
class Poller
|
|
8
|
+
attr_reader :adapter, :thread, :pid, :mutex, :interval, :last_synced_at
|
|
9
|
+
|
|
10
|
+
def self.instances
|
|
11
|
+
@instances ||= Concurrent::Map.new
|
|
12
|
+
end
|
|
13
|
+
private_class_method :instances
|
|
14
|
+
|
|
15
|
+
def self.get(key, options = {})
|
|
16
|
+
instances.compute_if_absent(key) { new(options) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.reset
|
|
20
|
+
instances.each {|_,poller| poller.stop }.clear
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(options = {})
|
|
24
|
+
@thread = nil
|
|
25
|
+
@pid = Process.pid
|
|
26
|
+
@mutex = Mutex.new
|
|
27
|
+
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
|
28
|
+
@remote_adapter = options.fetch(:remote_adapter)
|
|
29
|
+
@interval = options.fetch(:interval, 10).to_f
|
|
30
|
+
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
|
31
|
+
@adapter = Adapters::Memory.new
|
|
32
|
+
|
|
33
|
+
if @interval < 1
|
|
34
|
+
warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
|
|
35
|
+
@interval = 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@start_automatically = options.fetch(:start_automatically, true)
|
|
39
|
+
|
|
40
|
+
if options.fetch(:shutdown_automatically, true)
|
|
41
|
+
at_exit { stop }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def start
|
|
46
|
+
reset if forked?
|
|
47
|
+
ensure_worker_running
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stop
|
|
51
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
|
52
|
+
operation: :stop,
|
|
53
|
+
})
|
|
54
|
+
@thread&.kill
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def run
|
|
58
|
+
loop do
|
|
59
|
+
sleep jitter
|
|
60
|
+
start = Concurrent.monotonic_time
|
|
61
|
+
begin
|
|
62
|
+
sync
|
|
63
|
+
rescue => exception
|
|
64
|
+
# you can instrument these using poller.flipper
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sleep_interval = interval - (Concurrent.monotonic_time - start)
|
|
68
|
+
sleep sleep_interval if sleep_interval.positive?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sync
|
|
73
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
|
|
74
|
+
@adapter.import @remote_adapter
|
|
75
|
+
@last_synced_at.update { |time| Concurrent.monotonic_time }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def jitter
|
|
82
|
+
rand
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def forked?
|
|
86
|
+
pid != Process.pid
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ensure_worker_running
|
|
90
|
+
# Return early if thread is alive and avoid the mutex lock and unlock.
|
|
91
|
+
return if thread_alive?
|
|
92
|
+
|
|
93
|
+
# If another thread is starting worker thread, then return early so this
|
|
94
|
+
# thread can enqueue and move on with life.
|
|
95
|
+
return unless mutex.try_lock
|
|
96
|
+
|
|
97
|
+
begin
|
|
98
|
+
return if thread_alive?
|
|
99
|
+
@thread = Thread.new { run }
|
|
100
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
|
101
|
+
operation: :thread_start,
|
|
102
|
+
})
|
|
103
|
+
ensure
|
|
104
|
+
mutex.unlock
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def thread_alive?
|
|
109
|
+
@thread && @thread.alive?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def reset
|
|
113
|
+
@pid = Process.pid
|
|
114
|
+
mutex.unlock if mutex.locked?
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/flipper/railtie.rb
CHANGED
|
@@ -2,14 +2,20 @@ module Flipper
|
|
|
2
2
|
class Railtie < Rails::Railtie
|
|
3
3
|
config.before_configuration do
|
|
4
4
|
config.flipper = ActiveSupport::OrderedOptions.new.update(
|
|
5
|
-
env_key:
|
|
6
|
-
memoize: true
|
|
7
|
-
preload: true
|
|
8
|
-
instrumenter: ActiveSupport::Notifications,
|
|
9
|
-
log: true
|
|
5
|
+
env_key: ENV.fetch('FLIPPER_ENV_KEY', 'flipper'),
|
|
6
|
+
memoize: ENV.fetch('FLIPPER_MEMOIZE', 'true').casecmp('true').zero?,
|
|
7
|
+
preload: ENV.fetch('FLIPPER_PRELOAD', 'true').casecmp('true').zero?,
|
|
8
|
+
instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
|
|
9
|
+
log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?
|
|
10
10
|
)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
initializer "flipper.identifier" do
|
|
14
|
+
ActiveSupport.on_load(:active_record) do
|
|
15
|
+
ActiveRecord::Base.include Flipper::Identifier
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
initializer "flipper.default", before: :load_config_initializers do |app|
|
|
14
20
|
Flipper.configure do |config|
|
|
15
21
|
config.default do
|
|
@@ -18,28 +24,23 @@ module Flipper
|
|
|
18
24
|
end
|
|
19
25
|
end
|
|
20
26
|
|
|
21
|
-
initializer "flipper.memoizer", after: :load_config_initializers do |app|
|
|
22
|
-
config = app.config.flipper
|
|
23
|
-
|
|
24
|
-
if config.memoize
|
|
25
|
-
app.middleware.use Flipper::Middleware::Memoizer, {
|
|
26
|
-
env_key: config.env_key,
|
|
27
|
-
preload: config.preload,
|
|
28
|
-
if: config.memoize.respond_to?(:call) ? config.memoize : nil
|
|
29
|
-
}
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
27
|
initializer "flipper.log", after: :load_config_initializers do |app|
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
flipper = app.config.flipper
|
|
29
|
+
|
|
30
|
+
if flipper.log && flipper.instrumenter == ActiveSupport::Notifications
|
|
36
31
|
require "flipper/instrumentation/log_subscriber"
|
|
37
32
|
end
|
|
38
33
|
end
|
|
39
34
|
|
|
40
|
-
initializer "flipper.
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
initializer "flipper.memoizer", after: :load_config_initializers do |app|
|
|
36
|
+
flipper = app.config.flipper
|
|
37
|
+
|
|
38
|
+
if flipper.memoize
|
|
39
|
+
app.middleware.use Flipper::Middleware::Memoizer, {
|
|
40
|
+
env_key: flipper.env_key,
|
|
41
|
+
preload: flipper.preload,
|
|
42
|
+
if: flipper.memoize.respond_to?(:call) ? flipper.memoize : nil
|
|
43
|
+
}
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
end
|
|
@@ -33,7 +33,15 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
|
33
33
|
expect(subject.class.ancestors).to include(Flipper::Adapter)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
it 'knows how to get adapter from source' do
|
|
37
|
+
adapter = Flipper::Adapters::Memory.new
|
|
38
|
+
flipper = Flipper.new(adapter)
|
|
39
|
+
expect(subject.class.from(adapter).class.ancestors).to include(Flipper::Adapter)
|
|
40
|
+
expect(subject.class.from(flipper).class.ancestors).to include(Flipper::Adapter)
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
it 'returns correct default values for the gates if none are enabled' do
|
|
44
|
+
expect(subject.get(feature)).to eq(subject.class.default_config)
|
|
37
45
|
expect(subject.get(feature)).to eq(subject.default_config)
|
|
38
46
|
end
|
|
39
47
|
|
|
@@ -304,4 +312,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
|
304
312
|
subject.enable(feature, boolean_gate, flipper.boolean(true))
|
|
305
313
|
expect(subject.get(feature)).to eq(subject.default_config.merge(boolean: "true"))
|
|
306
314
|
end
|
|
315
|
+
|
|
316
|
+
it 'can import and export' do
|
|
317
|
+
adapter = Flipper::Adapters::Memory.new
|
|
318
|
+
source_flipper = Flipper.new(adapter)
|
|
319
|
+
source_flipper.enable(:stats)
|
|
320
|
+
export = adapter.export
|
|
321
|
+
|
|
322
|
+
# some adapters cannot import so if they return false lets assert it
|
|
323
|
+
# didn't happen
|
|
324
|
+
if subject.import(export)
|
|
325
|
+
expect(flipper[:stats]).to be_enabled
|
|
326
|
+
else
|
|
327
|
+
expect(flipper[:stats]).not_to be_enabled
|
|
328
|
+
end
|
|
329
|
+
end
|
|
307
330
|
end
|