flipper 0.22.0 → 0.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +26 -20
- data/.github/workflows/examples.yml +62 -0
- data/.rspec +1 -0
- data/.tool-versions +1 -0
- data/Changelog.md +152 -3
- data/Dockerfile +1 -1
- data/Gemfile +9 -6
- data/README.md +15 -67
- data/Rakefile +4 -2
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_multiple_actors_ips.rb +20 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +19 -0
- data/docker-compose.yml +37 -34
- data/docs/README.md +1 -0
- data/docs/images/banner.jpg +0 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- data/examples/dsl.rb +3 -3
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/instrumentation.rb +1 -0
- data/examples/instrumentation_last_accessed_at.rb +38 -0
- data/flipper.gemspec +2 -2
- data/lib/flipper/actor.rb +4 -0
- data/lib/flipper/adapter.rb +23 -7
- data/lib/flipper/adapters/dual_write.rb +10 -16
- data/lib/flipper/adapters/failover.rb +83 -0
- data/lib/flipper/adapters/failsafe.rb +76 -0
- data/lib/flipper/adapters/http/client.rb +18 -12
- data/lib/flipper/adapters/http/error.rb +19 -1
- data/lib/flipper/adapters/http.rb +14 -4
- data/lib/flipper/adapters/instrumented.rb +25 -2
- data/lib/flipper/adapters/memoizable.rb +27 -18
- data/lib/flipper/adapters/memory.rb +56 -39
- data/lib/flipper/adapters/operation_logger.rb +16 -3
- data/lib/flipper/adapters/poll/poller.rb +2 -0
- data/lib/flipper/adapters/poll.rb +39 -0
- data/lib/flipper/adapters/pstore.rb +2 -5
- data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -6
- data/lib/flipper/adapters/sync/synchronizer.rb +2 -1
- data/lib/flipper/adapters/sync.rb +9 -15
- data/lib/flipper/dsl.rb +9 -11
- data/lib/flipper/errors.rb +3 -20
- data/lib/flipper/export.rb +26 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- data/lib/flipper/feature.rb +32 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +12 -11
- data/lib/flipper/gate_values.rb +0 -16
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/group.rb +5 -7
- data/lib/flipper/gates/percentage_of_actors.rb +10 -13
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/identifier.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +7 -3
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/instrumenters/memory.rb +6 -2
- data/lib/flipper/metadata.rb +1 -1
- data/lib/flipper/middleware/memoizer.rb +2 -12
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/railtie.rb +23 -22
- data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
- data/lib/flipper/test/shared_adapter_test.rb +24 -0
- data/lib/flipper/typecast.rb +28 -15
- data/lib/flipper/types/actor.rb +19 -13
- data/lib/flipper/types/group.rb +12 -5
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +6 -4
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/actor_spec.rb +10 -2
- data/spec/flipper/adapter_spec.rb +29 -4
- data/spec/flipper/adapters/dual_write_spec.rb +0 -2
- data/spec/flipper/adapters/failover_spec.rb +129 -0
- data/spec/flipper/adapters/failsafe_spec.rb +58 -0
- data/spec/flipper/adapters/http_spec.rb +64 -6
- data/spec/flipper/adapters/instrumented_spec.rb +28 -12
- data/spec/flipper/adapters/memoizable_spec.rb +30 -12
- data/spec/flipper/adapters/memory_spec.rb +3 -4
- data/spec/flipper/adapters/operation_logger_spec.rb +29 -12
- data/spec/flipper/adapters/pstore_spec.rb +0 -2
- data/spec/flipper/adapters/read_only_spec.rb +0 -1
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync_spec.rb +0 -2
- data/spec/flipper/configuration_spec.rb +0 -1
- data/spec/flipper/dsl_spec.rb +38 -12
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- data/spec/flipper/feature_check_context_spec.rb +17 -19
- data/spec/flipper/feature_spec.rb +76 -33
- data/spec/flipper/gate_spec.rb +0 -2
- data/spec/flipper/gate_values_spec.rb +2 -34
- data/spec/flipper/gates/actor_spec.rb +0 -2
- data/spec/flipper/gates/boolean_spec.rb +1 -3
- data/spec/flipper/gates/group_spec.rb +2 -5
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
- data/spec/flipper/identifier_spec.rb +0 -1
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -1
- data/spec/flipper/instrumenters/memory_spec.rb +18 -1
- data/spec/flipper/instrumenters/noop_spec.rb +14 -8
- data/spec/flipper/middleware/memoizer_spec.rb +0 -23
- data/spec/flipper/middleware/setup_env_spec.rb +0 -2
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/railtie_spec.rb +73 -33
- data/spec/flipper/registry_spec.rb +0 -1
- data/spec/flipper/typecast_spec.rb +82 -4
- data/spec/flipper/types/actor_spec.rb +45 -46
- data/spec/flipper/types/boolean_spec.rb +0 -1
- data/spec/flipper/types/group_spec.rb +2 -3
- data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
- data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
- data/spec/flipper/types/percentage_spec.rb +0 -1
- data/spec/flipper_integration_spec.rb +62 -51
- data/spec/flipper_spec.rb +7 -2
- data/spec/{helper.rb → spec_helper.rb} +4 -2
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +2 -6
- metadata +63 -19
- data/docs/Adapters.md +0 -124
- data/docs/Caveats.md +0 -4
- data/docs/Gates.md +0 -167
- data/docs/Instrumentation.md +0 -27
- data/docs/Optimization.md +0 -137
- data/docs/api/README.md +0 -884
- data/docs/http/README.md +0 -36
- data/docs/read-only/README.md +0 -24
data/docker-compose.yml
CHANGED
@@ -1,34 +1,37 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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.
|
data/docs/images/banner.jpg
CHANGED
Binary file
|
data/examples/api/basic.ru
CHANGED
@@ -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
|
data/examples/api/memoized.ru
CHANGED
@@ -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
|
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
|
-
|
62
|
-
puts Flipper.actor(
|
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
|
data/examples/instrumentation.rb
CHANGED
@@ -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
data/lib/flipper/adapter.rb
CHANGED
@@ -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
|
-
#
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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(
|
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
|
-
|
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)
|