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
@@ -1,3 +1,4 @@
1
+ require 'json'
1
2
  require 'pstore'
2
3
  require 'set'
3
4
  require 'flipper'
@@ -9,22 +10,14 @@ module Flipper
9
10
  class PStore
10
11
  include ::Flipper::Adapter
11
12
 
12
- FeaturesKey = :flipper_features
13
-
14
- # Public: The name of the adapter.
15
- attr_reader :name
16
-
17
13
  # Public: The path to where the file is stored.
18
14
  attr_reader :path
19
15
 
20
- # Public: PStore's thread_safe option.
21
- attr_reader :thread_safe
22
-
23
16
  # Public
24
- def initialize(path = 'flipper.pstore', thread_safe = false)
17
+ def initialize(path = 'flipper.pstore', thread_safe = true)
25
18
  @path = path
26
19
  @store = ::PStore.new(path, thread_safe)
27
- @name = :pstore
20
+ @features_key = :flipper_features
28
21
  end
29
22
 
30
23
  # Public: The set of known features.
@@ -37,7 +30,7 @@ module Flipper
37
30
  # Public: Adds a feature to the set of known features.
38
31
  def add(feature)
39
32
  @store.transaction do
40
- set_add FeaturesKey, feature.key
33
+ set_add @features_key, feature.key
41
34
  end
42
35
  true
43
36
  end
@@ -46,7 +39,7 @@ module Flipper
46
39
  # all the values for the feature.
47
40
  def remove(feature)
48
41
  @store.transaction do
49
- set_delete FeaturesKey, feature.key
42
+ set_delete @features_key, feature.key
50
43
  clear_gates(feature)
51
44
  end
52
45
  true
@@ -73,7 +66,7 @@ module Flipper
73
66
  end
74
67
  end
75
68
 
76
- def get_all
69
+ def get_all(**kwargs)
77
70
  @store.transaction do
78
71
  features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) }
79
72
  read_many_features(features)
@@ -84,10 +77,15 @@ module Flipper
84
77
  def enable(feature, gate, thing)
85
78
  @store.transaction do
86
79
  case gate.data_type
87
- when :boolean, :integer
80
+ when :boolean
81
+ clear_gates(feature)
82
+ write key(feature, gate), thing.value.to_s
83
+ when :integer
88
84
  write key(feature, gate), thing.value.to_s
89
85
  when :set
90
86
  set_add key(feature, gate), thing.value.to_s
87
+ when :json
88
+ write key(feature, gate), Typecast.to_json(thing.value)
91
89
  else
92
90
  raise "#{gate} is not supported by this adapter yet"
93
91
  end
@@ -109,6 +107,10 @@ module Flipper
109
107
  @store.transaction do
110
108
  set_delete key(feature, gate), thing.value.to_s
111
109
  end
110
+ when :json
111
+ @store.transaction do
112
+ delete key(feature, gate)
113
+ end
112
114
  else
113
115
  raise "#{gate} is not supported by this adapter yet"
114
116
  end
@@ -135,7 +137,7 @@ module Flipper
135
137
  end
136
138
 
137
139
  def read_feature_keys
138
- set_members FeaturesKey
140
+ set_members @features_key
139
141
  end
140
142
 
141
143
  def read_many_features(features)
@@ -150,12 +152,16 @@ module Flipper
150
152
  result = {}
151
153
 
152
154
  feature.gates.each do |gate|
155
+ key = key(feature, gate)
153
156
  result[gate.key] =
154
157
  case gate.data_type
155
158
  when :boolean, :integer
156
- read key(feature, gate)
159
+ read key
157
160
  when :set
158
- set_members key(feature, gate)
161
+ set_members key
162
+ when :json
163
+ value = read(key)
164
+ Typecast.from_json(value)
159
165
  else
160
166
  raise "#{gate} is not supported by this adapter yet"
161
167
  end
@@ -213,3 +219,7 @@ module Flipper
213
219
  end
214
220
  end
215
221
  end
222
+
223
+ Flipper.configure do |config|
224
+ config.adapter { Flipper::Adapters::PStore.new }
225
+ end
@@ -3,8 +3,8 @@ require 'flipper'
3
3
  module Flipper
4
4
  module Adapters
5
5
  # Public: Adapter that wraps another adapter and raises for any writes.
6
- class ReadOnly
7
- include ::Flipper::Adapter
6
+ class ReadOnly < Wrapper
7
+ WRITE_METHODS = %i[add remove clear enable disable]
8
8
 
9
9
  class WriteAttempted < Error
10
10
  def initialize(message = nil)
@@ -12,49 +12,16 @@ module Flipper
12
12
  end
13
13
  end
14
14
 
15
- # Internal: The name of the adapter.
16
- attr_reader :name
17
-
18
- # Public
19
- def initialize(adapter)
20
- @adapter = adapter
21
- @name = :read_only
22
- end
23
-
24
- def features
25
- @adapter.features
26
- end
27
-
28
- def get(feature)
29
- @adapter.get(feature)
30
- end
31
-
32
- def get_multi(features)
33
- @adapter.get_multi(features)
15
+ def read_only?
16
+ true
34
17
  end
35
18
 
36
- def get_all
37
- @adapter.get_all
38
- end
19
+ private
39
20
 
40
- def add(_feature)
41
- raise WriteAttempted
42
- end
43
-
44
- def remove(_feature)
45
- raise WriteAttempted
46
- end
47
-
48
- def clear(_feature)
49
- raise WriteAttempted
50
- end
51
-
52
- def enable(_feature, _gate, _thing)
53
- raise WriteAttempted
54
- end
21
+ def wrap(method, *args, **kwargs)
22
+ raise WriteAttempted if WRITE_METHODS.include?(method)
55
23
 
56
- def disable(_feature, _gate, _thing)
57
- raise WriteAttempted
24
+ yield
58
25
  end
59
26
  end
60
27
  end
@@ -0,0 +1,45 @@
1
+ module Flipper
2
+ module Adapters
3
+ # An adapter that ensures a feature exists before checking it.
4
+ class Strict < Wrapper
5
+ attr_reader :handler
6
+
7
+ class NotFound < ::Flipper::Error
8
+ def initialize(name)
9
+ super "Could not find feature #{name.inspect}. Call `Flipper.add(#{name.inspect})` to create it."
10
+ end
11
+ end
12
+
13
+ def initialize(adapter, handler = nil, &block)
14
+ super(adapter)
15
+ @handler = block || handler
16
+ end
17
+
18
+ def get(feature)
19
+ assert_feature_exists(feature)
20
+ super
21
+ end
22
+
23
+ def get_multi(features)
24
+ features.each { |feature| assert_feature_exists(feature) }
25
+ super
26
+ end
27
+
28
+ private
29
+
30
+ def assert_feature_exists(feature)
31
+ return if @adapter.features.include?(feature.key)
32
+
33
+ case handler
34
+ when Proc then handler.call(feature)
35
+ when :warn then warn NotFound.new(feature.key).message
36
+ when :noop, false, nil
37
+ # noop
38
+ else # truthy or :raise
39
+ raise NotFound.new(feature.key)
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -9,6 +9,7 @@ module Flipper
9
9
  class FeatureSynchronizer
10
10
  extend Forwardable
11
11
 
12
+ def_delegator :@local_gate_values, :expression, :local_expression
12
13
  def_delegator :@local_gate_values, :boolean, :local_boolean
13
14
  def_delegator :@local_gate_values, :actors, :local_actors
14
15
  def_delegator :@local_gate_values, :groups, :local_groups
@@ -17,6 +18,7 @@ module Flipper
17
18
  def_delegator :@local_gate_values, :percentage_of_time,
18
19
  :local_percentage_of_time
19
20
 
21
+ def_delegator :@remote_gate_values, :expression, :remote_expression
20
22
  def_delegator :@remote_gate_values, :boolean, :remote_boolean
21
23
  def_delegator :@remote_gate_values, :actors, :remote_actors
22
24
  def_delegator :@remote_gate_values, :groups, :remote_groups
@@ -40,8 +42,9 @@ module Flipper
40
42
  @feature.enable
41
43
  else
42
44
  @feature.disable if local_boolean_enabled?
43
- sync_actors
44
45
  sync_groups
46
+ sync_actors
47
+ sync_expression
45
48
  sync_percentage_of_actors
46
49
  sync_percentage_of_time
47
50
  end
@@ -49,6 +52,16 @@ module Flipper
49
52
 
50
53
  private
51
54
 
55
+ def sync_expression
56
+ return if local_expression == remote_expression
57
+
58
+ if remote_expression.nil?
59
+ @feature.disable_expression
60
+ else
61
+ @feature.enable_expression remote_expression
62
+ end
63
+ end
64
+
52
65
  def sync_actors
53
66
  remote_actors_added = remote_actors - local_actors
54
67
  remote_actors_added.each do |flipper_id|
@@ -7,11 +7,6 @@ module Flipper
7
7
  # Private: Number of seconds between syncs (default: 10).
8
8
  DEFAULT_INTERVAL = 10
9
9
 
10
- # Private
11
- def self.now
12
- Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
13
- end
14
-
15
10
  # Public: The Float or Integer number of seconds between invocations of
16
11
  # the wrapped synchronizer.
17
12
  attr_reader :interval
@@ -19,7 +14,7 @@ module Flipper
19
14
  # Public: Initializes a new interval synchronizer.
20
15
  #
21
16
  # synchronizer - The Synchronizer to call when the interval has passed.
22
- # interval - The Integer number of milliseconds between invocations of
17
+ # interval - The Integer number of seconds between invocations of
23
18
  # the wrapped synchronizer.
24
19
  def initialize(synchronizer, interval: nil)
25
20
  @synchronizer = synchronizer
@@ -46,7 +41,7 @@ module Flipper
46
41
  end
47
42
 
48
43
  def now
49
- self.class.now
44
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
50
45
  end
51
46
  end
52
47
  end
@@ -1,5 +1,6 @@
1
1
  require "flipper/feature"
2
2
  require "flipper/gate_values"
3
+ require "flipper/adapters/actor_limit"
3
4
  require "flipper/adapters/sync/feature_synchronizer"
4
5
 
5
6
  module Flipper
@@ -15,28 +16,34 @@ module Flipper
15
16
  # adapter should be brought in line with.
16
17
  # options - The Hash of options.
17
18
  # :instrumenter - The instrumenter used to instrument.
19
+ # :raise - Should errors be raised (default: true).
20
+ # :cache_bust - Should cache busting be used for remote get_all (default: false).
18
21
  def initialize(local, remote, options = {})
19
22
  @local = local
20
23
  @remote = remote
21
24
  @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
22
25
  @raise = options.fetch(:raise, true)
26
+ @cache_bust = options.fetch(:cache_bust, false)
23
27
  end
24
28
 
25
29
  # Public: Forces a sync.
26
30
  def call
27
- @instrumenter.instrument("synchronizer_call.flipper") { sync }
31
+ @instrumenter.instrument("synchronizer_call.flipper") do
32
+ Flipper::Adapters::ActorLimit.with_sync_mode { sync }
33
+ end
28
34
  end
29
35
 
30
36
  private
31
37
 
32
38
  def sync
33
39
  local_get_all = @local.get_all
34
- remote_get_all = @remote.get_all
40
+ remote_get_all = @remote.get_all(cache_bust: @cache_bust)
35
41
 
36
42
  # Sync all the gate values.
37
43
  remote_get_all.each do |feature_key, remote_gates_hash|
38
- feature = Feature.new(feature_key, @local)
39
- local_gates_hash = local_get_all[feature_key] || @local.default_config
44
+ feature = Feature.new(feature_key, @local, instrumenter: @instrumenter)
45
+ # Check if feature_key is in hash before accessing to prevent unintended hash modification
46
+ local_gates_hash = local_get_all.key?(feature_key) ? local_get_all[feature_key] : @local.default_config
40
47
  local_gate_values = GateValues.new(local_gates_hash)
41
48
  remote_gate_values = GateValues.new(remote_gates_hash)
42
49
  FeatureSynchronizer.new(feature, local_gate_values, remote_gate_values).call
@@ -44,11 +51,11 @@ module Flipper
44
51
 
45
52
  # Add features that are missing in local and present in remote.
46
53
  features_to_add = remote_get_all.keys - local_get_all.keys
47
- features_to_add.each { |key| Feature.new(key, @local).add }
54
+ features_to_add.each { |key| Feature.new(key, @local, instrumenter: @instrumenter).add }
48
55
 
49
56
  # Remove features that are present in local and missing in remote.
50
57
  features_to_remove = local_get_all.keys - remote_get_all.keys
51
- features_to_remove.each { |key| Feature.new(key, @local).remove }
58
+ features_to_remove.each { |key| Feature.new(key, @local, instrumenter: @instrumenter).remove }
52
59
 
53
60
  nil
54
61
  rescue => exception
@@ -8,21 +8,17 @@ module Flipper
8
8
  class Sync
9
9
  include ::Flipper::Adapter
10
10
 
11
- # Public: The name of the adapter.
12
- attr_reader :name
13
-
14
11
  # Public: The synchronizer that will keep the local and remote in sync.
15
- attr_reader :synchronizer
12
+ attr_reader :synchronizer, :local, :remote
16
13
 
17
14
  # Public: Build a new sync instance.
18
15
  #
19
16
  # local - The local flipper adapter that should serve reads.
20
- # remote - The remote flipper adpater that should serve writes and update
17
+ # remote - The remote flipper adapter that should serve writes and update
21
18
  # the local on an interval.
22
19
  # interval - The Float or Integer number of seconds between syncs from
23
20
  # remote to local. Default value is set in IntervalSynchronizer.
24
21
  def initialize(local, remote, options = {})
25
- @name = :sync
26
22
  @local = local
27
23
  @remote = remote
28
24
  @synchronizer = options.fetch(:synchronizer) do
@@ -34,62 +30,60 @@ module Flipper
34
30
  synchronizer = Synchronizer.new(@local, @remote, sync_options)
35
31
  IntervalSynchronizer.new(synchronizer, interval: options[:interval])
36
32
  end
37
- sync
33
+ synchronize
34
+ end
35
+
36
+ def adapter_stack
37
+ "#{name}(local: #{@local.adapter_stack}, remote: #{@remote.adapter_stack})"
38
38
  end
39
39
 
40
40
  def features
41
- sync
41
+ synchronize
42
42
  @local.features
43
43
  end
44
44
 
45
45
  def get(feature)
46
- sync
46
+ synchronize
47
47
  @local.get(feature)
48
48
  end
49
49
 
50
50
  def get_multi(features)
51
- sync
51
+ synchronize
52
52
  @local.get_multi(features)
53
53
  end
54
54
 
55
- def get_all
56
- sync
57
- @local.get_all
55
+ def get_all(**kwargs)
56
+ synchronize
57
+ @local.get_all(**kwargs)
58
58
  end
59
59
 
60
60
  def add(feature)
61
- result = @remote.add(feature)
62
- @local.add(feature)
63
- result
61
+ @remote.add(feature).tap { @local.add(feature) }
64
62
  end
65
63
 
66
64
  def remove(feature)
67
- result = @remote.remove(feature)
68
- @local.remove(feature)
69
- result
65
+ @remote.remove(feature).tap { @local.remove(feature) }
70
66
  end
71
67
 
72
68
  def clear(feature)
73
- result = @remote.clear(feature)
74
- @local.clear(feature)
75
- result
69
+ @remote.clear(feature).tap { @local.clear(feature) }
76
70
  end
77
71
 
78
72
  def enable(feature, gate, thing)
79
- result = @remote.enable(feature, gate, thing)
80
- @local.enable(feature, gate, thing)
81
- result
73
+ @remote.enable(feature, gate, thing).tap do
74
+ @local.enable(feature, gate, thing)
75
+ end
82
76
  end
83
77
 
84
78
  def disable(feature, gate, thing)
85
- result = @remote.disable(feature, gate, thing)
86
- @local.disable(feature, gate, thing)
87
- result
79
+ @remote.disable(feature, gate, thing).tap do
80
+ @local.disable(feature, gate, thing)
81
+ end
88
82
  end
89
83
 
90
84
  private
91
85
 
92
- def sync
86
+ def synchronize
93
87
  @synchronizer.call
94
88
  end
95
89
  end
@@ -0,0 +1,54 @@
1
+ module Flipper
2
+ module Adapters
3
+ # A base class for any adapter that wraps another adapter. By default, all methods
4
+ # delegate to the wrapped adapter. Implement `#wrap` to customize the behavior of
5
+ # all delegated methods, or override individual methods as needed.
6
+ class Wrapper
7
+ include Flipper::Adapter
8
+
9
+ METHODS = [
10
+ :import,
11
+ :export,
12
+ :features,
13
+ :add,
14
+ :remove,
15
+ :clear,
16
+ :get,
17
+ :get_multi,
18
+ :get_all,
19
+ :enable,
20
+ :disable,
21
+ ].freeze
22
+
23
+ attr_reader :adapter
24
+
25
+ def initialize(adapter)
26
+ @adapter = adapter
27
+ end
28
+
29
+ METHODS.each do |method|
30
+ if RUBY_VERSION >= '3.0'
31
+ define_method(method) do |*args, **kwargs|
32
+ wrap(method, *args, **kwargs) { @adapter.public_send(method, *args, **kwargs) }
33
+ end
34
+ else
35
+ define_method(method) do |*args|
36
+ wrap(method, *args) { @adapter.public_send(method, *args) }
37
+ end
38
+ end
39
+ end
40
+
41
+ # Override this method to customize the behavior of all delegated methods, and just yield to
42
+ # the block to call the wrapped adapter.
43
+ if RUBY_VERSION >= '3.0'
44
+ def wrap(method, *args, **kwargs, &block)
45
+ block.call
46
+ end
47
+ else
48
+ def wrap(method, *args, &block)
49
+ block.call
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end