flipper 0.26.0 → 1.3.6

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