flipper 0.28.3 → 1.4.2
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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +56 -10
- data/.github/workflows/examples.yml +54 -11
- data/.github/workflows/release.yml +54 -0
- data/.superset/config.json +4 -0
- data/CLAUDE.md +93 -0
- data/Changelog.md +1 -559
- data/Gemfile +17 -8
- data/README.md +34 -29
- data/Rakefile +2 -2
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/app.ru +12 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/basic.rb +22 -0
- data/examples/cloud/cloud_setup.rb +20 -0
- data/examples/cloud/forked.rb +36 -0
- data/examples/cloud/import.rb +17 -0
- data/examples/cloud/poll_interval/README.md +111 -0
- data/examples/cloud/poll_interval/client.rb +108 -0
- data/examples/cloud/poll_interval/server.rb +98 -0
- data/examples/cloud/threaded.rb +33 -0
- data/examples/dsl.rb +0 -14
- data/examples/expressions.rb +237 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +8 -4
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +27 -1
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/actor_limit.rb +54 -0
- data/lib/flipper/adapters/cache_base.rb +161 -0
- data/lib/flipper/adapters/dual_write.rb +7 -5
- data/lib/flipper/adapters/failover.rb +9 -7
- data/lib/flipper/adapters/failsafe.rb +2 -6
- data/lib/flipper/adapters/http/client.rb +53 -14
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +56 -16
- data/lib/flipper/adapters/instrumented.rb +2 -6
- data/lib/flipper/adapters/memoizable.rb +17 -22
- data/lib/flipper/adapters/memory.rb +5 -7
- data/lib/flipper/adapters/operation_logger.rb +18 -92
- data/lib/flipper/adapters/poll.rb +16 -3
- data/lib/flipper/adapters/pstore.rb +18 -12
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +75 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
- data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
- data/lib/flipper/adapters/sync.rb +7 -7
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +314 -0
- data/lib/flipper/cloud/configuration.rb +267 -0
- data/lib/flipper/cloud/dsl.rb +27 -0
- data/lib/flipper/cloud/message_verifier.rb +95 -0
- data/lib/flipper/cloud/middleware.rb +63 -0
- data/lib/flipper/cloud/migrate.rb +71 -0
- data/lib/flipper/cloud/routes.rb +14 -0
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +54 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +48 -43
- data/lib/flipper/engine.rb +102 -0
- data/lib/flipper/export.rb +0 -2
- data/lib/flipper/exporters/json/export.rb +1 -1
- data/lib/flipper/exporters/json/v1.rb +1 -1
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +9 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/feature_enabled.rb +34 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +16 -0
- data/lib/flipper/feature.rb +63 -1
- data/lib/flipper/gate.rb +2 -1
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +0 -4
- data/lib/flipper/metadata.rb +8 -1
- data/lib/flipper/middleware/memoizer.rb +30 -14
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +54 -12
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
- data/lib/flipper/test/shared_adapter_test.rb +77 -58
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +37 -9
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +61 -8
- data/lib/generators/flipper/setup_generator.rb +68 -0
- data/lib/generators/flipper/templates/initializer.rb +45 -0
- data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
- data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
- data/lib/generators/flipper/update_generator.rb +35 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +21 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
- data/spec/flipper/adapters/dual_write_spec.rb +15 -2
- data/spec/flipper/adapters/failover_spec.rb +12 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +375 -74
- data/spec/flipper/adapters/instrumented_spec.rb +1 -1
- data/spec/flipper/adapters/memoizable_spec.rb +21 -21
- data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- data/spec/flipper/adapters/strict_spec.rb +122 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -0
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
- data/spec/flipper/adapters/sync_spec.rb +13 -0
- data/spec/flipper/cli_spec.rb +217 -0
- data/spec/flipper/cloud/configuration_spec.rb +251 -0
- data/spec/flipper/cloud/dsl_spec.rb +82 -0
- data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
- data/spec/flipper/cloud/middleware_spec.rb +289 -0
- data/spec/flipper/cloud/migrate_spec.rb +160 -0
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +208 -0
- data/spec/flipper/cloud_spec.rb +186 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +34 -73
- data/spec/flipper/engine_spec.rb +374 -0
- data/spec/flipper/exporters/json/v1_spec.rb +3 -3
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +29 -0
- data/spec/flipper/feature_spec.rb +380 -10
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +190 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
- data/spec/flipper/middleware/memoizer_spec.rb +107 -7
- data/spec/flipper/model/active_record_spec.rb +72 -0
- data/spec/flipper/poller_spec.rb +347 -4
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +43 -7
- data/spec/flipper/types/actor_spec.rb +18 -1
- data/spec/flipper_integration_spec.rb +247 -16
- data/spec/flipper_spec.rb +92 -29
- data/spec/spec_helper.rb +24 -17
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +34 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +22 -2
- data/test_rails/system/test_help_test.rb +52 -0
- metadata +139 -75
- data/.tool-versions +0 -1
- data/lib/flipper/railtie.rb +0 -47
- data/spec/flipper/railtie_spec.rb +0 -109
|
@@ -3,9 +3,6 @@ module Flipper
|
|
|
3
3
|
class Failover
|
|
4
4
|
include ::Flipper::Adapter
|
|
5
5
|
|
|
6
|
-
# Public: The name of the adapter.
|
|
7
|
-
attr_reader :name
|
|
8
|
-
|
|
9
6
|
# Public: Build a new failover instance.
|
|
10
7
|
#
|
|
11
8
|
# primary - The primary flipper adapter.
|
|
@@ -16,8 +13,9 @@ module Flipper
|
|
|
16
13
|
# primary is updated
|
|
17
14
|
# :errors - Array of exception types for which to failover
|
|
18
15
|
|
|
16
|
+
attr_reader :primary, :secondary
|
|
17
|
+
|
|
19
18
|
def initialize(primary, secondary, options = {})
|
|
20
|
-
@name = :failover
|
|
21
19
|
@primary = primary
|
|
22
20
|
@secondary = secondary
|
|
23
21
|
|
|
@@ -25,6 +23,10 @@ module Flipper
|
|
|
25
23
|
@errors = options.fetch(:errors, [ StandardError ])
|
|
26
24
|
end
|
|
27
25
|
|
|
26
|
+
def adapter_stack
|
|
27
|
+
"#{name}(primary: #{@primary.adapter_stack}, secondary: #{@secondary.adapter_stack})"
|
|
28
|
+
end
|
|
29
|
+
|
|
28
30
|
def features
|
|
29
31
|
@primary.features
|
|
30
32
|
rescue *@errors
|
|
@@ -43,10 +45,10 @@ module Flipper
|
|
|
43
45
|
@secondary.get_multi(features)
|
|
44
46
|
end
|
|
45
47
|
|
|
46
|
-
def get_all
|
|
47
|
-
@primary.get_all
|
|
48
|
+
def get_all(**kwargs)
|
|
49
|
+
@primary.get_all(**kwargs)
|
|
48
50
|
rescue *@errors
|
|
49
|
-
@secondary.get_all
|
|
51
|
+
@secondary.get_all(**kwargs)
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def add(feature)
|
|
@@ -3,9 +3,6 @@ module Flipper
|
|
|
3
3
|
class Failsafe
|
|
4
4
|
include ::Flipper::Adapter
|
|
5
5
|
|
|
6
|
-
# Public: The name of the adapter.
|
|
7
|
-
attr_reader :name
|
|
8
|
-
|
|
9
6
|
# Public: Build a new Failsafe instance.
|
|
10
7
|
#
|
|
11
8
|
# adapter - Flipper adapter to guard.
|
|
@@ -15,7 +12,6 @@ module Flipper
|
|
|
15
12
|
def initialize(adapter, options = {})
|
|
16
13
|
@adapter = adapter
|
|
17
14
|
@errors = options.fetch(:errors, [StandardError])
|
|
18
|
-
@name = :failsafe
|
|
19
15
|
end
|
|
20
16
|
|
|
21
17
|
def features
|
|
@@ -54,8 +50,8 @@ module Flipper
|
|
|
54
50
|
{}
|
|
55
51
|
end
|
|
56
52
|
|
|
57
|
-
def get_all
|
|
58
|
-
@adapter.get_all
|
|
53
|
+
def get_all(**kwargs)
|
|
54
|
+
@adapter.get_all(**kwargs)
|
|
59
55
|
rescue *@errors
|
|
60
56
|
{}
|
|
61
57
|
end
|
|
@@ -7,20 +7,28 @@ module Flipper
|
|
|
7
7
|
class Http
|
|
8
8
|
class Client
|
|
9
9
|
DEFAULT_HEADERS = {
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
10
|
+
'content-type' => 'application/json',
|
|
11
|
+
'accept' => 'application/json',
|
|
12
|
+
'user-agent' => "Flipper HTTP Adapter v#{VERSION}",
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
15
|
HTTPS_SCHEME = "https".freeze
|
|
16
16
|
|
|
17
|
+
CLIENT_FRAMEWORKS = {
|
|
18
|
+
rails: -> { Rails.version if defined?(Rails) },
|
|
19
|
+
sinatra: -> { Sinatra::VERSION if defined?(Sinatra) },
|
|
20
|
+
hanami: -> { Hanami::VERSION if defined?(Hanami) },
|
|
21
|
+
sidekiq: -> { Sidekiq::VERSION if defined?(Sidekiq) },
|
|
22
|
+
good_job: -> { GoodJob::VERSION if defined?(GoodJob) },
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
attr_reader :uri, :headers
|
|
18
26
|
attr_reader :basic_auth_username, :basic_auth_password
|
|
19
|
-
attr_reader :read_timeout, :open_timeout, :write_timeout
|
|
27
|
+
attr_reader :read_timeout, :open_timeout, :write_timeout
|
|
28
|
+
attr_reader :max_retries, :debug_output
|
|
20
29
|
|
|
21
30
|
def initialize(options = {})
|
|
22
31
|
@uri = URI(options.fetch(:url))
|
|
23
|
-
@headers = DEFAULT_HEADERS.merge(options[:headers] || {})
|
|
24
32
|
@basic_auth_username = options[:basic_auth_username]
|
|
25
33
|
@basic_auth_password = options[:basic_auth_password]
|
|
26
34
|
@read_timeout = options[:read_timeout]
|
|
@@ -28,10 +36,28 @@ module Flipper
|
|
|
28
36
|
@write_timeout = options[:write_timeout]
|
|
29
37
|
@max_retries = options.key?(:max_retries) ? options[:max_retries] : 0
|
|
30
38
|
@debug_output = options[:debug_output]
|
|
39
|
+
|
|
40
|
+
@headers = {}
|
|
41
|
+
DEFAULT_HEADERS.each { |key, value| add_header key, value }
|
|
42
|
+
if options[:headers]
|
|
43
|
+
options[:headers].each { |key, value| add_header key, value }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_header(key, value)
|
|
48
|
+
@headers[normalize_header_key(key)] = value
|
|
31
49
|
end
|
|
32
50
|
|
|
33
|
-
def get(path)
|
|
34
|
-
|
|
51
|
+
def get(path, options = {})
|
|
52
|
+
headers = @headers.dup
|
|
53
|
+
|
|
54
|
+
if options[:headers]
|
|
55
|
+
options[:headers].each do |key, value|
|
|
56
|
+
headers[normalize_header_key(key)] = value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
perform Net::HTTP::Get, path, headers
|
|
35
61
|
end
|
|
36
62
|
|
|
37
63
|
def post(path, body = nil)
|
|
@@ -77,18 +103,23 @@ module Flipper
|
|
|
77
103
|
|
|
78
104
|
def build_request(http_method, uri, headers, options)
|
|
79
105
|
request_headers = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
106
|
+
'client-language' => "ruby",
|
|
107
|
+
'client-language-version' => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
|
|
108
|
+
'client-platform' => RUBY_PLATFORM,
|
|
109
|
+
'client-engine' => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
|
|
110
|
+
'client-pid' => Process.pid.to_s,
|
|
111
|
+
'client-thread' => Thread.current.object_id.to_s,
|
|
112
|
+
'client-hostname' => Socket.gethostname,
|
|
87
113
|
}.merge(headers)
|
|
88
114
|
|
|
89
115
|
body = options[:body]
|
|
90
116
|
request = http_method.new(uri.request_uri)
|
|
91
117
|
request.initialize_http_header(request_headers)
|
|
118
|
+
|
|
119
|
+
client_frameworks.each do |framework, version|
|
|
120
|
+
request.add_field("client-framework", [framework, version].join("="))
|
|
121
|
+
end
|
|
122
|
+
|
|
92
123
|
request.body = body if body
|
|
93
124
|
|
|
94
125
|
if @basic_auth_username && @basic_auth_password
|
|
@@ -97,6 +128,14 @@ module Flipper
|
|
|
97
128
|
|
|
98
129
|
request
|
|
99
130
|
end
|
|
131
|
+
|
|
132
|
+
def client_frameworks
|
|
133
|
+
CLIENT_FRAMEWORKS.transform_values { |detect| detect.call rescue nil }.compact
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def normalize_header_key(key)
|
|
137
|
+
key.to_s.downcase.gsub('_'.freeze, '-'.freeze)
|
|
138
|
+
end
|
|
100
139
|
end
|
|
101
140
|
end
|
|
102
141
|
end
|
|
@@ -11,7 +11,7 @@ module Flipper
|
|
|
11
11
|
message = "Failed with status: #{response.code}"
|
|
12
12
|
|
|
13
13
|
begin
|
|
14
|
-
data =
|
|
14
|
+
data = Typecast.from_json(response.body)
|
|
15
15
|
|
|
16
16
|
if error_message = data["message"]
|
|
17
17
|
message << "\n\n#{data["message"]}"
|
|
@@ -20,7 +20,7 @@ module Flipper
|
|
|
20
20
|
if more_info = data["more_info"]
|
|
21
21
|
message << "\n#{data["more_info"]}"
|
|
22
22
|
end
|
|
23
|
-
rescue
|
|
23
|
+
rescue
|
|
24
24
|
# welp we tried
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -10,7 +10,7 @@ module Flipper
|
|
|
10
10
|
class Http
|
|
11
11
|
include Flipper::Adapter
|
|
12
12
|
|
|
13
|
-
attr_reader :
|
|
13
|
+
attr_reader :client
|
|
14
14
|
|
|
15
15
|
def initialize(options = {})
|
|
16
16
|
@client = Client.new(url: options.fetch(:url),
|
|
@@ -22,13 +22,16 @@ module Flipper
|
|
|
22
22
|
write_timeout: options[:write_timeout],
|
|
23
23
|
max_retries: options[:max_retries],
|
|
24
24
|
debug_output: options[:debug_output])
|
|
25
|
-
@
|
|
25
|
+
@last_get_all_etag = nil
|
|
26
|
+
@last_get_all_result = nil
|
|
27
|
+
@last_get_all_response = nil
|
|
28
|
+
@get_all_mutex = Mutex.new
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def get(feature)
|
|
29
32
|
response = @client.get("/features/#{feature.key}")
|
|
30
33
|
if response.is_a?(Net::HTTPOK)
|
|
31
|
-
parsed_response =
|
|
34
|
+
parsed_response = Typecast.from_json(response.body)
|
|
32
35
|
result_for_feature(feature, parsed_response.fetch('gates'))
|
|
33
36
|
elsif response.is_a?(Net::HTTPNotFound)
|
|
34
37
|
default_config
|
|
@@ -42,7 +45,7 @@ module Flipper
|
|
|
42
45
|
response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
|
|
43
46
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
|
44
47
|
|
|
45
|
-
parsed_response =
|
|
48
|
+
parsed_response = Typecast.from_json(response.body)
|
|
46
49
|
parsed_features = parsed_response.fetch('features')
|
|
47
50
|
gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
|
|
48
51
|
hash[parsed_feature['key']] = parsed_feature['gates']
|
|
@@ -56,30 +59,61 @@ module Flipper
|
|
|
56
59
|
result
|
|
57
60
|
end
|
|
58
61
|
|
|
59
|
-
def get_all
|
|
60
|
-
|
|
62
|
+
def get_all(cache_bust: false)
|
|
63
|
+
options = {}
|
|
64
|
+
path = "/features?exclude_gate_names=true"
|
|
65
|
+
path += "&_cb=#{Time.now.to_i}" if cache_bust
|
|
66
|
+
etag = @get_all_mutex.synchronize { @last_get_all_etag }
|
|
67
|
+
|
|
68
|
+
if etag && !cache_bust
|
|
69
|
+
options[:headers] = { if_none_match: etag }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
response = @client.get(path, options)
|
|
73
|
+
@get_all_mutex.synchronize { @last_get_all_response = response }
|
|
74
|
+
|
|
75
|
+
if response.is_a?(Net::HTTPNotModified)
|
|
76
|
+
cached_result = @get_all_mutex.synchronize { @last_get_all_result }
|
|
77
|
+
|
|
78
|
+
if cached_result
|
|
79
|
+
return cached_result
|
|
80
|
+
else
|
|
81
|
+
raise Error, response
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
61
85
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
|
62
86
|
|
|
63
|
-
parsed_response =
|
|
64
|
-
parsed_features = parsed_response
|
|
87
|
+
parsed_response = response.body.empty? ? {} : Typecast.from_json(response.body)
|
|
88
|
+
parsed_features = parsed_response['features'] || []
|
|
65
89
|
gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
|
|
66
90
|
hash[parsed_feature['key']] = parsed_feature['gates']
|
|
67
91
|
hash
|
|
68
92
|
end
|
|
69
93
|
|
|
70
94
|
result = {}
|
|
71
|
-
gates_by_key.
|
|
95
|
+
gates_by_key.each_key do |key|
|
|
72
96
|
feature = Feature.new(key, self)
|
|
73
97
|
result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
|
|
74
98
|
end
|
|
99
|
+
|
|
100
|
+
@get_all_mutex.synchronize do
|
|
101
|
+
@last_get_all_etag = response['etag'] if response['etag']
|
|
102
|
+
@last_get_all_result = result
|
|
103
|
+
end
|
|
104
|
+
|
|
75
105
|
result
|
|
76
106
|
end
|
|
77
107
|
|
|
108
|
+
def last_get_all_response
|
|
109
|
+
@get_all_mutex.synchronize { @last_get_all_response }
|
|
110
|
+
end
|
|
111
|
+
|
|
78
112
|
def features
|
|
79
113
|
response = @client.get('/features?exclude_gate_names=true')
|
|
80
114
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
|
81
115
|
|
|
82
|
-
parsed_response =
|
|
116
|
+
parsed_response = Typecast.from_json(response.body)
|
|
83
117
|
parsed_response['features'].map { |feature| feature['key'] }.to_set
|
|
84
118
|
end
|
|
85
119
|
|
|
@@ -97,7 +131,7 @@ module Flipper
|
|
|
97
131
|
end
|
|
98
132
|
|
|
99
133
|
def enable(feature, gate, thing)
|
|
100
|
-
body = request_body_for_gate(gate, thing.value
|
|
134
|
+
body = request_body_for_gate(gate, thing.value)
|
|
101
135
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
|
102
136
|
response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
|
103
137
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
|
@@ -105,7 +139,7 @@ module Flipper
|
|
|
105
139
|
end
|
|
106
140
|
|
|
107
141
|
def disable(feature, gate, thing)
|
|
108
|
-
body = request_body_for_gate(gate, thing.value
|
|
142
|
+
body = request_body_for_gate(gate, thing.value)
|
|
109
143
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
|
110
144
|
response = case gate.key
|
|
111
145
|
when :percentage_of_actors, :percentage_of_time
|
|
@@ -138,11 +172,13 @@ module Flipper
|
|
|
138
172
|
when :boolean
|
|
139
173
|
{}
|
|
140
174
|
when :groups
|
|
141
|
-
{ name: value }
|
|
175
|
+
{ name: value.to_s }
|
|
142
176
|
when :actors
|
|
143
|
-
{ flipper_id: value }
|
|
177
|
+
{ flipper_id: value.to_s }
|
|
144
178
|
when :percentage_of_actors, :percentage_of_time
|
|
145
|
-
{ percentage: value }
|
|
179
|
+
{ percentage: value.to_s }
|
|
180
|
+
when :expression
|
|
181
|
+
value
|
|
146
182
|
else
|
|
147
183
|
raise "#{gate.key} is not a valid flipper gate key"
|
|
148
184
|
end
|
|
@@ -166,13 +202,17 @@ module Flipper
|
|
|
166
202
|
case gate.data_type
|
|
167
203
|
when :boolean, :integer
|
|
168
204
|
value ? value.to_s : value
|
|
205
|
+
when :json
|
|
206
|
+
value
|
|
169
207
|
when :set
|
|
170
208
|
value ? value.to_set : Set.new
|
|
171
209
|
else
|
|
172
|
-
unsupported_data_type
|
|
210
|
+
unsupported_data_type gate.data_type
|
|
173
211
|
end
|
|
174
212
|
end
|
|
175
213
|
|
|
214
|
+
private
|
|
215
|
+
|
|
176
216
|
def unsupported_data_type(data_type)
|
|
177
217
|
raise "#{data_type} is not supported by this adapter"
|
|
178
218
|
end
|
|
@@ -13,9 +13,6 @@ module Flipper
|
|
|
13
13
|
# Private: What is used to instrument all the things.
|
|
14
14
|
attr_reader :instrumenter
|
|
15
15
|
|
|
16
|
-
# Public: The name of the adapter.
|
|
17
|
-
attr_reader :name
|
|
18
|
-
|
|
19
16
|
# Internal: Initializes a new adapter instance.
|
|
20
17
|
#
|
|
21
18
|
# adapter - Vanilla adapter instance to wrap.
|
|
@@ -25,7 +22,6 @@ module Flipper
|
|
|
25
22
|
#
|
|
26
23
|
def initialize(adapter, options = {})
|
|
27
24
|
@adapter = adapter
|
|
28
|
-
@name = :instrumented
|
|
29
25
|
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
|
30
26
|
end
|
|
31
27
|
|
|
@@ -105,14 +101,14 @@ module Flipper
|
|
|
105
101
|
end
|
|
106
102
|
end
|
|
107
103
|
|
|
108
|
-
def get_all
|
|
104
|
+
def get_all(**kwargs)
|
|
109
105
|
default_payload = {
|
|
110
106
|
operation: :get_all,
|
|
111
107
|
adapter_name: @adapter.name,
|
|
112
108
|
}
|
|
113
109
|
|
|
114
110
|
@instrumenter.instrument(InstrumentationName, default_payload) do |payload|
|
|
115
|
-
payload[:result] = @adapter.get_all
|
|
111
|
+
payload[:result] = @adapter.get_all(**kwargs)
|
|
116
112
|
end
|
|
117
113
|
end
|
|
118
114
|
|
|
@@ -8,35 +8,25 @@ module Flipper
|
|
|
8
8
|
class Memoizable
|
|
9
9
|
include ::Flipper::Adapter
|
|
10
10
|
|
|
11
|
-
FeaturesKey = :flipper_features
|
|
12
|
-
GetAllKey = :all_memoized
|
|
13
|
-
|
|
14
11
|
# Internal
|
|
15
12
|
attr_reader :cache
|
|
16
13
|
|
|
17
|
-
# Public: The name of the adapter.
|
|
18
|
-
attr_reader :name
|
|
19
|
-
|
|
20
14
|
# Internal: The adapter this adapter is wrapping.
|
|
21
15
|
attr_reader :adapter
|
|
22
16
|
|
|
23
|
-
# Private
|
|
24
|
-
def self.key_for(key)
|
|
25
|
-
"feature/#{key}"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
17
|
# Public
|
|
29
18
|
def initialize(adapter, cache = nil)
|
|
30
19
|
@adapter = adapter
|
|
31
|
-
@name = :memoizable
|
|
32
20
|
@cache = cache || {}
|
|
33
21
|
@memoize = false
|
|
22
|
+
@features_key = :flipper_features
|
|
23
|
+
@get_all_key = :all_memoized
|
|
34
24
|
end
|
|
35
25
|
|
|
36
26
|
# Public
|
|
37
27
|
def features
|
|
38
28
|
if memoizing?
|
|
39
|
-
cache.fetch(
|
|
29
|
+
cache.fetch(@features_key) { cache[@features_key] = @adapter.features }
|
|
40
30
|
else
|
|
41
31
|
@adapter.features
|
|
42
32
|
end
|
|
@@ -91,21 +81,21 @@ module Flipper
|
|
|
91
81
|
end
|
|
92
82
|
end
|
|
93
83
|
|
|
94
|
-
def get_all
|
|
84
|
+
def get_all(**kwargs)
|
|
95
85
|
if memoizing?
|
|
96
86
|
response = nil
|
|
97
|
-
if cache[
|
|
87
|
+
if cache[@get_all_key]
|
|
98
88
|
response = {}
|
|
99
|
-
cache[
|
|
89
|
+
cache[@features_key].each do |key|
|
|
100
90
|
response[key] = cache[key_for(key)]
|
|
101
91
|
end
|
|
102
92
|
else
|
|
103
|
-
response = @adapter.get_all
|
|
93
|
+
response = @adapter.get_all(**kwargs)
|
|
104
94
|
response.each do |key, value|
|
|
105
95
|
cache[key_for(key)] = value
|
|
106
96
|
end
|
|
107
|
-
cache[
|
|
108
|
-
cache[
|
|
97
|
+
cache[@features_key] = response.keys.to_set
|
|
98
|
+
cache[@get_all_key] = true
|
|
109
99
|
end
|
|
110
100
|
|
|
111
101
|
# Ensures that looking up other features that do not exist doesn't
|
|
@@ -113,7 +103,7 @@ module Flipper
|
|
|
113
103
|
response.default_proc = ->(memo, key) { memo[key] = default_config }
|
|
114
104
|
response
|
|
115
105
|
else
|
|
116
|
-
@adapter.get_all
|
|
106
|
+
@adapter.get_all(**kwargs)
|
|
117
107
|
end
|
|
118
108
|
end
|
|
119
109
|
|
|
@@ -127,6 +117,11 @@ module Flipper
|
|
|
127
117
|
@adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
|
|
128
118
|
end
|
|
129
119
|
|
|
120
|
+
# Public
|
|
121
|
+
def read_only?
|
|
122
|
+
@adapter.read_only?
|
|
123
|
+
end
|
|
124
|
+
|
|
130
125
|
def import(source)
|
|
131
126
|
@adapter.import(source).tap { cache.clear if memoizing? }
|
|
132
127
|
end
|
|
@@ -161,7 +156,7 @@ module Flipper
|
|
|
161
156
|
private
|
|
162
157
|
|
|
163
158
|
def key_for(key)
|
|
164
|
-
|
|
159
|
+
"feature/#{key}"
|
|
165
160
|
end
|
|
166
161
|
|
|
167
162
|
def expire_feature(feature)
|
|
@@ -169,7 +164,7 @@ module Flipper
|
|
|
169
164
|
end
|
|
170
165
|
|
|
171
166
|
def expire_features_set
|
|
172
|
-
cache.delete(
|
|
167
|
+
cache.delete(@features_key) if memoizing?
|
|
173
168
|
end
|
|
174
169
|
end
|
|
175
170
|
end
|
|
@@ -8,15 +8,9 @@ module Flipper
|
|
|
8
8
|
class Memory
|
|
9
9
|
include ::Flipper::Adapter
|
|
10
10
|
|
|
11
|
-
FeaturesKey = :features
|
|
12
|
-
|
|
13
|
-
# Public: The name of the adapter.
|
|
14
|
-
attr_reader :name
|
|
15
|
-
|
|
16
11
|
# Public
|
|
17
12
|
def initialize(source = nil, threadsafe: true)
|
|
18
13
|
@source = Typecast.features_hash(source)
|
|
19
|
-
@name = :memory
|
|
20
14
|
@lock = Mutex.new if threadsafe
|
|
21
15
|
reset
|
|
22
16
|
end
|
|
@@ -60,7 +54,7 @@ module Flipper
|
|
|
60
54
|
end
|
|
61
55
|
end
|
|
62
56
|
|
|
63
|
-
def get_all
|
|
57
|
+
def get_all(**kwargs)
|
|
64
58
|
synchronize { Typecast.features_hash(@source) }
|
|
65
59
|
end
|
|
66
60
|
|
|
@@ -77,6 +71,8 @@ module Flipper
|
|
|
77
71
|
@source[feature.key][gate.key] = thing.value.to_s
|
|
78
72
|
when :set
|
|
79
73
|
@source[feature.key][gate.key] << thing.value.to_s
|
|
74
|
+
when :json
|
|
75
|
+
@source[feature.key][gate.key] = thing.value
|
|
80
76
|
else
|
|
81
77
|
raise "#{gate} is not supported by this adapter yet"
|
|
82
78
|
end
|
|
@@ -97,6 +93,8 @@ module Flipper
|
|
|
97
93
|
@source[feature.key][gate.key] = thing.value.to_s
|
|
98
94
|
when :set
|
|
99
95
|
@source[feature.key][gate.key].delete thing.value.to_s
|
|
96
|
+
when :json
|
|
97
|
+
@source[feature.key].delete(gate.key)
|
|
100
98
|
else
|
|
101
99
|
raise "#{gate} is not supported by this adapter yet"
|
|
102
100
|
end
|
|
@@ -5,115 +5,34 @@ module Flipper
|
|
|
5
5
|
# Public: Adapter that wraps another adapter and stores the operations.
|
|
6
6
|
#
|
|
7
7
|
# Useful in tests to verify calls and such. Never use outside of testing.
|
|
8
|
-
class OperationLogger
|
|
9
|
-
include Flipper::Adapter
|
|
8
|
+
class OperationLogger < Wrapper
|
|
10
9
|
|
|
11
10
|
class Operation
|
|
12
|
-
attr_reader :type, :args
|
|
11
|
+
attr_reader :type, :args, :kwargs
|
|
13
12
|
|
|
14
|
-
def initialize(type, args)
|
|
13
|
+
def initialize(type, args, kwargs = {})
|
|
15
14
|
@type = type
|
|
16
15
|
@args = args
|
|
16
|
+
@kwargs = kwargs
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
OperationTypes = [
|
|
21
|
-
:import,
|
|
22
|
-
:export,
|
|
23
|
-
:features,
|
|
24
|
-
:add,
|
|
25
|
-
:remove,
|
|
26
|
-
:clear,
|
|
27
|
-
:get,
|
|
28
|
-
:get_multi,
|
|
29
|
-
:get_all,
|
|
30
|
-
:enable,
|
|
31
|
-
:disable,
|
|
32
|
-
].freeze
|
|
33
|
-
|
|
34
20
|
# Internal: An array of the operations that have happened.
|
|
35
21
|
attr_reader :operations
|
|
36
22
|
|
|
37
|
-
# Internal: The name of the adapter.
|
|
38
|
-
attr_reader :name
|
|
39
|
-
|
|
40
23
|
# Public
|
|
41
24
|
def initialize(adapter, operations = nil)
|
|
42
|
-
|
|
43
|
-
@name = :operation_logger
|
|
25
|
+
super(adapter)
|
|
44
26
|
@operations = operations || []
|
|
45
27
|
end
|
|
46
28
|
|
|
47
|
-
# Public: The set of known features.
|
|
48
|
-
def features
|
|
49
|
-
@operations << Operation.new(:features, [])
|
|
50
|
-
@adapter.features
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Public: Adds a feature to the set of known features.
|
|
54
|
-
def add(feature)
|
|
55
|
-
@operations << Operation.new(:add, [feature])
|
|
56
|
-
@adapter.add(feature)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Public: Removes a feature from the set of known features and clears
|
|
60
|
-
# all the values for the feature.
|
|
61
|
-
def remove(feature)
|
|
62
|
-
@operations << Operation.new(:remove, [feature])
|
|
63
|
-
@adapter.remove(feature)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Public: Clears all the gate values for a feature.
|
|
67
|
-
def clear(feature)
|
|
68
|
-
@operations << Operation.new(:clear, [feature])
|
|
69
|
-
@adapter.clear(feature)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Public
|
|
73
|
-
def get(feature)
|
|
74
|
-
@operations << Operation.new(:get, [feature])
|
|
75
|
-
@adapter.get(feature)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Public
|
|
79
|
-
def get_multi(features)
|
|
80
|
-
@operations << Operation.new(:get_multi, [features])
|
|
81
|
-
@adapter.get_multi(features)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Public
|
|
85
|
-
def get_all
|
|
86
|
-
@operations << Operation.new(:get_all, [])
|
|
87
|
-
@adapter.get_all
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Public
|
|
91
|
-
def enable(feature, gate, thing)
|
|
92
|
-
@operations << Operation.new(:enable, [feature, gate, thing])
|
|
93
|
-
@adapter.enable(feature, gate, thing)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Public
|
|
97
|
-
def disable(feature, gate, thing)
|
|
98
|
-
@operations << Operation.new(:disable, [feature, gate, thing])
|
|
99
|
-
@adapter.disable(feature, gate, thing)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Public
|
|
103
|
-
def import(source)
|
|
104
|
-
@operations << Operation.new(:import, [source])
|
|
105
|
-
@adapter.import(source)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Public
|
|
109
|
-
def export(format: :json, version: 1)
|
|
110
|
-
@operations << Operation.new(:export, [format, version])
|
|
111
|
-
@adapter.export(format: format, version: version)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
29
|
# Public: Count the number of times a certain operation happened.
|
|
115
|
-
def count(type)
|
|
116
|
-
type
|
|
30
|
+
def count(type = nil)
|
|
31
|
+
if type
|
|
32
|
+
type(type).size
|
|
33
|
+
else
|
|
34
|
+
@operations.size
|
|
35
|
+
end
|
|
117
36
|
end
|
|
118
37
|
|
|
119
38
|
# Public: Get all operations of a certain type.
|
|
@@ -135,6 +54,13 @@ module Flipper
|
|
|
135
54
|
inspect_id = ::Kernel::format "%x", (object_id * 2)
|
|
136
55
|
%(#<#{self.class}:0x#{inspect_id} @name=#{name.inspect}, @operations=#{@operations.inspect}, @adapter=#{@adapter.inspect}>)
|
|
137
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def wrap(method, *args, **kwargs, &block)
|
|
61
|
+
@operations << Operation.new(method, args, kwargs)
|
|
62
|
+
block.call
|
|
63
|
+
end
|
|
138
64
|
end
|
|
139
65
|
end
|
|
140
66
|
end
|