flipper 0.24.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +45 -14
- data/.github/workflows/examples.yml +39 -16
- data/Changelog.md +2 -443
- data/Gemfile +19 -11
- data/README.md +31 -27
- data/Rakefile +6 -4
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_multiple_actors_ips.rb +20 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +27 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- data/examples/cloud/app.ru +12 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/basic.rb +22 -0
- data/examples/cloud/cloud_setup.rb +20 -0
- data/examples/cloud/forked.rb +36 -0
- data/examples/cloud/import.rb +17 -0
- data/examples/cloud/threaded.rb +33 -0
- data/examples/dsl.rb +1 -15
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/expressions.rb +213 -0
- data/examples/instrumentation.rb +1 -0
- data/examples/instrumentation_last_accessed_at.rb +1 -0
- data/examples/mirroring.rb +59 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +10 -6
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +33 -7
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +72 -0
- data/lib/flipper/adapters/http/client.rb +44 -20
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +31 -16
- data/lib/flipper/adapters/instrumented.rb +25 -6
- data/lib/flipper/adapters/memoizable.rb +33 -21
- data/lib/flipper/adapters/memory.rb +81 -46
- data/lib/flipper/adapters/operation_logger.rb +17 -78
- data/lib/flipper/adapters/poll/poller.rb +2 -0
- data/lib/flipper/adapters/poll.rb +37 -0
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +45 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +263 -0
- data/lib/flipper/cloud/dsl.rb +27 -0
- data/lib/flipper/cloud/message_verifier.rb +95 -0
- data/lib/flipper/cloud/middleware.rb +63 -0
- data/lib/flipper/cloud/routes.rb +14 -0
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +53 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +46 -45
- data/lib/flipper/engine.rb +102 -0
- data/lib/flipper/errors.rb +3 -20
- data/lib/flipper/export.rb +26 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +11 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +9 -0
- data/lib/flipper/feature.rb +87 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +13 -11
- data/lib/flipper/gate_values.rb +5 -18
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/gates/group.rb +5 -7
- data/lib/flipper/gates/percentage_of_actors.rb +10 -13
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/identifier.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +34 -6
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/metadata.rb +7 -1
- data/lib/flipper/middleware/memoizer.rb +28 -22
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +118 -0
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
- data/lib/flipper/test/shared_adapter_test.rb +101 -58
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +59 -18
- data/lib/flipper/types/actor.rb +13 -13
- data/lib/flipper/types/group.rb +4 -4
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +50 -11
- data/lib/generators/flipper/setup_generator.rb +63 -0
- data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
- data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
- data/lib/generators/flipper/update_generator.rb +35 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +30 -2
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/dual_write_spec.rb +2 -2
- data/spec/flipper/adapters/failsafe_spec.rb +58 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +137 -55
- data/spec/flipper/adapters/instrumented_spec.rb +29 -11
- data/spec/flipper/adapters/memoizable_spec.rb +51 -31
- data/spec/flipper/adapters/memory_spec.rb +14 -3
- data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- data/spec/flipper/adapters/strict_spec.rb +64 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cli_spec.rb +164 -0
- data/spec/flipper/cloud/configuration_spec.rb +251 -0
- data/spec/flipper/cloud/dsl_spec.rb +82 -0
- data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
- data/spec/flipper/cloud/middleware_spec.rb +289 -0
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +208 -0
- data/spec/flipper/cloud_spec.rb +181 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +54 -73
- data/spec/flipper/engine_spec.rb +373 -0
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/duration_spec.rb +43 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +13 -0
- data/spec/flipper/feature_check_context_spec.rb +17 -17
- data/spec/flipper/feature_spec.rb +436 -33
- data/spec/flipper/gate_values_spec.rb +2 -33
- data/spec/flipper/gates/boolean_spec.rb +1 -1
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/gates/group_spec.rb +2 -3
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +23 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
- data/spec/flipper/middleware/memoizer_spec.rb +74 -24
- data/spec/flipper/model/active_record_spec.rb +61 -0
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +121 -6
- data/spec/flipper/types/actor_spec.rb +63 -46
- data/spec/flipper/types/group_spec.rb +2 -2
- data/spec/flipper_integration_spec.rb +168 -58
- data/spec/flipper_spec.rb +93 -29
- data/spec/spec_helper.rb +8 -14
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +23 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +19 -2
- data/test_rails/system/test_help_test.rb +51 -0
- metadata +223 -19
- data/lib/flipper/railtie.rb +0 -47
- data/spec/flipper/railtie_spec.rb +0 -73
@@ -7,13 +7,17 @@ module Flipper
|
|
7
7
|
# gates for the feature.
|
8
8
|
attr_reader :values
|
9
9
|
|
10
|
-
# Public: The
|
11
|
-
attr_reader :
|
10
|
+
# Public: The actors we want to know if a feature is enabled for.
|
11
|
+
attr_reader :actors
|
12
12
|
|
13
|
-
def initialize(
|
14
|
-
@feature_name =
|
15
|
-
@values =
|
16
|
-
@
|
13
|
+
def initialize(feature_name:, values:, actors:)
|
14
|
+
@feature_name = feature_name
|
15
|
+
@values = values
|
16
|
+
@actors = actors
|
17
|
+
end
|
18
|
+
|
19
|
+
def actors?
|
20
|
+
!@actors.nil? && !@actors.empty?
|
17
21
|
end
|
18
22
|
|
19
23
|
# Public: Convenience method for groups value like Feature has.
|
data/lib/flipper/gate.rb
CHANGED
@@ -18,28 +18,29 @@ module Flipper
|
|
18
18
|
raise 'Not implemented'
|
19
19
|
end
|
20
20
|
|
21
|
-
def enabled?(
|
21
|
+
def enabled?(value)
|
22
22
|
raise 'Not implemented'
|
23
23
|
end
|
24
24
|
|
25
|
-
# Internal: Check if a gate is open for
|
25
|
+
# Internal: Check if a gate is open for one or more actors. Implemented
|
26
|
+
# in subclass.
|
26
27
|
#
|
27
|
-
# Returns true if gate open for
|
28
|
-
def open?(
|
28
|
+
# Returns true if gate open for any actor, false if not.
|
29
|
+
def open?(actors, value, options = {})
|
29
30
|
false
|
30
31
|
end
|
31
32
|
|
32
|
-
# Internal: Check if a gate is protects
|
33
|
+
# Internal: Check if a gate is protects an actor. Implemented in subclass.
|
33
34
|
#
|
34
|
-
# Returns true if gate protects
|
35
|
-
def protects?(
|
35
|
+
# Returns true if gate protects actor, false if not.
|
36
|
+
def protects?(actor)
|
36
37
|
false
|
37
38
|
end
|
38
39
|
|
39
|
-
# Internal: Allows gate to wrap
|
40
|
-
# types so adapters always get
|
41
|
-
def wrap(
|
42
|
-
|
40
|
+
# Internal: Allows gate to wrap actor using one of the supported flipper
|
41
|
+
# types so adapters always get someactor that responds to value.
|
42
|
+
def wrap(actor)
|
43
|
+
actor
|
43
44
|
end
|
44
45
|
|
45
46
|
# Public: Pretty string version for debugging.
|
@@ -59,3 +60,4 @@ require 'flipper/gates/boolean'
|
|
59
60
|
require 'flipper/gates/group'
|
60
61
|
require 'flipper/gates/percentage_of_actors'
|
61
62
|
require 'flipper/gates/percentage_of_time'
|
63
|
+
require 'flipper/gates/expression'
|
data/lib/flipper/gate_values.rb
CHANGED
@@ -3,19 +3,10 @@ require 'flipper/typecast'
|
|
3
3
|
|
4
4
|
module Flipper
|
5
5
|
class GateValues
|
6
|
-
# Private: Array of instance variables that are readable through the []
|
7
|
-
# instance method.
|
8
|
-
LegitIvars = {
|
9
|
-
'boolean' => '@boolean',
|
10
|
-
'actors' => '@actors',
|
11
|
-
'groups' => '@groups',
|
12
|
-
'percentage_of_time' => '@percentage_of_time',
|
13
|
-
'percentage_of_actors' => '@percentage_of_actors',
|
14
|
-
}.freeze
|
15
|
-
|
16
6
|
attr_reader :boolean
|
17
7
|
attr_reader :actors
|
18
8
|
attr_reader :groups
|
9
|
+
attr_reader :expression
|
19
10
|
attr_reader :percentage_of_actors
|
20
11
|
attr_reader :percentage_of_time
|
21
12
|
|
@@ -23,14 +14,9 @@ module Flipper
|
|
23
14
|
@boolean = Typecast.to_boolean(adapter_values[:boolean])
|
24
15
|
@actors = Typecast.to_set(adapter_values[:actors])
|
25
16
|
@groups = Typecast.to_set(adapter_values[:groups])
|
26
|
-
@
|
27
|
-
@
|
28
|
-
|
29
|
-
|
30
|
-
def [](key)
|
31
|
-
if ivar = LegitIvars[key.to_s]
|
32
|
-
instance_variable_get(ivar)
|
33
|
-
end
|
17
|
+
@expression = adapter_values[:expression]
|
18
|
+
@percentage_of_actors = Typecast.to_number(adapter_values[:percentage_of_actors])
|
19
|
+
@percentage_of_time = Typecast.to_number(adapter_values[:percentage_of_time])
|
34
20
|
end
|
35
21
|
|
36
22
|
def eql?(other)
|
@@ -38,6 +24,7 @@ module Flipper
|
|
38
24
|
boolean == other.boolean &&
|
39
25
|
actors == other.actors &&
|
40
26
|
groups == other.groups &&
|
27
|
+
expression == other.expression &&
|
41
28
|
percentage_of_actors == other.percentage_of_actors &&
|
42
29
|
percentage_of_time == other.percentage_of_time
|
43
30
|
end
|
data/lib/flipper/gates/actor.rb
CHANGED
@@ -19,30 +19,23 @@ module Flipper
|
|
19
19
|
!value.empty?
|
20
20
|
end
|
21
21
|
|
22
|
-
# Internal: Checks if the gate is open for
|
22
|
+
# Internal: Checks if the gate is open for an actor.
|
23
23
|
#
|
24
|
-
# Returns true if gate open for
|
24
|
+
# Returns true if gate open for actor, false if not.
|
25
25
|
def open?(context)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
if protects?(context.thing)
|
31
|
-
actor = wrap(context.thing)
|
32
|
-
enabled_actor_ids = value
|
33
|
-
enabled_actor_ids.include?(actor.value)
|
34
|
-
else
|
35
|
-
false
|
36
|
-
end
|
26
|
+
return false unless context.actors?
|
27
|
+
|
28
|
+
context.actors.any? do |actor|
|
29
|
+
context.values.actors.include?(actor.value)
|
37
30
|
end
|
38
31
|
end
|
39
32
|
|
40
|
-
def wrap(
|
41
|
-
Types::Actor.wrap(
|
33
|
+
def wrap(actor)
|
34
|
+
Types::Actor.wrap(actor)
|
42
35
|
end
|
43
36
|
|
44
|
-
def protects?(
|
45
|
-
Types::Actor.wrappable?(
|
37
|
+
def protects?(actor)
|
38
|
+
Types::Actor.wrappable?(actor)
|
46
39
|
end
|
47
40
|
end
|
48
41
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require "flipper/expression"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Gates
|
5
|
+
class Expression < Gate
|
6
|
+
# Internal: The name of the gate. Used for instrumentation, etc.
|
7
|
+
def name
|
8
|
+
:expression
|
9
|
+
end
|
10
|
+
|
11
|
+
# Internal: Name converted to value safe for adapter.
|
12
|
+
def key
|
13
|
+
:expression
|
14
|
+
end
|
15
|
+
|
16
|
+
def data_type
|
17
|
+
:json
|
18
|
+
end
|
19
|
+
|
20
|
+
def enabled?(value)
|
21
|
+
!value.nil? && !value.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Internal: Checks if the gate is open for a thing.
|
25
|
+
#
|
26
|
+
# Returns true if gate open for thing, false if not.
|
27
|
+
def open?(context)
|
28
|
+
data = context.values.expression
|
29
|
+
return false if data.nil? || data.empty?
|
30
|
+
expression = Flipper::Expression.build(data)
|
31
|
+
|
32
|
+
if context.actors.nil? || context.actors.empty?
|
33
|
+
!!expression.evaluate(feature_name: context.feature_name, properties: DEFAULT_PROPERTIES)
|
34
|
+
else
|
35
|
+
context.actors.any? do |actor|
|
36
|
+
!!expression.evaluate(feature_name: context.feature_name, properties: properties(actor))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def protects?(thing)
|
42
|
+
thing.is_a?(Flipper::Expression) || thing.is_a?(Hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
def wrap(thing)
|
46
|
+
Flipper::Expression.build(thing)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Internal
|
52
|
+
DEFAULT_PROPERTIES = {}.freeze
|
53
|
+
|
54
|
+
def properties(actor)
|
55
|
+
return DEFAULT_PROPERTIES if actor.nil?
|
56
|
+
|
57
|
+
properties = {}
|
58
|
+
|
59
|
+
if actor.respond_to?(:flipper_properties)
|
60
|
+
properties.update(actor.flipper_properties)
|
61
|
+
else
|
62
|
+
warn "#{actor.inspect} does not respond to `flipper_properties` but should."
|
63
|
+
end
|
64
|
+
|
65
|
+
properties.transform_keys!(&:to_s)
|
66
|
+
|
67
|
+
if actor.respond_to?(:flipper_id)
|
68
|
+
properties["flipper_id".freeze] = actor.flipper_id
|
69
|
+
end
|
70
|
+
|
71
|
+
properties
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
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)
|
data/lib/flipper/identifier.rb
CHANGED
@@ -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
|
@@ -60,14 +65,37 @@ module Flipper
|
|
60
65
|
details = "result=#{result.inspect}"
|
61
66
|
|
62
67
|
name = '%s (%.1fms)' % [description, event.duration]
|
63
|
-
debug " #{
|
68
|
+
debug " #{color_name(name)} [ #{details} ]"
|
64
69
|
end
|
65
70
|
|
66
71
|
def logger
|
67
72
|
self.class.logger
|
68
73
|
end
|
74
|
+
|
75
|
+
def self.attach
|
76
|
+
attach_to InstrumentationNamespace
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.detach
|
80
|
+
# Rails 5.2 doesn't support this, that's fine
|
81
|
+
detach_from InstrumentationNamespace if respond_to?(:detach_from)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Rails 7.1 changed the signature of this function.
|
87
|
+
COLOR_OPTIONS = if Gem::Requirement.new(">=7.1").satisfied_by?(ActiveSupport.gem_version)
|
88
|
+
{ bold: true }.freeze
|
89
|
+
else
|
90
|
+
true
|
91
|
+
end
|
92
|
+
private_constant :COLOR_OPTIONS
|
93
|
+
|
94
|
+
def color_name(name)
|
95
|
+
color(name, CYAN, COLOR_OPTIONS)
|
96
|
+
end
|
69
97
|
end
|
70
98
|
end
|
71
99
|
|
72
|
-
Instrumentation::LogSubscriber.
|
100
|
+
Instrumentation::LogSubscriber.attach
|
73
101
|
end
|
@@ -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
|
@@ -45,7 +45,6 @@ module Flipper
|
|
45
45
|
gate_name = @payload[:gate_name]
|
46
46
|
operation = strip_trailing_question_mark(@payload[:operation])
|
47
47
|
result = @payload[:result]
|
48
|
-
thing = @payload[:thing]
|
49
48
|
|
50
49
|
update_timer "flipper.feature_operation.#{operation}"
|
51
50
|
|
@@ -72,6 +71,14 @@ module Flipper
|
|
72
71
|
update_timer "flipper.adapter.#{adapter_name}.#{operation}"
|
73
72
|
end
|
74
73
|
|
74
|
+
def update_poller_metrics
|
75
|
+
# noop
|
76
|
+
end
|
77
|
+
|
78
|
+
def update_synchronizer_call_metrics
|
79
|
+
# noop
|
80
|
+
end
|
81
|
+
|
75
82
|
QUESTION_MARK = '?'.freeze
|
76
83
|
|
77
84
|
# Private
|
data/lib/flipper/metadata.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
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}",
|
4
10
|
}.freeze
|
5
11
|
end
|
@@ -20,16 +20,19 @@ 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.'
|
26
34
|
end
|
27
35
|
|
28
|
-
if opts[:preload_all]
|
29
|
-
warn "Flipper::Middleware::Memoizer: `preload_all` is deprecated, use `preload: true`"
|
30
|
-
opts[:preload] = true
|
31
|
-
end
|
32
|
-
|
33
36
|
@app = app
|
34
37
|
@opts = opts
|
35
38
|
@env_key = opts.fetch(:env_key, 'flipper')
|
@@ -39,7 +42,7 @@ module Flipper
|
|
39
42
|
request = Rack::Request.new(env)
|
40
43
|
|
41
44
|
if memoize?(request)
|
42
|
-
memoized_call(
|
45
|
+
memoized_call(request)
|
43
46
|
else
|
44
47
|
@app.call(env)
|
45
48
|
end
|
@@ -57,31 +60,34 @@ module Flipper
|
|
57
60
|
end
|
58
61
|
end
|
59
62
|
|
60
|
-
def memoized_call(
|
61
|
-
|
62
|
-
flipper = env.fetch(@env_key) { Flipper }
|
63
|
+
def memoized_call(request)
|
64
|
+
flipper = request.env.fetch(@env_key) { Flipper }
|
63
65
|
|
64
66
|
# Already memoizing. This instance does not need to do anything.
|
65
67
|
if flipper.memoizing?
|
66
|
-
warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/
|
67
|
-
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)
|
68
70
|
end
|
69
71
|
|
70
|
-
|
72
|
+
begin
|
73
|
+
flipper.memoize = true
|
71
74
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
76
86
|
|
77
|
-
|
78
|
-
|
87
|
+
@app.call(request.env)
|
88
|
+
ensure
|
79
89
|
flipper.memoize = false
|
80
90
|
end
|
81
|
-
reset_on_body_close = true
|
82
|
-
response
|
83
|
-
ensure
|
84
|
-
flipper.memoize = false if flipper && !reset_on_body_close
|
85
91
|
end
|
86
92
|
end
|
87
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
|
+
start = Concurrent.monotonic_time
|
63
|
+
begin
|
64
|
+
sync
|
65
|
+
rescue => exception
|
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
|