flipper 0.26.0 → 0.27.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 (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