flipper 0.26.0 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -11
  3. data/.github/workflows/examples.yml +5 -10
  4. data/Changelog.md +16 -0
  5. data/Gemfile +6 -3
  6. data/benchmark/enabled_ips.rb +10 -0
  7. data/benchmark/enabled_profile.rb +20 -0
  8. data/benchmark/instrumentation_ips.rb +21 -0
  9. data/benchmark/typecast_ips.rb +19 -0
  10. data/examples/api/basic.ru +3 -4
  11. data/examples/api/custom_memoized.ru +3 -4
  12. data/examples/api/memoized.ru +3 -4
  13. data/flipper.gemspec +0 -2
  14. data/lib/flipper/adapter.rb +23 -7
  15. data/lib/flipper/adapters/http.rb +11 -3
  16. data/lib/flipper/adapters/instrumented.rb +25 -2
  17. data/lib/flipper/adapters/memoizable.rb +19 -2
  18. data/lib/flipper/adapters/memory.rb +56 -39
  19. data/lib/flipper/adapters/operation_logger.rb +16 -3
  20. data/lib/flipper/adapters/poll/poller.rb +2 -125
  21. data/lib/flipper/adapters/poll.rb +4 -0
  22. data/lib/flipper/dsl.rb +1 -5
  23. data/lib/flipper/export.rb +26 -0
  24. data/lib/flipper/exporter.rb +17 -0
  25. data/lib/flipper/exporters/json/export.rb +32 -0
  26. data/lib/flipper/exporters/json/v1.rb +33 -0
  27. data/lib/flipper/feature.rb +22 -18
  28. data/lib/flipper/feature_check_context.rb +4 -4
  29. data/lib/flipper/gate_values.rb +0 -16
  30. data/lib/flipper/gates/actor.rb +2 -12
  31. data/lib/flipper/gates/boolean.rb +1 -1
  32. data/lib/flipper/gates/group.rb +4 -8
  33. data/lib/flipper/gates/percentage_of_actors.rb +9 -11
  34. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  35. data/lib/flipper/instrumentation/subscriber.rb +8 -0
  36. data/lib/flipper/poller.rb +117 -0
  37. data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
  38. data/lib/flipper/test/shared_adapter_test.rb +24 -0
  39. data/lib/flipper/typecast.rb +28 -15
  40. data/lib/flipper/version.rb +1 -1
  41. data/lib/flipper.rb +2 -1
  42. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  43. data/spec/flipper/adapter_spec.rb +29 -2
  44. data/spec/flipper/adapters/http_spec.rb +25 -3
  45. data/spec/flipper/adapters/instrumented_spec.rb +28 -10
  46. data/spec/flipper/adapters/memoizable_spec.rb +30 -10
  47. data/spec/flipper/adapters/memory_spec.rb +3 -1
  48. data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
  49. data/spec/flipper/dsl_spec.rb +20 -3
  50. data/spec/flipper/export_spec.rb +13 -0
  51. data/spec/flipper/exporter_spec.rb +16 -0
  52. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  53. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  54. data/spec/flipper/feature_check_context_spec.rb +12 -12
  55. data/spec/flipper/gate_values_spec.rb +2 -33
  56. data/spec/flipper/gates/percentage_of_actors_spec.rb +1 -1
  57. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -0
  58. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
  59. data/spec/flipper/poller_spec.rb +47 -0
  60. data/spec/flipper/typecast_spec.rb +82 -3
  61. data/spec/flipper_spec.rb +7 -1
  62. data/spec/spec_helper.rb +1 -1
  63. data/spec/support/skippable.rb +18 -0
  64. metadata +25 -3
  65. data/.github/workflows/release.yml +0 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5edb0409042f8d4d5905b8d526fd1d240a75dbee2d947b8ae4adae0c534af5f
4
- data.tar.gz: 6bc01d5f7c0ee5f3d64d30faaa599b155d6220e249006d2ead7168bc47938202
3
+ metadata.gz: 202d50faf999fd9ed12d91ef7f160f786892b9476ad86b815fd4be5390cf8992
4
+ data.tar.gz: 1f59767d2f210820f488e97a0388f84aa2d474ac7b573e1650a69104d15f384a
5
5
  SHA512:
6
- metadata.gz: 60fee2e4a0d97a26b7403736b4422c0e69a0e9e452469e3f29e7af684001c38f2c957acdfa3f8f0554315d021a7e7091bb40898247e45d4658b14b31bc1de22f
7
- data.tar.gz: bf45e695be511513be967bffd75c4e48d3af0f05b05264ee3b13a9df8bba8af929ccbf6b7a1f05c6c3a7d5476ceb757e3dda852f8e42b745bf7171f42538fb14
6
+ metadata.gz: f4b8079927fe805c7c6200694c6b9d9b020539cf1835d62a713724adc1483c48399a3f0073c969d4f9619b0362d615c21813ed79470172ea7037e62cfbb791db
7
+ data.tar.gz: 8a389963c5386d4d9ef13be89e06f6f38e959dcb0c427b7950ffee628828d627ff567581da375d3e9eb8b3501ba7e24d0f4b82acdf6a04599fb0cf7a2385f250
@@ -15,7 +15,7 @@ jobs:
15
15
  --health-retries 5
16
16
  strategy:
17
17
  matrix:
18
- ruby: ['2.6', '2.7', '3.0', '3.1']
18
+ ruby: ['2.6', '2.7', '3.0', '3.1', '3.2']
19
19
  rails: ['5.2', '6.0.0', '6.1.0', '7.0.0']
20
20
  exclude:
21
21
  - ruby: "2.6"
@@ -26,15 +26,22 @@ jobs:
26
26
  rails: "5.2"
27
27
  - ruby: "3.1"
28
28
  rails: "6.0.0"
29
+ - ruby: "3.2"
30
+ rails: "5.2"
31
+ - ruby: "3.2"
32
+ rails: "6.0.0"
33
+ - ruby: "3.2"
34
+ rails: "6.1.0"
29
35
  env:
30
36
  SQLITE3_VERSION: 1.4.1
31
37
  REDIS_URL: redis://localhost:6379/0
32
38
  CI: true
39
+ RAILS_VERSION: ${{ matrix.rails }}
33
40
  steps:
34
41
  - name: Setup memcached
35
42
  uses: KeisukeYamashita/memcached-actions@v1
36
43
  - name: Start MongoDB
37
- uses: supercharge/mongodb-github-action@1.8.0
44
+ uses: supercharge/mongodb-github-action@1.9.0
38
45
  with:
39
46
  mongodb-version: 4.0
40
47
  - name: Check out repository code
@@ -46,17 +53,12 @@ jobs:
46
53
  key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('**/Gemfile.lock') }}
47
54
  restore-keys: |
48
55
  ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-
56
+ - name: Install libpq-dev
57
+ run: sudo apt-get -yqq install libpq-dev
49
58
  - name: Set up Ruby ${{ matrix.ruby }}
50
59
  uses: ruby/setup-ruby@v1
51
60
  with:
52
61
  ruby-version: ${{ matrix.ruby }}
53
- - name: Install libpq-dev
54
- run: sudo apt-get -yqq install libpq-dev
55
- - name: Install bundler
56
- run: gem install bundler
62
+ bundler-cache: true # 'bundle install' and cache gems
57
63
  - name: Run Rake with Rails ${{ matrix.rails }}
58
- env:
59
- RAILS_VERSION: ${{ matrix.rails }}
60
- run: |
61
- bundle install --jobs 4 --retry 3
62
- bundle exec rake
64
+ run: bundle exec rake
@@ -32,11 +32,12 @@ jobs:
32
32
  SQLITE3_VERSION: 1.4.1
33
33
  REDIS_URL: redis://localhost:6379/0
34
34
  CI: true
35
+ RAILS_VERSION: ${{ matrix.rails }}
35
36
  steps:
36
37
  - name: Setup memcached
37
38
  uses: KeisukeYamashita/memcached-actions@v1
38
39
  - name: Start MongoDB
39
- uses: supercharge/mongodb-github-action@1.8.0
40
+ uses: supercharge/mongodb-github-action@1.9.0
40
41
  with:
41
42
  mongodb-version: 4.0
42
43
  - name: Check out repository code
@@ -48,20 +49,14 @@ jobs:
48
49
  key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('**/Gemfile.lock') }}
49
50
  restore-keys: |
50
51
  ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-
52
+ - name: Install libpq-dev
53
+ run: sudo apt-get -yqq install libpq-dev
51
54
  - name: Set up Ruby ${{ matrix.ruby }}
52
55
  uses: ruby/setup-ruby@v1
53
56
  with:
54
57
  ruby-version: ${{ matrix.ruby }}
55
- - name: Install libpq-dev
56
- run: sudo apt-get -yqq install libpq-dev
57
- - name: Install bundler
58
- run: gem install bundler
59
- - name: Bundle install with Rails ${{ matrix.rails }}
60
- env:
61
- RAILS_VERSION: ${{ matrix.rails }}
62
- run: bundle install --jobs 4 --retry 3
58
+ bundler-cache: true # 'bundle install' and cache gems
63
59
  - name: Run Examples with Rails ${{ matrix.rails }}
64
60
  env:
65
61
  FLIPPER_CLOUD_TOKEN: ${{ secrets.FLIPPER_CLOUD_TOKEN }}
66
- RAILS_VERSION: ${{ matrix.rails }}
67
62
  run: script/examples
data/Changelog.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.27.0
6
+
7
+ * Easy Import/Export (https://github.com/jnunemaker/flipper/pull/709). This has some breaking changes but only if you are using flipper internals. If you are just using Flipper.* methods, you'll be fine.
8
+
9
+ ## 0.26.2
10
+
11
+ * Improve Active Record Adapter get/get_multi/get_all performance by 5-10x when dealing with thousands of gate values (https://github.com/jnunemaker/flipper/pull/707).
12
+
13
+ ## 0.26.1
14
+
15
+ * Improve `Flipper#enabled?` performance by ~37%-55% (https://github.com/jnunemaker/flipper/pull/706)
16
+ * Make Memory adapter threadsafe (https://github.com/jnunemaker/flipper/pull/702 and https://github.com/jnunemaker/flipper/pull/703)
17
+ * ActiveRecord adapter: wrap all reads/writes in `with_connection` (https://github.com/jnunemaker/flipper/pull/705)
18
+ * Improve performance of background polling (https://github.com/jnunemaker/flipper/pull/699)
19
+ * Remove executables directive from gem (https://github.com/jnunemaker/flipper/pull/693)
20
+
5
21
  ## 0.26.0
6
22
 
7
23
  * Cloud Background Polling (https://github.com/jnunemaker/flipper/pull/682)
data/Gemfile CHANGED
@@ -8,18 +8,21 @@ end
8
8
 
9
9
  gem 'debug'
10
10
  gem 'rake', '~> 12.3.3'
11
- gem 'shotgun', '~> 0.9'
12
11
  gem 'statsd-ruby', '~> 1.2.1'
13
12
  gem 'rspec', '~> 3.0'
14
- gem 'rack-test', '~> 0.6.3'
13
+ gem 'rack-test'
15
14
  gem 'sqlite3', "~> #{ENV['SQLITE3_VERSION'] || '1.4.1'}"
16
15
  gem 'rails', "~> #{ENV['RAILS_VERSION'] || '7.0.0'}"
17
- gem 'minitest', '~> 5.8'
16
+ gem 'minitest', '~> 5.18'
18
17
  gem 'minitest-documentation'
19
18
  gem 'webmock', '~> 3.0'
20
19
  gem 'ice_age'
21
20
  gem 'redis-namespace'
22
21
  gem 'webrick'
22
+ gem 'stackprof'
23
+ gem 'benchmark-ips'
24
+ gem 'stackprof-webnav'
25
+ gem 'flamegraph'
23
26
 
24
27
  group(:guard) do
25
28
  gem 'guard', '~> 2.15'
@@ -0,0 +1,10 @@
1
+ require 'bundler/setup'
2
+ require 'flipper'
3
+ require 'benchmark/ips'
4
+
5
+ actor = Flipper::Actor.new("User;1")
6
+
7
+ Benchmark.ips do |x|
8
+ x.report("with actor") { Flipper.enabled?(:foo, actor) }
9
+ x.report("without actor") { Flipper.enabled?(:foo) }
10
+ end
@@ -0,0 +1,20 @@
1
+ require 'bundler/setup'
2
+ require 'flipper'
3
+ require 'stackprof'
4
+ require 'benchmark/ips'
5
+
6
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
7
+ feature = flipper.feature(:foo)
8
+ actor = Flipper::Actor.new("User;1")
9
+
10
+ profile = StackProf.run(mode: :wall, interval: 1_000) do
11
+ 2_000_000.times do
12
+ feature.enabled?(actor)
13
+ end
14
+ end
15
+
16
+ result = StackProf::Report.new(profile)
17
+ puts
18
+ result.print_text
19
+ puts "\n\n\n"
20
+ result.print_method(/Flipper::Feature#enabled?/)
@@ -0,0 +1,21 @@
1
+ require 'bundler/setup'
2
+ require 'flipper'
3
+ require 'active_support/notifications'
4
+ require 'active_support/isolated_execution_state'
5
+ require 'benchmark/ips'
6
+
7
+ class FlipperSubscriber
8
+ def call(name, start, finish, id, payload)
9
+ end
10
+
11
+ ActiveSupport::Notifications.subscribe(/flipper/, new)
12
+ end
13
+
14
+ actor = Flipper::Actor.new("User;1")
15
+ bare = Flipper.new(Flipper::Adapters::Memory.new)
16
+ instrumented = Flipper.new(Flipper::Adapters::Memory.new, instrumenter: ActiveSupport::Notifications)
17
+
18
+ Benchmark.ips do |x|
19
+ x.report("with instrumentation") { instrumented.enabled?(:foo, actor) }
20
+ x.report("without instrumentation") { bare.enabled?(:foo, actor) }
21
+ end
@@ -0,0 +1,19 @@
1
+ require 'bundler/setup'
2
+ require 'flipper'
3
+ require 'benchmark/ips'
4
+
5
+ Benchmark.ips do |x|
6
+ x.report("Typecast.to_boolean true") { Flipper::Typecast.to_boolean(true) }
7
+ x.report("Typecast.to_boolean 1") { Flipper::Typecast.to_boolean(1) }
8
+ x.report("Typecast.to_boolean 'true'") { Flipper::Typecast.to_boolean('true'.freeze) }
9
+ x.report("Typecast.to_boolean '1'") { Flipper::Typecast.to_boolean('1'.freeze) }
10
+ x.report("Typecast.to_boolean false") { Flipper::Typecast.to_boolean(false) }
11
+
12
+ x.report("Typecast.to_integer 1") { Flipper::Typecast.to_integer(1) }
13
+ x.report("Typecast.to_integer '1'") { Flipper::Typecast.to_integer('1') }
14
+
15
+ x.report("Typecast.to_float 1") { Flipper::Typecast.to_float(1) }
16
+ x.report("Typecast.to_float '1'") { Flipper::Typecast.to_float('1'.freeze) }
17
+ x.report("Typecast.to_float 1.01") { Flipper::Typecast.to_float(1) }
18
+ x.report("Typecast.to_float '1.01'") { Flipper::Typecast.to_float('1'.freeze) }
19
+ end
@@ -1,19 +1,18 @@
1
1
  #
2
2
  # Usage:
3
- # # if you want it to not reload and be really fast
4
3
  # bin/rackup examples/api/basic.ru -p 9999
5
4
  #
6
- # # if you want reloading
7
- # bin/shotgun examples/api/basic.ru -p 9999
8
- #
9
5
  # http://localhost:9999/
10
6
  #
11
7
 
12
8
  require 'bundler/setup'
9
+ require 'rack/reloader'
13
10
  require "flipper/api"
14
11
  require "flipper/adapters/pstore"
15
12
 
16
13
  # You can uncomment this to get some default data:
17
14
  # Flipper.enable :logging
18
15
 
16
+ use Rack::Reloader
17
+
19
18
  run Flipper::Api.app
@@ -1,15 +1,12 @@
1
1
  #
2
2
  # Usage:
3
- # # if you want it to not reload and be really fast
4
3
  # bin/rackup examples/api/custom_memoized.ru -p 9999
5
4
  #
6
- # # if you want reloading
7
- # bin/shotgun examples/api/custom_memoized.ru -p 9999
8
- #
9
5
  # http://localhost:9999/
10
6
  #
11
7
 
12
8
  require 'bundler/setup'
9
+ require 'rack/reloader'
13
10
  require "active_support/notifications"
14
11
  require "flipper/api"
15
12
  require "flipper/adapters/pstore"
@@ -31,6 +28,8 @@ ActiveSupport::Notifications.subscribe(/.*/, ->(*args) {
31
28
  # You can uncomment this to get some default data:
32
29
  # flipper[:logging].enable_percentage_of_time 5
33
30
 
31
+ use Rack::Reloader
32
+
34
33
  run Flipper::Api.app(flipper) { |builder|
35
34
  builder.use Flipper::Middleware::SetupEnv, flipper
36
35
  builder.use Flipper::Middleware::Memoizer, preload: true
@@ -1,15 +1,12 @@
1
1
  #
2
2
  # Usage:
3
- # # if you want it to not reload and be really fast
4
3
  # bin/rackup examples/api/memoized.ru -p 9999
5
4
  #
6
- # # if you want reloading
7
- # bin/shotgun examples/api/memoized.ru -p 9999
8
- #
9
5
  # http://localhost:9999/
10
6
  #
11
7
 
12
8
  require 'bundler/setup'
9
+ require 'rack/reloader'
13
10
  require "active_support/notifications"
14
11
  require "flipper/api"
15
12
  require "flipper/adapters/pstore"
@@ -38,6 +35,8 @@ Flipper.register(:admins) { |actor|
38
35
  # You can uncomment this to get some default data:
39
36
  # Flipper.enable :logging
40
37
 
38
+ use Rack::Reloader
39
+
41
40
  run Flipper::Api.app { |builder|
42
41
  builder.use Flipper::Middleware::Memoizer, preload: true
43
42
  }
data/flipper.gemspec CHANGED
@@ -13,7 +13,6 @@ 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!
@@ -28,7 +27,6 @@ Gem::Specification.new do |gem|
28
27
  gem.homepage = 'https://github.com/jnunemaker/flipper'
29
28
  gem.license = 'MIT'
30
29
 
31
- gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
32
30
  gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
33
31
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
34
32
  gem.name = 'flipper'
@@ -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
@@ -20,6 +16,11 @@ module Flipper
20
16
  percentage_of_time: nil,
21
17
  }
22
18
  end
19
+
20
+ def from(source)
21
+ return source if source.is_a?(Flipper::Adapter)
22
+ source.adapter
23
+ end
23
24
  end
24
25
 
25
26
  # Public: Get all features and gate values in one call. Defaults to one call
@@ -43,9 +44,19 @@ module Flipper
43
44
 
44
45
  # Public: Ensure that adapter is in sync with source adapter provided.
45
46
  #
46
- # Returns result of Synchronizer#call.
47
- def import(source_adapter)
48
- Adapters::Sync::Synchronizer.new(self, source_adapter, raise: true).call
47
+ # source - The source dsl, adapter or export to import.
48
+ #
49
+ # Returns true if successful.
50
+ def import(source)
51
+ Adapters::Sync::Synchronizer.new(self, self.class.from(source), raise: true).call
52
+ true
53
+ end
54
+
55
+ # Public: Exports the adapter in a given format for a given format version.
56
+ #
57
+ # Returns a Flipper::Export instance.
58
+ def export(format: :json, version: 1)
59
+ Flipper::Exporter.build(format: format, version: version).call(self)
49
60
  end
50
61
 
51
62
  # Public: Default config for a feature's gate values.
@@ -54,3 +65,8 @@ module Flipper
54
65
  end
55
66
  end
56
67
  end
68
+
69
+ require "set"
70
+ require "flipper/exporter"
71
+ require "flipper/feature"
72
+ require "flipper/adapters/sync/synchronizer"
@@ -39,7 +39,7 @@ module Flipper
39
39
 
40
40
  def get_multi(features)
41
41
  csv_keys = features.map(&:key).join(',')
42
- response = @client.get("/features?keys=#{csv_keys}")
42
+ response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
43
43
  raise Error, response unless response.is_a?(Net::HTTPOK)
44
44
 
45
45
  parsed_response = JSON.parse(response.body)
@@ -57,7 +57,7 @@ module Flipper
57
57
  end
58
58
 
59
59
  def get_all
60
- response = @client.get("/features")
60
+ response = @client.get("/features?exclude_gate_names=true")
61
61
  raise Error, response unless response.is_a?(Net::HTTPOK)
62
62
 
63
63
  parsed_response = JSON.parse(response.body)
@@ -76,7 +76,7 @@ module Flipper
76
76
  end
77
77
 
78
78
  def features
79
- response = @client.get('/features')
79
+ response = @client.get('/features?exclude_gate_names=true')
80
80
  raise Error, response unless response.is_a?(Net::HTTPOK)
81
81
 
82
82
  parsed_response = JSON.parse(response.body)
@@ -123,6 +123,14 @@ module Flipper
123
123
  true
124
124
  end
125
125
 
126
+ def import(source)
127
+ adapter = self.class.from(source)
128
+ export = adapter.export(format: :json, version: 1)
129
+ response = @client.post("/import", export.contents)
130
+ raise Error, response unless response.is_a?(Net::HTTPNoContent)
131
+ true
132
+ end
133
+
126
134
  private
127
135
 
128
136
  def request_body_for_gate(gate, value)
@@ -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.
@@ -24,7 +24,6 @@ module Flipper
24
24
  # :instrumenter - What to use to instrument all the things.
25
25
  #
26
26
  def initialize(adapter, options = {})
27
- super(adapter)
28
27
  @adapter = adapter
29
28
  @name = :instrumented
30
29
  @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
@@ -146,6 +145,30 @@ module Flipper
146
145
  payload[:result] = @adapter.disable(feature, gate, thing)
147
146
  end
148
147
  end
148
+
149
+ def import(source)
150
+ default_payload = {
151
+ operation: :import,
152
+ adapter_name: @adapter.name,
153
+ }
154
+
155
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
156
+ payload[:result] = @adapter.import(source)
157
+ end
158
+ end
159
+
160
+ def export(format: :json, version: 1)
161
+ default_payload = {
162
+ operation: :export,
163
+ adapter_name: @adapter.name,
164
+ format: format,
165
+ version: version,
166
+ }
167
+
168
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
169
+ payload[:result] = @adapter.export(format: format, version: version)
170
+ end
171
+ end
149
172
  end
150
173
  end
151
174
  end
@@ -5,7 +5,7 @@ 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
11
  FeaturesKey = :flipper_features
@@ -27,7 +27,6 @@ module Flipper
27
27
 
28
28
  # Public
29
29
  def initialize(adapter, cache = nil)
30
- super(adapter)
31
30
  @adapter = adapter
32
31
  @name = :memoizable
33
32
  @cache = cache || {}
@@ -128,6 +127,14 @@ module Flipper
128
127
  @adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
129
128
  end
130
129
 
130
+ def import(source)
131
+ @adapter.import(source).tap { cache.clear if memoizing? }
132
+ end
133
+
134
+ def export(format: :json, version: 1)
135
+ @adapter.export(format: format, version: version)
136
+ end
137
+
131
138
  # Internal: Turns local caching on/off.
132
139
  #
133
140
  # value - The Boolean that decides if local caching is on.
@@ -141,6 +148,16 @@ module Flipper
141
148
  !!@memoize
142
149
  end
143
150
 
151
+ if RUBY_VERSION >= '3.0'
152
+ def method_missing(name, *args, **kwargs, &block)
153
+ @adapter.send name, *args, **kwargs, &block
154
+ end
155
+ else
156
+ def method_missing(name, *args, &block)
157
+ @adapter.send name, *args, &block
158
+ end
159
+ end
160
+
144
161
  private
145
162
 
146
163
  def key_for(key)
@@ -1,4 +1,6 @@
1
- require 'set'
1
+ require "flipper/adapter"
2
+ require "flipper/typecast"
3
+ require 'concurrent/atomic/read_write_lock'
2
4
 
3
5
  module Flipper
4
6
  module Adapters
@@ -14,86 +16,93 @@ module Flipper
14
16
 
15
17
  # Public
16
18
  def initialize(source = nil)
17
- @source = source || {}
19
+ @source = Typecast.features_hash(source)
18
20
  @name = :memory
21
+ @lock = Concurrent::ReadWriteLock.new
19
22
  end
20
23
 
21
24
  # Public: The set of known features.
22
25
  def features
23
- @source.keys.to_set
26
+ @lock.with_read_lock { @source.keys }.to_set
24
27
  end
25
28
 
26
29
  # Public: Adds a feature to the set of known features.
27
30
  def add(feature)
28
- @source[feature.key] ||= default_config
31
+ @lock.with_write_lock { @source[feature.key] ||= default_config }
29
32
  true
30
33
  end
31
34
 
32
35
  # Public: Removes a feature from the set of known features and clears
33
36
  # all the values for the feature.
34
37
  def remove(feature)
35
- @source.delete(feature.key)
38
+ @lock.with_write_lock { @source.delete(feature.key) }
36
39
  true
37
40
  end
38
41
 
39
42
  # Public: Clears all the gate values for a feature.
40
43
  def clear(feature)
41
- @source[feature.key] = default_config
44
+ @lock.with_write_lock { @source[feature.key] = default_config }
42
45
  true
43
46
  end
44
47
 
45
48
  # Public
46
49
  def get(feature)
47
- @source[feature.key] || default_config
50
+ @lock.with_read_lock { @source[feature.key] } || default_config
48
51
  end
49
52
 
50
53
  def get_multi(features)
51
- result = {}
52
- features.each do |feature|
53
- result[feature.key] = @source[feature.key] || default_config
54
+ @lock.with_read_lock do
55
+ result = {}
56
+ features.each do |feature|
57
+ result[feature.key] = @source[feature.key] || default_config
58
+ end
59
+ result
54
60
  end
55
- result
56
61
  end
57
62
 
58
63
  def get_all
59
- @source
64
+ @lock.with_read_lock { Typecast.features_hash(@source) }
60
65
  end
61
66
 
62
67
  # Public
63
68
  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"
69
+ @lock.with_write_lock do
70
+ @source[feature.key] ||= default_config
71
+
72
+ case gate.data_type
73
+ when :boolean
74
+ @source[feature.key] = default_config
75
+ @source[feature.key][gate.key] = thing.value.to_s
76
+ when :integer
77
+ @source[feature.key][gate.key] = thing.value.to_s
78
+ when :set
79
+ @source[feature.key][gate.key] << thing.value.to_s
80
+ else
81
+ raise "#{gate} is not supported by this adapter yet"
82
+ end
83
+
84
+ true
76
85
  end
77
-
78
- true
79
86
  end
80
87
 
81
88
  # Public
82
89
  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"
90
+ @lock.with_write_lock do
91
+ @source[feature.key] ||= default_config
92
+
93
+ case gate.data_type
94
+ when :boolean
95
+ @source[feature.key] = default_config
96
+ when :integer
97
+ @source[feature.key][gate.key] = thing.value.to_s
98
+ when :set
99
+ @source[feature.key][gate.key].delete thing.value.to_s
100
+ else
101
+ raise "#{gate} is not supported by this adapter yet"
102
+ end
103
+
104
+ true
94
105
  end
95
-
96
- true
97
106
  end
98
107
 
99
108
  # Public
@@ -104,6 +113,14 @@ module Flipper
104
113
  ]
105
114
  "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
106
115
  end
116
+
117
+ # Public: a more efficient implementation of import for this adapter
118
+ def import(source)
119
+ adapter = self.class.from(source)
120
+ get_all = Typecast.features_hash(adapter.get_all)
121
+ @lock.with_write_lock { @source.replace(get_all) }
122
+ true
123
+ end
107
124
  end
108
125
  end
109
126
  end