flipper 0.24.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +45 -14
- data/.github/workflows/examples.yml +39 -16
- data/Changelog.md +2 -443
- data/Gemfile +19 -11
- data/README.md +31 -27
- data/Rakefile +6 -4
- 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 +27 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +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/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/threaded.rb +33 -0
- data/examples/dsl.rb +1 -15
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/expressions.rb +213 -0
- data/examples/instrumentation.rb +1 -0
- data/examples/instrumentation_last_accessed_at.rb +1 -0
- data/examples/mirroring.rb +59 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +10 -6
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +33 -7
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +72 -0
- data/lib/flipper/adapters/http/client.rb +44 -20
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +31 -16
- data/lib/flipper/adapters/instrumented.rb +25 -6
- data/lib/flipper/adapters/memoizable.rb +33 -21
- data/lib/flipper/adapters/memory.rb +81 -46
- data/lib/flipper/adapters/operation_logger.rb +17 -78
- data/lib/flipper/adapters/poll/poller.rb +2 -0
- data/lib/flipper/adapters/poll.rb +37 -0
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +45 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +263 -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/routes.rb +14 -0
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -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 +98 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +53 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +46 -45
- data/lib/flipper/engine.rb +102 -0
- 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/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 +11 -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/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -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 +9 -0
- data/lib/flipper/feature.rb +87 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +13 -11
- data/lib/flipper/gate_values.rb +5 -18
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/expression.rb +75 -0
- 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 +34 -6
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/metadata.rb +7 -1
- data/lib/flipper/middleware/memoizer.rb +28 -22
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +118 -0
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
- data/lib/flipper/test/shared_adapter_test.rb +101 -58
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +59 -18
- data/lib/flipper/types/actor.rb +13 -13
- data/lib/flipper/types/group.rb +4 -4
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +50 -11
- data/lib/generators/flipper/setup_generator.rb +63 -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/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +30 -2
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/dual_write_spec.rb +2 -2
- data/spec/flipper/adapters/failsafe_spec.rb +58 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +137 -55
- data/spec/flipper/adapters/instrumented_spec.rb +29 -11
- data/spec/flipper/adapters/memoizable_spec.rb +51 -31
- data/spec/flipper/adapters/memory_spec.rb +14 -3
- data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- data/spec/flipper/adapters/strict_spec.rb +64 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cli_spec.rb +164 -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/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 +181 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +54 -73
- data/spec/flipper/engine_spec.rb +373 -0
- 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/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/duration_spec.rb +43 -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 +13 -0
- data/spec/flipper/feature_check_context_spec.rb +17 -17
- data/spec/flipper/feature_spec.rb +436 -33
- data/spec/flipper/gate_values_spec.rb +2 -33
- data/spec/flipper/gates/boolean_spec.rb +1 -1
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/gates/group_spec.rb +2 -3
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +23 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
- data/spec/flipper/middleware/memoizer_spec.rb +74 -24
- data/spec/flipper/model/active_record_spec.rb +61 -0
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +121 -6
- data/spec/flipper/types/actor_spec.rb +63 -46
- data/spec/flipper/types/group_spec.rb +2 -2
- data/spec/flipper_integration_spec.rb +168 -58
- data/spec/flipper_spec.rb +93 -29
- data/spec/spec_helper.rb +8 -14
- 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/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +23 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +19 -2
- data/test_rails/system/test_help_test.rb +51 -0
- metadata +223 -19
- data/lib/flipper/railtie.rb +0 -47
- data/spec/flipper/railtie_spec.rb +0 -73
@@ -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),
|
@@ -19,14 +19,15 @@ 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
|
-
@name = :http
|
24
25
|
end
|
25
26
|
|
26
27
|
def get(feature)
|
27
28
|
response = @client.get("/features/#{feature.key}")
|
28
29
|
if response.is_a?(Net::HTTPOK)
|
29
|
-
parsed_response =
|
30
|
+
parsed_response = Typecast.from_json(response.body)
|
30
31
|
result_for_feature(feature, parsed_response.fetch('gates'))
|
31
32
|
elsif response.is_a?(Net::HTTPNotFound)
|
32
33
|
default_config
|
@@ -37,10 +38,10 @@ module Flipper
|
|
37
38
|
|
38
39
|
def get_multi(features)
|
39
40
|
csv_keys = features.map(&:key).join(',')
|
40
|
-
response = @client.get("/features?keys=#{csv_keys}")
|
41
|
+
response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
|
41
42
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
42
43
|
|
43
|
-
parsed_response =
|
44
|
+
parsed_response = Typecast.from_json(response.body)
|
44
45
|
parsed_features = parsed_response.fetch('features')
|
45
46
|
gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
|
46
47
|
hash[parsed_feature['key']] = parsed_feature['gates']
|
@@ -55,10 +56,10 @@ module Flipper
|
|
55
56
|
end
|
56
57
|
|
57
58
|
def get_all
|
58
|
-
response = @client.get("/features")
|
59
|
+
response = @client.get("/features?exclude_gate_names=true")
|
59
60
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
60
61
|
|
61
|
-
parsed_response =
|
62
|
+
parsed_response = Typecast.from_json(response.body)
|
62
63
|
parsed_features = parsed_response.fetch('features')
|
63
64
|
gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
|
64
65
|
hash[parsed_feature['key']] = parsed_feature['gates']
|
@@ -66,7 +67,7 @@ module Flipper
|
|
66
67
|
end
|
67
68
|
|
68
69
|
result = {}
|
69
|
-
gates_by_key.
|
70
|
+
gates_by_key.each_key do |key|
|
70
71
|
feature = Feature.new(key, self)
|
71
72
|
result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
|
72
73
|
end
|
@@ -74,10 +75,10 @@ module Flipper
|
|
74
75
|
end
|
75
76
|
|
76
77
|
def features
|
77
|
-
response = @client.get('/features')
|
78
|
+
response = @client.get('/features?exclude_gate_names=true')
|
78
79
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
79
80
|
|
80
|
-
parsed_response =
|
81
|
+
parsed_response = Typecast.from_json(response.body)
|
81
82
|
parsed_response['features'].map { |feature| feature['key'] }.to_set
|
82
83
|
end
|
83
84
|
|
@@ -95,7 +96,7 @@ module Flipper
|
|
95
96
|
end
|
96
97
|
|
97
98
|
def enable(feature, gate, thing)
|
98
|
-
body = request_body_for_gate(gate, thing.value
|
99
|
+
body = request_body_for_gate(gate, thing.value)
|
99
100
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
100
101
|
response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
101
102
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
@@ -103,7 +104,7 @@ module Flipper
|
|
103
104
|
end
|
104
105
|
|
105
106
|
def disable(feature, gate, thing)
|
106
|
-
body = request_body_for_gate(gate, thing.value
|
107
|
+
body = request_body_for_gate(gate, thing.value)
|
107
108
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
108
109
|
response = case gate.key
|
109
110
|
when :percentage_of_actors, :percentage_of_time
|
@@ -121,6 +122,14 @@ module Flipper
|
|
121
122
|
true
|
122
123
|
end
|
123
124
|
|
125
|
+
def import(source)
|
126
|
+
adapter = self.class.from(source)
|
127
|
+
export = adapter.export(format: :json, version: 1)
|
128
|
+
response = @client.post("/import", export.contents)
|
129
|
+
raise Error, response unless response.is_a?(Net::HTTPNoContent)
|
130
|
+
true
|
131
|
+
end
|
132
|
+
|
124
133
|
private
|
125
134
|
|
126
135
|
def request_body_for_gate(gate, value)
|
@@ -128,11 +137,13 @@ module Flipper
|
|
128
137
|
when :boolean
|
129
138
|
{}
|
130
139
|
when :groups
|
131
|
-
{ name: value }
|
140
|
+
{ name: value.to_s }
|
132
141
|
when :actors
|
133
|
-
{ flipper_id: value }
|
142
|
+
{ flipper_id: value.to_s }
|
134
143
|
when :percentage_of_actors, :percentage_of_time
|
135
|
-
{ percentage: value }
|
144
|
+
{ percentage: value.to_s }
|
145
|
+
when :expression
|
146
|
+
value
|
136
147
|
else
|
137
148
|
raise "#{gate.key} is not a valid flipper gate key"
|
138
149
|
end
|
@@ -156,13 +167,17 @@ module Flipper
|
|
156
167
|
case gate.data_type
|
157
168
|
when :boolean, :integer
|
158
169
|
value ? value.to_s : value
|
170
|
+
when :json
|
171
|
+
value
|
159
172
|
when :set
|
160
173
|
value ? value.to_set : Set.new
|
161
174
|
else
|
162
|
-
unsupported_data_type
|
175
|
+
unsupported_data_type gate.data_type
|
163
176
|
end
|
164
177
|
end
|
165
178
|
|
179
|
+
private
|
180
|
+
|
166
181
|
def unsupported_data_type(data_type)
|
167
182
|
raise "#{data_type} is not supported by this adapter"
|
168
183
|
end
|
@@ -4,7 +4,7 @@ module Flipper
|
|
4
4
|
module Adapters
|
5
5
|
# Internal: Adapter that wraps another adapter and instruments all adapter
|
6
6
|
# operations.
|
7
|
-
class Instrumented
|
7
|
+
class Instrumented
|
8
8
|
include ::Flipper::Adapter
|
9
9
|
|
10
10
|
# Private: The name of instrumentation events.
|
@@ -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.
|
@@ -24,9 +21,7 @@ module Flipper
|
|
24
21
|
# :instrumenter - What to use to instrument all the things.
|
25
22
|
#
|
26
23
|
def initialize(adapter, options = {})
|
27
|
-
super(adapter)
|
28
24
|
@adapter = adapter
|
29
|
-
@name = :instrumented
|
30
25
|
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
31
26
|
end
|
32
27
|
|
@@ -146,6 +141,30 @@ module Flipper
|
|
146
141
|
payload[:result] = @adapter.disable(feature, gate, thing)
|
147
142
|
end
|
148
143
|
end
|
144
|
+
|
145
|
+
def import(source)
|
146
|
+
default_payload = {
|
147
|
+
operation: :import,
|
148
|
+
adapter_name: @adapter.name,
|
149
|
+
}
|
150
|
+
|
151
|
+
@instrumenter.instrument(InstrumentationName, default_payload) do |payload|
|
152
|
+
payload[:result] = @adapter.import(source)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def export(format: :json, version: 1)
|
157
|
+
default_payload = {
|
158
|
+
operation: :export,
|
159
|
+
adapter_name: @adapter.name,
|
160
|
+
format: format,
|
161
|
+
version: version,
|
162
|
+
}
|
163
|
+
|
164
|
+
@instrumenter.instrument(InstrumentationName, default_payload) do |payload|
|
165
|
+
payload[:result] = @adapter.export(format: format, version: version)
|
166
|
+
end
|
167
|
+
end
|
149
168
|
end
|
150
169
|
end
|
151
170
|
end
|
@@ -5,39 +5,28 @@ module Flipper
|
|
5
5
|
# Internal: Adapter that wraps another adapter with the ability to memoize
|
6
6
|
# adapter calls in memory. Used by flipper dsl and the memoizer middleware
|
7
7
|
# to make it possible to memoize adapter calls for the duration of a request.
|
8
|
-
class Memoizable
|
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
|
-
super(adapter)
|
31
19
|
@adapter = adapter
|
32
|
-
@name = :memoizable
|
33
20
|
@cache = cache || {}
|
34
21
|
@memoize = false
|
22
|
+
@features_key = :flipper_features
|
23
|
+
@get_all_key = :all_memoized
|
35
24
|
end
|
36
25
|
|
37
26
|
# Public
|
38
27
|
def features
|
39
28
|
if memoizing?
|
40
|
-
cache.fetch(
|
29
|
+
cache.fetch(@features_key) { cache[@features_key] = @adapter.features }
|
41
30
|
else
|
42
31
|
@adapter.features
|
43
32
|
end
|
@@ -95,9 +84,9 @@ module Flipper
|
|
95
84
|
def get_all
|
96
85
|
if memoizing?
|
97
86
|
response = nil
|
98
|
-
if cache[
|
87
|
+
if cache[@get_all_key]
|
99
88
|
response = {}
|
100
|
-
cache[
|
89
|
+
cache[@features_key].each do |key|
|
101
90
|
response[key] = cache[key_for(key)]
|
102
91
|
end
|
103
92
|
else
|
@@ -105,8 +94,8 @@ module Flipper
|
|
105
94
|
response.each do |key, value|
|
106
95
|
cache[key_for(key)] = value
|
107
96
|
end
|
108
|
-
cache[
|
109
|
-
cache[
|
97
|
+
cache[@features_key] = response.keys.to_set
|
98
|
+
cache[@get_all_key] = true
|
110
99
|
end
|
111
100
|
|
112
101
|
# Ensures that looking up other features that do not exist doesn't
|
@@ -128,6 +117,19 @@ module Flipper
|
|
128
117
|
@adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
|
129
118
|
end
|
130
119
|
|
120
|
+
# Public
|
121
|
+
def read_only?
|
122
|
+
@adapter.read_only?
|
123
|
+
end
|
124
|
+
|
125
|
+
def import(source)
|
126
|
+
@adapter.import(source).tap { cache.clear if memoizing? }
|
127
|
+
end
|
128
|
+
|
129
|
+
def export(format: :json, version: 1)
|
130
|
+
@adapter.export(format: format, version: version)
|
131
|
+
end
|
132
|
+
|
131
133
|
# Internal: Turns local caching on/off.
|
132
134
|
#
|
133
135
|
# value - The Boolean that decides if local caching is on.
|
@@ -141,10 +143,20 @@ module Flipper
|
|
141
143
|
!!@memoize
|
142
144
|
end
|
143
145
|
|
146
|
+
if RUBY_VERSION >= '3.0'
|
147
|
+
def method_missing(name, *args, **kwargs, &block)
|
148
|
+
@adapter.send name, *args, **kwargs, &block
|
149
|
+
end
|
150
|
+
else
|
151
|
+
def method_missing(name, *args, &block)
|
152
|
+
@adapter.send name, *args, &block
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
144
156
|
private
|
145
157
|
|
146
158
|
def key_for(key)
|
147
|
-
|
159
|
+
"feature/#{key}"
|
148
160
|
end
|
149
161
|
|
150
162
|
def expire_feature(feature)
|
@@ -152,7 +164,7 @@ module Flipper
|
|
152
164
|
end
|
153
165
|
|
154
166
|
def expire_features_set
|
155
|
-
cache.delete(
|
167
|
+
cache.delete(@features_key) if memoizing?
|
156
168
|
end
|
157
169
|
end
|
158
170
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
require
|
1
|
+
require "flipper/adapter"
|
2
|
+
require "flipper/typecast"
|
2
3
|
|
3
4
|
module Flipper
|
4
5
|
module Adapters
|
@@ -7,93 +8,99 @@ module Flipper
|
|
7
8
|
class Memory
|
8
9
|
include ::Flipper::Adapter
|
9
10
|
|
10
|
-
FeaturesKey = :features
|
11
|
-
|
12
|
-
# Public: The name of the adapter.
|
13
|
-
attr_reader :name
|
14
|
-
|
15
11
|
# Public
|
16
|
-
def initialize(source = nil)
|
17
|
-
@source = source
|
18
|
-
@
|
12
|
+
def initialize(source = nil, threadsafe: true)
|
13
|
+
@source = Typecast.features_hash(source)
|
14
|
+
@lock = Mutex.new if threadsafe
|
15
|
+
reset
|
19
16
|
end
|
20
17
|
|
21
18
|
# Public: The set of known features.
|
22
19
|
def features
|
23
|
-
@source.keys.to_set
|
20
|
+
synchronize { @source.keys }.to_set
|
24
21
|
end
|
25
22
|
|
26
23
|
# Public: Adds a feature to the set of known features.
|
27
24
|
def add(feature)
|
28
|
-
@source[feature.key] ||= default_config
|
25
|
+
synchronize { @source[feature.key] ||= default_config }
|
29
26
|
true
|
30
27
|
end
|
31
28
|
|
32
29
|
# Public: Removes a feature from the set of known features and clears
|
33
30
|
# all the values for the feature.
|
34
31
|
def remove(feature)
|
35
|
-
@source.delete(feature.key)
|
32
|
+
synchronize { @source.delete(feature.key) }
|
36
33
|
true
|
37
34
|
end
|
38
35
|
|
39
36
|
# Public: Clears all the gate values for a feature.
|
40
37
|
def clear(feature)
|
41
|
-
@source[feature.key] = default_config
|
38
|
+
synchronize { @source[feature.key] = default_config }
|
42
39
|
true
|
43
40
|
end
|
44
41
|
|
45
42
|
# Public
|
46
43
|
def get(feature)
|
47
|
-
@source[feature.key] || default_config
|
44
|
+
synchronize { @source[feature.key] } || default_config
|
48
45
|
end
|
49
46
|
|
50
47
|
def get_multi(features)
|
51
|
-
|
52
|
-
|
53
|
-
|
48
|
+
synchronize do
|
49
|
+
result = {}
|
50
|
+
features.each do |feature|
|
51
|
+
result[feature.key] = @source[feature.key] || default_config
|
52
|
+
end
|
53
|
+
result
|
54
54
|
end
|
55
|
-
result
|
56
55
|
end
|
57
56
|
|
58
57
|
def get_all
|
59
|
-
@source
|
58
|
+
synchronize { Typecast.features_hash(@source) }
|
60
59
|
end
|
61
60
|
|
62
61
|
# Public
|
63
62
|
def enable(feature, gate, thing)
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
63
|
+
synchronize do
|
64
|
+
@source[feature.key] ||= default_config
|
65
|
+
|
66
|
+
case gate.data_type
|
67
|
+
when :boolean
|
68
|
+
@source[feature.key] = default_config
|
69
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
70
|
+
when :integer
|
71
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
72
|
+
when :set
|
73
|
+
@source[feature.key][gate.key] << thing.value.to_s
|
74
|
+
when :json
|
75
|
+
@source[feature.key][gate.key] = thing.value
|
76
|
+
else
|
77
|
+
raise "#{gate} is not supported by this adapter yet"
|
78
|
+
end
|
79
|
+
|
80
|
+
true
|
76
81
|
end
|
77
|
-
|
78
|
-
true
|
79
82
|
end
|
80
83
|
|
81
84
|
# Public
|
82
85
|
def disable(feature, gate, thing)
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
86
|
+
synchronize do
|
87
|
+
@source[feature.key] ||= default_config
|
88
|
+
|
89
|
+
case gate.data_type
|
90
|
+
when :boolean
|
91
|
+
@source[feature.key] = default_config
|
92
|
+
when :integer
|
93
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
94
|
+
when :set
|
95
|
+
@source[feature.key][gate.key].delete thing.value.to_s
|
96
|
+
when :json
|
97
|
+
@source[feature.key].delete(gate.key)
|
98
|
+
else
|
99
|
+
raise "#{gate} is not supported by this adapter yet"
|
100
|
+
end
|
101
|
+
|
102
|
+
true
|
94
103
|
end
|
95
|
-
|
96
|
-
true
|
97
104
|
end
|
98
105
|
|
99
106
|
# Public
|
@@ -104,6 +111,34 @@ module Flipper
|
|
104
111
|
]
|
105
112
|
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
106
113
|
end
|
114
|
+
|
115
|
+
# Public: a more efficient implementation of import for this adapter
|
116
|
+
def import(source)
|
117
|
+
adapter = self.class.from(source)
|
118
|
+
get_all = Typecast.features_hash(adapter.get_all)
|
119
|
+
synchronize { @source.replace(get_all) }
|
120
|
+
true
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def reset
|
126
|
+
@pid = Process.pid
|
127
|
+
@lock&.unlock if @lock&.locked?
|
128
|
+
end
|
129
|
+
|
130
|
+
def forked?
|
131
|
+
@pid != Process.pid
|
132
|
+
end
|
133
|
+
|
134
|
+
def synchronize(&block)
|
135
|
+
if @lock
|
136
|
+
reset if forked?
|
137
|
+
@lock.synchronize(&block)
|
138
|
+
else
|
139
|
+
block.call
|
140
|
+
end
|
141
|
+
end
|
107
142
|
end
|
108
143
|
end
|
109
144
|
end
|
@@ -5,102 +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
|
-
:features,
|
22
|
-
:add,
|
23
|
-
:remove,
|
24
|
-
:clear,
|
25
|
-
:get,
|
26
|
-
:get_multi,
|
27
|
-
:get_all,
|
28
|
-
:enable,
|
29
|
-
:disable,
|
30
|
-
].freeze
|
31
|
-
|
32
20
|
# Internal: An array of the operations that have happened.
|
33
21
|
attr_reader :operations
|
34
22
|
|
35
|
-
# Internal: The name of the adapter.
|
36
|
-
attr_reader :name
|
37
|
-
|
38
23
|
# Public
|
39
24
|
def initialize(adapter, operations = nil)
|
40
25
|
super(adapter)
|
41
|
-
@adapter = adapter
|
42
|
-
@name = :operation_logger
|
43
26
|
@operations = operations || []
|
44
27
|
end
|
45
28
|
|
46
|
-
# Public: The set of known features.
|
47
|
-
def features
|
48
|
-
@operations << Operation.new(:features, [])
|
49
|
-
@adapter.features
|
50
|
-
end
|
51
|
-
|
52
|
-
# Public: Adds a feature to the set of known features.
|
53
|
-
def add(feature)
|
54
|
-
@operations << Operation.new(:add, [feature])
|
55
|
-
@adapter.add(feature)
|
56
|
-
end
|
57
|
-
|
58
|
-
# Public: Removes a feature from the set of known features and clears
|
59
|
-
# all the values for the feature.
|
60
|
-
def remove(feature)
|
61
|
-
@operations << Operation.new(:remove, [feature])
|
62
|
-
@adapter.remove(feature)
|
63
|
-
end
|
64
|
-
|
65
|
-
# Public: Clears all the gate values for a feature.
|
66
|
-
def clear(feature)
|
67
|
-
@operations << Operation.new(:clear, [feature])
|
68
|
-
@adapter.clear(feature)
|
69
|
-
end
|
70
|
-
|
71
|
-
# Public
|
72
|
-
def get(feature)
|
73
|
-
@operations << Operation.new(:get, [feature])
|
74
|
-
@adapter.get(feature)
|
75
|
-
end
|
76
|
-
|
77
|
-
# Public
|
78
|
-
def get_multi(features)
|
79
|
-
@operations << Operation.new(:get_multi, [features])
|
80
|
-
@adapter.get_multi(features)
|
81
|
-
end
|
82
|
-
|
83
|
-
# Public
|
84
|
-
def get_all
|
85
|
-
@operations << Operation.new(:get_all, [])
|
86
|
-
@adapter.get_all
|
87
|
-
end
|
88
|
-
|
89
|
-
# Public
|
90
|
-
def enable(feature, gate, thing)
|
91
|
-
@operations << Operation.new(:enable, [feature, gate, thing])
|
92
|
-
@adapter.enable(feature, gate, thing)
|
93
|
-
end
|
94
|
-
|
95
|
-
# Public
|
96
|
-
def disable(feature, gate, thing)
|
97
|
-
@operations << Operation.new(:disable, [feature, gate, thing])
|
98
|
-
@adapter.disable(feature, gate, thing)
|
99
|
-
end
|
100
|
-
|
101
29
|
# Public: Count the number of times a certain operation happened.
|
102
|
-
def count(type)
|
103
|
-
type
|
30
|
+
def count(type = nil)
|
31
|
+
if type
|
32
|
+
type(type).size
|
33
|
+
else
|
34
|
+
@operations.size
|
35
|
+
end
|
104
36
|
end
|
105
37
|
|
106
38
|
# Public: Get all operations of a certain type.
|
@@ -122,6 +54,13 @@ module Flipper
|
|
122
54
|
inspect_id = ::Kernel::format "%x", (object_id * 2)
|
123
55
|
%(#<#{self.class}:0x#{inspect_id} @name=#{name.inspect}, @operations=#{@operations.inspect}, @adapter=#{@adapter.inspect}>)
|
124
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
|
125
64
|
end
|
126
65
|
end
|
127
66
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'flipper/adapters/sync/synchronizer'
|
2
|
+
require 'flipper/poller'
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Adapters
|
6
|
+
class Poll
|
7
|
+
extend Forwardable
|
8
|
+
include ::Flipper::Adapter
|
9
|
+
|
10
|
+
# Deprecated
|
11
|
+
Poller = ::Flipper::Poller
|
12
|
+
|
13
|
+
attr_reader :adapter, :poller
|
14
|
+
|
15
|
+
def_delegators :synced_adapter, :features, :get, :get_multi, :get_all, :add, :remove, :clear, :enable, :disable
|
16
|
+
|
17
|
+
def initialize(poller, adapter)
|
18
|
+
@adapter = adapter
|
19
|
+
@poller = poller
|
20
|
+
@last_synced_at = 0
|
21
|
+
@poller.start
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def synced_adapter
|
27
|
+
@poller.start
|
28
|
+
poller_last_synced_at = @poller.last_synced_at.value
|
29
|
+
if poller_last_synced_at > @last_synced_at
|
30
|
+
Flipper::Adapters::Sync::Synchronizer.new(@adapter, @poller.adapter).call
|
31
|
+
@last_synced_at = poller_last_synced_at
|
32
|
+
end
|
33
|
+
@adapter
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|