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
data/docker-compose.yml CHANGED
@@ -1,34 +1,37 @@
1
- # postgres:
2
- # container_name: flipper_postgres
3
- # image: postgres:9.4
4
- redis:
5
- container_name: flipper_redis
6
- image: redis:2.8
7
- mongo:
8
- container_name: flipper_mongo
9
- image: mongo:3.3
10
- memcached:
11
- container_name: flipper_memcached
12
- image: memcached:1.4.33
13
- app:
14
- container_name: flipper_app
15
- build: .
16
- dockerfile: Dockerfile
17
- volumes:
18
- - .:/srv/app
19
- volumes_from:
20
- - bundle_cache
21
- links:
22
- # - postgres
23
- - redis
24
- - mongo
25
- - memcached
26
- environment:
27
- - REDIS_URL=redis://redis:6379
28
- - MONGODB_HOST=mongo
29
- - MEMCACHED_URL=memcached:11211
30
- bundle_cache:
31
- container_name: flipper_bundle_cache
32
- image: busybox
33
- volumes:
34
- - /bundle_cache
1
+ version: "2.4"
2
+ services:
3
+ # postgres:
4
+ # container_name: flipper_postgres
5
+ # image: postgres:9.4
6
+ redis:
7
+ container_name: flipper_redis
8
+ image: redis:6.2.5
9
+ mongo:
10
+ container_name: flipper_mongo
11
+ image: mongo:4.4.8
12
+ memcached:
13
+ container_name: flipper_memcached
14
+ image: memcached:1.4.33
15
+ app:
16
+ container_name: flipper_app
17
+ build:
18
+ context: .
19
+ dockerfile: Dockerfile
20
+ volumes:
21
+ - .:/srv/app
22
+ volumes_from:
23
+ - bundle_cache
24
+ links:
25
+ # - postgres
26
+ - redis
27
+ - mongo
28
+ - memcached
29
+ environment:
30
+ - REDIS_URL=redis://redis:6379
31
+ - MONGODB_HOST=mongo
32
+ - MEMCACHED_URL=memcached:11211
33
+ bundle_cache:
34
+ container_name: flipper_bundle_cache
35
+ image: busybox
36
+ volumes:
37
+ - /bundle_cache
data/docs/README.md ADDED
@@ -0,0 +1 @@
1
+ See [flippercloud.io](https://flippercloud.io/docs) for extensive docs.
Binary file
@@ -1,19 +1,18 @@
1
1
  #
2
2
  # Usage:
3
- # # if you want it to not reload and be really fast
4
3
  # bin/rackup examples/api/basic.ru -p 9999
5
4
  #
6
- # # if you want reloading
7
- # bin/shotgun examples/api/basic.ru -p 9999
8
- #
9
5
  # http://localhost:9999/
10
6
  #
11
7
 
12
8
  require 'bundler/setup'
9
+ require 'rack/reloader'
13
10
  require "flipper/api"
14
11
  require "flipper/adapters/pstore"
15
12
 
16
13
  # You can uncomment this to get some default data:
17
14
  # Flipper.enable :logging
18
15
 
16
+ use Rack::Reloader
17
+
19
18
  run Flipper::Api.app
@@ -1,15 +1,12 @@
1
1
  #
2
2
  # Usage:
3
- # # if you want it to not reload and be really fast
4
3
  # bin/rackup examples/api/custom_memoized.ru -p 9999
5
4
  #
6
- # # if you want reloading
7
- # bin/shotgun examples/api/custom_memoized.ru -p 9999
8
- #
9
5
  # http://localhost:9999/
10
6
  #
11
7
 
12
8
  require 'bundler/setup'
9
+ require 'rack/reloader'
13
10
  require "active_support/notifications"
14
11
  require "flipper/api"
15
12
  require "flipper/adapters/pstore"
@@ -31,6 +28,8 @@ ActiveSupport::Notifications.subscribe(/.*/, ->(*args) {
31
28
  # You can uncomment this to get some default data:
32
29
  # flipper[:logging].enable_percentage_of_time 5
33
30
 
31
+ use Rack::Reloader
32
+
34
33
  run Flipper::Api.app(flipper) { |builder|
35
34
  builder.use Flipper::Middleware::SetupEnv, flipper
36
35
  builder.use Flipper::Middleware::Memoizer, preload: true
@@ -1,15 +1,12 @@
1
1
  #
2
2
  # Usage:
3
- # # if you want it to not reload and be really fast
4
3
  # bin/rackup examples/api/memoized.ru -p 9999
5
4
  #
6
- # # if you want reloading
7
- # bin/shotgun examples/api/memoized.ru -p 9999
8
- #
9
5
  # http://localhost:9999/
10
6
  #
11
7
 
12
8
  require 'bundler/setup'
9
+ require 'rack/reloader'
13
10
  require "active_support/notifications"
14
11
  require "flipper/api"
15
12
  require "flipper/adapters/pstore"
@@ -38,6 +35,8 @@ Flipper.register(:admins) { |actor|
38
35
  # You can uncomment this to get some default data:
39
36
  # Flipper.enable :logging
40
37
 
38
+ use Rack::Reloader
39
+
41
40
  run Flipper::Api.app { |builder|
42
41
  builder.use Flipper::Middleware::Memoizer, preload: true
43
42
  }
data/examples/dsl.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'bundler/setup'
2
2
  require 'flipper'
3
3
 
4
- # create a thing with an identifier
4
+ # create an actor with an identifier
5
5
  class Person < Struct.new(:id)
6
6
  include Flipper::Identifier
7
7
  end
@@ -58,8 +58,8 @@ responds_to_flipper_id = Struct.new(:flipper_id).new(10)
58
58
  puts Flipper.actor(responds_to_flipper_id).inspect
59
59
 
60
60
  # get an instance of an actor using an object
61
- thing = Struct.new(:flipper_id).new(22)
62
- puts Flipper.actor(thing).inspect
61
+ actor = Struct.new(:flipper_id).new(22)
62
+ puts Flipper.actor(actor).inspect
63
63
 
64
64
  # register a top level group
65
65
  admins = Flipper.register(:admins) { |actor|
@@ -31,5 +31,7 @@ Flipper.enable_percentage_of_actors :pro_stats, 50
31
31
  Flipper.enable_group :tweets, :admins
32
32
  Flipper.enable_actor :posts, user2
33
33
 
34
- pp Flipper.features.select { |feature| feature.enabled?(user1) }.map(&:name)
35
- pp Flipper.features.select { |feature| feature.enabled?(user2) }.map(&:name)
34
+ pp Flipper.features.select { |feature| feature.enabled?(user1) }.map(&:name).sort
35
+ pp Flipper.features.select { |feature| feature.enabled?(user2) }.map(&:name).sort
36
+ pp Flipper.features.select { |feature| feature.enabled?(user1, user2) }.map(&:name).sort
37
+ pp Flipper.features.select { |feature| feature.enabled?([user2, user1]) }.map(&:name).sort
@@ -1,5 +1,6 @@
1
1
  require 'bundler/setup'
2
2
  require 'securerandom'
3
+ require 'active_support/isolated_execution_state'
3
4
  require 'active_support/notifications'
4
5
 
5
6
  class FlipperSubscriber
@@ -0,0 +1,38 @@
1
+ # Quick example of how to keep track of when a feature was last checked.
2
+ require 'bundler/setup'
3
+ require 'securerandom'
4
+ require 'active_support/isolated_execution_state'
5
+ require 'active_support/notifications'
6
+ require 'flipper'
7
+
8
+ class FlipperSubscriber
9
+ def self.stats
10
+ @stats ||= {}
11
+ end
12
+
13
+ def call(name, start, finish, id, payload)
14
+ if payload[:operation] == :enabled?
15
+ feature_name = payload.fetch(:feature_name)
16
+ self.class.stats[feature_name] = Time.now
17
+ end
18
+ end
19
+
20
+ ActiveSupport::Notifications.subscribe(/feature_operation.flipper/, new)
21
+ end
22
+
23
+ Flipper.configure do |config|
24
+ config.default {
25
+ Flipper.new(config.adapter, instrumenter: ActiveSupport::Notifications)
26
+ }
27
+ end
28
+
29
+ Flipper.enabled?(:search)
30
+ Flipper.enabled?(:booyeah)
31
+ Flipper.enabled?(:hooray)
32
+ sleep 1
33
+ Flipper.enabled?(:booyeah)
34
+ Flipper.enabled?(:hooray)
35
+ sleep 1
36
+ Flipper.enabled?(:hooray)
37
+
38
+ pp FlipperSubscriber.stats
data/flipper.gemspec CHANGED
@@ -13,7 +13,6 @@ end
13
13
 
14
14
  ignored_files = plugin_files
15
15
  ignored_files << Dir['script/*']
16
- ignored_files << '.travis.yml'
17
16
  ignored_files << '.gitignore'
18
17
  ignored_files << 'Guardfile'
19
18
  ignored_files.flatten!.uniq!
@@ -28,11 +27,12 @@ Gem::Specification.new do |gem|
28
27
  gem.homepage = 'https://github.com/jnunemaker/flipper'
29
28
  gem.license = 'MIT'
30
29
 
31
- gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
32
30
  gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
33
31
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
34
32
  gem.name = 'flipper'
35
33
  gem.require_paths = ['lib']
36
34
  gem.version = Flipper::VERSION
37
35
  gem.metadata = Flipper::METADATA
36
+
37
+ gem.add_dependency 'concurrent-ruby', '< 2'
38
38
  end
data/lib/flipper/actor.rb CHANGED
@@ -12,5 +12,9 @@ module Flipper
12
12
  self.class.eql?(other.class) && @flipper_id == other.flipper_id
13
13
  end
14
14
  alias_method :==, :eql?
15
+
16
+ def hash
17
+ flipper_id.hash
18
+ end
15
19
  end
16
20
  end
@@ -1,7 +1,3 @@
1
- require "set"
2
- require "flipper/feature"
3
- require "flipper/adapters/sync/synchronizer"
4
-
5
1
  module Flipper
6
2
  # Adding a module include so we have some hooks for stuff down the road
7
3
  module Adapter
@@ -20,6 +16,11 @@ module Flipper
20
16
  percentage_of_time: nil,
21
17
  }
22
18
  end
19
+
20
+ def from(source)
21
+ return source if source.is_a?(Flipper::Adapter)
22
+ source.adapter
23
+ end
23
24
  end
24
25
 
25
26
  # Public: Get all features and gate values in one call. Defaults to one call
@@ -43,9 +44,19 @@ module Flipper
43
44
 
44
45
  # Public: Ensure that adapter is in sync with source adapter provided.
45
46
  #
46
- # Returns result of Synchronizer#call.
47
- def import(source_adapter)
48
- Adapters::Sync::Synchronizer.new(self, source_adapter, raise: true).call
47
+ # source - The source dsl, adapter or export to import.
48
+ #
49
+ # Returns true if successful.
50
+ def import(source)
51
+ Adapters::Sync::Synchronizer.new(self, self.class.from(source), raise: true).call
52
+ true
53
+ end
54
+
55
+ # Public: Exports the adapter in a given format for a given format version.
56
+ #
57
+ # Returns a Flipper::Export instance.
58
+ def export(format: :json, version: 1)
59
+ Flipper::Exporter.build(format: format, version: version).call(self)
49
60
  end
50
61
 
51
62
  # Public: Default config for a feature's gate values.
@@ -54,3 +65,8 @@ module Flipper
54
65
  end
55
66
  end
56
67
  end
68
+
69
+ require "set"
70
+ require "flipper/exporter"
71
+ require "flipper/feature"
72
+ require "flipper/adapters/sync/synchronizer"
@@ -4,7 +4,7 @@ module Flipper
4
4
  include ::Flipper::Adapter
5
5
 
6
6
  # Public: The name of the adapter.
7
- attr_reader :name
7
+ attr_reader :name, :local, :remote
8
8
 
9
9
  # Public: Build a new sync instance.
10
10
  #
@@ -34,33 +34,27 @@ module Flipper
34
34
  end
35
35
 
36
36
  def add(feature)
37
- result = @remote.add(feature)
38
- @local.add(feature)
39
- result
37
+ @remote.add(feature).tap { @local.add(feature) }
40
38
  end
41
39
 
42
40
  def remove(feature)
43
- result = @remote.remove(feature)
44
- @local.remove(feature)
45
- result
41
+ @remote.remove(feature).tap { @local.remove(feature) }
46
42
  end
47
43
 
48
44
  def clear(feature)
49
- result = @remote.clear(feature)
50
- @local.clear(feature)
51
- result
45
+ @remote.clear(feature).tap { @local.clear(feature) }
52
46
  end
53
47
 
54
48
  def enable(feature, gate, thing)
55
- result = @remote.enable(feature, gate, thing)
56
- @local.enable(feature, gate, thing)
57
- result
49
+ @remote.enable(feature, gate, thing).tap do
50
+ @local.enable(feature, gate, thing)
51
+ end
58
52
  end
59
53
 
60
54
  def disable(feature, gate, thing)
61
- result = @remote.disable(feature, gate, thing)
62
- @local.disable(feature, gate, thing)
63
- result
55
+ @remote.disable(feature, gate, thing).tap do
56
+ @local.disable(feature, gate, thing)
57
+ end
64
58
  end
65
59
  end
66
60
  end
@@ -0,0 +1,83 @@
1
+ module Flipper
2
+ module Adapters
3
+ class Failover
4
+ include ::Flipper::Adapter
5
+
6
+ # Public: The name of the adapter.
7
+ attr_reader :name
8
+
9
+ # Public: Build a new failover instance.
10
+ #
11
+ # primary - The primary flipper adapter.
12
+ # secondary - The secondary flipper adapter which services reads when
13
+ # the primary adapter is unavailable.
14
+ # options - Hash of options:
15
+ # :dual_write - Boolean, whether to update secondary when
16
+ # primary is updated
17
+ # :errors - Array of exception types for which to failover
18
+
19
+ def initialize(primary, secondary, options = {})
20
+ @name = :failover
21
+ @primary = primary
22
+ @secondary = secondary
23
+
24
+ @dual_write = options.fetch(:dual_write, false)
25
+ @errors = options.fetch(:errors, [ StandardError ])
26
+ end
27
+
28
+ def features
29
+ @primary.features
30
+ rescue *@errors
31
+ @secondary.features
32
+ end
33
+
34
+ def get(feature)
35
+ @primary.get(feature)
36
+ rescue *@errors
37
+ @secondary.get(feature)
38
+ end
39
+
40
+ def get_multi(features)
41
+ @primary.get_multi(features)
42
+ rescue *@errors
43
+ @secondary.get_multi(features)
44
+ end
45
+
46
+ def get_all
47
+ @primary.get_all
48
+ rescue *@errors
49
+ @secondary.get_all
50
+ end
51
+
52
+ def add(feature)
53
+ @primary.add(feature).tap do
54
+ @secondary.add(feature) if @dual_write
55
+ end
56
+ end
57
+
58
+ def remove(feature)
59
+ @primary.remove(feature).tap do
60
+ @secondary.remove(feature) if @dual_write
61
+ end
62
+ end
63
+
64
+ def clear(feature)
65
+ @primary.clear(feature).tap do
66
+ @secondary.clear(feature) if @dual_write
67
+ end
68
+ end
69
+
70
+ def enable(feature, gate, thing)
71
+ @primary.enable(feature, gate, thing).tap do
72
+ @secondary.enable(feature, gate, thing) if @dual_write
73
+ end
74
+ end
75
+
76
+ def disable(feature, gate, thing)
77
+ @primary.disable(feature, gate, thing).tap do
78
+ @secondary.disable(feature, gate, thing) if @dual_write
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,76 @@
1
+ module Flipper
2
+ module Adapters
3
+ class Failsafe
4
+ include ::Flipper::Adapter
5
+
6
+ # Public: The name of the adapter.
7
+ attr_reader :name
8
+
9
+ # Public: Build a new Failsafe instance.
10
+ #
11
+ # adapter - Flipper adapter to guard.
12
+ # options - Hash of options:
13
+ # :errors - Array of exception types for which to fail safe
14
+
15
+ def initialize(adapter, options = {})
16
+ @adapter = adapter
17
+ @errors = options.fetch(:errors, [StandardError])
18
+ @name = :failsafe
19
+ end
20
+
21
+ def features
22
+ @adapter.features
23
+ rescue *@errors
24
+ Set.new
25
+ end
26
+
27
+ def add(feature)
28
+ @adapter.add(feature)
29
+ rescue *@errors
30
+ false
31
+ end
32
+
33
+ def remove(feature)
34
+ @adapter.remove(feature)
35
+ rescue *@errors
36
+ false
37
+ end
38
+
39
+ def clear(feature)
40
+ @adapter.clear(feature)
41
+ rescue *@errors
42
+ false
43
+ end
44
+
45
+ def get(feature)
46
+ @adapter.get(feature)
47
+ rescue *@errors
48
+ {}
49
+ end
50
+
51
+ def get_multi(features)
52
+ @adapter.get_multi(features)
53
+ rescue *@errors
54
+ {}
55
+ end
56
+
57
+ def get_all
58
+ @adapter.get_all
59
+ rescue *@errors
60
+ {}
61
+ end
62
+
63
+ def enable(feature, gate, thing)
64
+ @adapter.enable(feature, gate, thing)
65
+ rescue *@errors
66
+ false
67
+ end
68
+
69
+ def disable(feature, gate, thing)
70
+ @adapter.disable(feature, gate, thing)
71
+ rescue *@errors
72
+ false
73
+ end
74
+ end
75
+ end
76
+ end
@@ -14,6 +14,10 @@ module Flipper
14
14
 
15
15
  HTTPS_SCHEME = "https".freeze
16
16
 
17
+ attr_reader :uri, :headers
18
+ attr_reader :basic_auth_username, :basic_auth_password
19
+ attr_reader :read_timeout, :open_timeout, :write_timeout, :max_retries, :debug_output
20
+
17
21
  def initialize(options = {})
18
22
  @uri = URI(options.fetch(:url))
19
23
  @headers = DEFAULT_HEADERS.merge(options[:headers] || {})
@@ -22,6 +26,7 @@ module Flipper
22
26
  @read_timeout = options[:read_timeout]
23
27
  @open_timeout = options[:open_timeout]
24
28
  @write_timeout = options[:write_timeout]
29
+ @max_retries = options.key?(:max_retries) ? options[:max_retries] : 0
25
30
  @debug_output = options[:debug_output]
26
31
  end
27
32
 
@@ -58,7 +63,8 @@ module Flipper
58
63
  http = Net::HTTP.new(uri.host, uri.port)
59
64
  http.read_timeout = @read_timeout if @read_timeout
60
65
  http.open_timeout = @open_timeout if @open_timeout
61
- apply_write_timeout(http)
66
+ http.max_retries = @max_retries if @max_retries
67
+ http.write_timeout = @write_timeout if @write_timeout
62
68
  http.set_debug_output(@debug_output) if @debug_output
63
69
 
64
70
  if uri.scheme == HTTPS_SCHEME
@@ -70,9 +76,19 @@ module Flipper
70
76
  end
71
77
 
72
78
  def build_request(http_method, uri, headers, options)
79
+ request_headers = {
80
+ "Client-Language" => "ruby",
81
+ "Client-Language-Version" => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
82
+ "Client-Platform" => RUBY_PLATFORM,
83
+ "Client-Engine" => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
84
+ "Client-Pid" => Process.pid.to_s,
85
+ "Client-Thread" => Thread.current.object_id.to_s,
86
+ "Client-Hostname" => Socket.gethostname,
87
+ }.merge(headers)
88
+
73
89
  body = options[:body]
74
90
  request = http_method.new(uri.request_uri)
75
- request.initialize_http_header(headers) if headers
91
+ request.initialize_http_header(request_headers)
76
92
  request.body = body if body
77
93
 
78
94
  if @basic_auth_username && @basic_auth_password
@@ -81,16 +97,6 @@ module Flipper
81
97
 
82
98
  request
83
99
  end
84
-
85
- def apply_write_timeout(http)
86
- if @write_timeout
87
- if RUBY_VERSION >= '2.6.0'
88
- http.write_timeout = @write_timeout
89
- else
90
- Kernel.warn("Warning: option :write_timeout requires Ruby version 2.6.0 or later")
91
- end
92
- end
93
- end
94
100
  end
95
101
  end
96
102
  end
@@ -1,3 +1,5 @@
1
+ require "json"
2
+
1
3
  module Flipper
2
4
  module Adapters
3
5
  class Http
@@ -6,7 +8,23 @@ module Flipper
6
8
 
7
9
  def initialize(response)
8
10
  @response = response
9
- super("Failed with status: #{response.code}")
11
+ message = "Failed with status: #{response.code}"
12
+
13
+ begin
14
+ data = JSON.parse(response.body)
15
+
16
+ if error_message = data["message"]
17
+ message << "\n\n#{data["message"]}"
18
+ end
19
+
20
+ if more_info = data["more_info"]
21
+ message << "\n#{data["more_info"]}"
22
+ end
23
+ rescue => exception
24
+ # welp we tried
25
+ end
26
+
27
+ super(message)
10
28
  end
11
29
  end
12
30
  end
@@ -10,7 +10,7 @@ module Flipper
10
10
  class Http
11
11
  include Flipper::Adapter
12
12
 
13
- attr_reader :name
13
+ attr_reader :name, :client
14
14
 
15
15
  def initialize(options = {})
16
16
  @client = Client.new(url: options.fetch(:url),
@@ -19,6 +19,8 @@ module Flipper
19
19
  basic_auth_password: options[:basic_auth_password],
20
20
  read_timeout: options[:read_timeout],
21
21
  open_timeout: options[:open_timeout],
22
+ write_timeout: options[:write_timeout],
23
+ max_retries: options[:max_retries],
22
24
  debug_output: options[:debug_output])
23
25
  @name = :http
24
26
  end
@@ -37,7 +39,7 @@ module Flipper
37
39
 
38
40
  def get_multi(features)
39
41
  csv_keys = features.map(&:key).join(',')
40
- response = @client.get("/features?keys=#{csv_keys}")
42
+ response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
41
43
  raise Error, response unless response.is_a?(Net::HTTPOK)
42
44
 
43
45
  parsed_response = JSON.parse(response.body)
@@ -55,7 +57,7 @@ module Flipper
55
57
  end
56
58
 
57
59
  def get_all
58
- response = @client.get("/features")
60
+ response = @client.get("/features?exclude_gate_names=true")
59
61
  raise Error, response unless response.is_a?(Net::HTTPOK)
60
62
 
61
63
  parsed_response = JSON.parse(response.body)
@@ -74,7 +76,7 @@ module Flipper
74
76
  end
75
77
 
76
78
  def features
77
- response = @client.get('/features')
79
+ response = @client.get('/features?exclude_gate_names=true')
78
80
  raise Error, response unless response.is_a?(Net::HTTPOK)
79
81
 
80
82
  parsed_response = JSON.parse(response.body)
@@ -121,6 +123,14 @@ module Flipper
121
123
  true
122
124
  end
123
125
 
126
+ def import(source)
127
+ adapter = self.class.from(source)
128
+ export = adapter.export(format: :json, version: 1)
129
+ response = @client.post("/import", export.contents)
130
+ raise Error, response unless response.is_a?(Net::HTTPNoContent)
131
+ true
132
+ end
133
+
124
134
  private
125
135
 
126
136
  def request_body_for_gate(gate, value)