flipper 0.26.0 → 1.1.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 (199) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +19 -13
  4. data/.github/workflows/examples.yml +32 -15
  5. data/Changelog.md +294 -154
  6. data/Gemfile +15 -10
  7. data/README.md +13 -11
  8. data/benchmark/enabled_ips.rb +10 -0
  9. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  10. data/benchmark/enabled_profile.rb +20 -0
  11. data/benchmark/instrumentation_ips.rb +21 -0
  12. data/benchmark/typecast_ips.rb +27 -0
  13. data/docs/images/flipper_cloud.png +0 -0
  14. data/examples/api/basic.ru +3 -4
  15. data/examples/api/custom_memoized.ru +3 -4
  16. data/examples/api/memoized.ru +3 -4
  17. data/examples/cloud/app.ru +12 -0
  18. data/examples/cloud/backoff_policy.rb +13 -0
  19. data/examples/cloud/basic.rb +22 -0
  20. data/examples/cloud/cloud_setup.rb +20 -0
  21. data/examples/cloud/forked.rb +36 -0
  22. data/examples/cloud/import.rb +17 -0
  23. data/examples/cloud/threaded.rb +33 -0
  24. data/examples/dsl.rb +1 -15
  25. data/examples/enabled_for_actor.rb +4 -2
  26. data/examples/expressions.rb +213 -0
  27. data/examples/mirroring.rb +59 -0
  28. data/examples/strict.rb +18 -0
  29. data/flipper-cloud.gemspec +19 -0
  30. data/flipper.gemspec +3 -5
  31. data/lib/flipper/actor.rb +6 -3
  32. data/lib/flipper/adapter.rb +33 -7
  33. data/lib/flipper/adapter_builder.rb +44 -0
  34. data/lib/flipper/adapters/dual_write.rb +1 -3
  35. data/lib/flipper/adapters/failover.rb +0 -4
  36. data/lib/flipper/adapters/failsafe.rb +0 -4
  37. data/lib/flipper/adapters/http/client.rb +26 -7
  38. data/lib/flipper/adapters/http/error.rb +1 -1
  39. data/lib/flipper/adapters/http.rb +29 -16
  40. data/lib/flipper/adapters/instrumented.rb +25 -6
  41. data/lib/flipper/adapters/memoizable.rb +33 -21
  42. data/lib/flipper/adapters/memory.rb +81 -46
  43. data/lib/flipper/adapters/operation_logger.rb +16 -7
  44. data/lib/flipper/adapters/poll/poller.rb +2 -125
  45. data/lib/flipper/adapters/poll.rb +5 -3
  46. data/lib/flipper/adapters/pstore.rb +17 -11
  47. data/lib/flipper/adapters/read_only.rb +4 -4
  48. data/lib/flipper/adapters/strict.rb +47 -0
  49. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  50. data/lib/flipper/adapters/sync.rb +0 -4
  51. data/lib/flipper/cloud/configuration.rb +258 -0
  52. data/lib/flipper/cloud/dsl.rb +27 -0
  53. data/lib/flipper/cloud/message_verifier.rb +95 -0
  54. data/lib/flipper/cloud/middleware.rb +63 -0
  55. data/lib/flipper/cloud/routes.rb +14 -0
  56. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  57. data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
  58. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  59. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  60. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  61. data/lib/flipper/cloud/telemetry.rb +183 -0
  62. data/lib/flipper/cloud.rb +53 -0
  63. data/lib/flipper/configuration.rb +25 -4
  64. data/lib/flipper/dsl.rb +46 -45
  65. data/lib/flipper/engine.rb +88 -0
  66. data/lib/flipper/errors.rb +3 -3
  67. data/lib/flipper/export.rb +26 -0
  68. data/lib/flipper/exporter.rb +17 -0
  69. data/lib/flipper/exporters/json/export.rb +32 -0
  70. data/lib/flipper/exporters/json/v1.rb +33 -0
  71. data/lib/flipper/expression/builder.rb +73 -0
  72. data/lib/flipper/expression/constant.rb +25 -0
  73. data/lib/flipper/expression.rb +71 -0
  74. data/lib/flipper/expressions/all.rb +11 -0
  75. data/lib/flipper/expressions/any.rb +9 -0
  76. data/lib/flipper/expressions/boolean.rb +9 -0
  77. data/lib/flipper/expressions/comparable.rb +13 -0
  78. data/lib/flipper/expressions/duration.rb +28 -0
  79. data/lib/flipper/expressions/equal.rb +9 -0
  80. data/lib/flipper/expressions/greater_than.rb +9 -0
  81. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  82. data/lib/flipper/expressions/less_than.rb +9 -0
  83. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  84. data/lib/flipper/expressions/not_equal.rb +9 -0
  85. data/lib/flipper/expressions/now.rb +9 -0
  86. data/lib/flipper/expressions/number.rb +9 -0
  87. data/lib/flipper/expressions/percentage.rb +9 -0
  88. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  89. data/lib/flipper/expressions/property.rb +9 -0
  90. data/lib/flipper/expressions/random.rb +9 -0
  91. data/lib/flipper/expressions/string.rb +9 -0
  92. data/lib/flipper/expressions/time.rb +9 -0
  93. data/lib/flipper/feature.rb +87 -26
  94. data/lib/flipper/feature_check_context.rb +10 -6
  95. data/lib/flipper/gate.rb +13 -11
  96. data/lib/flipper/gate_values.rb +5 -18
  97. data/lib/flipper/gates/actor.rb +10 -17
  98. data/lib/flipper/gates/boolean.rb +1 -1
  99. data/lib/flipper/gates/expression.rb +75 -0
  100. data/lib/flipper/gates/group.rb +5 -7
  101. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  102. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  103. data/lib/flipper/identifier.rb +2 -2
  104. data/lib/flipper/instrumentation/log_subscriber.rb +24 -5
  105. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  106. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  107. data/lib/flipper/metadata.rb +5 -1
  108. data/lib/flipper/middleware/memoizer.rb +30 -14
  109. data/lib/flipper/poller.rb +117 -0
  110. data/lib/flipper/serializers/gzip.rb +24 -0
  111. data/lib/flipper/serializers/json.rb +19 -0
  112. data/lib/flipper/spec/shared_adapter_specs.rb +95 -54
  113. data/lib/flipper/test/shared_adapter_test.rb +91 -48
  114. data/lib/flipper/typecast.rb +56 -15
  115. data/lib/flipper/types/actor.rb +13 -13
  116. data/lib/flipper/types/group.rb +4 -4
  117. data/lib/flipper/types/percentage.rb +1 -1
  118. data/lib/flipper/version.rb +1 -1
  119. data/lib/flipper.rb +47 -10
  120. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  121. data/spec/flipper/adapter_builder_spec.rb +73 -0
  122. data/spec/flipper/adapter_spec.rb +30 -2
  123. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  124. data/spec/flipper/adapters/http_spec.rb +64 -8
  125. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  126. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  127. data/spec/flipper/adapters/memory_spec.rb +14 -3
  128. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  129. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  130. data/spec/flipper/adapters/strict_spec.rb +62 -0
  131. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  132. data/spec/flipper/cloud/configuration_spec.rb +252 -0
  133. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  134. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  135. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  136. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  137. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  138. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  139. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  140. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  141. data/spec/flipper/cloud_spec.rb +180 -0
  142. data/spec/flipper/configuration_spec.rb +17 -0
  143. data/spec/flipper/dsl_spec.rb +54 -73
  144. data/spec/flipper/engine_spec.rb +291 -0
  145. data/spec/flipper/export_spec.rb +13 -0
  146. data/spec/flipper/exporter_spec.rb +16 -0
  147. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  148. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  149. data/spec/flipper/expression/builder_spec.rb +248 -0
  150. data/spec/flipper/expression_spec.rb +188 -0
  151. data/spec/flipper/expressions/all_spec.rb +15 -0
  152. data/spec/flipper/expressions/any_spec.rb +15 -0
  153. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  154. data/spec/flipper/expressions/duration_spec.rb +43 -0
  155. data/spec/flipper/expressions/equal_spec.rb +24 -0
  156. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  157. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  158. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  159. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  160. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  161. data/spec/flipper/expressions/now_spec.rb +11 -0
  162. data/spec/flipper/expressions/number_spec.rb +21 -0
  163. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  164. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  165. data/spec/flipper/expressions/property_spec.rb +13 -0
  166. data/spec/flipper/expressions/random_spec.rb +9 -0
  167. data/spec/flipper/expressions/string_spec.rb +11 -0
  168. data/spec/flipper/expressions/time_spec.rb +13 -0
  169. data/spec/flipper/feature_check_context_spec.rb +17 -17
  170. data/spec/flipper/feature_spec.rb +436 -33
  171. data/spec/flipper/gate_values_spec.rb +2 -33
  172. data/spec/flipper/gates/boolean_spec.rb +1 -1
  173. data/spec/flipper/gates/expression_spec.rb +108 -0
  174. data/spec/flipper/gates/group_spec.rb +2 -3
  175. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  176. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  177. data/spec/flipper/identifier_spec.rb +4 -5
  178. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -5
  179. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
  180. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  181. data/spec/flipper/poller_spec.rb +47 -0
  182. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  183. data/spec/flipper/serializers/json_spec.rb +13 -0
  184. data/spec/flipper/typecast_spec.rb +121 -6
  185. data/spec/flipper/types/actor_spec.rb +63 -46
  186. data/spec/flipper/types/group_spec.rb +2 -2
  187. data/spec/flipper_integration_spec.rb +168 -58
  188. data/spec/flipper_spec.rb +92 -28
  189. data/spec/spec_helper.rb +6 -13
  190. data/spec/support/actor_names.yml +1 -0
  191. data/spec/support/climate_control.rb +7 -0
  192. data/spec/support/fake_backoff_policy.rb +15 -0
  193. data/spec/support/skippable.rb +18 -0
  194. data/spec/support/spec_helpers.rb +11 -3
  195. metadata +166 -13
  196. data/.github/workflows/release.yml +0 -44
  197. data/.tool-versions +0 -1
  198. data/lib/flipper/railtie.rb +0 -47
  199. data/spec/flipper/railtie_spec.rb +0 -109
@@ -10,7 +10,7 @@ module Flipper
10
10
  class Http
11
11
  include Flipper::Adapter
12
12
 
13
- attr_reader :name, :client
13
+ attr_reader :client
14
14
 
15
15
  def initialize(options = {})
16
16
  @client = Client.new(url: options.fetch(:url),
@@ -22,13 +22,12 @@ module Flipper
22
22
  write_timeout: options[:write_timeout],
23
23
  max_retries: options[:max_retries],
24
24
  debug_output: options[:debug_output])
25
- @name = :http
26
25
  end
27
26
 
28
27
  def get(feature)
29
28
  response = @client.get("/features/#{feature.key}")
30
29
  if response.is_a?(Net::HTTPOK)
31
- parsed_response = JSON.parse(response.body)
30
+ parsed_response = Typecast.from_json(response.body)
32
31
  result_for_feature(feature, parsed_response.fetch('gates'))
33
32
  elsif response.is_a?(Net::HTTPNotFound)
34
33
  default_config
@@ -39,10 +38,10 @@ module Flipper
39
38
 
40
39
  def get_multi(features)
41
40
  csv_keys = features.map(&:key).join(',')
42
- response = @client.get("/features?keys=#{csv_keys}")
41
+ response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
43
42
  raise Error, response unless response.is_a?(Net::HTTPOK)
44
43
 
45
- parsed_response = JSON.parse(response.body)
44
+ parsed_response = Typecast.from_json(response.body)
46
45
  parsed_features = parsed_response.fetch('features')
47
46
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
48
47
  hash[parsed_feature['key']] = parsed_feature['gates']
@@ -57,10 +56,10 @@ module Flipper
57
56
  end
58
57
 
59
58
  def get_all
60
- response = @client.get("/features")
59
+ response = @client.get("/features?exclude_gate_names=true")
61
60
  raise Error, response unless response.is_a?(Net::HTTPOK)
62
61
 
63
- parsed_response = JSON.parse(response.body)
62
+ parsed_response = Typecast.from_json(response.body)
64
63
  parsed_features = parsed_response.fetch('features')
65
64
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
66
65
  hash[parsed_feature['key']] = parsed_feature['gates']
@@ -68,7 +67,7 @@ module Flipper
68
67
  end
69
68
 
70
69
  result = {}
71
- gates_by_key.keys.each do |key|
70
+ gates_by_key.each_key do |key|
72
71
  feature = Feature.new(key, self)
73
72
  result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
74
73
  end
@@ -76,10 +75,10 @@ module Flipper
76
75
  end
77
76
 
78
77
  def features
79
- response = @client.get('/features')
78
+ response = @client.get('/features?exclude_gate_names=true')
80
79
  raise Error, response unless response.is_a?(Net::HTTPOK)
81
80
 
82
- parsed_response = JSON.parse(response.body)
81
+ parsed_response = Typecast.from_json(response.body)
83
82
  parsed_response['features'].map { |feature| feature['key'] }.to_set
84
83
  end
85
84
 
@@ -97,7 +96,7 @@ module Flipper
97
96
  end
98
97
 
99
98
  def enable(feature, gate, thing)
100
- body = request_body_for_gate(gate, thing.value.to_s)
99
+ body = request_body_for_gate(gate, thing.value)
101
100
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
102
101
  response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
103
102
  raise Error, response unless response.is_a?(Net::HTTPOK)
@@ -105,7 +104,7 @@ module Flipper
105
104
  end
106
105
 
107
106
  def disable(feature, gate, thing)
108
- body = request_body_for_gate(gate, thing.value.to_s)
107
+ body = request_body_for_gate(gate, thing.value)
109
108
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
110
109
  response = case gate.key
111
110
  when :percentage_of_actors, :percentage_of_time
@@ -123,6 +122,14 @@ module Flipper
123
122
  true
124
123
  end
125
124
 
125
+ def import(source)
126
+ adapter = self.class.from(source)
127
+ export = adapter.export(format: :json, version: 1)
128
+ response = @client.post("/import", export.contents)
129
+ raise Error, response unless response.is_a?(Net::HTTPNoContent)
130
+ true
131
+ end
132
+
126
133
  private
127
134
 
128
135
  def request_body_for_gate(gate, value)
@@ -130,11 +137,13 @@ module Flipper
130
137
  when :boolean
131
138
  {}
132
139
  when :groups
133
- { name: value }
140
+ { name: value.to_s }
134
141
  when :actors
135
- { flipper_id: value }
142
+ { flipper_id: value.to_s }
136
143
  when :percentage_of_actors, :percentage_of_time
137
- { percentage: value }
144
+ { percentage: value.to_s }
145
+ when :expression
146
+ value
138
147
  else
139
148
  raise "#{gate.key} is not a valid flipper gate key"
140
149
  end
@@ -158,13 +167,17 @@ module Flipper
158
167
  case gate.data_type
159
168
  when :boolean, :integer
160
169
  value ? value.to_s : value
170
+ when :json
171
+ value
161
172
  when :set
162
173
  value ? value.to_set : Set.new
163
174
  else
164
- unsupported_data_type(gate.data_type)
175
+ unsupported_data_type gate.data_type
165
176
  end
166
177
  end
167
178
 
179
+ private
180
+
168
181
  def unsupported_data_type(data_type)
169
182
  raise "#{data_type} is not supported by this adapter"
170
183
  end
@@ -4,7 +4,7 @@ module Flipper
4
4
  module Adapters
5
5
  # Internal: Adapter that wraps another adapter and instruments all adapter
6
6
  # operations.
7
- class Instrumented < SimpleDelegator
7
+ class Instrumented
8
8
  include ::Flipper::Adapter
9
9
 
10
10
  # Private: The name of instrumentation events.
@@ -13,9 +13,6 @@ module Flipper
13
13
  # Private: What is used to instrument all the things.
14
14
  attr_reader :instrumenter
15
15
 
16
- # Public: The name of the adapter.
17
- attr_reader :name
18
-
19
16
  # Internal: Initializes a new adapter instance.
20
17
  #
21
18
  # adapter - Vanilla adapter instance to wrap.
@@ -24,9 +21,7 @@ module Flipper
24
21
  # :instrumenter - What to use to instrument all the things.
25
22
  #
26
23
  def initialize(adapter, options = {})
27
- super(adapter)
28
24
  @adapter = adapter
29
- @name = :instrumented
30
25
  @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
31
26
  end
32
27
 
@@ -146,6 +141,30 @@ module Flipper
146
141
  payload[:result] = @adapter.disable(feature, gate, thing)
147
142
  end
148
143
  end
144
+
145
+ def import(source)
146
+ default_payload = {
147
+ operation: :import,
148
+ adapter_name: @adapter.name,
149
+ }
150
+
151
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
152
+ payload[:result] = @adapter.import(source)
153
+ end
154
+ end
155
+
156
+ def export(format: :json, version: 1)
157
+ default_payload = {
158
+ operation: :export,
159
+ adapter_name: @adapter.name,
160
+ format: format,
161
+ version: version,
162
+ }
163
+
164
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
165
+ payload[:result] = @adapter.export(format: format, version: version)
166
+ end
167
+ end
149
168
  end
150
169
  end
151
170
  end
@@ -5,39 +5,28 @@ module Flipper
5
5
  # Internal: Adapter that wraps another adapter with the ability to memoize
6
6
  # adapter calls in memory. Used by flipper dsl and the memoizer middleware
7
7
  # to make it possible to memoize adapter calls for the duration of a request.
8
- class Memoizable < SimpleDelegator
8
+ class Memoizable
9
9
  include ::Flipper::Adapter
10
10
 
11
- FeaturesKey = :flipper_features
12
- GetAllKey = :all_memoized
13
-
14
11
  # Internal
15
12
  attr_reader :cache
16
13
 
17
- # Public: The name of the adapter.
18
- attr_reader :name
19
-
20
14
  # Internal: The adapter this adapter is wrapping.
21
15
  attr_reader :adapter
22
16
 
23
- # Private
24
- def self.key_for(key)
25
- "feature/#{key}"
26
- end
27
-
28
17
  # Public
29
18
  def initialize(adapter, cache = nil)
30
- super(adapter)
31
19
  @adapter = adapter
32
- @name = :memoizable
33
20
  @cache = cache || {}
34
21
  @memoize = false
22
+ @features_key = :flipper_features
23
+ @get_all_key = :all_memoized
35
24
  end
36
25
 
37
26
  # Public
38
27
  def features
39
28
  if memoizing?
40
- cache.fetch(FeaturesKey) { cache[FeaturesKey] = @adapter.features }
29
+ cache.fetch(@features_key) { cache[@features_key] = @adapter.features }
41
30
  else
42
31
  @adapter.features
43
32
  end
@@ -95,9 +84,9 @@ module Flipper
95
84
  def get_all
96
85
  if memoizing?
97
86
  response = nil
98
- if cache[GetAllKey]
87
+ if cache[@get_all_key]
99
88
  response = {}
100
- cache[FeaturesKey].each do |key|
89
+ cache[@features_key].each do |key|
101
90
  response[key] = cache[key_for(key)]
102
91
  end
103
92
  else
@@ -105,8 +94,8 @@ module Flipper
105
94
  response.each do |key, value|
106
95
  cache[key_for(key)] = value
107
96
  end
108
- cache[FeaturesKey] = response.keys.to_set
109
- cache[GetAllKey] = true
97
+ cache[@features_key] = response.keys.to_set
98
+ cache[@get_all_key] = true
110
99
  end
111
100
 
112
101
  # Ensures that looking up other features that do not exist doesn't
@@ -128,6 +117,19 @@ module Flipper
128
117
  @adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
129
118
  end
130
119
 
120
+ # Public
121
+ def read_only?
122
+ @adapter.read_only?
123
+ end
124
+
125
+ def import(source)
126
+ @adapter.import(source).tap { cache.clear if memoizing? }
127
+ end
128
+
129
+ def export(format: :json, version: 1)
130
+ @adapter.export(format: format, version: version)
131
+ end
132
+
131
133
  # Internal: Turns local caching on/off.
132
134
  #
133
135
  # value - The Boolean that decides if local caching is on.
@@ -141,10 +143,20 @@ module Flipper
141
143
  !!@memoize
142
144
  end
143
145
 
146
+ if RUBY_VERSION >= '3.0'
147
+ def method_missing(name, *args, **kwargs, &block)
148
+ @adapter.send name, *args, **kwargs, &block
149
+ end
150
+ else
151
+ def method_missing(name, *args, &block)
152
+ @adapter.send name, *args, &block
153
+ end
154
+ end
155
+
144
156
  private
145
157
 
146
158
  def key_for(key)
147
- self.class.key_for(key)
159
+ "feature/#{key}"
148
160
  end
149
161
 
150
162
  def expire_feature(feature)
@@ -152,7 +164,7 @@ module Flipper
152
164
  end
153
165
 
154
166
  def expire_features_set
155
- cache.delete(FeaturesKey) if memoizing?
167
+ cache.delete(@features_key) if memoizing?
156
168
  end
157
169
  end
158
170
  end
@@ -1,4 +1,5 @@
1
- require 'set'
1
+ require "flipper/adapter"
2
+ require "flipper/typecast"
2
3
 
3
4
  module Flipper
4
5
  module Adapters
@@ -7,93 +8,99 @@ module Flipper
7
8
  class Memory
8
9
  include ::Flipper::Adapter
9
10
 
10
- FeaturesKey = :features
11
-
12
- # Public: The name of the adapter.
13
- attr_reader :name
14
-
15
11
  # Public
16
- def initialize(source = nil)
17
- @source = source || {}
18
- @name = :memory
12
+ def initialize(source = nil, threadsafe: true)
13
+ @source = Typecast.features_hash(source)
14
+ @lock = Mutex.new if threadsafe
15
+ reset
19
16
  end
20
17
 
21
18
  # Public: The set of known features.
22
19
  def features
23
- @source.keys.to_set
20
+ synchronize { @source.keys }.to_set
24
21
  end
25
22
 
26
23
  # Public: Adds a feature to the set of known features.
27
24
  def add(feature)
28
- @source[feature.key] ||= default_config
25
+ synchronize { @source[feature.key] ||= default_config }
29
26
  true
30
27
  end
31
28
 
32
29
  # Public: Removes a feature from the set of known features and clears
33
30
  # all the values for the feature.
34
31
  def remove(feature)
35
- @source.delete(feature.key)
32
+ synchronize { @source.delete(feature.key) }
36
33
  true
37
34
  end
38
35
 
39
36
  # Public: Clears all the gate values for a feature.
40
37
  def clear(feature)
41
- @source[feature.key] = default_config
38
+ synchronize { @source[feature.key] = default_config }
42
39
  true
43
40
  end
44
41
 
45
42
  # Public
46
43
  def get(feature)
47
- @source[feature.key] || default_config
44
+ synchronize { @source[feature.key] } || default_config
48
45
  end
49
46
 
50
47
  def get_multi(features)
51
- result = {}
52
- features.each do |feature|
53
- result[feature.key] = @source[feature.key] || default_config
48
+ synchronize do
49
+ result = {}
50
+ features.each do |feature|
51
+ result[feature.key] = @source[feature.key] || default_config
52
+ end
53
+ result
54
54
  end
55
- result
56
55
  end
57
56
 
58
57
  def get_all
59
- @source
58
+ synchronize { Typecast.features_hash(@source) }
60
59
  end
61
60
 
62
61
  # Public
63
62
  def enable(feature, gate, thing)
64
- @source[feature.key] ||= default_config
65
-
66
- case gate.data_type
67
- when :boolean
68
- clear(feature)
69
- @source[feature.key][gate.key] = thing.value.to_s
70
- when :integer
71
- @source[feature.key][gate.key] = thing.value.to_s
72
- when :set
73
- @source[feature.key][gate.key] << thing.value.to_s
74
- else
75
- raise "#{gate} is not supported by this adapter yet"
63
+ synchronize do
64
+ @source[feature.key] ||= default_config
65
+
66
+ case gate.data_type
67
+ when :boolean
68
+ @source[feature.key] = default_config
69
+ @source[feature.key][gate.key] = thing.value.to_s
70
+ when :integer
71
+ @source[feature.key][gate.key] = thing.value.to_s
72
+ when :set
73
+ @source[feature.key][gate.key] << thing.value.to_s
74
+ when :json
75
+ @source[feature.key][gate.key] = thing.value
76
+ else
77
+ raise "#{gate} is not supported by this adapter yet"
78
+ end
79
+
80
+ true
76
81
  end
77
-
78
- true
79
82
  end
80
83
 
81
84
  # Public
82
85
  def disable(feature, gate, thing)
83
- @source[feature.key] ||= default_config
84
-
85
- case gate.data_type
86
- when :boolean
87
- clear(feature)
88
- when :integer
89
- @source[feature.key][gate.key] = thing.value.to_s
90
- when :set
91
- @source[feature.key][gate.key].delete thing.value.to_s
92
- else
93
- raise "#{gate} is not supported by this adapter yet"
86
+ synchronize do
87
+ @source[feature.key] ||= default_config
88
+
89
+ case gate.data_type
90
+ when :boolean
91
+ @source[feature.key] = default_config
92
+ when :integer
93
+ @source[feature.key][gate.key] = thing.value.to_s
94
+ when :set
95
+ @source[feature.key][gate.key].delete thing.value.to_s
96
+ when :json
97
+ @source[feature.key].delete(gate.key)
98
+ else
99
+ raise "#{gate} is not supported by this adapter yet"
100
+ end
101
+
102
+ true
94
103
  end
95
-
96
- true
97
104
  end
98
105
 
99
106
  # Public
@@ -104,6 +111,34 @@ module Flipper
104
111
  ]
105
112
  "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
106
113
  end
114
+
115
+ # Public: a more efficient implementation of import for this adapter
116
+ def import(source)
117
+ adapter = self.class.from(source)
118
+ get_all = Typecast.features_hash(adapter.get_all)
119
+ synchronize { @source.replace(get_all) }
120
+ true
121
+ end
122
+
123
+ private
124
+
125
+ def reset
126
+ @pid = Process.pid
127
+ @lock&.unlock if @lock&.locked?
128
+ end
129
+
130
+ def forked?
131
+ @pid != Process.pid
132
+ end
133
+
134
+ def synchronize(&block)
135
+ if @lock
136
+ reset if forked?
137
+ @lock.synchronize(&block)
138
+ else
139
+ block.call
140
+ end
141
+ end
107
142
  end
108
143
  end
109
144
  end
@@ -5,8 +5,8 @@ module Flipper
5
5
  # Public: Adapter that wraps another adapter and stores the operations.
6
6
  #
7
7
  # Useful in tests to verify calls and such. Never use outside of testing.
8
- class OperationLogger < SimpleDelegator
9
- include ::Flipper::Adapter
8
+ class OperationLogger
9
+ include Flipper::Adapter
10
10
 
11
11
  class Operation
12
12
  attr_reader :type, :args
@@ -18,6 +18,8 @@ module Flipper
18
18
  end
19
19
 
20
20
  OperationTypes = [
21
+ :import,
22
+ :export,
21
23
  :features,
22
24
  :add,
23
25
  :remove,
@@ -32,14 +34,9 @@ module Flipper
32
34
  # Internal: An array of the operations that have happened.
33
35
  attr_reader :operations
34
36
 
35
- # Internal: The name of the adapter.
36
- attr_reader :name
37
-
38
37
  # Public
39
38
  def initialize(adapter, operations = nil)
40
- super(adapter)
41
39
  @adapter = adapter
42
- @name = :operation_logger
43
40
  @operations = operations || []
44
41
  end
45
42
 
@@ -98,6 +95,18 @@ module Flipper
98
95
  @adapter.disable(feature, gate, thing)
99
96
  end
100
97
 
98
+ # Public
99
+ def import(source)
100
+ @operations << Operation.new(:import, [source])
101
+ @adapter.import(source)
102
+ end
103
+
104
+ # Public
105
+ def export(format: :json, version: 1)
106
+ @operations << Operation.new(:export, [format, version])
107
+ @adapter.export(format: format, version: version)
108
+ end
109
+
101
110
  # Public: Count the number of times a certain operation happened.
102
111
  def count(type)
103
112
  type(type).size
@@ -1,125 +1,2 @@
1
- require 'logger'
2
- require 'concurrent/atomic/read_write_lock'
3
- require 'concurrent/utility/monotonic_time'
4
- require 'concurrent/map'
5
-
6
- module Flipper
7
- module Adapters
8
- class Poll
9
- class Poller
10
- attr_reader :thread, :pid, :mutex, :interval, :last_synced_at
11
-
12
- def self.instances
13
- @instances ||= Concurrent::Map.new
14
- end
15
- private_class_method :instances
16
-
17
- def self.get(key, options = {})
18
- instances.compute_if_absent(key) { new(options) }
19
- end
20
-
21
- def self.reset
22
- instances.each {|_,poller| poller.stop }.clear
23
- end
24
-
25
- def initialize(options = {})
26
- @thread = nil
27
- @pid = Process.pid
28
- @mutex = Mutex.new
29
- @adapter = Memory.new
30
- @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
31
- @remote_adapter = options.fetch(:remote_adapter)
32
- @interval = options.fetch(:interval, 10).to_f
33
- @lock = Concurrent::ReadWriteLock.new
34
- @last_synced_at = Concurrent::AtomicFixnum.new(0)
35
-
36
- if @interval < 1
37
- warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
38
- @interval = 1
39
- end
40
-
41
- @start_automatically = options.fetch(:start_automatically, true)
42
-
43
- if options.fetch(:shutdown_automatically, true)
44
- at_exit { stop }
45
- end
46
- end
47
-
48
- def adapter
49
- @lock.with_read_lock { Memory.new(@adapter.get_all.dup) }
50
- end
51
-
52
- def start
53
- reset if forked?
54
- ensure_worker_running
55
- end
56
-
57
- def stop
58
- @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
59
- operation: :stop,
60
- })
61
- @thread&.kill
62
- end
63
-
64
- def run
65
- loop do
66
- sleep jitter
67
- start = Concurrent.monotonic_time
68
- begin
69
- @instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
70
- adapter = Memory.new
71
- adapter.import(@remote_adapter)
72
-
73
- @lock.with_write_lock { @adapter.import(adapter) }
74
- @last_synced_at.update { |time| Concurrent.monotonic_time }
75
- end
76
- rescue => exception
77
- # you can instrument these using poller.flipper
78
- end
79
-
80
- sleep_interval = interval - (Concurrent.monotonic_time - start)
81
- sleep sleep_interval if sleep_interval.positive?
82
- end
83
- end
84
-
85
- private
86
-
87
- def jitter
88
- rand
89
- end
90
-
91
- def forked?
92
- pid != Process.pid
93
- end
94
-
95
- def ensure_worker_running
96
- # Return early if thread is alive and avoid the mutex lock and unlock.
97
- return if thread_alive?
98
-
99
- # If another thread is starting worker thread, then return early so this
100
- # thread can enqueue and move on with life.
101
- return unless mutex.try_lock
102
-
103
- begin
104
- return if thread_alive?
105
- @thread = Thread.new { run }
106
- @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
107
- operation: :thread_start,
108
- })
109
- ensure
110
- mutex.unlock
111
- end
112
- end
113
-
114
- def thread_alive?
115
- @thread && @thread.alive?
116
- end
117
-
118
- def reset
119
- @pid = Process.pid
120
- mutex.unlock if mutex.locked?
121
- end
122
- end
123
- end
124
- end
125
- end
1
+ warn "DEPRECATION WARNING: Flipper::Adapters::Poll::Poller is deprecated. Use Flipper::Poller instead."
2
+ require 'flipper/adapters/poll'