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.
Files changed (285) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +1 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/dependabot.yml +6 -0
  5. data/.github/workflows/ci.yml +110 -0
  6. data/.github/workflows/examples.yml +105 -0
  7. data/.github/workflows/release.yml +54 -0
  8. data/.rspec +1 -0
  9. data/CLAUDE.md +87 -0
  10. data/Changelog.md +2 -215
  11. data/Dockerfile +1 -1
  12. data/Gemfile +28 -20
  13. data/README.md +72 -62
  14. data/Rakefile +13 -3
  15. data/benchmark/enabled_ips.rb +10 -0
  16. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  17. data/benchmark/enabled_profile.rb +20 -0
  18. data/benchmark/instrumentation_ips.rb +21 -0
  19. data/benchmark/typecast_ips.rb +27 -0
  20. data/docker-compose.yml +37 -34
  21. data/docs/DockerCompose.md +0 -1
  22. data/docs/README.md +1 -0
  23. data/docs/images/banner.jpg +0 -0
  24. data/docs/images/flipper_cloud.png +0 -0
  25. data/examples/api/basic.ru +18 -0
  26. data/examples/api/custom_memoized.ru +36 -0
  27. data/examples/api/memoized.ru +42 -0
  28. data/examples/basic.rb +1 -12
  29. data/examples/cloud/app.ru +12 -0
  30. data/examples/cloud/backoff_policy.rb +13 -0
  31. data/examples/cloud/basic.rb +22 -0
  32. data/examples/cloud/cloud_setup.rb +20 -0
  33. data/examples/cloud/forked.rb +36 -0
  34. data/examples/cloud/import.rb +17 -0
  35. data/examples/cloud/poll_interval/README.md +111 -0
  36. data/examples/cloud/poll_interval/client.rb +108 -0
  37. data/examples/cloud/poll_interval/server.rb +98 -0
  38. data/examples/cloud/threaded.rb +33 -0
  39. data/examples/configuring_default.rb +2 -5
  40. data/examples/dsl.rb +10 -35
  41. data/examples/enabled_for_actor.rb +10 -15
  42. data/examples/expressions.rb +237 -0
  43. data/examples/group.rb +3 -6
  44. data/examples/group_dynamic_lookup.rb +5 -19
  45. data/examples/group_with_members.rb +4 -14
  46. data/examples/importing.rb +1 -1
  47. data/examples/individual_actor.rb +2 -5
  48. data/examples/instrumentation.rb +2 -2
  49. data/examples/instrumentation_last_accessed_at.rb +38 -0
  50. data/examples/memoizing.rb +35 -0
  51. data/examples/mirroring.rb +59 -0
  52. data/examples/percentage_of_actors.rb +6 -16
  53. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  54. data/examples/percentage_of_actors_group.rb +5 -18
  55. data/examples/percentage_of_time.rb +3 -6
  56. data/examples/strict.rb +18 -0
  57. data/exe/flipper +5 -0
  58. data/flipper-cloud.gemspec +19 -0
  59. data/flipper.gemspec +10 -7
  60. data/lib/flipper/actor.rb +10 -3
  61. data/lib/flipper/adapter.rb +50 -8
  62. data/lib/flipper/adapter_builder.rb +44 -0
  63. data/lib/flipper/adapters/actor_limit.rb +54 -0
  64. data/lib/flipper/adapters/cache_base.rb +161 -0
  65. data/lib/flipper/adapters/dual_write.rb +63 -0
  66. data/lib/flipper/adapters/failover.rb +85 -0
  67. data/lib/flipper/adapters/failsafe.rb +72 -0
  68. data/lib/flipper/adapters/http/client.rb +64 -7
  69. data/lib/flipper/adapters/http/error.rb +19 -1
  70. data/lib/flipper/adapters/http.rb +97 -43
  71. data/lib/flipper/adapters/instrumented.rb +47 -26
  72. data/lib/flipper/adapters/memoizable.rb +44 -40
  73. data/lib/flipper/adapters/memory.rb +75 -111
  74. data/lib/flipper/adapters/operation_logger.rb +22 -78
  75. data/lib/flipper/adapters/poll/poller.rb +2 -0
  76. data/lib/flipper/adapters/poll.rb +52 -0
  77. data/lib/flipper/adapters/pstore.rb +27 -17
  78. data/lib/flipper/adapters/read_only.rb +8 -41
  79. data/lib/flipper/adapters/strict.rb +45 -0
  80. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  81. data/lib/flipper/adapters/sync/interval_synchronizer.rb +2 -7
  82. data/lib/flipper/adapters/sync/synchronizer.rb +13 -6
  83. data/lib/flipper/adapters/sync.rb +23 -29
  84. data/lib/flipper/adapters/wrapper.rb +54 -0
  85. data/lib/flipper/cli.rb +314 -0
  86. data/lib/flipper/cloud/configuration.rb +271 -0
  87. data/lib/flipper/cloud/dsl.rb +27 -0
  88. data/lib/flipper/cloud/message_verifier.rb +95 -0
  89. data/lib/flipper/cloud/middleware.rb +63 -0
  90. data/lib/flipper/cloud/migrate.rb +71 -0
  91. data/lib/flipper/cloud/routes.rb +14 -0
  92. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  93. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  94. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  95. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  96. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  97. data/lib/flipper/cloud/telemetry.rb +191 -0
  98. data/lib/flipper/cloud.rb +54 -0
  99. data/lib/flipper/configuration.rb +54 -7
  100. data/lib/flipper/dsl.rb +58 -47
  101. data/lib/flipper/engine.rb +102 -0
  102. data/lib/flipper/errors.rb +3 -21
  103. data/lib/flipper/export.rb +24 -0
  104. data/lib/flipper/exporter.rb +17 -0
  105. data/lib/flipper/exporters/json/export.rb +32 -0
  106. data/lib/flipper/exporters/json/v1.rb +33 -0
  107. data/lib/flipper/expression/builder.rb +73 -0
  108. data/lib/flipper/expression/constant.rb +25 -0
  109. data/lib/flipper/expression.rb +71 -0
  110. data/lib/flipper/expressions/all.rb +9 -0
  111. data/lib/flipper/expressions/any.rb +9 -0
  112. data/lib/flipper/expressions/boolean.rb +9 -0
  113. data/lib/flipper/expressions/comparable.rb +13 -0
  114. data/lib/flipper/expressions/equal.rb +9 -0
  115. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  116. data/lib/flipper/expressions/greater_than.rb +9 -0
  117. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  118. data/lib/flipper/expressions/less_than.rb +9 -0
  119. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  120. data/lib/flipper/expressions/not_equal.rb +9 -0
  121. data/lib/flipper/expressions/now.rb +9 -0
  122. data/lib/flipper/expressions/number.rb +9 -0
  123. data/lib/flipper/expressions/percentage.rb +9 -0
  124. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  125. data/lib/flipper/expressions/property.rb +9 -0
  126. data/lib/flipper/expressions/random.rb +9 -0
  127. data/lib/flipper/expressions/string.rb +9 -0
  128. data/lib/flipper/expressions/time.rb +16 -0
  129. data/lib/flipper/feature.rb +95 -28
  130. data/lib/flipper/feature_check_context.rb +10 -6
  131. data/lib/flipper/gate.rb +13 -11
  132. data/lib/flipper/gate_values.rb +5 -18
  133. data/lib/flipper/gates/actor.rb +10 -17
  134. data/lib/flipper/gates/boolean.rb +1 -1
  135. data/lib/flipper/gates/expression.rb +75 -0
  136. data/lib/flipper/gates/group.rb +5 -7
  137. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  138. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  139. data/lib/flipper/identifier.rb +17 -0
  140. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  141. data/lib/flipper/instrumentation/statsd.rb +4 -2
  142. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  143. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  144. data/lib/flipper/instrumenters/memory.rb +6 -2
  145. data/lib/flipper/metadata.rb +8 -1
  146. data/lib/flipper/middleware/memoizer.rb +46 -27
  147. data/lib/flipper/middleware/setup_env.rb +13 -3
  148. data/lib/flipper/model/active_record.rb +23 -0
  149. data/lib/flipper/poller.rb +157 -0
  150. data/lib/flipper/serializers/gzip.rb +22 -0
  151. data/lib/flipper/serializers/json.rb +17 -0
  152. data/lib/flipper/spec/shared_adapter_specs.rb +122 -56
  153. data/lib/flipper/test/shared_adapter_test.rb +120 -52
  154. data/lib/flipper/test_help.rb +43 -0
  155. data/lib/flipper/typecast.rb +59 -18
  156. data/lib/flipper/types/actor.rb +19 -13
  157. data/lib/flipper/types/group.rb +12 -5
  158. data/lib/flipper/types/percentage.rb +1 -1
  159. data/lib/flipper/version.rb +11 -1
  160. data/lib/flipper.rb +71 -12
  161. data/lib/generators/flipper/setup_generator.rb +68 -0
  162. data/lib/generators/flipper/templates/initializer.rb +45 -0
  163. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  164. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  165. data/lib/generators/flipper/update_generator.rb +35 -0
  166. data/package-lock.json +41 -0
  167. data/package.json +10 -0
  168. data/spec/fixtures/environment.rb +1 -0
  169. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  170. data/spec/flipper/actor_spec.rb +10 -2
  171. data/spec/flipper/adapter_builder_spec.rb +72 -0
  172. data/spec/flipper/adapter_spec.rb +52 -6
  173. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  174. data/spec/flipper/adapters/dual_write_spec.rb +82 -0
  175. data/spec/flipper/adapters/failover_spec.rb +141 -0
  176. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  177. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  178. data/spec/flipper/adapters/http_spec.rb +402 -65
  179. data/spec/flipper/adapters/instrumented_spec.rb +31 -13
  180. data/spec/flipper/adapters/memoizable_spec.rb +51 -33
  181. data/spec/flipper/adapters/memory_spec.rb +33 -5
  182. data/spec/flipper/adapters/operation_logger_spec.rb +38 -12
  183. data/spec/flipper/adapters/poll_spec.rb +41 -0
  184. data/spec/flipper/adapters/pstore_spec.rb +0 -2
  185. data/spec/flipper/adapters/read_only_spec.rb +32 -18
  186. data/spec/flipper/adapters/strict_spec.rb +64 -0
  187. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -1
  188. data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
  189. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -1
  190. data/spec/flipper/adapters/sync_spec.rb +17 -6
  191. data/spec/flipper/cli_spec.rb +217 -0
  192. data/spec/flipper/cloud/configuration_spec.rb +257 -0
  193. data/spec/flipper/cloud/dsl_spec.rb +90 -0
  194. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  195. data/spec/flipper/cloud/middleware_spec.rb +307 -0
  196. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  197. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  198. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  199. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  200. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  201. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  202. data/spec/flipper/cloud_spec.rb +186 -0
  203. data/spec/flipper/configuration_spec.rb +37 -3
  204. data/spec/flipper/dsl_spec.rb +67 -80
  205. data/spec/flipper/engine_spec.rb +374 -0
  206. data/spec/flipper/export_spec.rb +13 -0
  207. data/spec/flipper/exporter_spec.rb +16 -0
  208. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  209. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  210. data/spec/flipper/expression/builder_spec.rb +248 -0
  211. data/spec/flipper/expression_spec.rb +188 -0
  212. data/spec/flipper/expressions/all_spec.rb +15 -0
  213. data/spec/flipper/expressions/any_spec.rb +15 -0
  214. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  215. data/spec/flipper/expressions/equal_spec.rb +24 -0
  216. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  217. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  218. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  219. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  220. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  221. data/spec/flipper/expressions/now_spec.rb +11 -0
  222. data/spec/flipper/expressions/number_spec.rb +21 -0
  223. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  224. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  225. data/spec/flipper/expressions/property_spec.rb +13 -0
  226. data/spec/flipper/expressions/random_spec.rb +9 -0
  227. data/spec/flipper/expressions/string_spec.rb +11 -0
  228. data/spec/flipper/expressions/time_spec.rb +29 -0
  229. data/spec/flipper/feature_check_context_spec.rb +18 -20
  230. data/spec/flipper/feature_spec.rb +461 -48
  231. data/spec/flipper/gate_spec.rb +0 -2
  232. data/spec/flipper/gate_values_spec.rb +2 -34
  233. data/spec/flipper/gates/actor_spec.rb +0 -2
  234. data/spec/flipper/gates/boolean_spec.rb +1 -3
  235. data/spec/flipper/gates/expression_spec.rb +190 -0
  236. data/spec/flipper/gates/group_spec.rb +2 -5
  237. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
  238. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
  239. data/spec/flipper/identifier_spec.rb +12 -0
  240. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -7
  241. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -3
  242. data/spec/flipper/instrumenters/memory_spec.rb +18 -1
  243. data/spec/flipper/instrumenters/noop_spec.rb +14 -8
  244. data/spec/flipper/middleware/memoizer_spec.rb +199 -62
  245. data/spec/flipper/middleware/setup_env_spec.rb +23 -5
  246. data/spec/flipper/model/active_record_spec.rb +72 -0
  247. data/spec/flipper/poller_spec.rb +390 -0
  248. data/spec/flipper/registry_spec.rb +0 -1
  249. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  250. data/spec/flipper/serializers/json_spec.rb +13 -0
  251. data/spec/flipper/typecast_spec.rb +121 -7
  252. data/spec/flipper/types/actor_spec.rb +63 -47
  253. data/spec/flipper/types/boolean_spec.rb +0 -1
  254. data/spec/flipper/types/group_spec.rb +24 -3
  255. data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
  256. data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
  257. data/spec/flipper/types/percentage_spec.rb +0 -1
  258. data/spec/{integration_spec.rb → flipper_integration_spec.rb} +301 -59
  259. data/spec/flipper_spec.rb +123 -29
  260. data/spec/{helper.rb → spec_helper.rb} +23 -21
  261. data/spec/support/actor_names.yml +1 -0
  262. data/spec/support/descriptions.yml +1 -0
  263. data/spec/support/fail_on_output.rb +8 -0
  264. data/spec/support/fake_backoff_policy.rb +15 -0
  265. data/spec/support/skippable.rb +18 -0
  266. data/spec/support/spec_helpers.rb +53 -6
  267. data/test/adapters/actor_limit_test.rb +20 -0
  268. data/test/test_helper.rb +2 -1
  269. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  270. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  271. data/test_rails/helper.rb +31 -0
  272. data/test_rails/system/test_help_test.rb +52 -0
  273. metadata +200 -82
  274. data/.rubocop.yml +0 -54
  275. data/.rubocop_todo.yml +0 -199
  276. data/docs/Adapters.md +0 -124
  277. data/docs/Caveats.md +0 -4
  278. data/docs/Gates.md +0 -167
  279. data/docs/Instrumentation.md +0 -27
  280. data/docs/Optimization.md +0 -114
  281. data/docs/api/README.md +0 -849
  282. data/docs/http/README.md +0 -35
  283. data/docs/read-only/README.md +0 -21
  284. data/examples/example_setup.rb +0 -8
  285. data/test/helper.rb +0 -11
@@ -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)
@@ -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) [ 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
@@ -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 " #{color(name, CYAN, true)} [ #{details} ]"
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.attach_to InstrumentationNamespace
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 /\.flipper$/,
6
- Flipper::Instrumentation::StatsdSubscriber
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
- 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
@@ -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 = (yield payload if block_given?)
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)
@@ -1,5 +1,12 @@
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}",
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
- # :preload_all - Boolean of whether or not to preload all features.
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, preload_all: true
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.' # rubocop:disable LineLength
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 skip_memoize?(request)
40
- @app.call(env)
44
+ if memoize?(request)
45
+ memoized_call(request)
41
46
  else
42
- memoized_call(env)
47
+ @app.call(env)
43
48
  end
44
49
  end
45
50
 
46
51
  private
47
52
 
48
- def skip_memoize?(request)
49
- @opts[:unless] && @opts[:unless].call(request)
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(env)
53
- reset_on_body_close = false
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
- if (preload = @opts[:preload])
61
- flipper.preload(preload)
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
- response = @app.call(env)
65
- response[2] = Rack::BodyProxy.new(response[2]) do
66
- flipper.memoize = original
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
- def initialize(app, flipper_or_block, options = {})
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