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
|
@@ -2,7 +2,7 @@ require 'flipper/adapters/memoizable'
|
|
|
2
2
|
require 'flipper/adapters/operation_logger'
|
|
3
3
|
|
|
4
4
|
RSpec.describe Flipper::Adapters::Memoizable do
|
|
5
|
-
let(:features_key) {
|
|
5
|
+
let(:features_key) { :flipper_features }
|
|
6
6
|
let(:adapter) { Flipper::Adapters::Memory.new }
|
|
7
7
|
let(:flipper) { Flipper.new(adapter) }
|
|
8
8
|
let(:cache) { {} }
|
|
@@ -54,7 +54,7 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
|
54
54
|
it 'memoizes feature' do
|
|
55
55
|
feature = flipper[:stats]
|
|
56
56
|
result = subject.get(feature)
|
|
57
|
-
expect(cache[
|
|
57
|
+
expect(cache["feature/#{feature.key}"]).to be(result)
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -83,8 +83,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
|
83
83
|
features = names.map { |name| flipper[name] }
|
|
84
84
|
results = subject.get_multi(features)
|
|
85
85
|
features.each do |feature|
|
|
86
|
-
expect(cache[
|
|
87
|
-
expect(cache[
|
|
86
|
+
expect(cache["feature/#{feature.key}"]).not_to be(nil)
|
|
87
|
+
expect(cache["feature/#{feature.key}"]).to be(results[feature.key])
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
end
|
|
@@ -115,10 +115,10 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
|
115
115
|
features = names.map { |name| flipper[name].tap(&:enable) }
|
|
116
116
|
results = subject.get_all
|
|
117
117
|
features.each do |feature|
|
|
118
|
-
expect(cache[
|
|
119
|
-
expect(cache[
|
|
118
|
+
expect(cache["feature/#{feature.key}"]).not_to be(nil)
|
|
119
|
+
expect(cache["feature/#{feature.key}"]).to be(results[feature.key])
|
|
120
120
|
end
|
|
121
|
-
expect(cache[
|
|
121
|
+
expect(cache[:flipper_features]).to eq(names.map(&:to_s).to_set)
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
it 'only calls get_all once for memoized adapter' do
|
|
@@ -188,9 +188,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
|
188
188
|
it 'unmemoizes feature' do
|
|
189
189
|
feature = flipper[:stats]
|
|
190
190
|
gate = feature.gate(:boolean)
|
|
191
|
-
cache[
|
|
192
|
-
subject.enable(feature, gate,
|
|
193
|
-
expect(cache[
|
|
191
|
+
cache["feature/#{feature.key}"] = { some: 'thing' }
|
|
192
|
+
subject.enable(feature, gate, Flipper::Types::Boolean.new)
|
|
193
|
+
expect(cache["feature/#{feature.key}"]).to be_nil
|
|
194
194
|
end
|
|
195
195
|
end
|
|
196
196
|
|
|
@@ -202,8 +202,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
|
202
202
|
it 'returns result' do
|
|
203
203
|
feature = flipper[:stats]
|
|
204
204
|
gate = feature.gate(:boolean)
|
|
205
|
-
result = subject.enable(feature, gate,
|
|
206
|
-
adapter_result = adapter.enable(feature, gate,
|
|
205
|
+
result = subject.enable(feature, gate, Flipper::Types::Boolean.new)
|
|
206
|
+
adapter_result = adapter.enable(feature, gate, Flipper::Types::Boolean.new)
|
|
207
207
|
expect(result).to eq(adapter_result)
|
|
208
208
|
end
|
|
209
209
|
end
|
|
@@ -218,9 +218,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
|
218
218
|
it 'unmemoizes feature' do
|
|
219
219
|
feature = flipper[:stats]
|
|
220
220
|
gate = feature.gate(:boolean)
|
|
221
|
-
cache[
|
|
222
|
-
subject.disable(feature, gate,
|
|
223
|
-
expect(cache[
|
|
221
|
+
cache["feature/#{feature.key}"] = { some: 'thing' }
|
|
222
|
+
subject.disable(feature, gate, Flipper::Types::Boolean.new)
|
|
223
|
+
expect(cache["feature/#{feature.key}"]).to be_nil
|
|
224
224
|
end
|
|
225
225
|
end
|
|
226
226
|
|
|
@@ -232,8 +232,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
|
232
232
|
it 'returns result' do
|
|
233
233
|
feature = flipper[:stats]
|
|
234
234
|
gate = feature.gate(:boolean)
|
|
235
|
-
result = subject.disable(feature, gate,
|
|
236
|
-
adapter_result = adapter.disable(feature, gate,
|
|
235
|
+
result = subject.disable(feature, gate, Flipper::Types::Boolean.new)
|
|
236
|
+
adapter_result = adapter.disable(feature, gate, Flipper::Types::Boolean.new)
|
|
237
237
|
expect(result).to eq(adapter_result)
|
|
238
238
|
end
|
|
239
239
|
end
|
|
@@ -332,9 +332,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
|
332
332
|
|
|
333
333
|
it 'unmemoizes the feature' do
|
|
334
334
|
feature = flipper[:stats]
|
|
335
|
-
cache[
|
|
335
|
+
cache["feature/#{feature.key}"] = { some: 'thing' }
|
|
336
336
|
subject.remove(feature)
|
|
337
|
-
expect(cache[
|
|
337
|
+
expect(cache["feature/#{feature.key}"]).to be_nil
|
|
338
338
|
end
|
|
339
339
|
end
|
|
340
340
|
|
|
@@ -357,9 +357,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
|
357
357
|
|
|
358
358
|
it 'unmemoizes feature' do
|
|
359
359
|
feature = flipper[:stats]
|
|
360
|
-
cache[
|
|
360
|
+
cache["feature/#{feature.key}"] = { some: 'thing' }
|
|
361
361
|
subject.clear(feature)
|
|
362
|
-
expect(cache[
|
|
362
|
+
expect(cache["feature/#{feature.key}"]).to be_nil
|
|
363
363
|
end
|
|
364
364
|
end
|
|
365
365
|
|
|
@@ -37,7 +37,7 @@ RSpec.describe Flipper::Adapters::OperationLogger do
|
|
|
37
37
|
before do
|
|
38
38
|
@feature = flipper[:stats]
|
|
39
39
|
@gate = @feature.gate(:boolean)
|
|
40
|
-
@thing =
|
|
40
|
+
@thing = Flipper::Types::Boolean.new
|
|
41
41
|
@result = subject.enable(@feature, @gate, @thing)
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -54,7 +54,7 @@ RSpec.describe Flipper::Adapters::OperationLogger do
|
|
|
54
54
|
before do
|
|
55
55
|
@feature = flipper[:stats]
|
|
56
56
|
@gate = @feature.gate(:boolean)
|
|
57
|
-
@thing =
|
|
57
|
+
@thing = Flipper::Types::Boolean.new
|
|
58
58
|
@result = subject.disable(@feature, @gate, @thing)
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'flipper/adapters/poll'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Flipper::Adapters::Poll do
|
|
4
|
+
let(:remote_adapter) {
|
|
5
|
+
adapter = Flipper::Adapters::Memory.new(threadsafe: true)
|
|
6
|
+
flipper = Flipper.new(adapter)
|
|
7
|
+
flipper.enable(:search)
|
|
8
|
+
flipper.enable(:analytics)
|
|
9
|
+
adapter
|
|
10
|
+
}
|
|
11
|
+
let(:local_adapter) { Flipper::Adapters::Memory.new(threadsafe: true) }
|
|
12
|
+
let(:poller) {
|
|
13
|
+
Flipper::Poller.get("for_spec", {
|
|
14
|
+
start_automatically: false,
|
|
15
|
+
remote_adapter: remote_adapter,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
it "syncs in main thread if local adapter is empty" do
|
|
20
|
+
instance = described_class.new(poller, local_adapter)
|
|
21
|
+
instance.features # call something to force sync
|
|
22
|
+
expect(local_adapter.features).to eq(remote_adapter.features)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "does not sync in main thread if local adapter is not empty" do
|
|
26
|
+
# make local not empty by importing remote
|
|
27
|
+
flipper = Flipper.new(local_adapter)
|
|
28
|
+
flipper.import(remote_adapter)
|
|
29
|
+
|
|
30
|
+
# make a fake poller to verify calls
|
|
31
|
+
poller = double("Poller", last_synced_at: Concurrent::AtomicFixnum.new(0))
|
|
32
|
+
expect(poller).to receive(:start).twice
|
|
33
|
+
expect(poller).not_to receive(:sync)
|
|
34
|
+
|
|
35
|
+
# create new instance and call something to force sync
|
|
36
|
+
instance = described_class.new(poller, local_adapter)
|
|
37
|
+
instance.features # call something to force sync
|
|
38
|
+
|
|
39
|
+
expect(local_adapter.features).to eq(remote_adapter.features)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -5,11 +5,12 @@ RSpec.describe Flipper::Adapters::ReadOnly do
|
|
|
5
5
|
let(:flipper) { Flipper.new(subject) }
|
|
6
6
|
let(:feature) { flipper[:stats] }
|
|
7
7
|
|
|
8
|
-
let(:boolean_gate)
|
|
9
|
-
let(:group_gate)
|
|
10
|
-
let(:actor_gate)
|
|
11
|
-
let(:
|
|
12
|
-
let(:
|
|
8
|
+
let(:boolean_gate) { feature.gate(:boolean) }
|
|
9
|
+
let(:group_gate) { feature.gate(:group) }
|
|
10
|
+
let(:actor_gate) { feature.gate(:actor) }
|
|
11
|
+
let(:expression_gate) { feature.gate(:expression) }
|
|
12
|
+
let(:actors_gate) { feature.gate(:percentage_of_actors) }
|
|
13
|
+
let(:time_gate) { feature.gate(:percentage_of_time) }
|
|
13
14
|
|
|
14
15
|
subject { described_class.new(adapter) }
|
|
15
16
|
|
|
@@ -41,18 +42,28 @@ RSpec.describe Flipper::Adapters::ReadOnly do
|
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
it 'can get feature' do
|
|
45
|
+
expression = Flipper.property(:plan).eq("basic")
|
|
44
46
|
actor22 = Flipper::Actor.new('22')
|
|
45
|
-
adapter.enable(feature, boolean_gate,
|
|
47
|
+
adapter.enable(feature, boolean_gate, Flipper::Types::Boolean.new)
|
|
46
48
|
adapter.enable(feature, group_gate, flipper.group(:admins))
|
|
47
|
-
adapter.enable(feature, actor_gate,
|
|
48
|
-
adapter.enable(feature, actors_gate,
|
|
49
|
-
adapter.enable(feature, time_gate,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
adapter.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))
|
|
50
|
+
adapter.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))
|
|
51
|
+
adapter.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))
|
|
52
|
+
adapter.enable(feature, expression_gate, expression)
|
|
53
|
+
|
|
54
|
+
expect(subject.get(feature)).to eq({
|
|
55
|
+
boolean: 'true',
|
|
56
|
+
groups: Set['admins'],
|
|
57
|
+
actors: Set['22'],
|
|
58
|
+
expression: {
|
|
59
|
+
"Equal" => [
|
|
60
|
+
{"Property" => ["plan"]},
|
|
61
|
+
"basic",
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
percentage_of_actors: '25',
|
|
65
|
+
percentage_of_time: '45',
|
|
66
|
+
})
|
|
56
67
|
end
|
|
57
68
|
|
|
58
69
|
it 'can get features' do
|
|
@@ -61,6 +72,10 @@ RSpec.describe Flipper::Adapters::ReadOnly do
|
|
|
61
72
|
expect(subject.features).to eq(Set['stats'])
|
|
62
73
|
end
|
|
63
74
|
|
|
75
|
+
it 'is configured as read only' do
|
|
76
|
+
expect(subject.read_only?).to eq(true)
|
|
77
|
+
end
|
|
78
|
+
|
|
64
79
|
it 'raises error on add' do
|
|
65
80
|
expect { subject.add(feature) }.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
|
|
66
81
|
end
|
|
@@ -74,12 +89,12 @@ RSpec.describe Flipper::Adapters::ReadOnly do
|
|
|
74
89
|
end
|
|
75
90
|
|
|
76
91
|
it 'raises error on enable' do
|
|
77
|
-
expect { subject.enable(feature, boolean_gate,
|
|
92
|
+
expect { subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new) }
|
|
78
93
|
.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
|
|
79
94
|
end
|
|
80
95
|
|
|
81
96
|
it 'raises error on disable' do
|
|
82
|
-
expect { subject.disable(feature, boolean_gate,
|
|
97
|
+
expect { subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new) }
|
|
83
98
|
.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
|
|
84
99
|
end
|
|
85
100
|
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
RSpec.describe Flipper::Adapters::Strict do
|
|
2
|
+
let(:flipper) { Flipper.new(subject) }
|
|
3
|
+
let(:feature) { flipper[:unknown] }
|
|
4
|
+
|
|
5
|
+
it_should_behave_like 'a flipper adapter' do
|
|
6
|
+
subject { described_class.new(Flipper::Adapters::Memory.new, :noop) }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
[true, :raise].each do |handler|
|
|
10
|
+
context "handler = #{handler}" do
|
|
11
|
+
subject { described_class.new(Flipper::Adapters::Memory.new, handler) }
|
|
12
|
+
|
|
13
|
+
context "#get" do
|
|
14
|
+
it "raises an error for unknown feature" do
|
|
15
|
+
expect { subject.get(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context "#get_multi" do
|
|
20
|
+
it "raises an error for unknown feature" do
|
|
21
|
+
expect { subject.get_multi([feature]) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context "#add" do
|
|
26
|
+
it "raises an error for unknown feature" do
|
|
27
|
+
expect { subject.add(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context "handler = :warn" do
|
|
34
|
+
subject { described_class.new(Flipper::Adapters::Memory.new, :warn) }
|
|
35
|
+
|
|
36
|
+
context "#get" do
|
|
37
|
+
it "warns for unknown feature" do
|
|
38
|
+
expect(capture_output { subject.get(feature) }).to match(/Could not find feature "unknown"/)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
context "#get_multi" do
|
|
43
|
+
it "warns for unknown feature" do
|
|
44
|
+
expect(capture_output { subject.get_multi([feature]) }).to match(/Could not find feature "unknown"/)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context "#add" do
|
|
49
|
+
it "warns for unknown feature" do
|
|
50
|
+
expect(capture_output { subject.add(feature) }).to match(/Could not find feature "unknown"/)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context "handler = Block" do
|
|
56
|
+
let(:unknown_features) { [] }
|
|
57
|
+
subject do
|
|
58
|
+
described_class.new(Flipper::Adapters::Memory.new) { |feature| unknown_features << feature.key}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
context "#get" do
|
|
63
|
+
it "calls block for unknown feature" do
|
|
64
|
+
subject.get(feature)
|
|
65
|
+
expect(unknown_features).to eq(["unknown"])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context "#get_multi" do
|
|
70
|
+
it "calls block for unknown feature" do
|
|
71
|
+
subject.get_multi([flipper[:foo], flipper[:bar]])
|
|
72
|
+
expect(unknown_features).to eq(["foo", "bar"])
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
context "#add" do
|
|
77
|
+
it "calls block for unknown feature" do
|
|
78
|
+
subject.add(feature)
|
|
79
|
+
expect(unknown_features).to eq(["unknown"])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe ".with_sync_mode" do
|
|
85
|
+
subject { described_class.new(Flipper::Adapters::Memory.new, :raise) }
|
|
86
|
+
|
|
87
|
+
it "bypasses strict checks for add" do
|
|
88
|
+
described_class.with_sync_mode do
|
|
89
|
+
expect { subject.add(feature) }.not_to raise_error
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "bypasses strict checks for get" do
|
|
94
|
+
described_class.with_sync_mode do
|
|
95
|
+
expect { subject.get(feature) }.not_to raise_error
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "bypasses strict checks for get_multi" do
|
|
100
|
+
described_class.with_sync_mode do
|
|
101
|
+
expect { subject.get_multi([feature]) }.not_to raise_error
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "restores previous sync mode after block" do
|
|
106
|
+
described_class.with_sync_mode do
|
|
107
|
+
# inside sync mode
|
|
108
|
+
end
|
|
109
|
+
expect { subject.add(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "restores previous sync mode even on error" do
|
|
113
|
+
begin
|
|
114
|
+
described_class.with_sync_mode do
|
|
115
|
+
raise "boom"
|
|
116
|
+
end
|
|
117
|
+
rescue RuntimeError
|
|
118
|
+
end
|
|
119
|
+
expect { subject.add(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -8,6 +8,8 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
|
|
|
8
8
|
end
|
|
9
9
|
let(:flipper) { Flipper.new(adapter) }
|
|
10
10
|
let(:feature) { flipper[:search] }
|
|
11
|
+
let(:plan_expression) { Flipper.property(:plan).eq("basic") }
|
|
12
|
+
let(:age_expression) { Flipper.property(:age).gte(21) }
|
|
11
13
|
|
|
12
14
|
context "when remote disabled" do
|
|
13
15
|
let(:remote) { Flipper::GateValues.new({}) }
|
|
@@ -63,6 +65,7 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
|
|
|
63
65
|
boolean: nil,
|
|
64
66
|
actors: Set["1"],
|
|
65
67
|
groups: Set["staff"],
|
|
68
|
+
expression: plan_expression.value,
|
|
66
69
|
percentage_of_time: 10,
|
|
67
70
|
percentage_of_actors: 15,
|
|
68
71
|
}
|
|
@@ -74,10 +77,46 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
|
|
|
74
77
|
expect(local_gate_values_hash.fetch(:boolean)).to be(nil)
|
|
75
78
|
expect(local_gate_values_hash.fetch(:actors)).to eq(Set["1"])
|
|
76
79
|
expect(local_gate_values_hash.fetch(:groups)).to eq(Set["staff"])
|
|
80
|
+
expect(local_gate_values_hash.fetch(:expression)).to eq(plan_expression.value)
|
|
77
81
|
expect(local_gate_values_hash.fetch(:percentage_of_time)).to eq("10")
|
|
78
82
|
expect(local_gate_values_hash.fetch(:percentage_of_actors)).to eq("15")
|
|
79
83
|
end
|
|
80
84
|
|
|
85
|
+
it "updates expression when remote is updated" do
|
|
86
|
+
any_expression = Flipper.any(plan_expression, age_expression)
|
|
87
|
+
remote = Flipper::GateValues.new(expression: any_expression.value)
|
|
88
|
+
feature.enable_expression(age_expression)
|
|
89
|
+
adapter.reset
|
|
90
|
+
|
|
91
|
+
described_class.new(feature, feature.gate_values, remote).call
|
|
92
|
+
|
|
93
|
+
expect(feature.expression_value).to eq(any_expression.value)
|
|
94
|
+
expect_only_enable
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "does nothing to expression if in sync" do
|
|
98
|
+
remote = Flipper::GateValues.new(expression: plan_expression.value)
|
|
99
|
+
feature.enable_expression(plan_expression)
|
|
100
|
+
adapter.reset
|
|
101
|
+
|
|
102
|
+
described_class.new(feature, feature.gate_values, remote).call
|
|
103
|
+
|
|
104
|
+
expect(feature.expression_value).to eq(plan_expression.value)
|
|
105
|
+
expect_no_enable_or_disable
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "updates expression when remote conditionally enabled but expression is nil" do
|
|
109
|
+
remote = Flipper::GateValues.new(expression: nil, actors: Set["1"])
|
|
110
|
+
feature.enable_expression(plan_expression)
|
|
111
|
+
feature.enable_actor(Flipper::Actor.new("1"))
|
|
112
|
+
adapter.reset
|
|
113
|
+
|
|
114
|
+
described_class.new(feature, feature.gate_values, remote).call
|
|
115
|
+
|
|
116
|
+
expect(feature.expression_value).to eq(nil)
|
|
117
|
+
expect_only_disable
|
|
118
|
+
end
|
|
119
|
+
|
|
81
120
|
it "adds remotely added actors" do
|
|
82
121
|
remote = Flipper::GateValues.new(actors: Set["1", "2"])
|
|
83
122
|
feature.enable_actor(Flipper::Actor.new("1"))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "flipper/adapters/memory"
|
|
2
|
+
require "flipper/adapters/actor_limit"
|
|
2
3
|
require "flipper/instrumenters/memory"
|
|
3
4
|
require "flipper/adapters/sync/synchronizer"
|
|
4
5
|
|
|
@@ -84,5 +85,91 @@ RSpec.describe Flipper::Adapters::Sync::Synchronizer do
|
|
|
84
85
|
|
|
85
86
|
expect(local_flipper.features.map(&:key)).to eq([])
|
|
86
87
|
end
|
|
88
|
+
|
|
89
|
+
it 'emits feature_operation.flipper events when syncing' do
|
|
90
|
+
remote_flipper.enable(:search)
|
|
91
|
+
|
|
92
|
+
subject.call
|
|
93
|
+
|
|
94
|
+
events = instrumenter.events_by_name("feature_operation.flipper")
|
|
95
|
+
enable_events = events.select { |e| e.payload[:operation] == :enable }
|
|
96
|
+
expect(enable_events).not_to be_empty
|
|
97
|
+
|
|
98
|
+
feature_names = enable_events.map { |e| e.payload[:feature_name].to_s }
|
|
99
|
+
expect(feature_names).to include("search")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'emits feature_operation.flipper events when adding features' do
|
|
103
|
+
remote_flipper.add(:new_feature)
|
|
104
|
+
|
|
105
|
+
subject.call
|
|
106
|
+
|
|
107
|
+
events = instrumenter.events_by_name("feature_operation.flipper")
|
|
108
|
+
add_events = events.select { |e| e.payload[:operation] == :add }
|
|
109
|
+
expect(add_events).not_to be_empty
|
|
110
|
+
|
|
111
|
+
feature_names = add_events.map { |e| e.payload[:feature_name].to_s }
|
|
112
|
+
expect(feature_names).to include("new_feature")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'emits feature_operation.flipper events when removing features' do
|
|
116
|
+
local_flipper.add(:old_feature)
|
|
117
|
+
|
|
118
|
+
subject.call
|
|
119
|
+
|
|
120
|
+
events = instrumenter.events_by_name("feature_operation.flipper")
|
|
121
|
+
remove_events = events.select { |e| e.payload[:operation] == :remove }
|
|
122
|
+
expect(remove_events).not_to be_empty
|
|
123
|
+
|
|
124
|
+
feature_names = remove_events.map { |e| e.payload[:feature_name].to_s }
|
|
125
|
+
expect(feature_names).to include("old_feature")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
context 'with ActorLimit adapter wrapping local' do
|
|
130
|
+
let(:limit) { 10 }
|
|
131
|
+
let(:limited_local) { Flipper::Adapters::ActorLimit.new(local, limit) }
|
|
132
|
+
let(:limited_local_flipper) { Flipper.new(limited_local) }
|
|
133
|
+
|
|
134
|
+
subject { described_class.new(limited_local, remote, instrumenter: instrumenter) }
|
|
135
|
+
|
|
136
|
+
it 'syncs actors even when remote has more actors than local limit' do
|
|
137
|
+
# Remote has more actors than local limit allows
|
|
138
|
+
20.times { |i| remote_flipper[:search].enable_actor Flipper::Actor.new("User;#{i}") }
|
|
139
|
+
|
|
140
|
+
# This should NOT raise - sync should bypass actor limits
|
|
141
|
+
expect { subject.call }.not_to raise_error
|
|
142
|
+
|
|
143
|
+
# All actors should be synced
|
|
144
|
+
expect(limited_local_flipper[:search].actors_value.size).to eq(20)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'syncs new actors added to remote after initial sync' do
|
|
148
|
+
# Initial state: remote has 20 actors, local limit is 10
|
|
149
|
+
20.times { |i| remote_flipper[:search].enable_actor Flipper::Actor.new("User;#{i}") }
|
|
150
|
+
|
|
151
|
+
# First sync - should work despite exceeding limit
|
|
152
|
+
subject.call
|
|
153
|
+
expect(limited_local_flipper[:search].actors_value.size).to eq(20)
|
|
154
|
+
|
|
155
|
+
# Add a 21st actor to remote (simulating Cloud adding a new actor)
|
|
156
|
+
remote_flipper[:search].enable_actor Flipper::Actor.new("User;20")
|
|
157
|
+
|
|
158
|
+
# Sync again - should pick up the new actor
|
|
159
|
+
expect { subject.call }.not_to raise_error
|
|
160
|
+
expect(limited_local_flipper[:search].actors_value.size).to eq(21)
|
|
161
|
+
expect(limited_local_flipper[:search].actors_value).to include("User;20")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'still enforces limit for direct enable operations' do
|
|
165
|
+
# First sync 20 actors from remote
|
|
166
|
+
20.times { |i| remote_flipper[:search].enable_actor Flipper::Actor.new("User;#{i}") }
|
|
167
|
+
subject.call
|
|
168
|
+
|
|
169
|
+
# Direct enable should still fail because we're over limit
|
|
170
|
+
expect {
|
|
171
|
+
limited_local_flipper[:search].enable_actor Flipper::Actor.new("User;new")
|
|
172
|
+
}.to raise_error(Flipper::Adapters::ActorLimit::LimitExceeded)
|
|
173
|
+
end
|
|
87
174
|
end
|
|
88
175
|
end
|
|
@@ -197,4 +197,17 @@ RSpec.describe Flipper::Adapters::Sync do
|
|
|
197
197
|
expect(remote_adapter).to receive(:get_all).and_raise(exception)
|
|
198
198
|
expect { subject.get_all }.not_to raise_error
|
|
199
199
|
end
|
|
200
|
+
|
|
201
|
+
describe '#adapter_stack' do
|
|
202
|
+
it 'returns the tree representation' do
|
|
203
|
+
expect(subject.adapter_stack).to eq("sync(local: operation_logger -> memory, remote: operation_logger -> memory)")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'shows nested adapters in the tree' do
|
|
207
|
+
memory = Flipper::Adapters::Memory.new
|
|
208
|
+
strict = Flipper::Adapters::Strict.new(Flipper::Adapters::Memory.new)
|
|
209
|
+
adapter = described_class.new(memory, strict, interval: 1)
|
|
210
|
+
expect(adapter.adapter_stack).to eq("sync(local: memory, remote: strict -> memory)")
|
|
211
|
+
end
|
|
212
|
+
end
|
|
200
213
|
end
|