flipper 0.16.0 → 1.4.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 +5 -5
- data/.codeclimate.yml +1 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +110 -0
- data/.github/workflows/examples.yml +105 -0
- data/.github/workflows/release.yml +54 -0
- data/.rspec +1 -0
- data/CLAUDE.md +87 -0
- data/Changelog.md +2 -215
- data/Dockerfile +1 -1
- data/Gemfile +28 -20
- data/README.md +72 -62
- data/Rakefile +13 -3
- 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/docker-compose.yml +37 -34
- data/docs/DockerCompose.md +0 -1
- data/docs/README.md +1 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/api/basic.ru +18 -0
- data/examples/api/custom_memoized.ru +36 -0
- data/examples/api/memoized.ru +42 -0
- data/examples/basic.rb +1 -12
- 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/configuring_default.rb +2 -5
- data/examples/dsl.rb +10 -35
- data/examples/enabled_for_actor.rb +10 -15
- data/examples/expressions.rb +237 -0
- data/examples/group.rb +3 -6
- data/examples/group_dynamic_lookup.rb +5 -19
- data/examples/group_with_members.rb +4 -14
- data/examples/importing.rb +1 -1
- data/examples/individual_actor.rb +2 -5
- data/examples/instrumentation.rb +2 -2
- data/examples/instrumentation_last_accessed_at.rb +38 -0
- data/examples/memoizing.rb +35 -0
- data/examples/mirroring.rb +59 -0
- data/examples/percentage_of_actors.rb +6 -16
- data/examples/percentage_of_actors_enabled_check.rb +7 -10
- data/examples/percentage_of_actors_group.rb +5 -18
- data/examples/percentage_of_time.rb +3 -6
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +10 -7
- data/lib/flipper/actor.rb +10 -3
- data/lib/flipper/adapter.rb +50 -8
- 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 +63 -0
- data/lib/flipper/adapters/failover.rb +85 -0
- data/lib/flipper/adapters/failsafe.rb +72 -0
- data/lib/flipper/adapters/http/client.rb +64 -7
- data/lib/flipper/adapters/http/error.rb +19 -1
- data/lib/flipper/adapters/http.rb +97 -43
- data/lib/flipper/adapters/instrumented.rb +47 -26
- data/lib/flipper/adapters/memoizable.rb +44 -40
- data/lib/flipper/adapters/memory.rb +75 -111
- data/lib/flipper/adapters/operation_logger.rb +22 -78
- data/lib/flipper/adapters/poll/poller.rb +2 -0
- data/lib/flipper/adapters/poll.rb +52 -0
- data/lib/flipper/adapters/pstore.rb +27 -17
- 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 +14 -1
- data/lib/flipper/adapters/sync/interval_synchronizer.rb +2 -7
- data/lib/flipper/adapters/sync/synchronizer.rb +13 -6
- data/lib/flipper/adapters/sync.rb +23 -29
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +314 -0
- data/lib/flipper/cloud/configuration.rb +271 -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 +54 -7
- data/lib/flipper/dsl.rb +58 -47
- data/lib/flipper/engine.rb +102 -0
- data/lib/flipper/errors.rb +3 -21
- 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/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 +95 -28
- 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 +17 -0
- 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/instrumenters/memory.rb +6 -2
- data/lib/flipper/metadata.rb +8 -1
- data/lib/flipper/middleware/memoizer.rb +46 -27
- data/lib/flipper/middleware/setup_env.rb +13 -3
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +157 -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 +122 -56
- data/lib/flipper/test/shared_adapter_test.rb +120 -52
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +59 -18
- data/lib/flipper/types/actor.rb +19 -13
- data/lib/flipper/types/group.rb +12 -5
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +71 -12
- 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/actor_spec.rb +10 -2
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +52 -6
- data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
- data/spec/flipper/adapters/dual_write_spec.rb +82 -0
- data/spec/flipper/adapters/failover_spec.rb +141 -0
- data/spec/flipper/adapters/failsafe_spec.rb +58 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +402 -65
- data/spec/flipper/adapters/instrumented_spec.rb +31 -13
- data/spec/flipper/adapters/memoizable_spec.rb +51 -33
- data/spec/flipper/adapters/memory_spec.rb +33 -5
- data/spec/flipper/adapters/operation_logger_spec.rb +38 -12
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/pstore_spec.rb +0 -2
- data/spec/flipper/adapters/read_only_spec.rb +32 -18
- data/spec/flipper/adapters/strict_spec.rb +64 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -1
- data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -1
- data/spec/flipper/adapters/sync_spec.rb +17 -6
- data/spec/flipper/cli_spec.rb +217 -0
- data/spec/flipper/cloud/configuration_spec.rb +257 -0
- data/spec/flipper/cloud/dsl_spec.rb +90 -0
- data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
- data/spec/flipper/cloud/middleware_spec.rb +307 -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 +37 -3
- data/spec/flipper/dsl_spec.rb +67 -80
- 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/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_check_context_spec.rb +18 -20
- data/spec/flipper/feature_spec.rb +461 -48
- data/spec/flipper/gate_spec.rb +0 -2
- data/spec/flipper/gate_values_spec.rb +2 -34
- data/spec/flipper/gates/actor_spec.rb +0 -2
- data/spec/flipper/gates/boolean_spec.rb +1 -3
- data/spec/flipper/gates/expression_spec.rb +190 -0
- data/spec/flipper/gates/group_spec.rb +2 -5
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
- data/spec/flipper/identifier_spec.rb +12 -0
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -7
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -3
- data/spec/flipper/instrumenters/memory_spec.rb +18 -1
- data/spec/flipper/instrumenters/noop_spec.rb +14 -8
- data/spec/flipper/middleware/memoizer_spec.rb +199 -62
- data/spec/flipper/middleware/setup_env_spec.rb +23 -5
- data/spec/flipper/model/active_record_spec.rb +72 -0
- data/spec/flipper/poller_spec.rb +390 -0
- data/spec/flipper/registry_spec.rb +0 -1
- 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 -7
- data/spec/flipper/types/actor_spec.rb +63 -47
- data/spec/flipper/types/boolean_spec.rb +0 -1
- data/spec/flipper/types/group_spec.rb +24 -3
- data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
- data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
- data/spec/flipper/types/percentage_spec.rb +0 -1
- data/spec/{integration_spec.rb → flipper_integration_spec.rb} +301 -59
- data/spec/flipper_spec.rb +123 -29
- data/spec/{helper.rb → spec_helper.rb} +23 -21
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/descriptions.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 +53 -6
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test/test_helper.rb +2 -1
- 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 +31 -0
- data/test_rails/system/test_help_test.rb +52 -0
- metadata +200 -82
- data/.rubocop.yml +0 -54
- data/.rubocop_todo.yml +0 -199
- data/docs/Adapters.md +0 -124
- data/docs/Caveats.md +0 -4
- data/docs/Gates.md +0 -167
- data/docs/Instrumentation.md +0 -27
- data/docs/Optimization.md +0 -114
- data/docs/api/README.md +0 -849
- data/docs/http/README.md +0 -35
- data/docs/read-only/README.md +0 -21
- data/examples/example_setup.rb +0 -8
- data/test/helper.rb +0 -11
data/lib/flipper/gates/group.rb
CHANGED
|
@@ -23,13 +23,11 @@ module Flipper
|
|
|
23
23
|
#
|
|
24
24
|
# Returns true if gate open for thing, false if not.
|
|
25
25
|
def open?(context)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
group = Flipper.group(name)
|
|
32
|
-
group.match?(context.thing, context)
|
|
26
|
+
return false unless context.actors?
|
|
27
|
+
|
|
28
|
+
context.values.groups.any? do |name|
|
|
29
|
+
context.actors.any? do |actor|
|
|
30
|
+
Flipper.group(name).match?(actor, context)
|
|
33
31
|
end
|
|
34
32
|
end
|
|
35
33
|
end
|
|
@@ -21,21 +21,18 @@ module Flipper
|
|
|
21
21
|
value > 0
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
#
|
|
24
|
+
# Private: this constant is used to support up to 3 decimal places
|
|
25
|
+
# in percentages.
|
|
26
|
+
SCALING_FACTOR = 1_000
|
|
27
|
+
private_constant :SCALING_FACTOR
|
|
28
|
+
|
|
29
|
+
# Internal: Checks if the gate is open for one or more actors.
|
|
25
30
|
#
|
|
26
|
-
# Returns true if gate open for
|
|
31
|
+
# Returns true if gate open for any actors, false if not.
|
|
27
32
|
def open?(context)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
actor = Types::Actor.wrap(context.thing)
|
|
32
|
-
id = "#{context.feature_name}#{actor.value}"
|
|
33
|
-
# this is to support up to 3 decimal places in percentages
|
|
34
|
-
scaling_factor = 1_000
|
|
35
|
-
Zlib.crc32(id) % (100 * scaling_factor) < percentage * scaling_factor
|
|
36
|
-
else
|
|
37
|
-
false
|
|
38
|
-
end
|
|
33
|
+
return false unless context.actors?
|
|
34
|
+
id = "#{context.feature_name}#{context.actors.map(&:value).sort.join}"
|
|
35
|
+
Zlib.crc32(id) % (100 * SCALING_FACTOR) < context.values.percentage_of_actors * SCALING_FACTOR
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
def protects?(thing)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Flipper
|
|
2
|
+
# A default implementation of `#flipper_id` for actors.
|
|
3
|
+
#
|
|
4
|
+
# class User < Struct.new(:id)
|
|
5
|
+
# include Flipper::Identifier
|
|
6
|
+
# end
|
|
7
|
+
#
|
|
8
|
+
# user = User.new(99)
|
|
9
|
+
# Flipper.enable :some_feature, user
|
|
10
|
+
# Flipper.enabled? :some_feature, user #=> true
|
|
11
|
+
#
|
|
12
|
+
module Identifier
|
|
13
|
+
def flipper_id
|
|
14
|
+
"#{self.class.name};#{id}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'securerandom'
|
|
2
|
+
require 'active_support/gem_version'
|
|
2
3
|
require 'active_support/notifications'
|
|
3
4
|
require 'active_support/log_subscriber'
|
|
4
5
|
|
|
@@ -10,7 +11,7 @@ module Flipper
|
|
|
10
11
|
# Example Output
|
|
11
12
|
#
|
|
12
13
|
# flipper[:search].enabled?(user)
|
|
13
|
-
# # Flipper feature(search) enabled? false (1.2ms) [
|
|
14
|
+
# # Flipper feature(search) enabled? false (1.2ms) [ actors=... ]
|
|
14
15
|
#
|
|
15
16
|
# Returns nothing.
|
|
16
17
|
def feature_operation(event)
|
|
@@ -20,15 +21,19 @@ module Flipper
|
|
|
20
21
|
gate_name = event.payload[:gate_name]
|
|
21
22
|
operation = event.payload[:operation]
|
|
22
23
|
result = event.payload[:result]
|
|
23
|
-
thing = event.payload[:thing]
|
|
24
24
|
|
|
25
25
|
description = "Flipper feature(#{feature_name}) #{operation} #{result.inspect}"
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
details = if event.payload.key?(:actors)
|
|
28
|
+
"actors=#{event.payload[:actors].inspect}"
|
|
29
|
+
else
|
|
30
|
+
"thing=#{event.payload[:thing].inspect}"
|
|
31
|
+
end
|
|
27
32
|
|
|
28
33
|
details += " gate_name=#{gate_name}" unless gate_name.nil?
|
|
29
34
|
|
|
30
35
|
name = '%s (%.1fms)' % [description, event.duration]
|
|
31
|
-
debug " #{
|
|
36
|
+
debug " #{color_name(name)} [ #{details} ]"
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
# Logs an adapter operation. If operation is for a feature, then that
|
|
@@ -48,11 +53,10 @@ module Flipper
|
|
|
48
53
|
|
|
49
54
|
feature_name = event.payload[:feature_name]
|
|
50
55
|
adapter_name = event.payload[:adapter_name]
|
|
51
|
-
gate_name = event.payload[:gate_name]
|
|
52
56
|
operation = event.payload[:operation]
|
|
53
57
|
result = event.payload[:result]
|
|
54
58
|
|
|
55
|
-
description = 'Flipper '
|
|
59
|
+
description = String.new('Flipper ')
|
|
56
60
|
description << "feature(#{feature_name}) " unless feature_name.nil?
|
|
57
61
|
description << "adapter(#{adapter_name}) "
|
|
58
62
|
description << "#{operation} "
|
|
@@ -60,14 +64,37 @@ module Flipper
|
|
|
60
64
|
details = "result=#{result.inspect}"
|
|
61
65
|
|
|
62
66
|
name = '%s (%.1fms)' % [description, event.duration]
|
|
63
|
-
debug " #{
|
|
67
|
+
debug " #{color_name(name)} [ #{details} ]"
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
def logger
|
|
67
71
|
self.class.logger
|
|
68
72
|
end
|
|
73
|
+
|
|
74
|
+
def self.attach
|
|
75
|
+
attach_to InstrumentationNamespace
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.detach
|
|
79
|
+
# Rails 5.2 doesn't support this, that's fine
|
|
80
|
+
detach_from InstrumentationNamespace if respond_to?(:detach_from)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Rails 7.1 changed the signature of this function.
|
|
86
|
+
COLOR_OPTIONS = if Gem::Requirement.new(">=7.1").satisfied_by?(ActiveSupport.gem_version)
|
|
87
|
+
{ bold: true }.freeze
|
|
88
|
+
else
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
private_constant :COLOR_OPTIONS
|
|
92
|
+
|
|
93
|
+
def color_name(name)
|
|
94
|
+
color(name, CYAN, COLOR_OPTIONS)
|
|
95
|
+
end
|
|
69
96
|
end
|
|
70
97
|
end
|
|
71
98
|
|
|
72
|
-
Instrumentation::LogSubscriber.
|
|
99
|
+
Instrumentation::LogSubscriber.attach
|
|
73
100
|
end
|
|
@@ -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
|
|
@@ -17,9 +17,13 @@ module Flipper
|
|
|
17
17
|
# block rather than the one passed to #instrument.
|
|
18
18
|
payload = payload.dup
|
|
19
19
|
|
|
20
|
-
result =
|
|
20
|
+
result = yield payload if block_given?
|
|
21
|
+
rescue Exception => e
|
|
22
|
+
payload[:exception] = [e.class.name, e.message]
|
|
23
|
+
payload[:exception_object] = e
|
|
24
|
+
raise e
|
|
25
|
+
ensure
|
|
21
26
|
@events << Event.new(name, payload, result)
|
|
22
|
-
result
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def events_by_name(name)
|
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
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require 'rack/body_proxy'
|
|
2
|
-
|
|
3
1
|
module Flipper
|
|
4
2
|
module Middleware
|
|
5
3
|
class Memoizer
|
|
@@ -10,22 +8,29 @@ module Flipper
|
|
|
10
8
|
#
|
|
11
9
|
# app - The app this middleware is included in.
|
|
12
10
|
# opts - The Hash of options.
|
|
13
|
-
# :
|
|
14
|
-
# :preload - Array of Symbol feature names to preload.
|
|
11
|
+
# :preload - Boolean to preload all features or Array of Symbol feature names to preload.
|
|
15
12
|
#
|
|
16
13
|
# Examples
|
|
17
14
|
#
|
|
18
15
|
# use Flipper::Middleware::Memoizer
|
|
19
16
|
#
|
|
20
17
|
# # using with preload_all features
|
|
21
|
-
# use Flipper::Middleware::Memoizer,
|
|
18
|
+
# use Flipper::Middleware::Memoizer, preload: true
|
|
22
19
|
#
|
|
23
20
|
# # using with preload specific features
|
|
24
21
|
# use Flipper::Middleware::Memoizer, preload: [:stats, :search, :some_feature]
|
|
25
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
|
+
#
|
|
26
31
|
def initialize(app, opts = {})
|
|
27
32
|
if opts.is_a?(Flipper::DSL) || opts.is_a?(Proc)
|
|
28
|
-
raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
|
|
33
|
+
raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
|
|
29
34
|
end
|
|
30
35
|
|
|
31
36
|
@app = app
|
|
@@ -36,39 +41,53 @@ module Flipper
|
|
|
36
41
|
def call(env)
|
|
37
42
|
request = Rack::Request.new(env)
|
|
38
43
|
|
|
39
|
-
if
|
|
40
|
-
|
|
44
|
+
if memoize?(request)
|
|
45
|
+
memoized_call(request)
|
|
41
46
|
else
|
|
42
|
-
|
|
47
|
+
@app.call(env)
|
|
43
48
|
end
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
private
|
|
47
52
|
|
|
48
|
-
def
|
|
49
|
-
|
|
53
|
+
def memoize?(request)
|
|
54
|
+
if @opts[:if]
|
|
55
|
+
@opts[:if].call(request)
|
|
56
|
+
elsif @opts[:unless]
|
|
57
|
+
!@opts[:unless].call(request)
|
|
58
|
+
else
|
|
59
|
+
true
|
|
60
|
+
end
|
|
50
61
|
end
|
|
51
62
|
|
|
52
|
-
def memoized_call(
|
|
53
|
-
|
|
54
|
-
flipper = env.fetch(@env_key) { Flipper }
|
|
55
|
-
original = flipper.memoizing?
|
|
56
|
-
flipper.memoize = true
|
|
57
|
-
|
|
58
|
-
flipper.preload_all if @opts[:preload_all]
|
|
63
|
+
def memoized_call(request)
|
|
64
|
+
flipper = request.env.fetch(@env_key) { Flipper }
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
# Already memoizing. This instance does not need to do anything.
|
|
67
|
+
if flipper.memoizing?
|
|
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)
|
|
62
70
|
end
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
begin
|
|
73
|
+
flipper.memoize = true
|
|
74
|
+
|
|
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
|
|
81
|
+
|
|
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
|
|
67
90
|
end
|
|
68
|
-
reset_on_body_close = true
|
|
69
|
-
response
|
|
70
|
-
ensure
|
|
71
|
-
flipper.memoize = original if flipper && !reset_on_body_close
|
|
72
91
|
end
|
|
73
92
|
end
|
|
74
93
|
end
|
|
@@ -7,7 +7,8 @@ module Flipper
|
|
|
7
7
|
#
|
|
8
8
|
# app - The app this middleware is included in.
|
|
9
9
|
# flipper_or_block - The Flipper::DSL instance or a block that yields a
|
|
10
|
-
# Flipper::DSL instance to use for all operations
|
|
10
|
+
# Flipper::DSL instance to use for all operations
|
|
11
|
+
# (optional, default: Flipper).
|
|
11
12
|
#
|
|
12
13
|
# Examples
|
|
13
14
|
#
|
|
@@ -19,18 +20,27 @@ module Flipper
|
|
|
19
20
|
# # using with a block that yields a flipper instance
|
|
20
21
|
# use Flipper::Middleware::SetupEnv, lambda { Flipper.new(...) }
|
|
21
22
|
#
|
|
22
|
-
|
|
23
|
+
# # using default configured Flipper instance
|
|
24
|
+
# Flipper.configure do |config|
|
|
25
|
+
# config.default { Flipper.new(...) }
|
|
26
|
+
# end
|
|
27
|
+
# use Flipper::Middleware::SetupEnv
|
|
28
|
+
def initialize(app, flipper_or_block = nil, options = {})
|
|
23
29
|
@app = app
|
|
24
30
|
@env_key = options.fetch(:env_key, 'flipper')
|
|
25
31
|
|
|
26
32
|
if flipper_or_block.respond_to?(:call)
|
|
27
33
|
@flipper_block = flipper_or_block
|
|
28
34
|
else
|
|
29
|
-
@flipper = flipper_or_block
|
|
35
|
+
@flipper = flipper_or_block || Flipper
|
|
30
36
|
end
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
def call(env)
|
|
40
|
+
dup.call!(env)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def call!(env)
|
|
34
44
|
env[@env_key] ||= flipper
|
|
35
45
|
@app.call(env)
|
|
36
46
|
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,157 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
require 'concurrent/utility/monotonic_time'
|
|
3
|
+
require 'concurrent/map'
|
|
4
|
+
require 'concurrent/atomic/atomic_fixnum'
|
|
5
|
+
require 'concurrent/atomic/atomic_boolean'
|
|
6
|
+
|
|
7
|
+
module Flipper
|
|
8
|
+
class Poller
|
|
9
|
+
attr_reader :adapter, :thread, :pid, :mutex, :interval, :last_synced_at
|
|
10
|
+
|
|
11
|
+
def self.instances
|
|
12
|
+
@instances ||= Concurrent::Map.new
|
|
13
|
+
end
|
|
14
|
+
private_class_method :instances
|
|
15
|
+
|
|
16
|
+
def self.get(key, options = {})
|
|
17
|
+
instances.compute_if_absent(key) { new(options) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.reset
|
|
21
|
+
instances.each {|_, instance| instance.stop }.clear
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
MINIMUM_POLL_INTERVAL = 10
|
|
25
|
+
|
|
26
|
+
def initialize(options = {})
|
|
27
|
+
@thread = nil
|
|
28
|
+
@pid = Process.pid
|
|
29
|
+
@mutex = Mutex.new
|
|
30
|
+
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
|
31
|
+
@remote_adapter = options.fetch(:remote_adapter)
|
|
32
|
+
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
|
33
|
+
@adapter = Adapters::Memory.new(nil, threadsafe: true)
|
|
34
|
+
@shutdown_requested = Concurrent::AtomicBoolean.new(false)
|
|
35
|
+
|
|
36
|
+
self.interval = options.fetch(:interval, 10)
|
|
37
|
+
@initial_interval = @interval
|
|
38
|
+
|
|
39
|
+
@start_automatically = options.fetch(:start_automatically, true)
|
|
40
|
+
|
|
41
|
+
if options.fetch(:shutdown_automatically, true)
|
|
42
|
+
at_exit { stop }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def start
|
|
47
|
+
reset if forked?
|
|
48
|
+
return if @shutdown_requested.true?
|
|
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
|
+
begin
|
|
76
|
+
@adapter.import @remote_adapter
|
|
77
|
+
@last_synced_at.update { |time| Concurrent.monotonic_time }
|
|
78
|
+
ensure
|
|
79
|
+
apply_response_headers
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Internal: Sets the interval in seconds for how often to poll.
|
|
85
|
+
def interval=(value)
|
|
86
|
+
requested_interval = Flipper::Typecast.to_float(value)
|
|
87
|
+
new_interval = [requested_interval, MINIMUM_POLL_INTERVAL].max
|
|
88
|
+
|
|
89
|
+
if requested_interval < MINIMUM_POLL_INTERVAL
|
|
90
|
+
warn "Flipper::Cloud poll interval must be greater than or equal to #{MINIMUM_POLL_INTERVAL} but was #{requested_interval}. Setting interval to #{MINIMUM_POLL_INTERVAL}."
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@interval = new_interval
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def jitter
|
|
99
|
+
# Cap jitter at 30 seconds to prevent excessive delays for large intervals
|
|
100
|
+
max_jitter = [interval * 0.1, 30].min
|
|
101
|
+
rand * max_jitter
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def forked?
|
|
105
|
+
pid != Process.pid
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ensure_worker_running
|
|
109
|
+
# Return early if thread is alive and avoid the mutex lock and unlock.
|
|
110
|
+
return if thread_alive?
|
|
111
|
+
|
|
112
|
+
# If another thread is starting worker thread, then return early so this
|
|
113
|
+
# thread can enqueue and move on with life.
|
|
114
|
+
return unless mutex.try_lock
|
|
115
|
+
|
|
116
|
+
begin
|
|
117
|
+
return if thread_alive?
|
|
118
|
+
@thread = Thread.new { run }
|
|
119
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
|
120
|
+
operation: :thread_start,
|
|
121
|
+
})
|
|
122
|
+
ensure
|
|
123
|
+
mutex.unlock
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def thread_alive?
|
|
128
|
+
@thread && @thread.alive?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def reset
|
|
132
|
+
@pid = Process.pid
|
|
133
|
+
@shutdown_requested.make_false
|
|
134
|
+
mutex.unlock if mutex.locked?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def apply_response_headers
|
|
138
|
+
return unless @remote_adapter.respond_to?(:last_get_all_response)
|
|
139
|
+
|
|
140
|
+
if response = @remote_adapter.last_get_all_response
|
|
141
|
+
# shutdown based on response header
|
|
142
|
+
if Flipper::Typecast.to_boolean(response["poll-shutdown"])
|
|
143
|
+
@shutdown_requested.make_true
|
|
144
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
|
145
|
+
operation: :shutdown_requested,
|
|
146
|
+
})
|
|
147
|
+
stop
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# update interval based on response header
|
|
151
|
+
if interval = response["poll-interval"]
|
|
152
|
+
self.interval = [Flipper::Typecast.to_float(interval), @initial_interval].max
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
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
|