flipper 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/Guardfile +3 -8
  2. data/README.md +26 -38
  3. data/examples/percentage_of_actors.rb +17 -12
  4. data/examples/percentage_of_random.rb +3 -7
  5. data/lib/flipper.rb +8 -1
  6. data/lib/flipper/adapter.rb +2 -208
  7. data/lib/flipper/adapters/decorator.rb +9 -0
  8. data/lib/flipper/adapters/instrumented.rb +92 -0
  9. data/lib/flipper/adapters/memoizable.rb +88 -0
  10. data/lib/flipper/adapters/memory.rb +89 -7
  11. data/lib/flipper/adapters/operation_logger.rb +31 -45
  12. data/lib/flipper/decorator.rb +6 -0
  13. data/lib/flipper/dsl.rb +29 -2
  14. data/lib/flipper/feature.rb +83 -49
  15. data/lib/flipper/gate.rb +24 -41
  16. data/lib/flipper/gates/actor.rb +24 -24
  17. data/lib/flipper/gates/boolean.rb +28 -15
  18. data/lib/flipper/gates/group.rb +25 -34
  19. data/lib/flipper/gates/percentage_of_actors.rb +21 -13
  20. data/lib/flipper/gates/percentage_of_random.rb +20 -12
  21. data/lib/flipper/instrumentation/log_subscriber.rb +14 -22
  22. data/lib/flipper/middleware/memoizer.rb +23 -0
  23. data/lib/flipper/spec/shared_adapter_specs.rb +141 -92
  24. data/lib/flipper/types/boolean.rb +5 -1
  25. data/lib/flipper/version.rb +1 -1
  26. data/spec/flipper/adapters/instrumented_spec.rb +92 -0
  27. data/spec/flipper/adapters/memoizable_spec.rb +184 -0
  28. data/spec/flipper/adapters/memory_spec.rb +1 -11
  29. data/spec/flipper/adapters/operation_logger_spec.rb +93 -0
  30. data/spec/flipper/dsl_spec.rb +18 -43
  31. data/spec/flipper/feature_spec.rb +25 -9
  32. data/spec/flipper/gate_spec.rb +8 -20
  33. data/spec/flipper/gates/actor_spec.rb +6 -14
  34. data/spec/flipper/gates/boolean_spec.rb +80 -13
  35. data/spec/flipper/gates/group_spec.rb +8 -18
  36. data/spec/flipper/gates/percentage_of_actors_spec.rb +12 -28
  37. data/spec/flipper/gates/percentage_of_random_spec.rb +6 -14
  38. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -8
  39. data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +3 -6
  40. data/spec/flipper/middleware/{local_cache_spec.rb → memoizer_spec.rb} +25 -55
  41. data/spec/flipper/types/boolean_spec.rb +13 -3
  42. data/spec/flipper_spec.rb +7 -0
  43. data/spec/helper.rb +21 -3
  44. data/spec/integration_spec.rb +115 -116
  45. metadata +17 -27
  46. data/lib/flipper/adapters/memoized.rb +0 -55
  47. data/lib/flipper/key.rb +0 -38
  48. data/lib/flipper/middleware/local_cache.rb +0 -36
  49. data/lib/flipper/toggle.rb +0 -54
  50. data/lib/flipper/toggles/boolean.rb +0 -54
  51. data/lib/flipper/toggles/set.rb +0 -25
  52. data/lib/flipper/toggles/value.rb +0 -25
  53. data/spec/flipper/adapter_spec.rb +0 -463
  54. data/spec/flipper/adapters/memoized_spec.rb +0 -93
  55. data/spec/flipper/key_spec.rb +0 -23
  56. data/spec/flipper/toggle_spec.rb +0 -22
  57. data/spec/flipper/toggles/boolean_spec.rb +0 -40
  58. data/spec/flipper/toggles/set_spec.rb +0 -35
  59. data/spec/flipper/toggles/value_spec.rb +0 -55
data/Guardfile CHANGED
@@ -13,13 +13,8 @@ rspec_options = {
13
13
  }
14
14
 
15
15
  guard 'rspec', rspec_options do
16
- watch(%r{^spec/.+_spec\.rb$})
17
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
18
- watch(%r{shared_adapter_specs\.rb$}) { |m|
19
- [
20
- "spec/flipper/adapters/memory_spec.rb",
21
- "spec/flipper/adapters/memoized_spec.rb",
22
- ]
23
- }
16
+ watch(%r{^spec/.+_spec\.rb$}) { "spec" }
17
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
18
+ watch(%r{shared_adapter_specs\.rb$}) { "spec" }
24
19
  watch('spec/helper.rb') { "spec" }
25
20
  end
data/README.md CHANGED
@@ -4,6 +4,20 @@ Feature flipping is the act of enabling or disabling features or parts of your a
4
4
 
5
5
  The goal of this gem is to make turning features on or off so easy that everyone does it. Whatever your data store, throughput, or experience, feature flipping should be easy and have minimal impact on your application.
6
6
 
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'flipper'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself with:
18
+
19
+ $ gem install flipper
20
+
7
21
  ## Coming Soon™
8
22
 
9
23
  * [Web UI](https://github.com/jnunemaker/flipper-ui) (think resque UI for features toggling/status)
@@ -155,56 +169,30 @@ Randomness is not a good idea for enabling new features in the UI. Most of the t
155
169
 
156
170
  I plan on supporting [in-memory](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb), [Mongo](https://github.com/jnunemaker/flipper-mongo), and [Redis](https://github.com/jnunemaker/flipper-redis) as adapters for flipper. Others are welcome so please let me know if you create one.
157
171
 
158
- ### Memory
159
-
160
- You can use the [in-memory adapter](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb) for tests if you want. That is pretty much all I use it for.
161
-
162
- ### Mongo
163
-
164
- Currently, the [mongo adapter](https://github.com/jnunemaker/flipper-mongo) comes in two flavors.
165
-
166
- The [vanilla mongo adapter](https://github.com/jnunemaker/flipper-mongo/blob/master/lib/flipper/adapters/mongo.rb) stores each key in its own document. This means for each gate checked per feature there will be a query to mongo.
167
-
168
- Personally, the adapter I prefer is the [single document adapter](https://github.com/jnunemaker/flipper-mongo/blob/master/lib/flipper/adapters/mongo_single_document.rb), which stores all features and gates in a single document. If you combine this adapter with the [local cache middleware](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/middleware/local_cache.rb), the document will only be queried once per request, which is pretty awesome.
169
-
170
- ### Redis
171
-
172
- Redis is great for this type of stuff and it only took a few minutes to implement a [redis adapter](https://github.com/jnunemaker/flipper-redis). The only real problem with redis right now is that automated failover isn't that easy so relying on it for every code path in my app would make me nervous.
172
+ * [memory adapter](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb) - Great for tests.
173
+ * [mongo adapter](https://github.com/jnunemaker/flipper-mongo)
174
+ * [redis adapter](https://github.com/jnunemaker/flipper-redis)
175
+ * [cassanity adapter](https://github.com/jnunemaker/flipper-cassanity)
173
176
 
174
177
  ## Optimization
175
178
 
176
- One optimization that flipper provides is a local cache. Once you have a flipper instance you can use the local cache to store each adapter key lookup in memory for as long as you want.
179
+ One optimization that flipper provides is a memoizing middleware. The memoizing middleware ensures that you only make one adapter call per feature per request.
177
180
 
178
- Out of the box there is a middleware that will store each key lookup for the duration of the request. This means you will only actually query your adapter's data store once per feature per gate that is checked. You can use the middleware from a Rails initializer like so:
181
+ This means if you check the same feature over and over, it will only make one mongo, redis, or whatever call per feature for the length of the request.
182
+
183
+ You can use the middleware from a Rails initializer like so:
179
184
 
180
185
  ```ruby
181
- require 'flipper/middleware/local_cache'
186
+ require 'flipper/middleware/memoizer'
182
187
 
183
188
  # create flipper dsl instance, see above examples for more details
184
189
  flipper = Flipper.new(...)
185
190
 
186
- # ensure entire request is wrapped, `use` would probably be ok instead of
187
- # `insert_after`, but I noticed that Rails used `insert_after` for their
188
- # identity map, which this is akin to, and figured it was for a reason.
189
- Rails.application.config.middleware.insert_after \
190
- ActionDispatch::Callbacks,
191
- Flipper::Middleware::LocalCache,
192
- flipper
191
+ # add the middleware
192
+ Rails.application.config.middleware.use Flipper::Middleware::Memoizer, flipper
193
193
  ```
194
194
 
195
- ## Installation
196
-
197
- Add this line to your application's Gemfile:
198
-
199
- gem 'flipper'
200
-
201
- And then execute:
202
-
203
- $ bundle
204
-
205
- Or install it yourself with:
206
-
207
- $ gem install flipper
195
+ **Note**: Be sure that the middlware is high enough up in your stack that all feature checks are wrapped.
208
196
 
209
197
  ## Contributing
210
198
 
@@ -19,20 +19,25 @@ class User
19
19
  alias_method :flipper_id, :id
20
20
  end
21
21
 
22
- pitt = User.new(1)
23
- clooney = User.new(10)
22
+ total = 10_000
24
23
 
25
- puts "Stats for pitt: #{stats.enabled?(pitt)}"
26
- puts "Stats for clooney: #{stats.enabled?(clooney)}"
24
+ # create array of fake users
25
+ users = (1..total).map { |n| User.new(n) }
27
26
 
28
- puts "\nEnabling stats for 5 percent...\n\n"
29
- stats.enable(Flipper::Types::PercentageOfActors.new(5))
27
+ perform_test = lambda { |number|
28
+ flipper[:stats].enable flipper.actors(number)
30
29
 
31
- puts "Stats for pitt: #{stats.enabled?(pitt)}"
32
- puts "Stats for clooney: #{stats.enabled?(clooney)}"
30
+ enabled = users.map { |user|
31
+ flipper[:stats].enabled?(user) ? true : nil
32
+ }.compact
33
33
 
34
- puts "\nEnabling stats for 50 percent...\n\n"
35
- stats.enable(Flipper::Types::PercentageOfActors.new(50))
34
+ actual = (enabled.size / total.to_f * 100).round(2)
36
35
 
37
- puts "Stats for pitt: #{stats.enabled?(pitt)}"
38
- puts "Stats for clooney: #{stats.enabled?(clooney)}"
36
+ puts "percentage: #{actual.to_s.rjust(6, ' ')} vs #{number.to_s.rjust(3, ' ')}"
37
+ }
38
+
39
+ puts "percentage: Actual vs Hoped For"
40
+
41
+ [1, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100].each do |number|
42
+ perform_test.call number
43
+ end
@@ -14,13 +14,9 @@ perform_test = lambda do |number|
14
14
  enabled = []
15
15
  disabled = []
16
16
 
17
- (1..total).each do |number|
18
- if logging.enabled?
19
- enabled << number
20
- else
21
- disabled << number
22
- end
23
- end
17
+ enabled = (1..total).map { |n|
18
+ logging.enabled? ? true : nil
19
+ }.compact
24
20
 
25
21
  actual = (enabled.size / total.to_f * 100).round(2)
26
22
 
data/lib/flipper.rb CHANGED
@@ -30,6 +30,13 @@ module Flipper
30
30
  raise DuplicateGroup, %Q{Group #{name.inspect} has already been registered}
31
31
  end
32
32
 
33
+ # Public: Clears the group registry.
34
+ #
35
+ # Returns nothing.
36
+ def self.unregister_groups
37
+ groups.clear
38
+ end
39
+
33
40
  # Internal: Fetches a group by name.
34
41
  #
35
42
  # name - The Symbol name of the group.
@@ -57,10 +64,10 @@ module Flipper
57
64
  end
58
65
  end
59
66
 
67
+ require 'flipper/adapter'
60
68
  require 'flipper/dsl'
61
69
  require 'flipper/errors'
62
70
  require 'flipper/feature'
63
71
  require 'flipper/gate'
64
72
  require 'flipper/registry'
65
- require 'flipper/toggle'
66
73
  require 'flipper/type'
@@ -1,211 +1,5 @@
1
- require 'flipper/instrumenters/noop'
2
-
3
1
  module Flipper
4
- # Internal: Adapter wrapper that wraps vanilla adapter instances. Adds things
5
- # like local caching and convenience methods for adding/reading features from
6
- # the adapter.
7
- #
8
- # So what is this local cache crap?
9
- #
10
- # The main goal of the local cache is to prevent multiple queries to an
11
- # adapter for the same key for a given amount of time (per request, per
12
- # background job, etc.).
13
- #
14
- # To facilitate with this, there is an included local cache middleware
15
- # that enables local caching for the length of a web request. The local
16
- # cache is enabled and cleared before each request and cleared and reset
17
- # to original value after each request.
18
- #
19
- # Examples
20
- #
21
- # To see an example adapter that this would wrap, checkout the [memory
22
- # adapter included with flipper](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb).
23
- class Adapter
24
- # Private: The name of instrumentation events.
25
- InstrumentationName = "adapter_operation.#{InstrumentationNamespace}"
26
-
27
- # Private: The name of the key that stores the set of known features.
28
- FeaturesKey = 'features'
29
-
30
- # Internal: Wraps vanilla adapter instance for use internally in flipper.
31
- #
32
- # object - Either an instance of Flipper::Adapter or a vanilla adapter instance
33
- #
34
- # Examples
35
- #
36
- # adapter = Flipper::Adapters::Memory.new
37
- # instance = Flipper::Adapter.new(adapter)
38
- #
39
- # Flipper::Adapter.wrap(instance)
40
- # # => Flipper::Adapter instance
41
- #
42
- # Flipper::Adapter.wrap(adapter)
43
- # # => Flipper::Adapter instance
44
- #
45
- # Returns Flipper::Adapter instance
46
- def self.wrap(object, options = {})
47
- if object.is_a?(Flipper::Adapter)
48
- object
49
- else
50
- new(object, options)
51
- end
52
- end
53
-
54
- # Private: What adapter is being wrapped and will ultimately be used.
55
- attr_reader :adapter
56
-
57
- # Private: The name of the adapter. Based on the class name.
58
- attr_reader :name
59
-
60
- # Private: What is used to store the local cache.
61
- attr_reader :local_cache
62
-
63
- # Private: What is used to instrument all the things.
64
- attr_reader :instrumenter
65
-
66
- # Internal: Initializes a new adapter instance.
67
- #
68
- # adapter - Vanilla adapter instance to wrap. Just needs to respond to
69
- # read, write, delete, set_members, set_add, and set_delete.
70
- #
71
- # options - The Hash of options.
72
- # :local_cache - Where to store the local cache data (default: {}).
73
- # Must respond to fetch(key, block), delete(key)
74
- # and clear.
75
- # :instrumenter - What to use to instrument all the things.
76
- #
77
- def initialize(adapter, options = {})
78
- @adapter = adapter
79
- @name = adapter.class.name.split('::').last.downcase.to_sym
80
- @local_cache = options[:local_cache] || {}
81
- @instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
82
- end
83
-
84
- # Public: Turns local caching on/off.
85
- #
86
- # value - The Boolean that decides if local caching is on.
87
- def use_local_cache=(value)
88
- local_cache.clear
89
- @use_local_cache = value
90
- end
91
-
92
- # Public: Returns true for using local cache, false for not.
93
- def using_local_cache?
94
- @use_local_cache == true
95
- end
96
-
97
- # Public: Reads a key.
98
- def read(key)
99
- perform_read(:read, key)
100
- end
101
-
102
- # Public: Set a key to a value.
103
- def write(key, value)
104
- perform_update(:write, key, value.to_s)
105
- end
106
-
107
- # Public: Deletes a key.
108
- def delete(key)
109
- perform_delete(:delete, key)
110
- end
111
-
112
- # Public: Returns the members of a set.
113
- def set_members(key)
114
- perform_read(:set_members, key)
115
- end
116
-
117
- # Public: Adds a value to a set.
118
- def set_add(key, value)
119
- perform_update(:set_add, key, value.to_s)
120
- end
121
-
122
- # Public: Deletes a value from a set.
123
- def set_delete(key, value)
124
- perform_update(:set_delete, key, value.to_s)
125
- end
126
-
127
- # Public: Determines equality for an adapter instance when compared to
128
- # another object.
129
- def eql?(other)
130
- self.class.eql?(other.class) && adapter == other.adapter
131
- end
132
- alias_method :==, :eql?
133
-
134
- # Public: Returns all the features that the adapter knows of.
135
- def features
136
- set_members(FeaturesKey)
137
- end
138
-
139
- # Internal: Adds a known feature to the set of features.
140
- def feature_add(name)
141
- set_add(FeaturesKey, name.to_s)
142
- end
143
-
144
- # Public: Pretty string version for debugging.
145
- def inspect
146
- attributes = [
147
- "name=#{name.inspect}",
148
- "use_local_cache=#{@use_local_cache.inspect}"
149
- ]
150
- "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
151
- end
152
-
153
- # Private
154
- def perform_read(operation, key)
155
- if using_local_cache?
156
- local_cache.fetch(key.to_s) {
157
- local_cache[key.to_s] = @adapter.send(operation, key)
158
- }
159
- else
160
- payload = {
161
- :key => key,
162
- :operation => operation,
163
- :adapter_name => @name,
164
- }
165
-
166
- @instrumenter.instrument(InstrumentationName, payload) { |payload|
167
- payload[:result] = @adapter.send(operation, key)
168
- }
169
- end
170
- end
171
-
172
- # Private
173
- def perform_update(operation, key, value)
174
- payload = {
175
- :key => key,
176
- :value => value,
177
- :operation => operation,
178
- :adapter_name => @name,
179
- }
180
-
181
- result = @instrumenter.instrument(InstrumentationName, payload) { |payload|
182
- payload[:result] = @adapter.send(operation, key, value)
183
- }
184
-
185
- if using_local_cache?
186
- local_cache.delete(key.to_s)
187
- end
188
-
189
- result
190
- end
191
-
192
- # Private
193
- def perform_delete(operation, key)
194
- payload = {
195
- :key => key,
196
- :operation => operation,
197
- :adapter_name => @name,
198
- }
199
-
200
- result = @instrumenter.instrument(InstrumentationName, payload) { |payload|
201
- payload[:result] = @adapter.send(operation, key)
202
- }
203
-
204
- if using_local_cache?
205
- local_cache.delete(key.to_s)
206
- end
207
-
208
- result
209
- end
2
+ module Adapter
3
+ # adding a module include so we have some hooks for stuff down the road
210
4
  end
211
5
  end
@@ -0,0 +1,9 @@
1
+ require 'flipper/decorator'
2
+
3
+ module Flipper
4
+ module Adapters
5
+ class Decorator < ::Flipper::Decorator
6
+ include Flipper::Adapter
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,92 @@
1
+ require 'flipper/adapters/decorator'
2
+ require 'flipper/instrumenters/noop'
3
+
4
+ module Flipper
5
+ module Adapters
6
+ class Instrumented < Decorator
7
+ # Private: The name of instrumentation events.
8
+ InstrumentationName = "adapter_operation.#{InstrumentationNamespace}"
9
+
10
+ # Private: What is used to instrument all the things.
11
+ attr_reader :instrumenter
12
+
13
+ # Internal: Initializes a new adapter instance.
14
+ #
15
+ # adapter - Vanilla adapter instance to wrap.
16
+ #
17
+ # options - The Hash of options.
18
+ # :instrumenter - What to use to instrument all the things.
19
+ #
20
+ def initialize(adapter, options = {})
21
+ super(adapter)
22
+ @name = :instrumented
23
+ @instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
24
+ end
25
+
26
+ def get(feature)
27
+ payload = {
28
+ :operation => :get,
29
+ :adapter_name => name,
30
+ :feature_name => feature.name,
31
+ }
32
+
33
+ @instrumenter.instrument(InstrumentationName, payload) { |payload|
34
+ payload[:result] = super
35
+ }
36
+ end
37
+
38
+ # Public: Enable feature gate for thing.
39
+ def enable(feature, gate, thing)
40
+ payload = {
41
+ :operation => :enable,
42
+ :adapter_name => name,
43
+ :feature_name => feature.name,
44
+ :gate_name => gate.name,
45
+ }
46
+
47
+ @instrumenter.instrument(InstrumentationName, payload) { |payload|
48
+ payload[:result] = super
49
+ }
50
+ end
51
+
52
+ # Public: Disable feature gate for thing.
53
+ def disable(feature, gate, thing)
54
+ payload = {
55
+ :operation => :disable,
56
+ :adapter_name => name,
57
+ :feature_name => feature.name,
58
+ :gate_name => gate.name,
59
+ }
60
+
61
+ @instrumenter.instrument(InstrumentationName, payload) { |payload|
62
+ payload[:result] = super
63
+ }
64
+ end
65
+
66
+ # Public: Returns all the features that the adapter knows of.
67
+ def features
68
+ payload = {
69
+ :operation => :features,
70
+ :adapter_name => name,
71
+ }
72
+
73
+ @instrumenter.instrument(InstrumentationName, payload) { |payload|
74
+ payload[:result] = super
75
+ }
76
+ end
77
+
78
+ # Internal: Adds a known feature to the set of features.
79
+ def add(feature)
80
+ payload = {
81
+ :operation => :add,
82
+ :adapter_name => name,
83
+ :feature_name => feature.name,
84
+ }
85
+
86
+ @instrumenter.instrument(InstrumentationName, payload) { |payload|
87
+ payload[:result] = super
88
+ }
89
+ end
90
+ end
91
+ end
92
+ end