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
data/flipper.gemspec CHANGED
@@ -6,14 +6,13 @@ plugin_files = []
6
6
  plugin_test_files = []
7
7
 
8
8
  Dir['flipper-*.gemspec'].map do |gemspec|
9
- spec = eval(File.read(gemspec))
9
+ spec = Gem::Specification.load(gemspec)
10
10
  plugin_files << spec.files
11
11
  plugin_test_files << spec.files
12
12
  end
13
13
 
14
14
  ignored_files = plugin_files
15
15
  ignored_files << Dir['script/*']
16
- ignored_files << '.travis.yml'
17
16
  ignored_files << '.gitignore'
18
17
  ignored_files << 'Guardfile'
19
18
  ignored_files.flatten!.uniq!
@@ -23,17 +22,21 @@ ignored_test_files.flatten!.uniq!
23
22
 
24
23
  Gem::Specification.new do |gem|
25
24
  gem.authors = ['John Nunemaker']
26
- gem.email = ['nunemaker@gmail.com']
27
- gem.summary = 'Feature flipper for ANYTHING'
28
- gem.description = 'Feature flipper is the act of enabling/disabling features in your application, ideally without re-deploying or changing anything in your code base. Flipper makes this extremely easy to do with any backend you would like to use.'
29
- gem.homepage = 'https://github.com/jnunemaker/flipper'
25
+ gem.email = 'support@flippercloud.io'
26
+ gem.summary = 'Beautiful, performant feature flags for Ruby and Rails.'
27
+ gem.homepage = 'https://www.flippercloud.io/docs'
30
28
  gem.license = 'MIT'
31
29
 
32
- gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
30
+ gem.bindir = "exe"
31
+ gem.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
33
32
  gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
34
33
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
35
34
  gem.name = 'flipper'
36
35
  gem.require_paths = ['lib']
37
36
  gem.version = Flipper::VERSION
38
37
  gem.metadata = Flipper::METADATA
38
+
39
+ gem.add_dependency 'concurrent-ruby', '< 2'
40
+
41
+ gem.required_ruby_version = ">= #{Flipper::REQUIRED_RUBY_VERSION}"
39
42
  end
data/lib/flipper/actor.rb CHANGED
@@ -2,15 +2,22 @@
2
2
  # to Flipper::Feature#enabled?.
3
3
  module Flipper
4
4
  class Actor
5
- attr_reader :flipper_id
5
+ attr_reader :flipper_id, :flipper_properties
6
6
 
7
- def initialize(flipper_id)
7
+ def initialize(flipper_id, flipper_properties = {})
8
8
  @flipper_id = flipper_id
9
+ @flipper_properties = flipper_properties
9
10
  end
10
11
 
11
12
  def eql?(other)
12
- self.class.eql?(other.class) && @flipper_id == other.flipper_id
13
+ self.class.eql?(other.class) &&
14
+ @flipper_id == other.flipper_id &&
15
+ @flipper_properties == other.flipper_properties
13
16
  end
14
17
  alias_method :==, :eql?
18
+
19
+ def hash
20
+ flipper_id.hash
21
+ end
15
22
  end
16
23
  end
@@ -1,7 +1,3 @@
1
- require "set"
2
- require "flipper/feature"
3
- require "flipper/adapters/sync/synchronizer"
4
-
5
1
  module Flipper
6
2
  # Adding a module include so we have some hooks for stuff down the road
7
3
  module Adapter
@@ -16,16 +12,26 @@ module Flipper
16
12
  boolean: nil,
17
13
  groups: Set.new,
18
14
  actors: Set.new,
15
+ expression: nil,
19
16
  percentage_of_actors: nil,
20
17
  percentage_of_time: nil,
21
18
  }
22
19
  end
20
+
21
+ def from(source)
22
+ return source if source.is_a?(Flipper::Adapter)
23
+ source.adapter
24
+ end
25
+ end
26
+
27
+ def read_only?
28
+ false
23
29
  end
24
30
 
25
31
  # Public: Get all features and gate values in one call. Defaults to one call
26
32
  # to features and another to get_multi. Feel free to override per adapter to
27
33
  # make this more efficient.
28
- def get_all
34
+ def get_all(**kwargs)
29
35
  instances = features.map { |key| Flipper::Feature.new(key, self) }
30
36
  get_multi(instances)
31
37
  end
@@ -43,14 +49,50 @@ module Flipper
43
49
 
44
50
  # Public: Ensure that adapter is in sync with source adapter provided.
45
51
  #
46
- # Returns result of Synchronizer#call.
47
- def import(source_adapter)
48
- Adapters::Sync::Synchronizer.new(self, source_adapter, raise: true).call
52
+ # source - The source dsl, adapter or export to import.
53
+ #
54
+ # Returns true if successful.
55
+ def import(source)
56
+ Adapters::Sync::Synchronizer.new(self, self.class.from(source), raise: true).call
57
+ true
58
+ end
59
+
60
+ # Public: Exports the adapter in a given format for a given format version.
61
+ #
62
+ # Returns a Flipper::Export instance.
63
+ def export(format: :json, version: 1)
64
+ Flipper::Exporter.build(format: format, version: version).call(self)
49
65
  end
50
66
 
51
67
  # Public: Default config for a feature's gate values.
52
68
  def default_config
53
69
  self.class.default_config
54
70
  end
71
+
72
+ # Public: default name of the adapter
73
+ def name
74
+ @name ||= self.class.name.split('::').last.split(/(?=[A-Z])/).join('_').downcase.to_sym
75
+ end
76
+
77
+ # Public: Returns a string representation of the adapter stack for debugging.
78
+ # Shows the full chain of wrapped adapters.
79
+ #
80
+ # Examples:
81
+ # "memoizable -> active_support_cache_store -> active_record"
82
+ # "memoizable -> failover(primary: redis, secondary: memory)"
83
+ #
84
+ # Returns a String.
85
+ def adapter_stack
86
+ if respond_to?(:adapter) && adapter
87
+ "#{name} -> #{adapter.adapter_stack}"
88
+ else
89
+ name.to_s
90
+ end
91
+ end
55
92
  end
56
93
  end
94
+
95
+ require "set"
96
+ require "flipper/exporter"
97
+ require "flipper/feature"
98
+ require "flipper/adapters/sync/synchronizer"
@@ -0,0 +1,44 @@
1
+ module Flipper
2
+ # Builds an adapter from a stack of adapters.
3
+ #
4
+ # adapter = Flipper::AdapterBuilder.new do
5
+ # use Flipper::Adapters::Strict
6
+ # use Flipper::Adapters::Memoizable
7
+ # store Flipper::Adapters::Memory
8
+ # end.to_adapter
9
+ #
10
+ class AdapterBuilder
11
+ def initialize(&block)
12
+ @stack = []
13
+
14
+ # Default to memory adapter
15
+ store Flipper::Adapters::Memory
16
+
17
+ block.arity == 0 ? instance_eval(&block) : block.call(self) if block
18
+ end
19
+
20
+ if RUBY_VERSION >= '3.0'
21
+ def use(klass, *args, **kwargs, &block)
22
+ @stack.push ->(adapter) { klass.new(adapter, *args, **kwargs, &block) }
23
+ end
24
+ else
25
+ def use(klass, *args, &block)
26
+ @stack.push ->(adapter) { klass.new(adapter, *args, &block) }
27
+ end
28
+ end
29
+
30
+ if RUBY_VERSION >= '3.0'
31
+ def store(adapter, *args, **kwargs, &block)
32
+ @store = adapter.respond_to?(:call) ? adapter : -> { adapter.new(*args, **kwargs, &block) }
33
+ end
34
+ else
35
+ def store(adapter, *args, &block)
36
+ @store = adapter.respond_to?(:call) ? adapter : -> { adapter.new(*args, &block) }
37
+ end
38
+ end
39
+
40
+ def to_adapter
41
+ @stack.reverse.inject(@store.call) { |adapter, wrapper| wrapper.call(adapter) }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,54 @@
1
+ require "flipper/adapters/wrapper"
2
+
3
+ module Flipper
4
+ module Adapters
5
+ class ActorLimit < Wrapper
6
+ LimitExceeded = Class.new(Flipper::Error)
7
+
8
+ attr_reader :limit
9
+
10
+ class << self
11
+ # Returns whether sync mode is enabled for the current thread.
12
+ # When sync mode is enabled, actor limits are not enforced,
13
+ # allowing sync operations to bring local state in line with
14
+ # remote state regardless of limits.
15
+ def sync_mode
16
+ Thread.current[:flipper_actor_limit_sync_mode]
17
+ end
18
+
19
+ def sync_mode=(value)
20
+ Thread.current[:flipper_actor_limit_sync_mode] = value
21
+ end
22
+
23
+ # Executes a block with sync mode enabled. Actor limits will
24
+ # not be enforced within the block.
25
+ def with_sync_mode
26
+ old_value = sync_mode
27
+ self.sync_mode = true
28
+ yield
29
+ ensure
30
+ self.sync_mode = old_value
31
+ end
32
+ end
33
+
34
+ def initialize(adapter, limit = 100)
35
+ super(adapter)
36
+ @limit = limit
37
+ end
38
+
39
+ def enable(feature, gate, resource)
40
+ if gate.is_a?(Flipper::Gates::Actor) && !self.class.sync_mode && over_limit?(feature)
41
+ raise LimitExceeded, "Actor limit of #{@limit} exceeded for feature #{feature.key}. See https://www.flippercloud.io/docs/features/actors#limitations"
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def over_limit?(feature)
50
+ feature.actors_value.size >= @limit
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,161 @@
1
+ module Flipper
2
+ module Adapters
3
+ # Base class for caching adapters. Inherit from this and then override
4
+ # cache_fetch, cache_read_multi, cache_write, and cache_delete.
5
+ class CacheBase
6
+ include ::Flipper::Adapter
7
+
8
+ # Public: The adapter being cached.
9
+ attr_reader :adapter
10
+
11
+ # Public: The ActiveSupport::Cache::Store to cache with.
12
+ attr_reader :cache
13
+
14
+ # Public: The ttl for all cached data.
15
+ attr_reader :ttl
16
+
17
+ # Public: The cache key where the set of known features is cached.
18
+ attr_reader :features_cache_key
19
+
20
+ # Public: The cache key where the set of all features with gates is cached.
21
+ attr_reader :get_all_cache_key
22
+
23
+ # Public: Alias expires_in to ttl for compatibility.
24
+ alias_method :expires_in, :ttl
25
+
26
+ def initialize(adapter, cache, ttl = 300, prefix: nil)
27
+ @adapter = adapter
28
+ @cache = cache
29
+ @ttl = ttl
30
+
31
+ @cache_version = 'v1'.freeze
32
+ @namespace = "flipper/#{@cache_version}"
33
+ @namespace = @namespace.prepend(prefix) if prefix
34
+ @features_cache_key = "#{@namespace}/features"
35
+ @get_all_cache_key = "#{@namespace}/get_all"
36
+ end
37
+
38
+ # Public: Expire the cache for the set of all features with gates.
39
+ def expire_get_all_cache
40
+ cache_delete @get_all_cache_key
41
+ end
42
+
43
+ # Public: Expire the cache for the set of known feature names.
44
+ def expire_features_cache
45
+ cache_delete @features_cache_key
46
+ expire_get_all_cache
47
+ end
48
+
49
+ # Public: Expire the cache for a given feature.
50
+ def expire_feature_cache(key)
51
+ cache_delete feature_cache_key(key)
52
+ expire_get_all_cache
53
+ end
54
+
55
+ # Public
56
+ def features
57
+ read_feature_keys
58
+ end
59
+
60
+ # Public
61
+ def add(feature)
62
+ result = @adapter.add(feature)
63
+ expire_features_cache
64
+ result
65
+ end
66
+
67
+ # Public
68
+ def remove(feature)
69
+ result = @adapter.remove(feature)
70
+ expire_features_cache
71
+ expire_feature_cache(feature.key)
72
+ result
73
+ end
74
+
75
+ # Public
76
+ def clear(feature)
77
+ result = @adapter.clear(feature)
78
+ expire_feature_cache(feature.key)
79
+ result
80
+ end
81
+
82
+ # Public
83
+ def get(feature)
84
+ read_feature(feature)
85
+ end
86
+
87
+ # Public
88
+ def get_multi(features)
89
+ read_many_features(features)
90
+ end
91
+
92
+ # Public
93
+ def get_all(**kwargs)
94
+ cache_fetch(@get_all_cache_key) {
95
+ result = read_all_features(**kwargs)
96
+ cache_write @features_cache_key, result.keys.to_set
97
+ result
98
+ }
99
+ end
100
+
101
+ # Public
102
+ def enable(feature, gate, thing)
103
+ result = @adapter.enable(feature, gate, thing)
104
+ expire_feature_cache(feature.key)
105
+ result
106
+ end
107
+
108
+ # Public
109
+ def disable(feature, gate, thing)
110
+ result = @adapter.disable(feature, gate, thing)
111
+ expire_feature_cache(feature.key)
112
+ result
113
+ end
114
+
115
+ # Public: Generate the cache key for a given feature.
116
+ #
117
+ # key - The String or Symbol feature key.
118
+ def feature_cache_key(key)
119
+ "#{@namespace}/feature/#{key}"
120
+ end
121
+
122
+ private
123
+
124
+ def read_all_features(**kwargs)
125
+ @adapter.get_all(**kwargs)
126
+ end
127
+
128
+ # Private: Returns the Set of known feature keys.
129
+ def read_feature_keys
130
+ cache_fetch(@features_cache_key) { @adapter.features }
131
+ end
132
+
133
+ # Private: Read through caching for a single feature.
134
+ def read_feature(feature)
135
+ cache_fetch(feature_cache_key(feature.key)) { @adapter.get(feature) }
136
+ end
137
+
138
+ # Private: Given an array of features, attempts to read through cache in
139
+ # as few network calls as possible.
140
+ def read_many_features(features)
141
+ keys = features.map { |feature| feature_cache_key(feature.key) }
142
+ cache_result = cache_read_multi(keys)
143
+ uncached_features = features.reject { |feature| cache_result[feature_cache_key(feature)] }
144
+
145
+ if uncached_features.any?
146
+ response = @adapter.get_multi(uncached_features)
147
+ response.each do |key, value|
148
+ cache_write feature_cache_key(key), value
149
+ cache_result[feature_cache_key(key)] = value
150
+ end
151
+ end
152
+
153
+ result = {}
154
+ features.each do |feature|
155
+ result[feature.key] = cache_result[feature_cache_key(feature.key)]
156
+ end
157
+ result
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,63 @@
1
+ module Flipper
2
+ module Adapters
3
+ class DualWrite
4
+ include ::Flipper::Adapter
5
+
6
+ attr_reader :local, :remote
7
+
8
+ # Public: Build a new sync instance.
9
+ #
10
+ # local - The local flipper adapter that should serve reads.
11
+ # remote - The remote flipper adapter that writes should go to first (in
12
+ # addition to the local adapter).
13
+ def initialize(local, remote, options = {})
14
+ @local = local
15
+ @remote = remote
16
+ end
17
+
18
+ def adapter_stack
19
+ "#{name}(local: #{@local.adapter_stack}, remote: #{@remote.adapter_stack})"
20
+ end
21
+
22
+ def features
23
+ @local.features
24
+ end
25
+
26
+ def get(feature)
27
+ @local.get(feature)
28
+ end
29
+
30
+ def get_multi(features)
31
+ @local.get_multi(features)
32
+ end
33
+
34
+ def get_all(**kwargs)
35
+ @local.get_all(**kwargs)
36
+ end
37
+
38
+ def add(feature)
39
+ @remote.add(feature).tap { @local.add(feature) }
40
+ end
41
+
42
+ def remove(feature)
43
+ @remote.remove(feature).tap { @local.remove(feature) }
44
+ end
45
+
46
+ def clear(feature)
47
+ @remote.clear(feature).tap { @local.clear(feature) }
48
+ end
49
+
50
+ def enable(feature, gate, thing)
51
+ @remote.enable(feature, gate, thing).tap do
52
+ @local.enable(feature, gate, thing)
53
+ end
54
+ end
55
+
56
+ def disable(feature, gate, thing)
57
+ @remote.disable(feature, gate, thing).tap do
58
+ @local.disable(feature, gate, thing)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,85 @@
1
+ module Flipper
2
+ module Adapters
3
+ class Failover
4
+ include ::Flipper::Adapter
5
+
6
+ # Public: Build a new failover instance.
7
+ #
8
+ # primary - The primary flipper adapter.
9
+ # secondary - The secondary flipper adapter which services reads when
10
+ # the primary adapter is unavailable.
11
+ # options - Hash of options:
12
+ # :dual_write - Boolean, whether to update secondary when
13
+ # primary is updated
14
+ # :errors - Array of exception types for which to failover
15
+
16
+ attr_reader :primary, :secondary
17
+
18
+ def initialize(primary, secondary, options = {})
19
+ @primary = primary
20
+ @secondary = secondary
21
+
22
+ @dual_write = options.fetch(:dual_write, false)
23
+ @errors = options.fetch(:errors, [ StandardError ])
24
+ end
25
+
26
+ def adapter_stack
27
+ "#{name}(primary: #{@primary.adapter_stack}, secondary: #{@secondary.adapter_stack})"
28
+ end
29
+
30
+ def features
31
+ @primary.features
32
+ rescue *@errors
33
+ @secondary.features
34
+ end
35
+
36
+ def get(feature)
37
+ @primary.get(feature)
38
+ rescue *@errors
39
+ @secondary.get(feature)
40
+ end
41
+
42
+ def get_multi(features)
43
+ @primary.get_multi(features)
44
+ rescue *@errors
45
+ @secondary.get_multi(features)
46
+ end
47
+
48
+ def get_all(**kwargs)
49
+ @primary.get_all(**kwargs)
50
+ rescue *@errors
51
+ @secondary.get_all(**kwargs)
52
+ end
53
+
54
+ def add(feature)
55
+ @primary.add(feature).tap do
56
+ @secondary.add(feature) if @dual_write
57
+ end
58
+ end
59
+
60
+ def remove(feature)
61
+ @primary.remove(feature).tap do
62
+ @secondary.remove(feature) if @dual_write
63
+ end
64
+ end
65
+
66
+ def clear(feature)
67
+ @primary.clear(feature).tap do
68
+ @secondary.clear(feature) if @dual_write
69
+ end
70
+ end
71
+
72
+ def enable(feature, gate, thing)
73
+ @primary.enable(feature, gate, thing).tap do
74
+ @secondary.enable(feature, gate, thing) if @dual_write
75
+ end
76
+ end
77
+
78
+ def disable(feature, gate, thing)
79
+ @primary.disable(feature, gate, thing).tap do
80
+ @secondary.disable(feature, gate, thing) if @dual_write
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,72 @@
1
+ module Flipper
2
+ module Adapters
3
+ class Failsafe
4
+ include ::Flipper::Adapter
5
+
6
+ # Public: Build a new Failsafe instance.
7
+ #
8
+ # adapter - Flipper adapter to guard.
9
+ # options - Hash of options:
10
+ # :errors - Array of exception types for which to fail safe
11
+
12
+ def initialize(adapter, options = {})
13
+ @adapter = adapter
14
+ @errors = options.fetch(:errors, [StandardError])
15
+ end
16
+
17
+ def features
18
+ @adapter.features
19
+ rescue *@errors
20
+ Set.new
21
+ end
22
+
23
+ def add(feature)
24
+ @adapter.add(feature)
25
+ rescue *@errors
26
+ false
27
+ end
28
+
29
+ def remove(feature)
30
+ @adapter.remove(feature)
31
+ rescue *@errors
32
+ false
33
+ end
34
+
35
+ def clear(feature)
36
+ @adapter.clear(feature)
37
+ rescue *@errors
38
+ false
39
+ end
40
+
41
+ def get(feature)
42
+ @adapter.get(feature)
43
+ rescue *@errors
44
+ {}
45
+ end
46
+
47
+ def get_multi(features)
48
+ @adapter.get_multi(features)
49
+ rescue *@errors
50
+ {}
51
+ end
52
+
53
+ def get_all(**kwargs)
54
+ @adapter.get_all(**kwargs)
55
+ rescue *@errors
56
+ {}
57
+ end
58
+
59
+ def enable(feature, gate, thing)
60
+ @adapter.enable(feature, gate, thing)
61
+ rescue *@errors
62
+ false
63
+ end
64
+
65
+ def disable(feature, gate, thing)
66
+ @adapter.disable(feature, gate, thing)
67
+ rescue *@errors
68
+ false
69
+ end
70
+ end
71
+ end
72
+ end