flipper 0.3.0 → 0.4.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 (67) hide show
  1. data/.rspec +1 -0
  2. data/Changelog.md +12 -0
  3. data/Gemfile +4 -7
  4. data/Guardfile +16 -4
  5. data/README.md +63 -34
  6. data/examples/basic.rb +1 -1
  7. data/examples/dsl.rb +10 -12
  8. data/examples/group.rb +10 -4
  9. data/examples/individual_actor.rb +9 -6
  10. data/examples/instrumentation.rb +39 -0
  11. data/examples/percentage_of_actors.rb +12 -9
  12. data/examples/percentage_of_random.rb +4 -2
  13. data/lib/flipper.rb +43 -10
  14. data/lib/flipper/adapter.rb +106 -21
  15. data/lib/flipper/adapters/memoized.rb +7 -0
  16. data/lib/flipper/adapters/memory.rb +10 -3
  17. data/lib/flipper/adapters/operation_logger.rb +7 -0
  18. data/lib/flipper/dsl.rb +73 -16
  19. data/lib/flipper/errors.rb +6 -0
  20. data/lib/flipper/feature.rb +117 -19
  21. data/lib/flipper/gate.rb +72 -4
  22. data/lib/flipper/gates/actor.rb +41 -12
  23. data/lib/flipper/gates/boolean.rb +21 -11
  24. data/lib/flipper/gates/group.rb +45 -12
  25. data/lib/flipper/gates/percentage_of_actors.rb +29 -10
  26. data/lib/flipper/gates/percentage_of_random.rb +22 -9
  27. data/lib/flipper/instrumentation/log_subscriber.rb +107 -0
  28. data/lib/flipper/instrumentation/metriks.rb +6 -0
  29. data/lib/flipper/instrumentation/metriks_subscriber.rb +92 -0
  30. data/lib/flipper/instrumenters/memory.rb +25 -0
  31. data/lib/flipper/instrumenters/noop.rb +9 -0
  32. data/lib/flipper/key.rb +23 -4
  33. data/lib/flipper/registry.rb +22 -6
  34. data/lib/flipper/spec/shared_adapter_specs.rb +59 -12
  35. data/lib/flipper/toggle.rb +19 -2
  36. data/lib/flipper/toggles/boolean.rb +36 -3
  37. data/lib/flipper/toggles/set.rb +9 -3
  38. data/lib/flipper/toggles/value.rb +9 -3
  39. data/lib/flipper/type.rb +1 -0
  40. data/lib/flipper/types/actor.rb +12 -14
  41. data/lib/flipper/types/percentage.rb +8 -2
  42. data/lib/flipper/version.rb +1 -1
  43. data/spec/flipper/adapter_spec.rb +163 -27
  44. data/spec/flipper/adapters/memoized_spec.rb +6 -6
  45. data/spec/flipper/dsl_spec.rb +51 -54
  46. data/spec/flipper/feature_spec.rb +179 -17
  47. data/spec/flipper/gate_spec.rb +47 -0
  48. data/spec/flipper/gates/actor_spec.rb +52 -0
  49. data/spec/flipper/gates/boolean_spec.rb +52 -0
  50. data/spec/flipper/gates/group_spec.rb +79 -0
  51. data/spec/flipper/gates/percentage_of_actors_spec.rb +98 -0
  52. data/spec/flipper/gates/percentage_of_random_spec.rb +54 -0
  53. data/spec/flipper/instrumentation/log_subscriber_spec.rb +104 -0
  54. data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +69 -0
  55. data/spec/flipper/instrumenters/memory_spec.rb +26 -0
  56. data/spec/flipper/instrumenters/noop_spec.rb +22 -0
  57. data/spec/flipper/key_spec.rb +8 -2
  58. data/spec/flipper/registry_spec.rb +20 -2
  59. data/spec/flipper/toggle_spec.rb +22 -0
  60. data/spec/flipper/toggles/boolean_spec.rb +40 -0
  61. data/spec/flipper/toggles/set_spec.rb +35 -0
  62. data/spec/flipper/toggles/value_spec.rb +55 -0
  63. data/spec/flipper/types/actor_spec.rb +28 -33
  64. data/spec/flipper_spec.rb +16 -3
  65. data/spec/helper.rb +37 -3
  66. data/spec/integration_spec.rb +90 -83
  67. metadata +40 -4
@@ -3,25 +3,44 @@ require 'zlib'
3
3
  module Flipper
4
4
  module Gates
5
5
  class PercentageOfActors < Gate
6
- Key = :perc_actors
6
+ # Internal: The name of the gate. Used for instrumentation, etc.
7
+ def name
8
+ :percentage_of_actors
9
+ end
7
10
 
8
- def type_key
9
- Key
11
+ # Internal: The piece of the adapter key that is unique to the gate class.
12
+ def key
13
+ :perc_actors
10
14
  end
11
15
 
12
- def open?(actor)
13
- percentage = toggle.value
16
+ # Internal: Checks if the gate is open for a thing.
17
+ #
18
+ # Returns true if gate open for thing, false if not.
19
+ def open?(thing)
20
+ instrument(:open?, thing) { |payload|
21
+ percentage = toggle.value.to_i
14
22
 
15
- if percentage.nil?
16
- false
17
- else
18
- Zlib.crc32(actor.identifier.to_s) % 100 < percentage
19
- end
23
+ if Types::Actor.wrappable?(thing)
24
+ actor = Types::Actor.wrap(thing)
25
+ key = "#{@feature.name}#{actor.value}"
26
+ Zlib.crc32(key) % 100 < percentage
27
+ else
28
+ false
29
+ end
30
+ }
20
31
  end
21
32
 
22
33
  def protects?(thing)
23
34
  thing.is_a?(Flipper::Types::PercentageOfActors)
24
35
  end
36
+
37
+ def description
38
+ if enabled?
39
+ "#{toggle.value}% of actors"
40
+ else
41
+ 'disabled'
42
+ end
43
+ end
25
44
  end
26
45
  end
27
46
  end
@@ -1,25 +1,38 @@
1
1
  module Flipper
2
2
  module Gates
3
3
  class PercentageOfRandom < Gate
4
- Key = :perc_time
4
+ # Internal: The name of the gate. Used for instrumentation, etc.
5
+ def name
6
+ :percentage_of_random
7
+ end
5
8
 
6
- def type_key
7
- Key
9
+ # Internal: The piece of the adapter key that is unique to the gate class.
10
+ def key
11
+ :perc_time
8
12
  end
9
13
 
10
- def open?(actor)
11
- percentage = toggle.value
14
+ # Internal: Checks if the gate is open for a thing.
15
+ #
16
+ # Returns true if gate open for thing, false if not.
17
+ def open?(thing)
18
+ instrument(:open?, thing) { |payload|
19
+ percentage = toggle.value.to_i
12
20
 
13
- if percentage.nil?
14
- false
15
- else
16
21
  rand < (percentage / 100.0)
17
- end
22
+ }
18
23
  end
19
24
 
20
25
  def protects?(thing)
21
26
  thing.is_a?(Flipper::Types::PercentageOfRandom)
22
27
  end
28
+
29
+ def description
30
+ if enabled?
31
+ "#{toggle.value}% of the time"
32
+ else
33
+ 'disabled'
34
+ end
35
+ end
23
36
  end
24
37
  end
25
38
  end
@@ -0,0 +1,107 @@
1
+ require 'securerandom'
2
+ require 'active_support/notifications'
3
+ require 'active_support/log_subscriber'
4
+
5
+ module Flipper
6
+ module Instrumentation
7
+ class LogSubscriber < ::ActiveSupport::LogSubscriber
8
+ # Logs a feature operation.
9
+ #
10
+ # Example Output
11
+ #
12
+ # flipper[:search].enabled?(user)
13
+ # # Flipper feature(search) enabled? false (1.2ms) [ thing=#<struct flipper_id="1"> ]
14
+ #
15
+ # Returns nothing.
16
+ def feature_operation(event)
17
+ return unless logger.debug?
18
+
19
+ feature_name = event.payload[:feature_name]
20
+ gate_name = event.payload[:gate_name]
21
+ operation = event.payload[:operation]
22
+ result = event.payload[:result]
23
+ thing = event.payload[:thing]
24
+
25
+ description = "Flipper feature(#{feature_name}) #{operation} #{result.inspect}"
26
+ details = "thing=#{thing.inspect}"
27
+
28
+ unless gate_name.nil?
29
+ details += " gate_name=#{gate_name}"
30
+ end
31
+
32
+ name = '%s (%.1fms)' % [description, event.duration]
33
+ debug " #{color(name, CYAN, true)} [ #{details} ]"
34
+ end
35
+
36
+ # Logs an adapter operation. If operation is for a feature, then that
37
+ # feature is included in log output.
38
+ #
39
+ # Example Output
40
+ #
41
+ # # log output for adapter operation with feature
42
+ # # Flipper feature(search) adapter(memory) set_add("search/actors") (0.0ms) [ result=#<Set: {"1"}> value="1" ]
43
+ #
44
+ # # log output for adapter operation with no feature
45
+ # # Flipper adapter(memory) set_add("features") (0.0ms) [ result=#<Set: {"search"}> value="search" ]
46
+ #
47
+ # Returns nothing.
48
+ def adapter_operation(event)
49
+ return unless logger.debug?
50
+
51
+ adapter_name = event.payload[:adapter_name]
52
+ operation = event.payload[:operation]
53
+ result = event.payload[:result]
54
+ value = event.payload[:value]
55
+ key = event.payload[:key]
56
+
57
+ feature_description = if key.respond_to?(:feature_name)
58
+ "Flipper feature(#{key.feature_name})"
59
+ else
60
+ "Flipper"
61
+ end
62
+
63
+ adapter_description = "adapter(#{adapter_name})"
64
+ operation_description = "#{operation}(#{key.to_s.inspect})"
65
+ description = "#{feature_description} #{adapter_description} #{operation_description}"
66
+ details = "result=#{result.inspect}"
67
+
68
+ if event.payload.key?(:value)
69
+ details += " value=#{value.inspect}"
70
+ end
71
+
72
+ name = '%s (%.1fms)' % [description, event.duration]
73
+ debug " #{color(name, CYAN, true)} [ #{details} ]"
74
+ end
75
+
76
+ # Logs a gate operation.
77
+ #
78
+ # Example Output
79
+ #
80
+ # flipper[:search].enabled?(user)
81
+ # # Flipper feature(search) gate(boolean) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
82
+ # # Flipper feature(search) gate(group) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
83
+ # # Flipper feature(search) gate(actor) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
84
+ # # Flipper feature(search) gate(percentage_of_actors) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
85
+ # # Flipper feature(search) gate(percentage_of_random) open false (0.1ms) [ thing=#<struct flipper_id="1"> ]
86
+ #
87
+ # Returns nothing.
88
+ def gate_operation(event)
89
+ return unless logger.debug?
90
+
91
+ feature_name = event.payload[:feature_name]
92
+ gate_name = event.payload[:gate_name]
93
+ operation = event.payload[:operation]
94
+ result = event.payload[:result]
95
+ thing = event.payload[:thing]
96
+
97
+ description = "Flipper feature(#{feature_name}) gate(#{gate_name}) #{operation} #{result.inspect}"
98
+ details = "thing=#{thing.inspect}"
99
+
100
+ name = '%s (%.1fms)' % [description, event.duration]
101
+ debug " #{color(name, CYAN, true)} [ #{details} ]"
102
+ end
103
+ end
104
+ end
105
+
106
+ Instrumentation::LogSubscriber.attach_to InstrumentationNamespace
107
+ end
@@ -0,0 +1,6 @@
1
+ require 'securerandom'
2
+ require 'active_support/notifications'
3
+ require 'flipper/instrumentation/metriks_subscriber'
4
+
5
+ ActiveSupport::Notifications.subscribe /\.flipper$/,
6
+ Flipper::Instrumentation::MetriksSubscriber
@@ -0,0 +1,92 @@
1
+ # Note: You should never need to require this file directly if you are using
2
+ # ActiveSupport::Notifications. Instead, you should require the metriks file
3
+ # that lives in the same directory as this file. The benefit is that it
4
+ # subscribes to the correct events and does everything for your.
5
+ require 'metriks'
6
+
7
+ module Flipper
8
+ module Instrumentation
9
+ class MetriksSubscriber
10
+ # Public: Use this as the subscribed block.
11
+ def self.call(name, start, ending, transaction_id, payload)
12
+ new(name, start, ending, transaction_id, payload).update
13
+ end
14
+
15
+ # Private: Initializes a new event processing instance.
16
+ def initialize(name, start, ending, transaction_id, payload)
17
+ @name = name
18
+ @start = start
19
+ @ending = ending
20
+ @payload = payload
21
+ @duration = ending - start
22
+ @transaction_id = transaction_id
23
+ end
24
+
25
+ def update
26
+ operation_type = @name.split('.').first
27
+ method_name = "update_#{operation_type}_metrics"
28
+
29
+ if respond_to?(method_name)
30
+ send(method_name)
31
+ else
32
+ puts "Could not update #{operation_type} metrics as MetriksSubscriber did not respond to `#{method_name}`"
33
+ end
34
+ end
35
+
36
+ def update_feature_operation_metrics
37
+ feature_name = @payload[:feature_name]
38
+ gate_name = @payload[:gate_name]
39
+ operation = strip_trailing_question_mark(@payload[:operation])
40
+ result = @payload[:result]
41
+ thing = @payload[:thing]
42
+
43
+ Metriks.timer("flipper.feature_operation.#{operation}").update(@duration)
44
+
45
+ if @payload[:operation] == :enabled?
46
+ metric_name = if result
47
+ "flipper.feature.#{feature_name}.enabled"
48
+ else
49
+ "flipper.feature.#{feature_name}.disabled"
50
+ end
51
+
52
+ Metriks.meter(metric_name).mark
53
+ end
54
+ end
55
+
56
+ def update_adapter_operation_metrics
57
+ adapter_name = @payload[:adapter_name]
58
+ operation = @payload[:operation]
59
+ result = @payload[:result]
60
+ value = @payload[:value]
61
+ key = @payload[:key]
62
+
63
+ Metriks.timer("flipper.adapter.#{adapter_name}.#{operation}").update(@duration)
64
+ end
65
+
66
+ def update_gate_operation_metrics
67
+ feature_name = @payload[:feature_name]
68
+ gate_name = @payload[:gate_name]
69
+ operation = strip_trailing_question_mark(@payload[:operation])
70
+ result = @payload[:result]
71
+ thing = @payload[:thing]
72
+
73
+ Metriks.timer("flipper.gate_operation.#{gate_name}.#{operation}").update(@duration)
74
+ Metriks.timer("flipper.feature.#{feature_name}.gate_operation.#{gate_name}.#{operation}").update(@duration)
75
+
76
+ if @payload[:operation] == :open?
77
+ metric_name = if result
78
+ "flipper.feature.#{feature_name}.gate.#{gate_name}.open"
79
+ else
80
+ "flipper.feature.#{feature_name}.gate.#{gate_name}.closed"
81
+ end
82
+
83
+ Metriks.meter(metric_name).mark
84
+ end
85
+ end
86
+
87
+ def strip_trailing_question_mark(operation)
88
+ operation.to_s.gsub(/\?$/, '')
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,25 @@
1
+ module Flipper
2
+ module Instrumenters
3
+ # Instrumentor that is useful for tests as it stores each of the events that
4
+ # are instrumented.
5
+ class Memory
6
+ Event = Struct.new(:name, :payload, :result)
7
+
8
+ attr_reader :events
9
+
10
+ def initialize
11
+ @events = []
12
+ end
13
+
14
+ def instrument(name, payload = {})
15
+ result = if block_given?
16
+ yield payload
17
+ else
18
+ nil
19
+ end
20
+ @events << Event.new(name, payload, result)
21
+ result
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Instrumenters
3
+ class Noop
4
+ def self.instrument(name, payload = {})
5
+ yield payload if block_given?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,19 +1,38 @@
1
1
  module Flipper
2
+ # Private: Used internally in flipper to create key to be used for feature in
3
+ # the adapter. You should never need to use this.
2
4
  class Key
5
+ # Private
3
6
  Separator = '/'
4
7
 
5
- attr_reader :prefix, :suffix
8
+ # Private
9
+ attr_reader :feature_name
6
10
 
7
- def initialize(prefix, suffix)
8
- @prefix, @suffix = prefix, suffix
11
+ # Private
12
+ attr_reader :gate_key
13
+
14
+ # Internal
15
+ def initialize(feature_name, gate_key)
16
+ @feature_name, @gate_key = feature_name, gate_key
9
17
  end
10
18
 
19
+ # Private
11
20
  def separator
12
21
  Separator.dup
13
22
  end
14
23
 
24
+ # Private
15
25
  def to_s
16
- "#{prefix}#{separator}#{suffix}"
26
+ "#{feature_name}#{separator}#{gate_key}"
27
+ end
28
+
29
+ # Internal: Pretty string version for debugging.
30
+ def inspect
31
+ attributes = [
32
+ "feature_name=#{feature_name.inspect}",
33
+ "gate_key=#{gate_key.inspect}",
34
+ ]
35
+ "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
17
36
  end
18
37
  end
19
38
  end
@@ -1,12 +1,22 @@
1
1
  require 'thread'
2
2
 
3
3
  module Flipper
4
+ # Internal: Used to store registry of groups by name.
4
5
  class Registry
5
6
  include Enumerable
6
7
 
7
8
  class Error < StandardError; end
8
9
  class DuplicateKey < Error; end
9
- class MissingKey < Error; end
10
+
11
+ class KeyNotFound < Error
12
+ # Public: The key that was not found
13
+ attr_reader :key
14
+
15
+ def initialize(key)
16
+ @key = key
17
+ super("Key #{key.inspect} not found")
18
+ end
19
+ end
10
20
 
11
21
  def initialize(source = {})
12
22
  @mutex = Mutex.new
@@ -22,19 +32,25 @@ module Flipper
22
32
  end
23
33
 
24
34
  def add(key, value)
25
- @mutex.synchronize do
35
+ key = key.to_sym
36
+
37
+ @mutex.synchronize {
26
38
  if @source[key]
27
39
  raise DuplicateKey, "#{key} is already registered"
28
40
  else
29
41
  @source[key] = value
30
42
  end
31
- end
43
+ }
32
44
  end
33
45
 
34
46
  def get(key)
35
- @mutex.synchronize do
36
- @source[key]
37
- end
47
+ key = key.to_sym
48
+
49
+ @mutex.synchronize {
50
+ @source.fetch(key) {
51
+ raise KeyNotFound.new(key)
52
+ }
53
+ }
38
54
  end
39
55
 
40
56
  def each(&block)
@@ -1,5 +1,12 @@
1
1
  require 'set'
2
2
 
3
+ shared_examples_for 'a working percentage' do
4
+ it "does not raise when used" do
5
+ feature.enable percentage
6
+ expect { feature.enabled?(actor) }.to_not raise_error
7
+ end
8
+ end
9
+
3
10
  # Requires the following methods
4
11
  # subject
5
12
  # read_key(key)
@@ -35,24 +42,24 @@ shared_examples_for 'a flipper adapter' do
35
42
 
36
43
  describe "#set_add" do
37
44
  it "adds value to store" do
38
- subject.set_add(key, 1)
39
- read_key(key).should eq(Set[1])
45
+ subject.set_add(key, '1')
46
+ read_key(key).should eq(Set['1'])
40
47
  end
41
48
 
42
49
  it "does not add same value more than once" do
43
- subject.set_add(key, 1)
44
- subject.set_add(key, 1)
45
- subject.set_add(key, 1)
46
- subject.set_add(key, 2)
47
- read_key(key).should eq(Set[1, 2])
50
+ subject.set_add(key, '1')
51
+ subject.set_add(key, '1')
52
+ subject.set_add(key, '1')
53
+ subject.set_add(key, '2')
54
+ read_key(key).should eq(Set['1', '2'])
48
55
  end
49
56
  end
50
57
 
51
58
  describe "#set_delete" do
52
59
  it "removes value from set if key in store" do
53
- write_key key, Set[1, 2]
54
- subject.set_delete(key, 1)
55
- read_key(key).should eq(Set[2])
60
+ write_key key, Set['1', '2']
61
+ subject.set_delete(key, '1')
62
+ read_key(key).should eq(Set['2'])
56
63
  end
57
64
 
58
65
  it "works fine if key not in store" do
@@ -66,12 +73,52 @@ shared_examples_for 'a flipper adapter' do
66
73
  end
67
74
 
68
75
  it "returns set if in store" do
69
- write_key key, Set[1, 2]
70
- subject.set_members(key).should eq(Set[1, 2])
76
+ write_key key, Set['1', '2']
77
+ subject.set_members(key).should eq(Set['1', '2'])
71
78
  end
72
79
  end
73
80
 
74
81
  it "should work with Flipper.new" do
75
82
  Flipper.new(subject).should_not be_nil
76
83
  end
84
+
85
+ context "working with values" do
86
+ it "always uses strings" do
87
+ subject.read(key).should be_nil
88
+ subject.write key, true
89
+ subject.read(key).should eq('true')
90
+
91
+ subject.write key, 22
92
+ subject.read(key).should eq('22')
93
+
94
+ subject.delete(key)
95
+ subject.read(key).should be_nil
96
+ end
97
+ end
98
+
99
+ context "working with sets" do
100
+ it "always uses strings" do
101
+ subject.set_add key, 1
102
+ subject.set_add key, 2
103
+ subject.set_members(key).should eq(Set['1', '2'])
104
+ subject.set_delete key, 2
105
+ subject.set_members(key).should eq(Set['1'])
106
+ end
107
+ end
108
+
109
+ context "integration spot-checks" do
110
+ let(:instance) { Flipper.new(subject) }
111
+ let(:feature) { instance[:feature] }
112
+ let(:actor) { Struct.new(:id).new(1) }
113
+
114
+ context "percentage of actors" do
115
+ let(:percentage) { instance.actors(10) }
116
+ it_should_behave_like 'a working percentage'
117
+ end
118
+
119
+ context "random percentage" do
120
+ let(:percentage) { instance.random(10) }
121
+ it_should_behave_like 'a working percentage'
122
+ end
123
+ end
77
124
  end