flipper 0.26.0 → 1.3.6
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 +61 -16
- data/.github/workflows/examples.yml +55 -18
- data/CLAUDE.md +74 -0
- data/Changelog.md +1 -486
- data/Gemfile +23 -11
- data/README.md +31 -27
- data/Rakefile +2 -2
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_multiple_actors_ips.rb +20 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +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/mirroring.rb +59 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +8 -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 +0 -4
- data/lib/flipper/adapters/http/client.rb +40 -12
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +30 -17
- 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 -125
- data/lib/flipper/adapters/poll.rb +20 -3
- 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 +266 -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 +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 +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 -3
- data/lib/flipper/export.rb +24 -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 +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/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 +94 -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 +35 -8
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +8 -5
- 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 +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 +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/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/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +138 -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/poll_spec.rb +41 -0
- 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 +166 -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 +186 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +54 -76
- data/spec/flipper/engine_spec.rb +374 -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 +453 -39
- 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 +24 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
- data/spec/flipper/middleware/memoizer_spec.rb +79 -10
- data/spec/flipper/model/active_record_spec.rb +72 -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 +94 -30
- data/spec/spec_helper.rb +18 -18
- 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 +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 +203 -20
- 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
@@ -2,5 +2,7 @@ require 'securerandom'
|
|
2
2
|
require 'active_support/notifications'
|
3
3
|
require 'flipper/instrumentation/statsd_subscriber'
|
4
4
|
|
5
|
-
ActiveSupport::Notifications.subscribe
|
6
|
-
|
5
|
+
ActiveSupport::Notifications.subscribe(
|
6
|
+
/\.flipper$/,
|
7
|
+
Flipper::Instrumentation::StatsdSubscriber
|
8
|
+
)
|
@@ -12,13 +12,11 @@ module Flipper
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def update_timer(metric)
|
15
|
-
|
16
|
-
self.class.client.timing metric, (@duration * 1_000).round
|
17
|
-
end
|
15
|
+
self.class.client&.timing metric, (@duration * 1_000).round
|
18
16
|
end
|
19
17
|
|
20
18
|
def update_counter(metric)
|
21
|
-
self.class.client
|
19
|
+
self.class.client&.increment metric
|
22
20
|
end
|
23
21
|
end
|
24
22
|
end
|
@@ -42,10 +42,8 @@ module Flipper
|
|
42
42
|
# Private
|
43
43
|
def update_feature_operation_metrics
|
44
44
|
feature_name = @payload[:feature_name]
|
45
|
-
gate_name = @payload[:gate_name]
|
46
45
|
operation = strip_trailing_question_mark(@payload[:operation])
|
47
46
|
result = @payload[:result]
|
48
|
-
thing = @payload[:thing]
|
49
47
|
|
50
48
|
update_timer "flipper.feature_operation.#{operation}"
|
51
49
|
|
@@ -65,13 +63,18 @@ module Flipper
|
|
65
63
|
def update_adapter_operation_metrics
|
66
64
|
adapter_name = @payload[:adapter_name]
|
67
65
|
operation = @payload[:operation]
|
68
|
-
result = @payload[:result]
|
69
|
-
value = @payload[:value]
|
70
|
-
key = @payload[:key]
|
71
66
|
|
72
67
|
update_timer "flipper.adapter.#{adapter_name}.#{operation}"
|
73
68
|
end
|
74
69
|
|
70
|
+
def update_poller_metrics
|
71
|
+
# noop
|
72
|
+
end
|
73
|
+
|
74
|
+
def update_synchronizer_call_metrics
|
75
|
+
# noop
|
76
|
+
end
|
77
|
+
|
75
78
|
QUESTION_MARK = '?'.freeze
|
76
79
|
|
77
80
|
# Private
|
data/lib/flipper/metadata.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
|
+
require_relative './version'
|
2
|
+
|
1
3
|
module Flipper
|
2
4
|
METADATA = {
|
3
|
-
|
5
|
+
"documentation_uri" => "https://www.flippercloud.io/docs",
|
6
|
+
"homepage_uri" => "https://www.flippercloud.io",
|
7
|
+
"source_code_uri" => "https://github.com/flippercloud/flipper",
|
8
|
+
"bug_tracker_uri" => "https://github.com/flippercloud/flipper/issues",
|
9
|
+
"changelog_uri" => "https://github.com/flippercloud/flipper/releases/tag/v#{Flipper::VERSION}",
|
10
|
+
"funding_uri" => "https://github.com/sponsors/flippercloud",
|
4
11
|
}.freeze
|
5
12
|
end
|
@@ -20,6 +20,14 @@ module Flipper
|
|
20
20
|
# # using with preload specific features
|
21
21
|
# use Flipper::Middleware::Memoizer, preload: [:stats, :search, :some_feature]
|
22
22
|
#
|
23
|
+
# # using with preload block that returns true/false
|
24
|
+
# use Flipper::Middleware::Memoizer, preload: ->(request) { !request.path.start_with?('/assets') }
|
25
|
+
#
|
26
|
+
# # using with preload block that returns specific features
|
27
|
+
# use Flipper::Middleware::Memoizer, preload: ->(request) {
|
28
|
+
# request.path.starts_with?('/admin') ? [:stats, :search] : false
|
29
|
+
# }
|
30
|
+
#
|
23
31
|
def initialize(app, opts = {})
|
24
32
|
if opts.is_a?(Flipper::DSL) || opts.is_a?(Proc)
|
25
33
|
raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
|
@@ -34,7 +42,7 @@ module Flipper
|
|
34
42
|
request = Rack::Request.new(env)
|
35
43
|
|
36
44
|
if memoize?(request)
|
37
|
-
memoized_call(
|
45
|
+
memoized_call(request)
|
38
46
|
else
|
39
47
|
@app.call(env)
|
40
48
|
end
|
@@ -52,26 +60,34 @@ module Flipper
|
|
52
60
|
end
|
53
61
|
end
|
54
62
|
|
55
|
-
def memoized_call(
|
56
|
-
|
57
|
-
flipper = env.fetch(@env_key) { Flipper }
|
63
|
+
def memoized_call(request)
|
64
|
+
flipper = request.env.fetch(@env_key) { Flipper }
|
58
65
|
|
59
66
|
# Already memoizing. This instance does not need to do anything.
|
60
67
|
if flipper.memoizing?
|
61
|
-
warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/
|
62
|
-
return @app.call(env)
|
68
|
+
warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/flippercloud/flipper/pull/523"
|
69
|
+
return @app.call(request.env)
|
63
70
|
end
|
64
71
|
|
65
|
-
|
72
|
+
begin
|
73
|
+
flipper.memoize = true
|
66
74
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
75
|
+
# Preloading is pointless without memoizing.
|
76
|
+
preload = if @opts[:preload].respond_to?(:call)
|
77
|
+
@opts[:preload].call(request)
|
78
|
+
else
|
79
|
+
@opts[:preload]
|
80
|
+
end
|
71
81
|
|
72
|
-
|
73
|
-
|
74
|
-
|
82
|
+
case preload
|
83
|
+
when true then flipper.preload_all
|
84
|
+
when Array then flipper.preload(preload)
|
85
|
+
end
|
86
|
+
|
87
|
+
@app.call(request.env)
|
88
|
+
ensure
|
89
|
+
flipper.memoize = false
|
90
|
+
end
|
75
91
|
end
|
76
92
|
end
|
77
93
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Model
|
3
|
+
module ActiveRecord
|
4
|
+
# The id of the record when used as an actor.
|
5
|
+
#
|
6
|
+
# class User < ActiveRecord::Base
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# user = User.first
|
10
|
+
# Flipper.enable :some_feature, user
|
11
|
+
# Flipper.enabled? :some_feature, user #=> true
|
12
|
+
#
|
13
|
+
def flipper_id
|
14
|
+
"#{self.class.base_class.name};#{id}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Properties used to evaluate expressions
|
18
|
+
def flipper_properties
|
19
|
+
{"type" => self.class.name}.merge(attributes)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'concurrent/utility/monotonic_time'
|
3
|
+
require 'concurrent/map'
|
4
|
+
require 'concurrent/atomic/atomic_fixnum'
|
5
|
+
|
6
|
+
module Flipper
|
7
|
+
class Poller
|
8
|
+
attr_reader :adapter, :thread, :pid, :mutex, :interval, :last_synced_at
|
9
|
+
|
10
|
+
def self.instances
|
11
|
+
@instances ||= Concurrent::Map.new
|
12
|
+
end
|
13
|
+
private_class_method :instances
|
14
|
+
|
15
|
+
def self.get(key, options = {})
|
16
|
+
instances.compute_if_absent(key) { new(options) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.reset
|
20
|
+
instances.each {|_, instance| instance.stop }.clear
|
21
|
+
end
|
22
|
+
|
23
|
+
MINIMUM_POLL_INTERVAL = 10
|
24
|
+
|
25
|
+
def initialize(options = {})
|
26
|
+
@thread = nil
|
27
|
+
@pid = Process.pid
|
28
|
+
@mutex = Mutex.new
|
29
|
+
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
30
|
+
@remote_adapter = options.fetch(:remote_adapter)
|
31
|
+
@interval = options.fetch(:interval, 10).to_f
|
32
|
+
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
33
|
+
@adapter = Adapters::Memory.new(nil, threadsafe: true)
|
34
|
+
|
35
|
+
if @interval < MINIMUM_POLL_INTERVAL
|
36
|
+
warn "Flipper::Cloud poll interval must be greater than or equal to #{MINIMUM_POLL_INTERVAL} but was #{@interval}. Setting @interval to #{MINIMUM_POLL_INTERVAL}."
|
37
|
+
@interval = MINIMUM_POLL_INTERVAL
|
38
|
+
end
|
39
|
+
|
40
|
+
@start_automatically = options.fetch(:start_automatically, true)
|
41
|
+
|
42
|
+
if options.fetch(:shutdown_automatically, true)
|
43
|
+
at_exit { stop }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def start
|
48
|
+
reset if forked?
|
49
|
+
ensure_worker_running
|
50
|
+
end
|
51
|
+
|
52
|
+
def stop
|
53
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
54
|
+
operation: :stop,
|
55
|
+
})
|
56
|
+
@thread&.kill
|
57
|
+
end
|
58
|
+
|
59
|
+
def run
|
60
|
+
loop do
|
61
|
+
sleep jitter
|
62
|
+
|
63
|
+
begin
|
64
|
+
sync
|
65
|
+
rescue
|
66
|
+
# you can instrument these using poller.flipper
|
67
|
+
end
|
68
|
+
|
69
|
+
sleep interval
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def sync
|
74
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
|
75
|
+
@adapter.import @remote_adapter
|
76
|
+
@last_synced_at.update { |time| Concurrent.monotonic_time }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def jitter
|
83
|
+
rand
|
84
|
+
end
|
85
|
+
|
86
|
+
def forked?
|
87
|
+
pid != Process.pid
|
88
|
+
end
|
89
|
+
|
90
|
+
def ensure_worker_running
|
91
|
+
# Return early if thread is alive and avoid the mutex lock and unlock.
|
92
|
+
return if thread_alive?
|
93
|
+
|
94
|
+
# If another thread is starting worker thread, then return early so this
|
95
|
+
# thread can enqueue and move on with life.
|
96
|
+
return unless mutex.try_lock
|
97
|
+
|
98
|
+
begin
|
99
|
+
return if thread_alive?
|
100
|
+
@thread = Thread.new { run }
|
101
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
102
|
+
operation: :thread_start,
|
103
|
+
})
|
104
|
+
ensure
|
105
|
+
mutex.unlock
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def thread_alive?
|
110
|
+
@thread && @thread.alive?
|
111
|
+
end
|
112
|
+
|
113
|
+
def reset
|
114
|
+
@pid = Process.pid
|
115
|
+
mutex.unlock if mutex.locked?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "zlib"
|
2
|
+
require "stringio"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Serializers
|
6
|
+
class Gzip
|
7
|
+
def self.serialize(source)
|
8
|
+
return if source.nil?
|
9
|
+
output = StringIO.new
|
10
|
+
gz = Zlib::GzipWriter.new(output)
|
11
|
+
gz.write(source)
|
12
|
+
gz.close
|
13
|
+
output.string
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.deserialize(source)
|
17
|
+
return if source.nil?
|
18
|
+
Zlib::GzipReader.wrap(StringIO.new(source), &:read)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Serializers
|
5
|
+
class Json
|
6
|
+
def self.serialize(source)
|
7
|
+
return if source.nil?
|
8
|
+
JSON.generate(source)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.deserialize(source)
|
12
|
+
return if source.nil?
|
13
|
+
JSON.parse(source)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -4,11 +4,12 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
4
4
|
let(:flipper) { Flipper.new(subject) }
|
5
5
|
let(:feature) { flipper[:stats] }
|
6
6
|
|
7
|
-
let(:boolean_gate)
|
8
|
-
let(:
|
9
|
-
let(:
|
10
|
-
let(:
|
11
|
-
let(:
|
7
|
+
let(:boolean_gate) { feature.gate(:boolean) }
|
8
|
+
let(:expression_gate) { feature.gate(:expression) }
|
9
|
+
let(:group_gate) { feature.gate(:group) }
|
10
|
+
let(:actor_gate) { feature.gate(:actor) }
|
11
|
+
let(:actors_gate) { feature.gate(:percentage_of_actors) }
|
12
|
+
let(:time_gate) { feature.gate(:percentage_of_time) }
|
12
13
|
|
13
14
|
before do
|
14
15
|
Flipper.register(:admins) do |actor|
|
@@ -33,17 +34,25 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
33
34
|
expect(subject.class.ancestors).to include(Flipper::Adapter)
|
34
35
|
end
|
35
36
|
|
37
|
+
it 'knows how to get adapter from source' do
|
38
|
+
adapter = Flipper::Adapters::Memory.new
|
39
|
+
flipper = Flipper.new(adapter)
|
40
|
+
expect(subject.class.from(adapter).class.ancestors).to include(Flipper::Adapter)
|
41
|
+
expect(subject.class.from(flipper).class.ancestors).to include(Flipper::Adapter)
|
42
|
+
end
|
43
|
+
|
36
44
|
it 'returns correct default values for the gates if none are enabled' do
|
45
|
+
expect(subject.get(feature)).to eq(subject.class.default_config)
|
37
46
|
expect(subject.get(feature)).to eq(subject.default_config)
|
38
47
|
end
|
39
48
|
|
40
49
|
it 'can enable, disable and get value for boolean gate' do
|
41
|
-
expect(subject.enable(feature, boolean_gate,
|
50
|
+
expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
42
51
|
|
43
52
|
result = subject.get(feature)
|
44
53
|
expect(result[:boolean]).to eq('true')
|
45
54
|
|
46
|
-
expect(subject.disable(feature, boolean_gate,
|
55
|
+
expect(subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new(false))).to eq(true)
|
47
56
|
|
48
57
|
result = subject.get(feature)
|
49
58
|
expect(result[:boolean]).to eq(nil)
|
@@ -51,17 +60,34 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
51
60
|
|
52
61
|
it 'fully disables all enabled things when boolean gate disabled' do
|
53
62
|
actor22 = Flipper::Actor.new('22')
|
54
|
-
expect(subject.enable(feature, boolean_gate,
|
63
|
+
expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
55
64
|
expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true)
|
56
|
-
expect(subject.enable(feature, actor_gate,
|
57
|
-
expect(subject.enable(feature, actors_gate,
|
58
|
-
expect(subject.enable(feature, time_gate,
|
59
|
-
|
60
|
-
expect(subject.disable(feature, boolean_gate, flipper.boolean(false))).to eq(true)
|
65
|
+
expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true)
|
66
|
+
expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true)
|
67
|
+
expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))).to eq(true)
|
61
68
|
|
69
|
+
expect(subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new(false))).to eq(true)
|
62
70
|
expect(subject.get(feature)).to eq(subject.default_config)
|
63
71
|
end
|
64
72
|
|
73
|
+
it 'can enable, disable and get value for expression gate' do
|
74
|
+
basic_expression = Flipper.property(:plan).eq("basic")
|
75
|
+
age_expression = Flipper.property(:age).gte(21)
|
76
|
+
any_expression = Flipper.any(basic_expression, age_expression)
|
77
|
+
|
78
|
+
expect(subject.enable(feature, expression_gate, any_expression)).to eq(true)
|
79
|
+
result = subject.get(feature)
|
80
|
+
expect(result[:expression]).to eq(any_expression.value)
|
81
|
+
|
82
|
+
expect(subject.enable(feature, expression_gate, basic_expression)).to eq(true)
|
83
|
+
result = subject.get(feature)
|
84
|
+
expect(result[:expression]).to eq(basic_expression.value)
|
85
|
+
|
86
|
+
expect(subject.disable(feature, expression_gate, basic_expression)).to eq(true)
|
87
|
+
result = subject.get(feature)
|
88
|
+
expect(result[:expression]).to be(nil)
|
89
|
+
end
|
90
|
+
|
65
91
|
it 'can enable, disable and get value for group gate' do
|
66
92
|
expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true)
|
67
93
|
expect(subject.enable(feature, group_gate, flipper.group(:early_access))).to eq(true)
|
@@ -82,34 +108,34 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
82
108
|
actor22 = Flipper::Actor.new('22')
|
83
109
|
actor_asdf = Flipper::Actor.new('asdf')
|
84
110
|
|
85
|
-
expect(
|
86
|
-
expect(
|
111
|
+
expect(feature.enable(actor22)).to be(true)
|
112
|
+
expect(feature.enable(actor_asdf)).to be(true)
|
87
113
|
|
88
|
-
|
89
|
-
expect(
|
114
|
+
expect(feature).to be_enabled(actor22)
|
115
|
+
expect(feature).to be_enabled(actor_asdf)
|
90
116
|
|
91
|
-
expect(
|
92
|
-
|
93
|
-
expect(
|
117
|
+
expect(feature.disable(actor22)).to be(true)
|
118
|
+
expect(feature).not_to be_enabled(actor22)
|
119
|
+
expect(feature).to be_enabled(actor_asdf)
|
94
120
|
|
95
|
-
expect(
|
96
|
-
|
97
|
-
expect(
|
121
|
+
expect(feature.disable(actor_asdf)).to eq(true)
|
122
|
+
expect(feature).not_to be_enabled(actor22)
|
123
|
+
expect(feature).not_to be_enabled(actor_asdf)
|
98
124
|
end
|
99
125
|
|
100
126
|
it 'can enable, disable and get value for percentage of actors gate' do
|
101
|
-
expect(subject.enable(feature, actors_gate,
|
127
|
+
expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(15))).to eq(true)
|
102
128
|
result = subject.get(feature)
|
103
129
|
expect(result[:percentage_of_actors]).to eq('15')
|
104
130
|
|
105
|
-
expect(subject.disable(feature, actors_gate,
|
131
|
+
expect(subject.disable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(0))).to eq(true)
|
106
132
|
result = subject.get(feature)
|
107
133
|
expect(result[:percentage_of_actors]).to eq('0')
|
108
134
|
end
|
109
135
|
|
110
136
|
it 'can enable percentage of actors gate many times and consistently return values' do
|
111
137
|
(1..100).each do |percentage|
|
112
|
-
expect(subject.enable(feature, actors_gate,
|
138
|
+
expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(percentage))).to eq(true)
|
113
139
|
result = subject.get(feature)
|
114
140
|
expect(result[:percentage_of_actors]).to eq(percentage.to_s)
|
115
141
|
end
|
@@ -117,25 +143,25 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
117
143
|
|
118
144
|
it 'can disable percentage of actors gate many times and consistently return values' do
|
119
145
|
(1..100).each do |percentage|
|
120
|
-
expect(subject.disable(feature, actors_gate,
|
146
|
+
expect(subject.disable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(percentage))).to eq(true)
|
121
147
|
result = subject.get(feature)
|
122
148
|
expect(result[:percentage_of_actors]).to eq(percentage.to_s)
|
123
149
|
end
|
124
150
|
end
|
125
151
|
|
126
152
|
it 'can enable, disable and get value for percentage of time gate' do
|
127
|
-
expect(subject.enable(feature, time_gate,
|
153
|
+
expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(10))).to eq(true)
|
128
154
|
result = subject.get(feature)
|
129
155
|
expect(result[:percentage_of_time]).to eq('10')
|
130
156
|
|
131
|
-
expect(subject.disable(feature, time_gate,
|
157
|
+
expect(subject.disable(feature, time_gate, Flipper::Types::PercentageOfTime.new(0))).to eq(true)
|
132
158
|
result = subject.get(feature)
|
133
159
|
expect(result[:percentage_of_time]).to eq('0')
|
134
160
|
end
|
135
161
|
|
136
162
|
it 'can enable percentage of time gate many times and consistently return values' do
|
137
163
|
(1..100).each do |percentage|
|
138
|
-
expect(subject.enable(feature, time_gate,
|
164
|
+
expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(percentage))).to eq(true)
|
139
165
|
result = subject.get(feature)
|
140
166
|
expect(result[:percentage_of_time]).to eq(percentage.to_s)
|
141
167
|
end
|
@@ -143,22 +169,23 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
143
169
|
|
144
170
|
it 'can disable percentage of time gate many times and consistently return values' do
|
145
171
|
(1..100).each do |percentage|
|
146
|
-
expect(subject.disable(feature, time_gate,
|
172
|
+
expect(subject.disable(feature, time_gate, Flipper::Types::PercentageOfTime.new(percentage))).to eq(true)
|
147
173
|
result = subject.get(feature)
|
148
174
|
expect(result[:percentage_of_time]).to eq(percentage.to_s)
|
149
175
|
end
|
150
176
|
end
|
151
177
|
|
152
178
|
it 'converts boolean value to a string' do
|
153
|
-
expect(subject.enable(feature, boolean_gate,
|
179
|
+
expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
154
180
|
result = subject.get(feature)
|
155
181
|
expect(result[:boolean]).to eq('true')
|
156
182
|
end
|
157
183
|
|
158
184
|
it 'converts the actor value to a string' do
|
159
|
-
|
160
|
-
|
161
|
-
|
185
|
+
actor = Flipper::Actor.new(22)
|
186
|
+
expect(feature).not_to be_enabled(actor)
|
187
|
+
feature.enable_actor actor
|
188
|
+
expect(feature).to be_enabled(actor)
|
162
189
|
end
|
163
190
|
|
164
191
|
it 'converts group value to a string' do
|
@@ -168,13 +195,13 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
168
195
|
end
|
169
196
|
|
170
197
|
it 'converts percentage of time integer value to a string' do
|
171
|
-
expect(subject.enable(feature, time_gate,
|
198
|
+
expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(10))).to eq(true)
|
172
199
|
result = subject.get(feature)
|
173
200
|
expect(result[:percentage_of_time]).to eq('10')
|
174
201
|
end
|
175
202
|
|
176
203
|
it 'converts percentage of actors integer value to a string' do
|
177
|
-
expect(subject.enable(feature, actors_gate,
|
204
|
+
expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(10))).to eq(true)
|
178
205
|
result = subject.get(feature)
|
179
206
|
expect(result[:percentage_of_actors]).to eq('10')
|
180
207
|
end
|
@@ -197,11 +224,11 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
197
224
|
|
198
225
|
it 'clears all the gate values for the feature on remove' do
|
199
226
|
actor22 = Flipper::Actor.new('22')
|
200
|
-
expect(subject.enable(feature, boolean_gate,
|
227
|
+
expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
201
228
|
expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true)
|
202
|
-
expect(subject.enable(feature, actor_gate,
|
203
|
-
expect(subject.enable(feature, actors_gate,
|
204
|
-
expect(subject.enable(feature, time_gate,
|
229
|
+
expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true)
|
230
|
+
expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true)
|
231
|
+
expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))).to eq(true)
|
205
232
|
|
206
233
|
expect(subject.remove(feature)).to eq(true)
|
207
234
|
|
@@ -213,11 +240,11 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
213
240
|
subject.add(feature)
|
214
241
|
expect(subject.features).to include(feature.key)
|
215
242
|
|
216
|
-
expect(subject.enable(feature, boolean_gate,
|
243
|
+
expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
217
244
|
expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true)
|
218
|
-
expect(subject.enable(feature, actor_gate,
|
219
|
-
expect(subject.enable(feature, actors_gate,
|
220
|
-
expect(subject.enable(feature, time_gate,
|
245
|
+
expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true)
|
246
|
+
expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true)
|
247
|
+
expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))).to eq(true)
|
221
248
|
|
222
249
|
expect(subject.clear(feature)).to eq(true)
|
223
250
|
expect(subject.features).to include(feature.key)
|
@@ -230,7 +257,7 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
230
257
|
|
231
258
|
it 'can get multiple features' do
|
232
259
|
expect(subject.add(flipper[:stats])).to eq(true)
|
233
|
-
expect(subject.enable(flipper[:stats], boolean_gate,
|
260
|
+
expect(subject.enable(flipper[:stats], boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
234
261
|
expect(subject.add(flipper[:search])).to eq(true)
|
235
262
|
|
236
263
|
result = subject.get_multi([flipper[:stats], flipper[:search], flipper[:other]])
|
@@ -246,16 +273,16 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
246
273
|
|
247
274
|
it 'can get all features' do
|
248
275
|
expect(subject.add(flipper[:stats])).to eq(true)
|
249
|
-
expect(subject.enable(flipper[:stats], boolean_gate,
|
276
|
+
expect(subject.enable(flipper[:stats], boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
250
277
|
expect(subject.add(flipper[:search])).to eq(true)
|
278
|
+
flipper.enable :analytics, Flipper.property(:plan).eq("pro")
|
251
279
|
|
252
280
|
result = subject.get_all
|
253
|
-
expect(result).to be_instance_of(Hash)
|
254
281
|
|
255
|
-
|
256
|
-
|
257
|
-
expect(
|
258
|
-
expect(
|
282
|
+
expect(result).to be_instance_of(Hash)
|
283
|
+
expect(result["stats"]).to eq(subject.default_config.merge(boolean: 'true'))
|
284
|
+
expect(result["search"]).to eq(subject.default_config)
|
285
|
+
expect(result["analytics"]).to eq(subject.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, "pro"]}))
|
259
286
|
end
|
260
287
|
|
261
288
|
it 'includes explicitly disabled features when getting all features' do
|
@@ -269,9 +296,9 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
269
296
|
|
270
297
|
it 'can double enable an actor without error' do
|
271
298
|
actor = Flipper::Actor.new('Flipper::Actor;22')
|
272
|
-
expect(
|
273
|
-
expect(
|
274
|
-
expect(
|
299
|
+
expect(feature.enable(actor)).to be(true)
|
300
|
+
expect(feature.enable(actor)).to be(true)
|
301
|
+
expect(feature).to be_enabled(actor)
|
275
302
|
end
|
276
303
|
|
277
304
|
it 'can double enable a group without error' do
|
@@ -281,13 +308,13 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
281
308
|
end
|
282
309
|
|
283
310
|
it 'can double enable percentage without error' do
|
284
|
-
expect(subject.enable(feature, actors_gate,
|
285
|
-
expect(subject.enable(feature, actors_gate,
|
311
|
+
expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true)
|
312
|
+
expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true)
|
286
313
|
end
|
287
314
|
|
288
315
|
it 'can double enable without error' do
|
289
|
-
expect(subject.enable(feature, boolean_gate,
|
290
|
-
expect(subject.enable(feature, boolean_gate,
|
316
|
+
expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
317
|
+
expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
291
318
|
end
|
292
319
|
|
293
320
|
it 'can get_all features when there are none' do
|
@@ -297,11 +324,26 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
297
324
|
|
298
325
|
it 'clears other gate values on enable' do
|
299
326
|
actor = Flipper::Actor.new('Flipper::Actor;22')
|
300
|
-
subject.enable(feature, actors_gate,
|
301
|
-
subject.enable(feature, time_gate,
|
327
|
+
subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))
|
328
|
+
subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(25))
|
302
329
|
subject.enable(feature, group_gate, flipper.group(:admins))
|
303
|
-
subject.enable(feature, actor_gate,
|
304
|
-
subject.enable(feature, boolean_gate,
|
330
|
+
subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor))
|
331
|
+
subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new(true))
|
305
332
|
expect(subject.get(feature)).to eq(subject.default_config.merge(boolean: "true"))
|
306
333
|
end
|
334
|
+
|
335
|
+
it 'can import and export' do
|
336
|
+
adapter = Flipper::Adapters::Memory.new
|
337
|
+
source_flipper = Flipper.new(adapter)
|
338
|
+
source_flipper.enable(:stats)
|
339
|
+
export = adapter.export
|
340
|
+
|
341
|
+
# some adapters cannot import so if they return false lets assert it
|
342
|
+
# didn't happen
|
343
|
+
if subject.import(export)
|
344
|
+
expect(flipper[:stats]).to be_enabled
|
345
|
+
else
|
346
|
+
expect(flipper[:stats]).not_to be_enabled
|
347
|
+
end
|
348
|
+
end
|
307
349
|
end
|