flipper 0.24.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (226) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +45 -14
  5. data/.github/workflows/examples.yml +39 -16
  6. data/Changelog.md +2 -443
  7. data/Gemfile +19 -11
  8. data/README.md +31 -27
  9. data/Rakefile +6 -4
  10. data/benchmark/enabled_ips.rb +10 -0
  11. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  12. data/benchmark/enabled_profile.rb +20 -0
  13. data/benchmark/instrumentation_ips.rb +21 -0
  14. data/benchmark/typecast_ips.rb +27 -0
  15. data/docs/images/banner.jpg +0 -0
  16. data/docs/images/flipper_cloud.png +0 -0
  17. data/examples/api/basic.ru +3 -4
  18. data/examples/api/custom_memoized.ru +3 -4
  19. data/examples/api/memoized.ru +3 -4
  20. data/examples/cloud/app.ru +12 -0
  21. data/examples/cloud/backoff_policy.rb +13 -0
  22. data/examples/cloud/basic.rb +22 -0
  23. data/examples/cloud/cloud_setup.rb +20 -0
  24. data/examples/cloud/forked.rb +36 -0
  25. data/examples/cloud/import.rb +17 -0
  26. data/examples/cloud/threaded.rb +33 -0
  27. data/examples/dsl.rb +1 -15
  28. data/examples/enabled_for_actor.rb +4 -2
  29. data/examples/expressions.rb +213 -0
  30. data/examples/instrumentation.rb +1 -0
  31. data/examples/instrumentation_last_accessed_at.rb +1 -0
  32. data/examples/mirroring.rb +59 -0
  33. data/examples/strict.rb +18 -0
  34. data/exe/flipper +5 -0
  35. data/flipper-cloud.gemspec +19 -0
  36. data/flipper.gemspec +10 -6
  37. data/lib/flipper/actor.rb +6 -3
  38. data/lib/flipper/adapter.rb +33 -7
  39. data/lib/flipper/adapter_builder.rb +44 -0
  40. data/lib/flipper/adapters/actor_limit.rb +28 -0
  41. data/lib/flipper/adapters/cache_base.rb +143 -0
  42. data/lib/flipper/adapters/dual_write.rb +1 -3
  43. data/lib/flipper/adapters/failover.rb +0 -4
  44. data/lib/flipper/adapters/failsafe.rb +72 -0
  45. data/lib/flipper/adapters/http/client.rb +44 -20
  46. data/lib/flipper/adapters/http/error.rb +1 -1
  47. data/lib/flipper/adapters/http.rb +31 -16
  48. data/lib/flipper/adapters/instrumented.rb +25 -6
  49. data/lib/flipper/adapters/memoizable.rb +33 -21
  50. data/lib/flipper/adapters/memory.rb +81 -46
  51. data/lib/flipper/adapters/operation_logger.rb +17 -78
  52. data/lib/flipper/adapters/poll/poller.rb +2 -0
  53. data/lib/flipper/adapters/poll.rb +37 -0
  54. data/lib/flipper/adapters/pstore.rb +17 -11
  55. data/lib/flipper/adapters/read_only.rb +8 -41
  56. data/lib/flipper/adapters/strict.rb +45 -0
  57. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  58. data/lib/flipper/adapters/sync.rb +0 -4
  59. data/lib/flipper/adapters/wrapper.rb +54 -0
  60. data/lib/flipper/cli.rb +263 -0
  61. data/lib/flipper/cloud/configuration.rb +263 -0
  62. data/lib/flipper/cloud/dsl.rb +27 -0
  63. data/lib/flipper/cloud/message_verifier.rb +95 -0
  64. data/lib/flipper/cloud/middleware.rb +63 -0
  65. data/lib/flipper/cloud/routes.rb +14 -0
  66. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  67. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  68. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  69. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  70. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  71. data/lib/flipper/cloud/telemetry.rb +191 -0
  72. data/lib/flipper/cloud.rb +53 -0
  73. data/lib/flipper/configuration.rb +25 -4
  74. data/lib/flipper/dsl.rb +46 -45
  75. data/lib/flipper/engine.rb +102 -0
  76. data/lib/flipper/errors.rb +3 -20
  77. data/lib/flipper/export.rb +26 -0
  78. data/lib/flipper/exporter.rb +17 -0
  79. data/lib/flipper/exporters/json/export.rb +32 -0
  80. data/lib/flipper/exporters/json/v1.rb +33 -0
  81. data/lib/flipper/expression/builder.rb +73 -0
  82. data/lib/flipper/expression/constant.rb +25 -0
  83. data/lib/flipper/expression.rb +71 -0
  84. data/lib/flipper/expressions/all.rb +11 -0
  85. data/lib/flipper/expressions/any.rb +9 -0
  86. data/lib/flipper/expressions/boolean.rb +9 -0
  87. data/lib/flipper/expressions/comparable.rb +13 -0
  88. data/lib/flipper/expressions/duration.rb +28 -0
  89. data/lib/flipper/expressions/equal.rb +9 -0
  90. data/lib/flipper/expressions/greater_than.rb +9 -0
  91. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/less_than.rb +9 -0
  93. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  94. data/lib/flipper/expressions/not_equal.rb +9 -0
  95. data/lib/flipper/expressions/now.rb +9 -0
  96. data/lib/flipper/expressions/number.rb +9 -0
  97. data/lib/flipper/expressions/percentage.rb +9 -0
  98. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  99. data/lib/flipper/expressions/property.rb +9 -0
  100. data/lib/flipper/expressions/random.rb +9 -0
  101. data/lib/flipper/expressions/string.rb +9 -0
  102. data/lib/flipper/expressions/time.rb +9 -0
  103. data/lib/flipper/feature.rb +87 -26
  104. data/lib/flipper/feature_check_context.rb +10 -6
  105. data/lib/flipper/gate.rb +13 -11
  106. data/lib/flipper/gate_values.rb +5 -18
  107. data/lib/flipper/gates/actor.rb +10 -17
  108. data/lib/flipper/gates/boolean.rb +1 -1
  109. data/lib/flipper/gates/expression.rb +75 -0
  110. data/lib/flipper/gates/group.rb +5 -7
  111. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  112. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  113. data/lib/flipper/identifier.rb +2 -2
  114. data/lib/flipper/instrumentation/log_subscriber.rb +34 -6
  115. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  116. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  117. data/lib/flipper/metadata.rb +7 -1
  118. data/lib/flipper/middleware/memoizer.rb +28 -22
  119. data/lib/flipper/model/active_record.rb +23 -0
  120. data/lib/flipper/poller.rb +118 -0
  121. data/lib/flipper/serializers/gzip.rb +22 -0
  122. data/lib/flipper/serializers/json.rb +17 -0
  123. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  124. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  125. data/lib/flipper/test_help.rb +43 -0
  126. data/lib/flipper/typecast.rb +59 -18
  127. data/lib/flipper/types/actor.rb +13 -13
  128. data/lib/flipper/types/group.rb +4 -4
  129. data/lib/flipper/types/percentage.rb +1 -1
  130. data/lib/flipper/version.rb +11 -1
  131. data/lib/flipper.rb +50 -11
  132. data/lib/generators/flipper/setup_generator.rb +63 -0
  133. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  134. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  135. data/lib/generators/flipper/update_generator.rb +35 -0
  136. data/package-lock.json +41 -0
  137. data/package.json +10 -0
  138. data/spec/fixtures/environment.rb +1 -0
  139. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  140. data/spec/flipper/adapter_builder_spec.rb +72 -0
  141. data/spec/flipper/adapter_spec.rb +30 -2
  142. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  143. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  144. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  145. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  146. data/spec/flipper/adapters/http_spec.rb +137 -55
  147. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  148. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  149. data/spec/flipper/adapters/memory_spec.rb +14 -3
  150. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  151. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  152. data/spec/flipper/adapters/strict_spec.rb +64 -0
  153. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  154. data/spec/flipper/cli_spec.rb +164 -0
  155. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  156. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  157. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  158. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  159. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  160. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  161. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  162. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  163. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  164. data/spec/flipper/cloud_spec.rb +181 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -73
  167. data/spec/flipper/engine_spec.rb +373 -0
  168. data/spec/flipper/export_spec.rb +13 -0
  169. data/spec/flipper/exporter_spec.rb +16 -0
  170. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  171. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  172. data/spec/flipper/expression/builder_spec.rb +248 -0
  173. data/spec/flipper/expression_spec.rb +188 -0
  174. data/spec/flipper/expressions/all_spec.rb +15 -0
  175. data/spec/flipper/expressions/any_spec.rb +15 -0
  176. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  177. data/spec/flipper/expressions/duration_spec.rb +43 -0
  178. data/spec/flipper/expressions/equal_spec.rb +24 -0
  179. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  180. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  181. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  182. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  183. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  184. data/spec/flipper/expressions/now_spec.rb +11 -0
  185. data/spec/flipper/expressions/number_spec.rb +21 -0
  186. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  187. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  188. data/spec/flipper/expressions/property_spec.rb +13 -0
  189. data/spec/flipper/expressions/random_spec.rb +9 -0
  190. data/spec/flipper/expressions/string_spec.rb +11 -0
  191. data/spec/flipper/expressions/time_spec.rb +13 -0
  192. data/spec/flipper/feature_check_context_spec.rb +17 -17
  193. data/spec/flipper/feature_spec.rb +436 -33
  194. data/spec/flipper/gate_values_spec.rb +2 -33
  195. data/spec/flipper/gates/boolean_spec.rb +1 -1
  196. data/spec/flipper/gates/expression_spec.rb +108 -0
  197. data/spec/flipper/gates/group_spec.rb +2 -3
  198. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  199. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  200. data/spec/flipper/identifier_spec.rb +4 -5
  201. data/spec/flipper/instrumentation/log_subscriber_spec.rb +23 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
  203. data/spec/flipper/middleware/memoizer_spec.rb +74 -24
  204. data/spec/flipper/model/active_record_spec.rb +61 -0
  205. data/spec/flipper/poller_spec.rb +47 -0
  206. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  207. data/spec/flipper/serializers/json_spec.rb +13 -0
  208. data/spec/flipper/typecast_spec.rb +121 -6
  209. data/spec/flipper/types/actor_spec.rb +63 -46
  210. data/spec/flipper/types/group_spec.rb +2 -2
  211. data/spec/flipper_integration_spec.rb +168 -58
  212. data/spec/flipper_spec.rb +93 -29
  213. data/spec/spec_helper.rb +8 -14
  214. data/spec/support/actor_names.yml +1 -0
  215. data/spec/support/fail_on_output.rb +8 -0
  216. data/spec/support/fake_backoff_policy.rb +15 -0
  217. data/spec/support/skippable.rb +18 -0
  218. data/spec/support/spec_helpers.rb +23 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +19 -2
  223. data/test_rails/system/test_help_test.rb +51 -0
  224. metadata +223 -19
  225. data/lib/flipper/railtie.rb +0 -47
  226. 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 thing we want to know if a feature is enabled for.
11
- attr_reader :thing
10
+ # Public: The actors we want to know if a feature is enabled for.
11
+ attr_reader :actors
12
12
 
13
- def initialize(options = {})
14
- @feature_name = options.fetch(:feature_name)
15
- @values = options.fetch(:values)
16
- @thing = options.fetch(:thing)
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?(_value)
21
+ def enabled?(value)
22
22
  raise 'Not implemented'
23
23
  end
24
24
 
25
- # Internal: Check if a gate is open for a thing. Implemented in subclass.
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 thing, false if not.
28
- def open?(_thing, _value, _options = {})
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 a thing. Implemented in subclass.
33
+ # Internal: Check if a gate is protects an actor. Implemented in subclass.
33
34
  #
34
- # Returns true if gate protects thing, false if not.
35
- def protects?(_thing)
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 thing using one of the supported flipper
40
- # types so adapters always get something that responds to value.
41
- def wrap(thing)
42
- thing
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'
@@ -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
- @percentage_of_actors = Typecast.to_percentage(adapter_values[:percentage_of_actors])
27
- @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time])
28
- end
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
@@ -19,30 +19,23 @@ module Flipper
19
19
  !value.empty?
20
20
  end
21
21
 
22
- # Internal: Checks if the gate is open for a thing.
22
+ # Internal: Checks if the gate is open for an actor.
23
23
  #
24
- # Returns true if gate open for thing, false if not.
24
+ # Returns true if gate open for actor, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- if context.thing.nil?
28
- false
29
- else
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(thing)
41
- Types::Actor.wrap(thing)
33
+ def wrap(actor)
34
+ Types::Actor.wrap(actor)
42
35
  end
43
36
 
44
- def protects?(thing)
45
- Types::Actor.wrappable?(thing)
37
+ def protects?(actor)
38
+ Types::Actor.wrappable?(actor)
46
39
  end
47
40
  end
48
41
  end
@@ -24,7 +24,7 @@ module Flipper
24
24
  # Returns true if explicitly set to true, false if explicitly set to false
25
25
  # or nil if not explicitly set.
26
26
  def open?(context)
27
- context.values[key]
27
+ context.values.boolean
28
28
  end
29
29
 
30
30
  def wrap(thing)
@@ -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
@@ -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
- value = context.values[key]
27
- if context.thing.nil?
28
- false
29
- else
30
- value.any? do |name|
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
- # Internal: Checks if the gate is open for a thing.
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 thing, false if not.
31
+ # Returns true if gate open for any actors, false if not.
27
32
  def open?(context)
28
- percentage = context.values[key]
29
-
30
- if Types::Actor.wrappable?(context.thing)
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)
@@ -23,8 +23,7 @@ module Flipper
23
23
  #
24
24
  # Returns true if gate open for thing, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- rand < (value / 100.0)
26
+ rand < (context.values.percentage_of_time / 100.0)
28
27
  end
29
28
 
30
29
  def protects?(thing)
@@ -6,8 +6,8 @@ module Flipper
6
6
  # end
7
7
  #
8
8
  # user = User.new(99)
9
- # Flipper.enable :thing, user
10
- # Flipper.enabled? :thing, user #=> true
9
+ # Flipper.enable :some_feature, user
10
+ # Flipper.enabled? :some_feature, user #=> true
11
11
  #
12
12
  module Identifier
13
13
  def flipper_id
@@ -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) [ thing=... ]
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
- details = "thing=#{thing.inspect}"
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 " #{color(name, CYAN, true)} [ #{details} ]"
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 " #{color(name, CYAN, true)} [ #{details} ]"
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.attach_to InstrumentationNamespace
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
- if self.class.client
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.increment metric if 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
@@ -1,5 +1,11 @@
1
+ require_relative './version'
2
+
1
3
  module Flipper
2
4
  METADATA = {
3
- 'changelog_uri' => 'https://github.com/jnunemaker/flipper/blob/master/Changelog.md',
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(env)
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(env)
61
- reset_on_body_close = false
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/jnunemaker/flipper/pull/523"
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
- flipper.memoize = true
72
+ begin
73
+ flipper.memoize = true
71
74
 
72
- case @opts[:preload]
73
- when true then flipper.preload_all
74
- when Array then flipper.preload(@opts[:preload])
75
- end
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
- response = @app.call(env)
78
- response[2] = Rack::BodyProxy.new(response[2]) do
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