flipper 0.26.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +19 -13
- data/.github/workflows/examples.yml +32 -15
- data/Changelog.md +294 -154
- data/Gemfile +15 -10
- data/README.md +13 -11
- 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/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/mirroring.rb +59 -0
- data/examples/strict.rb +18 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +3 -5
- 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/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +0 -4
- data/lib/flipper/adapters/http/client.rb +26 -7
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +29 -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 +16 -7
- data/lib/flipper/adapters/poll/poller.rb +2 -125
- data/lib/flipper/adapters/poll.rb +5 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +4 -4
- data/lib/flipper/adapters/strict.rb +47 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/cloud/configuration.rb +258 -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 +26 -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 +183 -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 +88 -0
- data/lib/flipper/errors.rb +3 -3
- 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 +24 -5
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/metadata.rb +5 -1
- data/lib/flipper/middleware/memoizer.rb +30 -14
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/serializers/gzip.rb +24 -0
- data/lib/flipper/serializers/json.rb +19 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +95 -54
- data/lib/flipper/test/shared_adapter_test.rb +91 -48
- data/lib/flipper/typecast.rb +56 -15
- 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 +1 -1
- data/lib/flipper.rb +47 -10
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_builder_spec.rb +73 -0
- data/spec/flipper/adapter_spec.rb +30 -2
- data/spec/flipper/adapters/dual_write_spec.rb +2 -2
- data/spec/flipper/adapters/http_spec.rb +64 -8
- 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 +62 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cloud/configuration_spec.rb +252 -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 +108 -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 +156 -0
- data/spec/flipper/cloud_spec.rb +180 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +54 -73
- data/spec/flipper/engine_spec.rb +291 -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 +15 -5
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
- data/spec/flipper/middleware/memoizer_spec.rb +67 -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 +92 -28
- data/spec/spec_helper.rb +6 -13
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/climate_control.rb +7 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +11 -3
- metadata +166 -13
- data/.github/workflows/release.yml +0 -44
- data/.tool-versions +0 -1
- data/lib/flipper/railtie.rb +0 -47
- data/spec/flipper/railtie_spec.rb +0 -109
@@ -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,12 @@ module Flipper
|
|
22
22
|
write_timeout: options[:write_timeout],
|
23
23
|
max_retries: options[:max_retries],
|
24
24
|
debug_output: options[:debug_output])
|
25
|
-
@name = :http
|
26
25
|
end
|
27
26
|
|
28
27
|
def get(feature)
|
29
28
|
response = @client.get("/features/#{feature.key}")
|
30
29
|
if response.is_a?(Net::HTTPOK)
|
31
|
-
parsed_response =
|
30
|
+
parsed_response = Typecast.from_json(response.body)
|
32
31
|
result_for_feature(feature, parsed_response.fetch('gates'))
|
33
32
|
elsif response.is_a?(Net::HTTPNotFound)
|
34
33
|
default_config
|
@@ -39,10 +38,10 @@ module Flipper
|
|
39
38
|
|
40
39
|
def get_multi(features)
|
41
40
|
csv_keys = features.map(&:key).join(',')
|
42
|
-
response = @client.get("/features?keys=#{csv_keys}")
|
41
|
+
response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
|
43
42
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
44
43
|
|
45
|
-
parsed_response =
|
44
|
+
parsed_response = Typecast.from_json(response.body)
|
46
45
|
parsed_features = parsed_response.fetch('features')
|
47
46
|
gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
|
48
47
|
hash[parsed_feature['key']] = parsed_feature['gates']
|
@@ -57,10 +56,10 @@ module Flipper
|
|
57
56
|
end
|
58
57
|
|
59
58
|
def get_all
|
60
|
-
response = @client.get("/features")
|
59
|
+
response = @client.get("/features?exclude_gate_names=true")
|
61
60
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
62
61
|
|
63
|
-
parsed_response =
|
62
|
+
parsed_response = Typecast.from_json(response.body)
|
64
63
|
parsed_features = parsed_response.fetch('features')
|
65
64
|
gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
|
66
65
|
hash[parsed_feature['key']] = parsed_feature['gates']
|
@@ -68,7 +67,7 @@ module Flipper
|
|
68
67
|
end
|
69
68
|
|
70
69
|
result = {}
|
71
|
-
gates_by_key.
|
70
|
+
gates_by_key.each_key do |key|
|
72
71
|
feature = Feature.new(key, self)
|
73
72
|
result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
|
74
73
|
end
|
@@ -76,10 +75,10 @@ module Flipper
|
|
76
75
|
end
|
77
76
|
|
78
77
|
def features
|
79
|
-
response = @client.get('/features')
|
78
|
+
response = @client.get('/features?exclude_gate_names=true')
|
80
79
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
81
80
|
|
82
|
-
parsed_response =
|
81
|
+
parsed_response = Typecast.from_json(response.body)
|
83
82
|
parsed_response['features'].map { |feature| feature['key'] }.to_set
|
84
83
|
end
|
85
84
|
|
@@ -97,7 +96,7 @@ module Flipper
|
|
97
96
|
end
|
98
97
|
|
99
98
|
def enable(feature, gate, thing)
|
100
|
-
body = request_body_for_gate(gate, thing.value
|
99
|
+
body = request_body_for_gate(gate, thing.value)
|
101
100
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
102
101
|
response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
103
102
|
raise Error, response unless response.is_a?(Net::HTTPOK)
|
@@ -105,7 +104,7 @@ module Flipper
|
|
105
104
|
end
|
106
105
|
|
107
106
|
def disable(feature, gate, thing)
|
108
|
-
body = request_body_for_gate(gate, thing.value
|
107
|
+
body = request_body_for_gate(gate, thing.value)
|
109
108
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
110
109
|
response = case gate.key
|
111
110
|
when :percentage_of_actors, :percentage_of_time
|
@@ -123,6 +122,14 @@ module Flipper
|
|
123
122
|
true
|
124
123
|
end
|
125
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
|
+
|
126
133
|
private
|
127
134
|
|
128
135
|
def request_body_for_gate(gate, value)
|
@@ -130,11 +137,13 @@ module Flipper
|
|
130
137
|
when :boolean
|
131
138
|
{}
|
132
139
|
when :groups
|
133
|
-
{ name: value }
|
140
|
+
{ name: value.to_s }
|
134
141
|
when :actors
|
135
|
-
{ flipper_id: value }
|
142
|
+
{ flipper_id: value.to_s }
|
136
143
|
when :percentage_of_actors, :percentage_of_time
|
137
|
-
{ percentage: value }
|
144
|
+
{ percentage: value.to_s }
|
145
|
+
when :expression
|
146
|
+
value
|
138
147
|
else
|
139
148
|
raise "#{gate.key} is not a valid flipper gate key"
|
140
149
|
end
|
@@ -158,13 +167,17 @@ module Flipper
|
|
158
167
|
case gate.data_type
|
159
168
|
when :boolean, :integer
|
160
169
|
value ? value.to_s : value
|
170
|
+
when :json
|
171
|
+
value
|
161
172
|
when :set
|
162
173
|
value ? value.to_set : Set.new
|
163
174
|
else
|
164
|
-
unsupported_data_type
|
175
|
+
unsupported_data_type gate.data_type
|
165
176
|
end
|
166
177
|
end
|
167
178
|
|
179
|
+
private
|
180
|
+
|
168
181
|
def unsupported_data_type(data_type)
|
169
182
|
raise "#{data_type} is not supported by this adapter"
|
170
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,8 +5,8 @@ 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
|
8
|
+
class OperationLogger
|
9
|
+
include Flipper::Adapter
|
10
10
|
|
11
11
|
class Operation
|
12
12
|
attr_reader :type, :args
|
@@ -18,6 +18,8 @@ module Flipper
|
|
18
18
|
end
|
19
19
|
|
20
20
|
OperationTypes = [
|
21
|
+
:import,
|
22
|
+
:export,
|
21
23
|
:features,
|
22
24
|
:add,
|
23
25
|
:remove,
|
@@ -32,14 +34,9 @@ module Flipper
|
|
32
34
|
# Internal: An array of the operations that have happened.
|
33
35
|
attr_reader :operations
|
34
36
|
|
35
|
-
# Internal: The name of the adapter.
|
36
|
-
attr_reader :name
|
37
|
-
|
38
37
|
# Public
|
39
38
|
def initialize(adapter, operations = nil)
|
40
|
-
super(adapter)
|
41
39
|
@adapter = adapter
|
42
|
-
@name = :operation_logger
|
43
40
|
@operations = operations || []
|
44
41
|
end
|
45
42
|
|
@@ -98,6 +95,18 @@ module Flipper
|
|
98
95
|
@adapter.disable(feature, gate, thing)
|
99
96
|
end
|
100
97
|
|
98
|
+
# Public
|
99
|
+
def import(source)
|
100
|
+
@operations << Operation.new(:import, [source])
|
101
|
+
@adapter.import(source)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Public
|
105
|
+
def export(format: :json, version: 1)
|
106
|
+
@operations << Operation.new(:export, [format, version])
|
107
|
+
@adapter.export(format: format, version: version)
|
108
|
+
end
|
109
|
+
|
101
110
|
# Public: Count the number of times a certain operation happened.
|
102
111
|
def count(type)
|
103
112
|
type(type).size
|
@@ -1,125 +1,2 @@
|
|
1
|
-
|
2
|
-
require '
|
3
|
-
require 'concurrent/utility/monotonic_time'
|
4
|
-
require 'concurrent/map'
|
5
|
-
|
6
|
-
module Flipper
|
7
|
-
module Adapters
|
8
|
-
class Poll
|
9
|
-
class Poller
|
10
|
-
attr_reader :thread, :pid, :mutex, :interval, :last_synced_at
|
11
|
-
|
12
|
-
def self.instances
|
13
|
-
@instances ||= Concurrent::Map.new
|
14
|
-
end
|
15
|
-
private_class_method :instances
|
16
|
-
|
17
|
-
def self.get(key, options = {})
|
18
|
-
instances.compute_if_absent(key) { new(options) }
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.reset
|
22
|
-
instances.each {|_,poller| poller.stop }.clear
|
23
|
-
end
|
24
|
-
|
25
|
-
def initialize(options = {})
|
26
|
-
@thread = nil
|
27
|
-
@pid = Process.pid
|
28
|
-
@mutex = Mutex.new
|
29
|
-
@adapter = Memory.new
|
30
|
-
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
31
|
-
@remote_adapter = options.fetch(:remote_adapter)
|
32
|
-
@interval = options.fetch(:interval, 10).to_f
|
33
|
-
@lock = Concurrent::ReadWriteLock.new
|
34
|
-
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
35
|
-
|
36
|
-
if @interval < 1
|
37
|
-
warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
|
38
|
-
@interval = 1
|
39
|
-
end
|
40
|
-
|
41
|
-
@start_automatically = options.fetch(:start_automatically, true)
|
42
|
-
|
43
|
-
if options.fetch(:shutdown_automatically, true)
|
44
|
-
at_exit { stop }
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def adapter
|
49
|
-
@lock.with_read_lock { Memory.new(@adapter.get_all.dup) }
|
50
|
-
end
|
51
|
-
|
52
|
-
def start
|
53
|
-
reset if forked?
|
54
|
-
ensure_worker_running
|
55
|
-
end
|
56
|
-
|
57
|
-
def stop
|
58
|
-
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
59
|
-
operation: :stop,
|
60
|
-
})
|
61
|
-
@thread&.kill
|
62
|
-
end
|
63
|
-
|
64
|
-
def run
|
65
|
-
loop do
|
66
|
-
sleep jitter
|
67
|
-
start = Concurrent.monotonic_time
|
68
|
-
begin
|
69
|
-
@instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
|
70
|
-
adapter = Memory.new
|
71
|
-
adapter.import(@remote_adapter)
|
72
|
-
|
73
|
-
@lock.with_write_lock { @adapter.import(adapter) }
|
74
|
-
@last_synced_at.update { |time| Concurrent.monotonic_time }
|
75
|
-
end
|
76
|
-
rescue => exception
|
77
|
-
# you can instrument these using poller.flipper
|
78
|
-
end
|
79
|
-
|
80
|
-
sleep_interval = interval - (Concurrent.monotonic_time - start)
|
81
|
-
sleep sleep_interval if sleep_interval.positive?
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
private
|
86
|
-
|
87
|
-
def jitter
|
88
|
-
rand
|
89
|
-
end
|
90
|
-
|
91
|
-
def forked?
|
92
|
-
pid != Process.pid
|
93
|
-
end
|
94
|
-
|
95
|
-
def ensure_worker_running
|
96
|
-
# Return early if thread is alive and avoid the mutex lock and unlock.
|
97
|
-
return if thread_alive?
|
98
|
-
|
99
|
-
# If another thread is starting worker thread, then return early so this
|
100
|
-
# thread can enqueue and move on with life.
|
101
|
-
return unless mutex.try_lock
|
102
|
-
|
103
|
-
begin
|
104
|
-
return if thread_alive?
|
105
|
-
@thread = Thread.new { run }
|
106
|
-
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
107
|
-
operation: :thread_start,
|
108
|
-
})
|
109
|
-
ensure
|
110
|
-
mutex.unlock
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def thread_alive?
|
115
|
-
@thread && @thread.alive?
|
116
|
-
end
|
117
|
-
|
118
|
-
def reset
|
119
|
-
@pid = Process.pid
|
120
|
-
mutex.unlock if mutex.locked?
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
1
|
+
warn "DEPRECATION WARNING: Flipper::Adapters::Poll::Poller is deprecated. Use Flipper::Poller instead."
|
2
|
+
require 'flipper/adapters/poll'
|