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.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +26 -20
  5. data/.github/workflows/examples.yml +62 -0
  6. data/.rspec +1 -0
  7. data/.tool-versions +1 -0
  8. data/Changelog.md +152 -3
  9. data/Dockerfile +1 -1
  10. data/Gemfile +9 -6
  11. data/README.md +15 -67
  12. data/Rakefile +4 -2
  13. data/benchmark/enabled_ips.rb +10 -0
  14. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  15. data/benchmark/enabled_profile.rb +20 -0
  16. data/benchmark/instrumentation_ips.rb +21 -0
  17. data/benchmark/typecast_ips.rb +19 -0
  18. data/docker-compose.yml +37 -34
  19. data/docs/README.md +1 -0
  20. data/docs/images/banner.jpg +0 -0
  21. data/examples/api/basic.ru +3 -4
  22. data/examples/api/custom_memoized.ru +3 -4
  23. data/examples/api/memoized.ru +3 -4
  24. data/examples/dsl.rb +3 -3
  25. data/examples/enabled_for_actor.rb +4 -2
  26. data/examples/instrumentation.rb +1 -0
  27. data/examples/instrumentation_last_accessed_at.rb +38 -0
  28. data/flipper.gemspec +2 -2
  29. data/lib/flipper/actor.rb +4 -0
  30. data/lib/flipper/adapter.rb +23 -7
  31. data/lib/flipper/adapters/dual_write.rb +10 -16
  32. data/lib/flipper/adapters/failover.rb +83 -0
  33. data/lib/flipper/adapters/failsafe.rb +76 -0
  34. data/lib/flipper/adapters/http/client.rb +18 -12
  35. data/lib/flipper/adapters/http/error.rb +19 -1
  36. data/lib/flipper/adapters/http.rb +14 -4
  37. data/lib/flipper/adapters/instrumented.rb +25 -2
  38. data/lib/flipper/adapters/memoizable.rb +27 -18
  39. data/lib/flipper/adapters/memory.rb +56 -39
  40. data/lib/flipper/adapters/operation_logger.rb +16 -3
  41. data/lib/flipper/adapters/poll/poller.rb +2 -0
  42. data/lib/flipper/adapters/poll.rb +39 -0
  43. data/lib/flipper/adapters/pstore.rb +2 -5
  44. data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -6
  45. data/lib/flipper/adapters/sync/synchronizer.rb +2 -1
  46. data/lib/flipper/adapters/sync.rb +9 -15
  47. data/lib/flipper/dsl.rb +9 -11
  48. data/lib/flipper/errors.rb +3 -20
  49. data/lib/flipper/export.rb +26 -0
  50. data/lib/flipper/exporter.rb +17 -0
  51. data/lib/flipper/exporters/json/export.rb +32 -0
  52. data/lib/flipper/exporters/json/v1.rb +33 -0
  53. data/lib/flipper/feature.rb +32 -26
  54. data/lib/flipper/feature_check_context.rb +10 -6
  55. data/lib/flipper/gate.rb +12 -11
  56. data/lib/flipper/gate_values.rb +0 -16
  57. data/lib/flipper/gates/actor.rb +10 -17
  58. data/lib/flipper/gates/boolean.rb +1 -1
  59. data/lib/flipper/gates/group.rb +5 -7
  60. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  61. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  62. data/lib/flipper/identifier.rb +2 -2
  63. data/lib/flipper/instrumentation/log_subscriber.rb +7 -3
  64. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  65. data/lib/flipper/instrumenters/memory.rb +6 -2
  66. data/lib/flipper/metadata.rb +1 -1
  67. data/lib/flipper/middleware/memoizer.rb +2 -12
  68. data/lib/flipper/poller.rb +117 -0
  69. data/lib/flipper/railtie.rb +23 -22
  70. data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
  71. data/lib/flipper/test/shared_adapter_test.rb +24 -0
  72. data/lib/flipper/typecast.rb +28 -15
  73. data/lib/flipper/types/actor.rb +19 -13
  74. data/lib/flipper/types/group.rb +12 -5
  75. data/lib/flipper/version.rb +1 -1
  76. data/lib/flipper.rb +6 -4
  77. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  78. data/spec/flipper/actor_spec.rb +10 -2
  79. data/spec/flipper/adapter_spec.rb +29 -4
  80. data/spec/flipper/adapters/dual_write_spec.rb +0 -2
  81. data/spec/flipper/adapters/failover_spec.rb +129 -0
  82. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  83. data/spec/flipper/adapters/http_spec.rb +64 -6
  84. data/spec/flipper/adapters/instrumented_spec.rb +28 -12
  85. data/spec/flipper/adapters/memoizable_spec.rb +30 -12
  86. data/spec/flipper/adapters/memory_spec.rb +3 -4
  87. data/spec/flipper/adapters/operation_logger_spec.rb +29 -12
  88. data/spec/flipper/adapters/pstore_spec.rb +0 -2
  89. data/spec/flipper/adapters/read_only_spec.rb +0 -1
  90. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +0 -1
  91. data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
  92. data/spec/flipper/adapters/sync/synchronizer_spec.rb +0 -1
  93. data/spec/flipper/adapters/sync_spec.rb +0 -2
  94. data/spec/flipper/configuration_spec.rb +0 -1
  95. data/spec/flipper/dsl_spec.rb +38 -12
  96. data/spec/flipper/export_spec.rb +13 -0
  97. data/spec/flipper/exporter_spec.rb +16 -0
  98. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  99. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  100. data/spec/flipper/feature_check_context_spec.rb +17 -19
  101. data/spec/flipper/feature_spec.rb +76 -33
  102. data/spec/flipper/gate_spec.rb +0 -2
  103. data/spec/flipper/gate_values_spec.rb +2 -34
  104. data/spec/flipper/gates/actor_spec.rb +0 -2
  105. data/spec/flipper/gates/boolean_spec.rb +1 -3
  106. data/spec/flipper/gates/group_spec.rb +2 -5
  107. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
  108. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
  109. data/spec/flipper/identifier_spec.rb +0 -1
  110. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -6
  111. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -1
  112. data/spec/flipper/instrumenters/memory_spec.rb +18 -1
  113. data/spec/flipper/instrumenters/noop_spec.rb +14 -8
  114. data/spec/flipper/middleware/memoizer_spec.rb +0 -23
  115. data/spec/flipper/middleware/setup_env_spec.rb +0 -2
  116. data/spec/flipper/poller_spec.rb +47 -0
  117. data/spec/flipper/railtie_spec.rb +73 -33
  118. data/spec/flipper/registry_spec.rb +0 -1
  119. data/spec/flipper/typecast_spec.rb +82 -4
  120. data/spec/flipper/types/actor_spec.rb +45 -46
  121. data/spec/flipper/types/boolean_spec.rb +0 -1
  122. data/spec/flipper/types/group_spec.rb +2 -3
  123. data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
  124. data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
  125. data/spec/flipper/types/percentage_spec.rb +0 -1
  126. data/spec/flipper_integration_spec.rb +62 -51
  127. data/spec/flipper_spec.rb +7 -2
  128. data/spec/{helper.rb → spec_helper.rb} +4 -2
  129. data/spec/support/skippable.rb +18 -0
  130. data/spec/support/spec_helpers.rb +2 -6
  131. metadata +63 -19
  132. data/docs/Adapters.md +0 -124
  133. data/docs/Caveats.md +0 -4
  134. data/docs/Gates.md +0 -167
  135. data/docs/Instrumentation.md +0 -27
  136. data/docs/Optimization.md +0 -137
  137. data/docs/api/README.md +0 -884
  138. data/docs/http/README.md +0 -36
  139. data/docs/read-only/README.md +0 -24
@@ -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 a thing.
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?(thing = nil)
103
- instrument(:enabled?) do |payload|
104
- values = gate_values
105
- thing = gate(:actor).wrap(thing) unless thing.nil?
106
- payload[:thing] = thing
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: values,
110
- thing: thing
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[gate.key]) }
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
- GateValues.new(adapter.get(self))
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[gate.key]) }
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
- Gates::Boolean.new,
344
- Gates::Actor.new,
345
- Gates::PercentageOfActors.new,
346
- Gates::PercentageOfTime.new,
347
- Gates::Group.new,
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
- gates.detect { |gate| gate.name == name.to_sym }
361
+ gates_hash[name.to_sym]
356
362
  end
357
363
 
358
- # Public: Find the gate that protects a thing.
364
+ # Public: Find the gate that protects an actor.
359
365
  #
360
- # thing - The object for which you would like to find a gate
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 thing
364
- def gate_for(thing)
365
- gates.detect { |gate| gate.protects?(thing) } || raise(GateNotFound, thing)
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 thing we want to know if a feature is enabled for.
11
- attr_reader :thing
10
+ # Public: The actors we want to know if a feature is enabled for.
11
+ attr_reader :actors
12
12
 
13
- def initialize(options = {})
14
- @feature_name = options.fetch(:feature_name)
15
- @values = options.fetch(:values)
16
- @thing = options.fetch(:thing)
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?(_value)
21
+ def enabled?(value)
22
22
  raise 'Not implemented'
23
23
  end
24
24
 
25
- # Internal: Check if a gate is open for a thing. Implemented in subclass.
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 thing, false if not.
28
- def open?(_thing, _value, _options = {})
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 a thing. Implemented in subclass.
33
+ # Internal: Check if a gate is protects an actor. Implemented in subclass.
33
34
  #
34
- # Returns true if gate protects thing, false if not.
35
- def protects?(_thing)
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 thing using one of the supported flipper
40
- # types so adapters always get something that responds to value.
41
- def wrap(thing)
42
- thing
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.
@@ -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 &&
@@ -19,30 +19,23 @@ module Flipper
19
19
  !value.empty?
20
20
  end
21
21
 
22
- # Internal: Checks if the gate is open for a thing.
22
+ # Internal: Checks if the gate is open for an actor.
23
23
  #
24
- # Returns true if gate open for thing, false if not.
24
+ # Returns true if gate open for actor, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- if context.thing.nil?
28
- false
29
- else
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(thing)
41
- Types::Actor.wrap(thing)
33
+ def wrap(actor)
34
+ Types::Actor.wrap(actor)
42
35
  end
43
36
 
44
- def protects?(thing)
45
- Types::Actor.wrappable?(thing)
37
+ def protects?(actor)
38
+ Types::Actor.wrappable?(actor)
46
39
  end
47
40
  end
48
41
  end
@@ -24,7 +24,7 @@ module Flipper
24
24
  # Returns true if explicitly set to true, false if explicitly set to false
25
25
  # or nil if not explicitly set.
26
26
  def open?(context)
27
- context.values[key]
27
+ context.values.boolean
28
28
  end
29
29
 
30
30
  def wrap(thing)
@@ -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
- value = context.values[key]
27
- if context.thing.nil?
28
- false
29
- else
30
- value.any? do |name|
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
- # Internal: Checks if the gate is open for a thing.
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 thing, false if not.
31
+ # Returns true if gate open for any actors, false if not.
27
32
  def open?(context)
28
- percentage = context.values[key]
29
-
30
- if Types::Actor.wrappable?(context.thing)
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)
@@ -23,8 +23,7 @@ module Flipper
23
23
  #
24
24
  # Returns true if gate open for thing, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- rand < (value / 100.0)
26
+ rand < (context.values.percentage_of_time / 100.0)
28
27
  end
29
28
 
30
29
  def protects?(thing)
@@ -6,8 +6,8 @@ module Flipper
6
6
  # end
7
7
  #
8
8
  # user = User.new(99)
9
- # Flipper.enable :thing, user
10
- # Flipper.enabled? :thing, user #=> true
9
+ # Flipper.enable :some_feature, user
10
+ # Flipper.enabled? :some_feature, user #=> true
11
11
  #
12
12
  module Identifier
13
13
  def flipper_id
@@ -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) [ thing=... ]
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
- details = "thing=#{thing.inspect}"
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 = (yield payload if block_given?)
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)
@@ -1,5 +1,5 @@
1
1
  module Flipper
2
2
  METADATA = {
3
- 'changelog_uri' => 'https://github.com/jnunemaker/flipper/blob/master/Changelog.md',
3
+ 'changelog_uri' => 'https://github.com/jnunemaker/flipper/blob/main/Changelog.md',
4
4
  }.freeze
5
5
  end
@@ -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
- response = @app.call(env)
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 && !reset_on_body_close
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
@@ -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: "flipper",
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
- config = app.config.flipper
35
- if config.log && config.instrumenter == ActiveSupport::Notifications
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.identifier" do
41
- ActiveSupport.on_load(:active_record) do
42
- ActiveRecord::Base.include Flipper::Identifier
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