flipper 0.22.0 → 0.28.0

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