flipper 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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