flipper 0.20.1 → 0.21.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +57 -0
  3. data/Changelog.md +66 -0
  4. data/Gemfile +1 -0
  5. data/README.md +103 -47
  6. data/docs/Adapters.md +9 -9
  7. data/docs/Caveats.md +2 -2
  8. data/docs/Gates.md +74 -74
  9. data/docs/Optimization.md +70 -47
  10. data/docs/http/README.md +12 -11
  11. data/docs/images/banner.jpg +0 -0
  12. data/docs/read-only/README.md +8 -5
  13. data/examples/basic.rb +1 -12
  14. data/examples/configuring_default.rb +2 -5
  15. data/examples/dsl.rb +13 -24
  16. data/examples/enabled_for_actor.rb +8 -15
  17. data/examples/group.rb +3 -6
  18. data/examples/group_dynamic_lookup.rb +5 -19
  19. data/examples/group_with_members.rb +4 -14
  20. data/examples/importing.rb +1 -1
  21. data/examples/individual_actor.rb +2 -5
  22. data/examples/instrumentation.rb +1 -2
  23. data/examples/memoizing.rb +3 -7
  24. data/examples/percentage_of_actors.rb +6 -16
  25. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  26. data/examples/percentage_of_actors_group.rb +5 -18
  27. data/examples/percentage_of_time.rb +3 -6
  28. data/lib/flipper.rb +4 -1
  29. data/lib/flipper/adapters/http.rb +32 -28
  30. data/lib/flipper/adapters/memory.rb +20 -94
  31. data/lib/flipper/adapters/pstore.rb +4 -0
  32. data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -1
  33. data/lib/flipper/configuration.rb +33 -7
  34. data/lib/flipper/errors.rb +2 -3
  35. data/lib/flipper/identifier.rb +17 -0
  36. data/lib/flipper/middleware/memoizer.rb +29 -14
  37. data/lib/flipper/railtie.rb +38 -0
  38. data/lib/flipper/version.rb +1 -1
  39. data/spec/flipper/adapters/http_spec.rb +74 -8
  40. data/spec/flipper/adapters/memory_spec.rb +21 -1
  41. data/spec/flipper/configuration_spec.rb +20 -2
  42. data/spec/flipper/identifier_spec.rb +14 -0
  43. data/spec/flipper/middleware/memoizer_spec.rb +95 -35
  44. data/spec/flipper/middleware/setup_env_spec.rb +0 -16
  45. data/spec/flipper/railtie_spec.rb +69 -0
  46. data/spec/flipper_spec.rb +0 -1
  47. data/spec/support/spec_helpers.rb +20 -0
  48. data/test/test_helper.rb +1 -0
  49. metadata +12 -5
  50. data/examples/example_setup.rb +0 -8
data/docs/Optimization.md CHANGED
@@ -1,52 +1,63 @@
1
1
  # Optimization
2
2
 
3
- ## Memoizing Middleware
3
+ ## Memoization
4
4
 
5
- One optimization that flipper provides is a memoizing middleware. The memoizing middleware ensures that you only make one adapter call per feature per request. 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.
5
+ By default, Flipper will preload and memoize all features to ensure one adapter call per request. This means no matter how many times you check features, Flipper will only make one network request to Postgres, MySQL, Redis, Mongo or whatever adapter you are using for the length of the request.
6
6
 
7
- You can use the middleware like so for Rails:
7
+ ### Preloading
8
+
9
+ Flipper will preload all features before each request by default, which is recommended if you have a limited number of features (< 100?) and they are used on most requests. If you have a lot of features, but only a few are used on most requests, you may want to customize preloading:
8
10
 
9
11
  ```ruby
10
- # setup default instance (perhaps in config/initializers/flipper.rb)
11
- Flipper.configure do |config|
12
- config.default do
13
- Flipper.new(...)
14
- end
12
+ # config/initializers/flipper.rb
13
+ Rails.application.configure do
14
+ # Load specific features that are used on most requests
15
+ config.flipper.preload = [:stats, :search, :some_feature]
16
+
17
+ # Or completely disable preloading
18
+ config.flipper.preload = false
15
19
  end
20
+ ```
21
+
22
+ Features that are not preloaded are still memoized, ensuring one adapter call per feature during a request.
23
+
24
+ ### Skip memoization
25
+
26
+ Prevent preloading and memoization on specific requests by setting `memoize` to a proc that evaluates to false.
16
27
 
17
- # This assumes you setup a default flipper instance using configure.
18
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer
28
+ ```ruby
29
+ # config/initializers/flipper.rb
30
+ Rails.application.configure do
31
+ config.flipper.memoize = ->(request) { !request.path.start_with?("/assets") }
32
+ end
19
33
  ```
20
34
 
21
- **Note**: Be sure that the middleware is high enough up in your stack that all feature checks are wrapped.
35
+ ### Disable memoization
22
36
 
23
- **Also Note**: If you haven't setup a default instance, you can pass the instance to `SetupEnv` as `Memoizer` uses whatever is setup in the `env`:
37
+ To disable memoization entirely:
24
38
 
25
39
  ```ruby
26
- Rails.configuration.middleware.use Flipper::Middleware::SetupEnv, -> { Flipper.new(...) }
27
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer
40
+ Rails.application.configure do
41
+ config.flipper.memoize = false
42
+ end
28
43
  ```
29
44
 
30
- ### Options
31
-
32
- The Memoizer middleware also supports a few options. Use either `preload` or `preload_all`, not both.
33
-
34
- * **`:preload`** - An `Array` of feature names (`Symbol`) to preload for every request. Useful if you have features that are used on every endpoint. `preload` uses `Adapter#get_multi` to attempt to load the features in one network call instead of N+1 network calls.
35
- ```ruby
36
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
37
- preload: [:stats, :search, :some_feature]
38
- ```
39
- * **`:preload_all`** - A Boolean value (default: false) of whether or not all features should be preloaded. Using this results in a `preload_all` call with the result of `Adapter#get_all`. Any subsequent feature checks will be memoized and perform no network calls. I wouldn't recommend using this unless you have few features (< 100?) and nearly all of them are used on every request.
40
- ```ruby
41
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
42
- preload_all: true
43
- ```
44
- * **`:unless`** - A block that prevents preloading and memoization if it evaluates to true.
45
- ```ruby
46
- # skip preloading and memoizing if path starts with /assets
47
- Rails.configuration.middleware.use Flipper::Middleware::Memoizer,
48
- unless: ->(request) { request.path.start_with?("/assets") }
49
- ```
45
+ ### Advanced
46
+
47
+ Memoization is implemented as a Rack middleware, which can be used manually in any Ruby app:
48
+
49
+ ```ruby
50
+ use Flipper::Middleware::Memoizer,
51
+ preload: true,
52
+ unless: ->(request) { request.path.start_with?("/assets") }
53
+ ```
54
+
55
+ **Also Note**: If you need to customize the instance of Flipper used by the memoizer, you can pass the instance to `SetupEnv`:
56
+
57
+ ```ruby
58
+ use Flipper::Middleware::SetupEnv, -> { Flipper.new(...) }
59
+ use Flipper::Middleware::Memoizer
60
+ ```
50
61
 
51
62
  ## Cache Adapters
52
63
 
@@ -61,11 +72,15 @@ https://github.com/petergoldstein/dalli
61
72
  Example using the Dalli cache adapter with the Memory adapter and a TTL of 600 seconds:
62
73
 
63
74
  ```ruby
64
- dalli_client = Dalli::Client.new('localhost:11211')
65
- memory_adapter = Flipper::Adapters::Memory.new
66
- adapter = Flipper::Adapters::Dalli.new(memory_adapter, dalli_client, 600)
67
- flipper = Flipper.new(adapter)
75
+ Flipper.configure do |config|
76
+ config.adapter do
77
+ dalli = Dalli::Client.new('localhost:11211')
78
+ adapter = Flipper::Adapters::Memory.new
79
+ Flipper::Adapters::Dalli.new(adapter, dalli, 600)
80
+ end
81
+ end
68
82
  ```
83
+
69
84
  ### RedisCache
70
85
 
71
86
  Applications using [Redis](https://redis.io/) via the [redis-rb](https://github.com/redis/redis-rb) client can take advantage of the RedisCache adapter.
@@ -75,12 +90,15 @@ Initialize `RedisCache` with a flipper [adapter](https://github.com/jnunemaker/
75
90
  Example using the RedisCache adapter with the Memory adapter and a TTL of 4800 seconds:
76
91
 
77
92
  ```ruby
78
- require 'flipper/adapters/redis_cache'
93
+ require 'flipper/adapters/redis_cache'
79
94
 
80
- redis = Redis.new(url: ENV['REDIS_URL'])
81
- memory_adapter = Flipper::Adapters::Memory.new
82
- adapter = Flipper::Adapters::RedisCache.new(memory_adapter, redis, 4800)
83
- flipper = Flipper.new(adapter)
95
+ Flipper.configure do |config|
96
+ config.adapter do
97
+ redis = Redis.new(url: ENV['REDIS_URL'])
98
+ memory_adapter = Flipper::Adapters::Memory.new
99
+ Flipper::Adapters::RedisCache.new(memory_adapter, redis, 4800)
100
+ end
101
+ end
84
102
  ```
85
103
 
86
104
  ### ActiveSupportCacheStore
@@ -105,10 +123,15 @@ Example using the ActiveSupportCacheStore adapter with ActiveSupport's [MemorySt
105
123
  require 'active_support/cache'
106
124
  require 'flipper/adapters/active_support_cache_store'
107
125
 
108
- memory_adapter = Flipper::Adapters::Memory.new
109
- cache = ActiveSupport::Cache::MemoryStore.new
110
- adapter = Flipper::Adapters::ActiveSupportCacheStore.new(memory_adapter, cache, expires_in: 5.minutes)
111
- flipper = Flipper.new(adapter)
126
+ Flipper.configure do |config|
127
+ config.adapter do
128
+ Flipper::Adapters::ActiveSupportCacheStore.new(
129
+ Flipper::Adapters::Memory.new,
130
+ ActiveSupport::Cache::MemoryStore.new # Or Rails.cache,
131
+ expires_in: 5.minutes
132
+ )
133
+ end
134
+ end
112
135
  ```
113
136
 
114
137
  Setting `expires_in` is optional and will set an expiration time on Flipper cache keys. If specified, all flipper keys will use this `expires_in` over the `expires_in` passed to your ActiveSupport cache constructor.
data/docs/http/README.md CHANGED
@@ -8,17 +8,18 @@ Initialize the HTTP adapter with a configuration Hash.
8
8
  ```ruby
9
9
  require 'flipper/adapters/http'
10
10
 
11
- configuration = {
12
- url: 'http://app.com/mount-point', # required
13
- headers: { 'X-Custom-Header' => 'foo' },
14
- basic_auth_username: 'user123',
15
- basic_auth_password: 'password123'
16
- read_timeout: 5,
17
- open_timeout: 2,
18
- }
19
-
20
- adapter = Flipper::Adapters::Http.new(configuration)
21
- flipper = Flipper.new(adapter)
11
+ Flipper.configure do |config|
12
+ config.adapter do
13
+ Flipper::Adapters::Http.new({
14
+ url: 'http://app.com/mount-point', # required
15
+ headers: { 'X-Custom-Header' => 'foo' },
16
+ basic_auth_username: 'user123',
17
+ basic_auth_password: 'password123'
18
+ read_timeout: 5,
19
+ open_timeout: 2,
20
+ })
21
+ end
22
+ end
22
23
  ```
23
24
 
24
25
  **Required keys**:
Binary file
@@ -5,17 +5,20 @@ A [read-only](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adap
5
5
  Use this adapter to wrap another adapter and raise an exception for any writes.
6
6
 
7
7
  Any attempted write raises `Flipper::Adapters::ReadOnly::WriteAttempted` with message `'write attempted while in read only mode'`
8
+
8
9
  ## Usage
10
+
9
11
  ```ruby
10
12
  # example wrapping memory adapter
11
13
  require 'flipper/adapters/read_only'
12
14
 
13
- memory_adapter = Flipper::Adapters::Memory.new
14
- read_only_adapter = Flipper::Adapters::ReadOnly.new(memory_adapter)
15
-
16
- flipper = Flipper.new(read_only_adapter)
15
+ Flipper.configure do |config|
16
+ config.adapter do
17
+ Flipper::Adapters::ReadOnly.new(Flipper::Adapters::Memory.new)
18
+ end
19
+ end
17
20
 
18
21
  # Enabling a feature
19
- > flipper[:dashboard_panel].enable
22
+ > Flipper[:dashboard_panel].enable
20
23
  => Flipper::Adapters::ReadOnly::WriteAttempted: write attempted while in read only mode
21
24
  ```
data/examples/basic.rb CHANGED
@@ -1,17 +1,6 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
- Flipper.configure do |config|
6
- config.default do
7
- # pick an adapter, this uses memory, any will do
8
- adapter = Flipper::Adapters::Memory.new
9
-
10
- # pass adapter to handy DSL instance
11
- Flipper.new(adapter)
12
- end
13
- end
14
-
15
4
  # check if search is enabled
16
5
  if Flipper.enabled?(:search)
17
6
  puts 'Search away!'
@@ -1,12 +1,9 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
4
  # sets up default adapter so Flipper works like Flipper::DSL
6
5
  Flipper.configure do |config|
7
- config.default do
8
- Flipper.new Flipper::Adapters::Memory.new
9
- end
6
+ config.adapter { Flipper::Adapters::Memory.new }
10
7
  end
11
8
 
12
9
  puts Flipper.enabled?(:search) # => false
data/examples/dsl.rb CHANGED
@@ -1,20 +1,9 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
- adapter = Flipper::Adapters::Memory.new
6
- flipper = Flipper.new(adapter)
7
-
8
4
  # create a thing with an identifier
9
- class Person
10
- attr_reader :id
11
-
12
- def initialize(id)
13
- @id = id
14
- end
15
-
16
- # Must respond to flipper_id
17
- alias_method :flipper_id, :id
5
+ class Person < Struct.new(:id)
6
+ include Flipper::Identifier
18
7
  end
19
8
 
20
9
  person = Person.new(1)
@@ -22,14 +11,14 @@ person = Person.new(1)
22
11
  puts "Stats are disabled by default\n\n"
23
12
 
24
13
  # is a feature enabled
25
- puts "flipper.enabled? :stats: #{flipper.enabled? :stats}"
14
+ puts "flipper.enabled? :stats: #{Flipper.enabled? :stats}"
26
15
 
27
16
  # is a feature on or off for a particular person
28
- puts "flipper.enabled? :stats, person: #{flipper.enabled? :stats, person}"
17
+ puts "Flipper.enabled? :stats, person: #{Flipper.enabled? :stats, person}"
29
18
 
30
19
  # get at a feature
31
- puts "\nYou can also get an individual feature like this:\nstats = flipper[:stats]\n\n"
32
- stats = flipper[:stats]
20
+ puts "\nYou can also get an individual feature like this:\nstats = Flipper[:stats]\n\n"
21
+ stats = Flipper[:stats]
33
22
 
34
23
  # is that feature enabled
35
24
  puts "stats.enabled?: #{stats.enabled?}"
@@ -39,7 +28,7 @@ puts "stats.enabled? person: #{stats.enabled? person}"
39
28
 
40
29
  # enable a feature by name
41
30
  puts "\nEnabling stats\n\n"
42
- flipper.enable :stats
31
+ Flipper.enable :stats
43
32
 
44
33
  # or, you can use the feature to enable
45
34
  stats.enable
@@ -49,7 +38,7 @@ puts "stats.enabled? person: #{stats.enabled? person}"
49
38
 
50
39
  # oh, no, let's turn this baby off
51
40
  puts "\nDisabling stats\n\n"
52
- flipper.disable :stats
41
+ Flipper.disable :stats
53
42
 
54
43
  # or we can disable using feature obviously
55
44
  stats.disable
@@ -59,18 +48,18 @@ puts "stats.enabled? person: #{stats.enabled? person}"
59
48
  puts
60
49
 
61
50
  # get an instance of the percentage of time type set to 5
62
- puts flipper.time(5).inspect
51
+ puts Flipper.time(5).inspect
63
52
 
64
53
  # get an instance of the percentage of actors type set to 15
65
- puts flipper.actors(15).inspect
54
+ puts Flipper.actors(15).inspect
66
55
 
67
56
  # get an instance of an actor using an object that responds to flipper_id
68
57
  responds_to_flipper_id = Struct.new(:flipper_id).new(10)
69
- puts flipper.actor(responds_to_flipper_id).inspect
58
+ puts Flipper.actor(responds_to_flipper_id).inspect
70
59
 
71
60
  # get an instance of an actor using an object
72
61
  thing = Struct.new(:flipper_id).new(22)
73
- puts flipper.actor(thing).inspect
62
+ puts Flipper.actor(thing).inspect
74
63
 
75
64
  # register a top level group
76
65
  admins = Flipper.register(:admins) { |actor|
@@ -1,5 +1,4 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
4
  # Some class that represents what will be trying to do something
@@ -22,21 +21,15 @@ end
22
21
  user1 = User.new(1, true)
23
22
  user2 = User.new(2, false)
24
23
 
25
- # pick an adapter
26
- adapter = Flipper::Adapters::Memory.new
27
-
28
- # get a handy dsl instance
29
- flipper = Flipper.new(adapter)
30
-
31
24
  Flipper.register :admins do |actor|
32
25
  actor.admin?
33
26
  end
34
27
 
35
- flipper[:search].enable
36
- flipper[:stats].enable_actor user1
37
- flipper[:pro_stats].enable_percentage_of_actors 50
38
- flipper[:tweets].enable_group :admins
39
- flipper[:posts].enable_actor user2
28
+ Flipper.enable :search
29
+ Flipper.enable_actor :stats, user1
30
+ Flipper.enable_percentage_of_actors :pro_stats, 50
31
+ Flipper.enable_group :tweets, :admins
32
+ Flipper.enable_actor :posts, user2
40
33
 
41
- pp flipper.features.select { |feature| feature.enabled?(user1) }.map(&:name)
42
- pp flipper.features.select { |feature| feature.enabled?(user2) }.map(&:name)
34
+ pp Flipper.features.select { |feature| feature.enabled?(user1) }.map(&:name)
35
+ pp Flipper.features.select { |feature| feature.enabled?(user2) }.map(&:name)
data/examples/group.rb CHANGED
@@ -1,10 +1,7 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
- adapter = Flipper::Adapters::Memory.new
6
- flipper = Flipper.new(adapter)
7
- stats = flipper[:stats]
4
+ stats = Flipper[:stats]
8
5
 
9
6
  # Register group
10
7
  Flipper.register(:admins) do |actor|
@@ -35,7 +32,7 @@ puts "Stats for admin: #{stats.enabled?(admin)}"
35
32
  puts "Stats for non_admin: #{stats.enabled?(non_admin)}"
36
33
 
37
34
  puts "\nEnabling Stats for admins...\n\n"
38
- stats.enable(flipper.group(:admins))
35
+ stats.enable_group :admins
39
36
 
40
37
  puts "Stats for admin: #{stats.enabled?(admin)}"
41
38
  puts "Stats for non_admin: #{stats.enabled?(non_admin)}"
@@ -1,10 +1,7 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
- adapter = Flipper::Adapters::Memory.new
6
- flipper = Flipper.new(adapter)
7
- stats = flipper[:stats]
4
+ stats = Flipper[:stats]
8
5
 
9
6
  # Register group
10
7
  Flipper.register(:enabled_team_member) do |actor, context|
@@ -15,19 +12,12 @@ Flipper.register(:enabled_team_member) do |actor, context|
15
12
  end
16
13
 
17
14
  # Some class that represents actor that will be trying to do something
18
- class User
19
- attr_reader :id
20
-
21
- def initialize(id)
22
- @id = id
23
- end
24
-
25
- def flipper_id
26
- "User;#{@id}"
27
- end
15
+ class User < Struct.new(:id)
16
+ include Flipper::Identifier
28
17
  end
29
18
 
30
19
  class Team
20
+ include Flipper::Identifier
31
21
  attr_reader :name
32
22
 
33
23
  def self.all
@@ -51,10 +41,6 @@ class Team
51
41
  def member?(actor)
52
42
  @members.map(&:id).include?(actor.id)
53
43
  end
54
-
55
- def flipper_id
56
- "Team:#{@name}"
57
- end
58
44
  end
59
45
 
60
46
  jnunemaker = User.new("jnunemaker")
@@ -1,10 +1,7 @@
1
- require File.expand_path('../example_setup', __FILE__)
2
-
1
+ require 'bundler/setup'
3
2
  require 'flipper'
4
3
 
5
- adapter = Flipper::Adapters::Memory.new
6
- flipper = Flipper.new(adapter)
7
- stats = flipper[:stats]
4
+ stats = Flipper[:stats]
8
5
 
9
6
  # Register group
10
7
  Flipper.register(:team_actor) do |actor|
@@ -12,15 +9,8 @@ Flipper.register(:team_actor) do |actor|
12
9
  end
13
10
 
14
11
  # Some class that represents actor that will be trying to do something
15
- class User
16
- attr_reader :id
17
-
18
- def initialize(id)
19
- @id = id
20
- end
21
-
22
- # Must respond to flipper_id
23
- alias_method :flipper_id, :id
12
+ class User < Struct.new(:id)
13
+ include Flipper::Identifier
24
14
  end
25
15
 
26
16
  class Team