flipper 0.26.0 → 0.26.2
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +13 -11
- data/.github/workflows/examples.yml +5 -10
- data/Changelog.md +12 -0
- data/Gemfile +4 -0
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +19 -0
- data/flipper.gemspec +0 -2
- data/lib/flipper/adapters/memory.rb +52 -39
- data/lib/flipper/adapters/poll/poller.rb +2 -125
- data/lib/flipper/adapters/poll.rb +4 -0
- data/lib/flipper/feature.rb +22 -18
- data/lib/flipper/feature_check_context.rb +4 -4
- data/lib/flipper/gate_values.rb +0 -16
- data/lib/flipper/gates/actor.rb +2 -12
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/group.rb +4 -8
- data/lib/flipper/gates/percentage_of_actors.rb +9 -11
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/typecast.rb +11 -15
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +1 -0
- data/spec/flipper/adapters/memory_spec.rb +3 -1
- data/spec/flipper/feature_check_context_spec.rb +12 -12
- data/spec/flipper/gate_values_spec.rb +2 -33
- data/spec/flipper/gates/percentage_of_actors_spec.rb +1 -1
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/typecast_spec.rb +3 -3
- data/spec/spec_helper.rb +1 -1
- metadata +9 -3
- data/.github/workflows/release.yml +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 670e45600b4c72208ed69da2792d2d4e1ac11d7a54f19c45c4b448ba6dacc404
|
4
|
+
data.tar.gz: 56a6ef12c569a7392212953604ed103a1fb187b57d5f827ea141a0d5eafc5ee1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a09649689708c91afbdc3e9f4eded2ec29ae1b3ac06a98f28197dcc4d3dd65983f73e0e9e44994d78b0672afbea1350ff7256dc12b7e5bb426961bfce858b4f6
|
7
|
+
data.tar.gz: '08eb67c5e5df45e734e8b6e50c6fd20cfbdf28dbdb8ba1a3e2d2af084250b9cd404fa21cc778989527d245154e05f5abc49aaa12a95b2ef7b12daf4bbea41830'
|
data/.github/workflows/ci.yml
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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,18 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
+
## 0.26.2
|
6
|
+
|
7
|
+
* 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).
|
8
|
+
|
9
|
+
## 0.26.1
|
10
|
+
|
11
|
+
* Improve `Flipper#enabled?` performance by ~37%-55% (https://github.com/jnunemaker/flipper/pull/706)
|
12
|
+
* Make Memory adapter threadsafe (https://github.com/jnunemaker/flipper/pull/702 and https://github.com/jnunemaker/flipper/pull/703)
|
13
|
+
* ActiveRecord adapter: wrap all reads/writes in `with_connection` (https://github.com/jnunemaker/flipper/pull/705)
|
14
|
+
* Improve performance of background polling (https://github.com/jnunemaker/flipper/pull/699)
|
15
|
+
* Remove executables directive from gem (https://github.com/jnunemaker/flipper/pull/693)
|
16
|
+
|
5
17
|
## 0.26.0
|
6
18
|
|
7
19
|
* Cloud Background Polling (https://github.com/jnunemaker/flipper/pull/682)
|
data/Gemfile
CHANGED
@@ -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
|
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,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'concurrent/atomic/read_write_lock'
|
2
2
|
|
3
3
|
module Flipper
|
4
4
|
module Adapters
|
@@ -14,86 +14,93 @@ module Flipper
|
|
14
14
|
|
15
15
|
# Public
|
16
16
|
def initialize(source = nil)
|
17
|
-
@source = source || {}
|
17
|
+
@source = Hash.new.update(source || {})
|
18
18
|
@name = :memory
|
19
|
+
@lock = Concurrent::ReadWriteLock.new
|
19
20
|
end
|
20
21
|
|
21
22
|
# Public: The set of known features.
|
22
23
|
def features
|
23
|
-
@source.keys.to_set
|
24
|
+
@lock.with_read_lock { @source.keys }.to_set
|
24
25
|
end
|
25
26
|
|
26
27
|
# Public: Adds a feature to the set of known features.
|
27
28
|
def add(feature)
|
28
|
-
@source[feature.key] ||= default_config
|
29
|
+
@lock.with_write_lock { @source[feature.key] ||= default_config }
|
29
30
|
true
|
30
31
|
end
|
31
32
|
|
32
33
|
# Public: Removes a feature from the set of known features and clears
|
33
34
|
# all the values for the feature.
|
34
35
|
def remove(feature)
|
35
|
-
@source.delete(feature.key)
|
36
|
+
@lock.with_write_lock { @source.delete(feature.key) }
|
36
37
|
true
|
37
38
|
end
|
38
39
|
|
39
40
|
# Public: Clears all the gate values for a feature.
|
40
41
|
def clear(feature)
|
41
|
-
@source[feature.key] = default_config
|
42
|
+
@lock.with_write_lock { @source[feature.key] = default_config }
|
42
43
|
true
|
43
44
|
end
|
44
45
|
|
45
46
|
# Public
|
46
47
|
def get(feature)
|
47
|
-
@source[feature.key] || default_config
|
48
|
+
@lock.with_read_lock { @source[feature.key] } || default_config
|
48
49
|
end
|
49
50
|
|
50
51
|
def get_multi(features)
|
51
|
-
|
52
|
-
|
53
|
-
|
52
|
+
@lock.with_read_lock do
|
53
|
+
result = {}
|
54
|
+
features.each do |feature|
|
55
|
+
result[feature.key] = @source[feature.key] || default_config
|
56
|
+
end
|
57
|
+
result
|
54
58
|
end
|
55
|
-
result
|
56
59
|
end
|
57
60
|
|
58
61
|
def get_all
|
59
|
-
@source
|
62
|
+
@lock.with_read_lock { @source.to_h }
|
60
63
|
end
|
61
64
|
|
62
65
|
# Public
|
63
66
|
def enable(feature, gate, thing)
|
64
|
-
@
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
67
|
+
@lock.with_write_lock do
|
68
|
+
@source[feature.key] ||= default_config
|
69
|
+
|
70
|
+
case gate.data_type
|
71
|
+
when :boolean
|
72
|
+
@source[feature.key] = default_config
|
73
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
74
|
+
when :integer
|
75
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
76
|
+
when :set
|
77
|
+
@source[feature.key][gate.key] << thing.value.to_s
|
78
|
+
else
|
79
|
+
raise "#{gate} is not supported by this adapter yet"
|
80
|
+
end
|
81
|
+
|
82
|
+
true
|
76
83
|
end
|
77
|
-
|
78
|
-
true
|
79
84
|
end
|
80
85
|
|
81
86
|
# Public
|
82
87
|
def disable(feature, gate, thing)
|
83
|
-
@
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
88
|
+
@lock.with_write_lock do
|
89
|
+
@source[feature.key] ||= default_config
|
90
|
+
|
91
|
+
case gate.data_type
|
92
|
+
when :boolean
|
93
|
+
@source[feature.key] = default_config
|
94
|
+
when :integer
|
95
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
96
|
+
when :set
|
97
|
+
@source[feature.key][gate.key].delete thing.value.to_s
|
98
|
+
else
|
99
|
+
raise "#{gate} is not supported by this adapter yet"
|
100
|
+
end
|
101
|
+
|
102
|
+
true
|
94
103
|
end
|
95
|
-
|
96
|
-
true
|
97
104
|
end
|
98
105
|
|
99
106
|
# Public
|
@@ -104,6 +111,12 @@ module Flipper
|
|
104
111
|
]
|
105
112
|
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
106
113
|
end
|
114
|
+
|
115
|
+
# Public: a more efficient implementation of import for this adapter
|
116
|
+
def import(source_adapter)
|
117
|
+
get_all = source_adapter.get_all
|
118
|
+
@lock.with_write_lock { @source.replace(get_all) }
|
119
|
+
end
|
107
120
|
end
|
108
121
|
end
|
109
122
|
end
|
@@ -1,125 +1,2 @@
|
|
1
|
-
|
2
|
-
require '
|
3
|
-
require 'concurrent/utility/monotonic_time'
|
4
|
-
require 'concurrent/map'
|
5
|
-
|
6
|
-
module Flipper
|
7
|
-
module Adapters
|
8
|
-
class Poll
|
9
|
-
class Poller
|
10
|
-
attr_reader :thread, :pid, :mutex, :interval, :last_synced_at
|
11
|
-
|
12
|
-
def self.instances
|
13
|
-
@instances ||= Concurrent::Map.new
|
14
|
-
end
|
15
|
-
private_class_method :instances
|
16
|
-
|
17
|
-
def self.get(key, options = {})
|
18
|
-
instances.compute_if_absent(key) { new(options) }
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.reset
|
22
|
-
instances.each {|_,poller| poller.stop }.clear
|
23
|
-
end
|
24
|
-
|
25
|
-
def initialize(options = {})
|
26
|
-
@thread = nil
|
27
|
-
@pid = Process.pid
|
28
|
-
@mutex = Mutex.new
|
29
|
-
@adapter = Memory.new
|
30
|
-
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
31
|
-
@remote_adapter = options.fetch(:remote_adapter)
|
32
|
-
@interval = options.fetch(:interval, 10).to_f
|
33
|
-
@lock = Concurrent::ReadWriteLock.new
|
34
|
-
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
35
|
-
|
36
|
-
if @interval < 1
|
37
|
-
warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
|
38
|
-
@interval = 1
|
39
|
-
end
|
40
|
-
|
41
|
-
@start_automatically = options.fetch(:start_automatically, true)
|
42
|
-
|
43
|
-
if options.fetch(:shutdown_automatically, true)
|
44
|
-
at_exit { stop }
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def adapter
|
49
|
-
@lock.with_read_lock { Memory.new(@adapter.get_all.dup) }
|
50
|
-
end
|
51
|
-
|
52
|
-
def start
|
53
|
-
reset if forked?
|
54
|
-
ensure_worker_running
|
55
|
-
end
|
56
|
-
|
57
|
-
def stop
|
58
|
-
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
59
|
-
operation: :stop,
|
60
|
-
})
|
61
|
-
@thread&.kill
|
62
|
-
end
|
63
|
-
|
64
|
-
def run
|
65
|
-
loop do
|
66
|
-
sleep jitter
|
67
|
-
start = Concurrent.monotonic_time
|
68
|
-
begin
|
69
|
-
@instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
|
70
|
-
adapter = Memory.new
|
71
|
-
adapter.import(@remote_adapter)
|
72
|
-
|
73
|
-
@lock.with_write_lock { @adapter.import(adapter) }
|
74
|
-
@last_synced_at.update { |time| Concurrent.monotonic_time }
|
75
|
-
end
|
76
|
-
rescue => exception
|
77
|
-
# you can instrument these using poller.flipper
|
78
|
-
end
|
79
|
-
|
80
|
-
sleep_interval = interval - (Concurrent.monotonic_time - start)
|
81
|
-
sleep sleep_interval if sleep_interval.positive?
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
private
|
86
|
-
|
87
|
-
def jitter
|
88
|
-
rand
|
89
|
-
end
|
90
|
-
|
91
|
-
def forked?
|
92
|
-
pid != Process.pid
|
93
|
-
end
|
94
|
-
|
95
|
-
def ensure_worker_running
|
96
|
-
# Return early if thread is alive and avoid the mutex lock and unlock.
|
97
|
-
return if thread_alive?
|
98
|
-
|
99
|
-
# If another thread is starting worker thread, then return early so this
|
100
|
-
# thread can enqueue and move on with life.
|
101
|
-
return unless mutex.try_lock
|
102
|
-
|
103
|
-
begin
|
104
|
-
return if thread_alive?
|
105
|
-
@thread = Thread.new { run }
|
106
|
-
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
107
|
-
operation: :thread_start,
|
108
|
-
})
|
109
|
-
ensure
|
110
|
-
mutex.unlock
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def thread_alive?
|
115
|
-
@thread && @thread.alive?
|
116
|
-
end
|
117
|
-
|
118
|
-
def reset
|
119
|
-
@pid = Process.pid
|
120
|
-
mutex.unlock if mutex.locked?
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
1
|
+
warn "DEPRECATION WARNING: Flipper::Adapters::Poll::Poller is deprecated. Use Flipper::Poller instead."
|
2
|
+
require 'flipper/adapters/poll'
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'flipper/adapters/sync/synchronizer'
|
2
|
+
require 'flipper/poller'
|
2
3
|
|
3
4
|
module Flipper
|
4
5
|
module Adapters
|
@@ -6,6 +7,9 @@ module Flipper
|
|
6
7
|
extend Forwardable
|
7
8
|
include ::Flipper::Adapter
|
8
9
|
|
10
|
+
# Deprecated
|
11
|
+
Poller = ::Flipper::Poller
|
12
|
+
|
9
13
|
# Public: The name of the adapter.
|
10
14
|
attr_reader :name, :adapter, :poller
|
11
15
|
|
data/lib/flipper/feature.rb
CHANGED
@@ -100,13 +100,12 @@ module Flipper
|
|
100
100
|
#
|
101
101
|
# Returns true if enabled, false if not.
|
102
102
|
def enabled?(thing = nil)
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
payload[:thing] = thing
|
103
|
+
thing = Types::Actor.wrap(thing) unless thing.nil?
|
104
|
+
|
105
|
+
instrument(:enabled?, thing: thing) do |payload|
|
107
106
|
context = FeatureCheckContext.new(
|
108
107
|
feature_name: @name,
|
109
|
-
values:
|
108
|
+
values: gate_values,
|
110
109
|
thing: thing
|
111
110
|
)
|
112
111
|
|
@@ -207,7 +206,7 @@ module Flipper
|
|
207
206
|
|
208
207
|
if values.boolean || values.percentage_of_time == 100
|
209
208
|
:on
|
210
|
-
elsif non_boolean_gates.detect { |gate| gate.enabled?(values
|
209
|
+
elsif non_boolean_gates.detect { |gate| gate.enabled?(values.send(gate.key)) }
|
211
210
|
:conditional
|
212
211
|
else
|
213
212
|
:off
|
@@ -232,7 +231,8 @@ module Flipper
|
|
232
231
|
|
233
232
|
# Public: Returns the raw gate values stored by the adapter.
|
234
233
|
def gate_values
|
235
|
-
|
234
|
+
adapter_values = adapter.get(self)
|
235
|
+
GateValues.new(adapter_values)
|
236
236
|
end
|
237
237
|
|
238
238
|
# Public: Get groups enabled for this feature.
|
@@ -290,7 +290,7 @@ module Flipper
|
|
290
290
|
# Returns an Array of Flipper::Gate instances.
|
291
291
|
def enabled_gates
|
292
292
|
values = gate_values
|
293
|
-
gates.select { |gate| gate.enabled?(values
|
293
|
+
gates.select { |gate| gate.enabled?(values.send(gate.key)) }
|
294
294
|
end
|
295
295
|
|
296
296
|
# Public: Get the names of the enabled gates.
|
@@ -339,20 +339,24 @@ module Flipper
|
|
339
339
|
#
|
340
340
|
# Returns an array of gates
|
341
341
|
def gates
|
342
|
-
@gates ||=
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
Gates::
|
348
|
-
|
342
|
+
@gates ||= gates_hash.values.freeze
|
343
|
+
end
|
344
|
+
|
345
|
+
def gates_hash
|
346
|
+
@gates_hash ||= {
|
347
|
+
boolean: Gates::Boolean.new,
|
348
|
+
actor: Gates::Actor.new,
|
349
|
+
percentage_of_actors: Gates::PercentageOfActors.new,
|
350
|
+
percentage_of_time: Gates::PercentageOfTime.new,
|
351
|
+
group: Gates::Group.new,
|
352
|
+
}.freeze
|
349
353
|
end
|
350
354
|
|
351
355
|
# Public: Find a gate by name.
|
352
356
|
#
|
353
357
|
# Returns a Flipper::Gate if found, nil if not.
|
354
358
|
def gate(name)
|
355
|
-
|
359
|
+
gates_hash[name.to_sym]
|
356
360
|
end
|
357
361
|
|
358
362
|
# Public: Find the gate that protects a thing.
|
@@ -368,8 +372,8 @@ module Flipper
|
|
368
372
|
private
|
369
373
|
|
370
374
|
# Private: Instrument a feature operation.
|
371
|
-
def instrument(operation)
|
372
|
-
@instrumenter.instrument(InstrumentationName) do |payload|
|
375
|
+
def instrument(operation, initial_payload = {})
|
376
|
+
@instrumenter.instrument(InstrumentationName, initial_payload) do |payload|
|
373
377
|
payload[:feature_name] = name
|
374
378
|
payload[:operation] = operation
|
375
379
|
payload[:result] = yield(payload) if block_given?
|
@@ -10,10 +10,10 @@ module Flipper
|
|
10
10
|
# Public: The thing we want to know if a feature is enabled for.
|
11
11
|
attr_reader :thing
|
12
12
|
|
13
|
-
def initialize(
|
14
|
-
@feature_name =
|
15
|
-
@values =
|
16
|
-
@thing =
|
13
|
+
def initialize(feature_name:, values:, thing:)
|
14
|
+
@feature_name = feature_name
|
15
|
+
@values = values
|
16
|
+
@thing = thing
|
17
17
|
end
|
18
18
|
|
19
19
|
# Public: Convenience method for groups value like Feature has.
|
data/lib/flipper/gate_values.rb
CHANGED
@@ -3,16 +3,6 @@ require 'flipper/typecast'
|
|
3
3
|
|
4
4
|
module Flipper
|
5
5
|
class GateValues
|
6
|
-
# Private: Array of instance variables that are readable through the []
|
7
|
-
# instance method.
|
8
|
-
LegitIvars = {
|
9
|
-
'boolean' => '@boolean',
|
10
|
-
'actors' => '@actors',
|
11
|
-
'groups' => '@groups',
|
12
|
-
'percentage_of_time' => '@percentage_of_time',
|
13
|
-
'percentage_of_actors' => '@percentage_of_actors',
|
14
|
-
}.freeze
|
15
|
-
|
16
6
|
attr_reader :boolean
|
17
7
|
attr_reader :actors
|
18
8
|
attr_reader :groups
|
@@ -27,12 +17,6 @@ module Flipper
|
|
27
17
|
@percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time])
|
28
18
|
end
|
29
19
|
|
30
|
-
def [](key)
|
31
|
-
if ivar = LegitIvars[key.to_s]
|
32
|
-
instance_variable_get(ivar)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
20
|
def eql?(other)
|
37
21
|
self.class.eql?(other.class) &&
|
38
22
|
boolean == other.boolean &&
|
data/lib/flipper/gates/actor.rb
CHANGED
@@ -23,18 +23,8 @@ module Flipper
|
|
23
23
|
#
|
24
24
|
# Returns true if gate open for thing, false if not.
|
25
25
|
def open?(context)
|
26
|
-
|
27
|
-
|
28
|
-
false
|
29
|
-
else
|
30
|
-
if protects?(context.thing)
|
31
|
-
actor = wrap(context.thing)
|
32
|
-
enabled_actor_ids = value
|
33
|
-
enabled_actor_ids.include?(actor.value)
|
34
|
-
else
|
35
|
-
false
|
36
|
-
end
|
37
|
-
end
|
26
|
+
return false if context.thing.nil?
|
27
|
+
context.values.actors.include?(context.thing.value)
|
38
28
|
end
|
39
29
|
|
40
30
|
def wrap(thing)
|
data/lib/flipper/gates/group.rb
CHANGED
@@ -23,14 +23,10 @@ module Flipper
|
|
23
23
|
#
|
24
24
|
# Returns true if gate open for thing, false if not.
|
25
25
|
def open?(context)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
value.any? do |name|
|
31
|
-
group = Flipper.group(name)
|
32
|
-
group.match?(context.thing, context)
|
33
|
-
end
|
26
|
+
return false if context.thing.nil?
|
27
|
+
|
28
|
+
context.values.groups.any? do |name|
|
29
|
+
Flipper.group(name).match?(context.thing, context)
|
34
30
|
end
|
35
31
|
end
|
36
32
|
|
@@ -21,21 +21,19 @@ module Flipper
|
|
21
21
|
value > 0
|
22
22
|
end
|
23
23
|
|
24
|
+
# Private: this constant is used to support up to 3 decimal places
|
25
|
+
# in percentages.
|
26
|
+
SCALING_FACTOR = 1_000
|
27
|
+
private_constant :SCALING_FACTOR
|
28
|
+
|
24
29
|
# Internal: Checks if the gate is open for a thing.
|
25
30
|
#
|
26
31
|
# Returns true if gate open for thing, false if not.
|
27
32
|
def open?(context)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
id = "#{context.feature_name}#{actor.value}"
|
33
|
-
# this is to support up to 3 decimal places in percentages
|
34
|
-
scaling_factor = 1_000
|
35
|
-
Zlib.crc32(id) % (100 * scaling_factor) < percentage * scaling_factor
|
36
|
-
else
|
37
|
-
false
|
38
|
-
end
|
33
|
+
return false if context.thing.nil?
|
34
|
+
|
35
|
+
id = "#{context.feature_name}#{context.thing.value}"
|
36
|
+
Zlib.crc32(id) % (100 * SCALING_FACTOR) < context.values.percentage_of_actors * SCALING_FACTOR
|
39
37
|
end
|
40
38
|
|
41
39
|
def protects?(thing)
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'concurrent/utility/monotonic_time'
|
3
|
+
require 'concurrent/map'
|
4
|
+
require 'concurrent/atomic/atomic_fixnum'
|
5
|
+
|
6
|
+
module Flipper
|
7
|
+
class Poller
|
8
|
+
attr_reader :adapter, :thread, :pid, :mutex, :interval, :last_synced_at
|
9
|
+
|
10
|
+
def self.instances
|
11
|
+
@instances ||= Concurrent::Map.new
|
12
|
+
end
|
13
|
+
private_class_method :instances
|
14
|
+
|
15
|
+
def self.get(key, options = {})
|
16
|
+
instances.compute_if_absent(key) { new(options) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.reset
|
20
|
+
instances.each {|_,poller| poller.stop }.clear
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
@thread = nil
|
25
|
+
@pid = Process.pid
|
26
|
+
@mutex = Mutex.new
|
27
|
+
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
28
|
+
@remote_adapter = options.fetch(:remote_adapter)
|
29
|
+
@interval = options.fetch(:interval, 10).to_f
|
30
|
+
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
31
|
+
@adapter = Adapters::Memory.new
|
32
|
+
|
33
|
+
if @interval < 1
|
34
|
+
warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
|
35
|
+
@interval = 1
|
36
|
+
end
|
37
|
+
|
38
|
+
@start_automatically = options.fetch(:start_automatically, true)
|
39
|
+
|
40
|
+
if options.fetch(:shutdown_automatically, true)
|
41
|
+
at_exit { stop }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def start
|
46
|
+
reset if forked?
|
47
|
+
ensure_worker_running
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop
|
51
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
52
|
+
operation: :stop,
|
53
|
+
})
|
54
|
+
@thread&.kill
|
55
|
+
end
|
56
|
+
|
57
|
+
def run
|
58
|
+
loop do
|
59
|
+
sleep jitter
|
60
|
+
start = Concurrent.monotonic_time
|
61
|
+
begin
|
62
|
+
sync
|
63
|
+
rescue => exception
|
64
|
+
# you can instrument these using poller.flipper
|
65
|
+
end
|
66
|
+
|
67
|
+
sleep_interval = interval - (Concurrent.monotonic_time - start)
|
68
|
+
sleep sleep_interval if sleep_interval.positive?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def sync
|
73
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
|
74
|
+
@adapter.import @remote_adapter
|
75
|
+
@last_synced_at.update { |time| Concurrent.monotonic_time }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def jitter
|
82
|
+
rand
|
83
|
+
end
|
84
|
+
|
85
|
+
def forked?
|
86
|
+
pid != Process.pid
|
87
|
+
end
|
88
|
+
|
89
|
+
def ensure_worker_running
|
90
|
+
# Return early if thread is alive and avoid the mutex lock and unlock.
|
91
|
+
return if thread_alive?
|
92
|
+
|
93
|
+
# If another thread is starting worker thread, then return early so this
|
94
|
+
# thread can enqueue and move on with life.
|
95
|
+
return unless mutex.try_lock
|
96
|
+
|
97
|
+
begin
|
98
|
+
return if thread_alive?
|
99
|
+
@thread = Thread.new { run }
|
100
|
+
@instrumenter.instrument("poller.#{InstrumentationNamespace}", {
|
101
|
+
operation: :thread_start,
|
102
|
+
})
|
103
|
+
ensure
|
104
|
+
mutex.unlock
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def thread_alive?
|
109
|
+
@thread && @thread.alive?
|
110
|
+
end
|
111
|
+
|
112
|
+
def reset
|
113
|
+
@pid = Process.pid
|
114
|
+
mutex.unlock if mutex.locked?
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/flipper/typecast.rb
CHANGED
@@ -21,11 +21,9 @@ module Flipper
|
|
21
21
|
# Returns an Integer representation of the value.
|
22
22
|
# Raises ArgumentError if conversion is not possible.
|
23
23
|
def self.to_integer(value)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
|
28
|
-
end
|
24
|
+
value.to_i
|
25
|
+
rescue NoMethodError
|
26
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
|
29
27
|
end
|
30
28
|
|
31
29
|
# Internal: Convert value to a float.
|
@@ -33,11 +31,9 @@ module Flipper
|
|
33
31
|
# Returns a Float representation of the value.
|
34
32
|
# Raises ArgumentError if conversion is not possible.
|
35
33
|
def self.to_float(value)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
raise ArgumentError, "#{value.inspect} cannot be converted to a float"
|
40
|
-
end
|
34
|
+
value.to_f
|
35
|
+
rescue NoMethodError
|
36
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to a float"
|
41
37
|
end
|
42
38
|
|
43
39
|
# Internal: Convert value to a percentage.
|
@@ -45,11 +41,11 @@ module Flipper
|
|
45
41
|
# Returns a Integer or Float representation of the value.
|
46
42
|
# Raises ArgumentError if conversion is not possible.
|
47
43
|
def self.to_percentage(value)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
44
|
+
result_to_f = value.to_f
|
45
|
+
result_to_i = result_to_f.to_i
|
46
|
+
result_to_f == result_to_i ? result_to_i : result_to_f
|
47
|
+
rescue NoMethodError
|
48
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to a percentage"
|
53
49
|
end
|
54
50
|
|
55
51
|
# Internal: Convert value to a set.
|
data/lib/flipper/version.rb
CHANGED
data/lib/flipper.rb
CHANGED
@@ -155,6 +155,7 @@ require 'flipper/instrumenters/noop'
|
|
155
155
|
require 'flipper/identifier'
|
156
156
|
require 'flipper/middleware/memoizer'
|
157
157
|
require 'flipper/middleware/setup_env'
|
158
|
+
require 'flipper/poller'
|
158
159
|
require 'flipper/registry'
|
159
160
|
require 'flipper/type'
|
160
161
|
require 'flipper/types/actor'
|
@@ -14,7 +14,9 @@ RSpec.describe Flipper::Adapters::Memory do
|
|
14
14
|
flipper.enable_actor :following, Flipper::Actor.new('3')
|
15
15
|
flipper.enable_group :following, Flipper::Types::Group.new(:staff)
|
16
16
|
|
17
|
-
|
17
|
+
dup = described_class.new(subject.get_all)
|
18
|
+
|
19
|
+
expect(dup.get_all).to eq({
|
18
20
|
"subscriptions" => subject.default_config.merge(boolean: "true"),
|
19
21
|
"search" => subject.default_config,
|
20
22
|
"logging" => subject.default_config.merge(:percentage_of_time => "30"),
|
@@ -11,7 +11,7 @@ RSpec.describe Flipper::FeatureCheckContext do
|
|
11
11
|
end
|
12
12
|
|
13
13
|
it 'initializes just fine' do
|
14
|
-
instance = described_class.new(options)
|
14
|
+
instance = described_class.new(**options)
|
15
15
|
expect(instance.feature_name).to eq(feature_name)
|
16
16
|
expect(instance.values).to eq(values)
|
17
17
|
expect(instance.thing).to eq(thing)
|
@@ -20,46 +20,46 @@ RSpec.describe Flipper::FeatureCheckContext do
|
|
20
20
|
it 'requires feature_name' do
|
21
21
|
options.delete(:feature_name)
|
22
22
|
expect do
|
23
|
-
described_class.new(options)
|
24
|
-
end.to raise_error(
|
23
|
+
described_class.new(**options)
|
24
|
+
end.to raise_error(ArgumentError)
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'requires values' do
|
28
28
|
options.delete(:values)
|
29
29
|
expect do
|
30
|
-
described_class.new(options)
|
31
|
-
end.to raise_error(
|
30
|
+
described_class.new(**options)
|
31
|
+
end.to raise_error(ArgumentError)
|
32
32
|
end
|
33
33
|
|
34
34
|
it 'requires thing' do
|
35
35
|
options.delete(:thing)
|
36
36
|
expect do
|
37
|
-
described_class.new(options)
|
38
|
-
end.to raise_error(
|
37
|
+
described_class.new(**options)
|
38
|
+
end.to raise_error(ArgumentError)
|
39
39
|
end
|
40
40
|
|
41
41
|
it 'knows actors_value' do
|
42
42
|
args = options.merge(values: Flipper::GateValues.new(actors: Set['User;1']))
|
43
|
-
expect(described_class.new(args).actors_value).to eq(Set['User;1'])
|
43
|
+
expect(described_class.new(**args).actors_value).to eq(Set['User;1'])
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'knows groups_value' do
|
47
47
|
args = options.merge(values: Flipper::GateValues.new(groups: Set['admins']))
|
48
|
-
expect(described_class.new(args).groups_value).to eq(Set['admins'])
|
48
|
+
expect(described_class.new(**args).groups_value).to eq(Set['admins'])
|
49
49
|
end
|
50
50
|
|
51
51
|
it 'knows boolean_value' do
|
52
|
-
instance = described_class.new(options.merge(values: Flipper::GateValues.new(boolean: true)))
|
52
|
+
instance = described_class.new(**options.merge(values: Flipper::GateValues.new(boolean: true)))
|
53
53
|
expect(instance.boolean_value).to eq(true)
|
54
54
|
end
|
55
55
|
|
56
56
|
it 'knows percentage_of_actors_value' do
|
57
57
|
args = options.merge(values: Flipper::GateValues.new(percentage_of_actors: 14))
|
58
|
-
expect(described_class.new(args).percentage_of_actors_value).to eq(14)
|
58
|
+
expect(described_class.new(**args).percentage_of_actors_value).to eq(14)
|
59
59
|
end
|
60
60
|
|
61
61
|
it 'knows percentage_of_time_value' do
|
62
62
|
args = options.merge(values: Flipper::GateValues.new(percentage_of_time: 41))
|
63
|
-
expect(described_class.new(args).percentage_of_time_value).to eq(41)
|
63
|
+
expect(described_class.new(**args).percentage_of_time_value).to eq(41)
|
64
64
|
end
|
65
65
|
end
|
@@ -80,13 +80,13 @@ RSpec.describe Flipper::GateValues do
|
|
80
80
|
it 'raises argument error for percentage of time value that cannot be converted to an integer' do
|
81
81
|
expect do
|
82
82
|
described_class.new(percentage_of_time: ['asdf'])
|
83
|
-
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to
|
83
|
+
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
|
84
84
|
end
|
85
85
|
|
86
86
|
it 'raises argument error for percentage of actors value that cannot be converted to an int' do
|
87
87
|
expect do
|
88
88
|
described_class.new(percentage_of_actors: ['asdf'])
|
89
|
-
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to
|
89
|
+
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
|
90
90
|
end
|
91
91
|
|
92
92
|
it 'raises argument error for actors value that cannot be converted to a set' do
|
@@ -100,35 +100,4 @@ RSpec.describe Flipper::GateValues do
|
|
100
100
|
described_class.new(groups: 'asdf')
|
101
101
|
end.to raise_error(ArgumentError, %("asdf" cannot be converted to a set))
|
102
102
|
end
|
103
|
-
|
104
|
-
describe '#[]' do
|
105
|
-
it 'can read the boolean value' do
|
106
|
-
expect(described_class.new(boolean: true)[:boolean]).to be(true)
|
107
|
-
expect(described_class.new(boolean: true)['boolean']).to be(true)
|
108
|
-
end
|
109
|
-
|
110
|
-
it 'can read the actors value' do
|
111
|
-
expect(described_class.new(actors: Set[1, 2])[:actors]).to eq(Set[1, 2])
|
112
|
-
expect(described_class.new(actors: Set[1, 2])['actors']).to eq(Set[1, 2])
|
113
|
-
end
|
114
|
-
|
115
|
-
it 'can read the groups value' do
|
116
|
-
expect(described_class.new(groups: Set[:admins])[:groups]).to eq(Set[:admins])
|
117
|
-
expect(described_class.new(groups: Set[:admins])['groups']).to eq(Set[:admins])
|
118
|
-
end
|
119
|
-
|
120
|
-
it 'can read the percentage of time value' do
|
121
|
-
expect(described_class.new(percentage_of_time: 15)[:percentage_of_time]).to eq(15)
|
122
|
-
expect(described_class.new(percentage_of_time: 15)['percentage_of_time']).to eq(15)
|
123
|
-
end
|
124
|
-
|
125
|
-
it 'can read the percentage of actors value' do
|
126
|
-
expect(described_class.new(percentage_of_actors: 15)[:percentage_of_actors]).to eq(15)
|
127
|
-
expect(described_class.new(percentage_of_actors: 15)['percentage_of_actors']).to eq(15)
|
128
|
-
end
|
129
|
-
|
130
|
-
it 'returns nil for value that is not present' do
|
131
|
-
expect(described_class.new({})['not legit']).to be(nil)
|
132
|
-
end
|
133
|
-
end
|
134
103
|
end
|
@@ -9,7 +9,7 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
|
|
9
9
|
Flipper::FeatureCheckContext.new(
|
10
10
|
feature_name: feature,
|
11
11
|
values: Flipper::GateValues.new(percentage_of_actors: percentage_of_actors_value),
|
12
|
-
thing:
|
12
|
+
thing: Flipper::Types::Actor.new(thing || Flipper::Actor.new(1))
|
13
13
|
)
|
14
14
|
end
|
15
15
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "flipper/poller"
|
2
|
+
|
3
|
+
RSpec.describe Flipper::Poller do
|
4
|
+
let(:remote_adapter) { Flipper::Adapters::Memory.new }
|
5
|
+
let(:remote) { Flipper.new(remote_adapter) }
|
6
|
+
let(:local) { Flipper.new(subject.adapter) }
|
7
|
+
|
8
|
+
subject do
|
9
|
+
described_class.new(
|
10
|
+
remote_adapter: remote_adapter,
|
11
|
+
start_automatically: false,
|
12
|
+
interval: Float::INFINITY
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
before do
|
17
|
+
allow(subject).to receive(:loop).and_yield # Make loop just call once
|
18
|
+
allow(subject).to receive(:sleep) # Disable sleep
|
19
|
+
allow(Thread).to receive(:new).and_yield # Disable separate thread
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#adapter" do
|
23
|
+
it "always returns same memory adapter instance" do
|
24
|
+
expect(subject.adapter).to be_a(Flipper::Adapters::Memory)
|
25
|
+
expect(subject.adapter.object_id).to eq(subject.adapter.object_id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#sync" do
|
30
|
+
it "syncs remote adapter to local adapter" do
|
31
|
+
remote.enable :polling
|
32
|
+
|
33
|
+
expect(local.enabled?(:polling)).to be(false)
|
34
|
+
subject.sync
|
35
|
+
expect(local.enabled?(:polling)).to be(true)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#start" do
|
40
|
+
it "starts the poller thread" do
|
41
|
+
expect(Thread).to receive(:new).and_yield
|
42
|
+
expect(subject).to receive(:loop).and_yield
|
43
|
+
expect(subject).to receive(:sync)
|
44
|
+
subject.start
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -56,7 +56,7 @@ RSpec.describe Flipper::Typecast do
|
|
56
56
|
nil => 0,
|
57
57
|
'' => 0,
|
58
58
|
0 => 0,
|
59
|
-
0.0 => 0
|
59
|
+
0.0 => 0,
|
60
60
|
1 => 1,
|
61
61
|
1.1 => 1.1,
|
62
62
|
'0.01' => 0.01,
|
@@ -100,13 +100,13 @@ RSpec.describe Flipper::Typecast do
|
|
100
100
|
it 'raises argument error for bad integer percentage' do
|
101
101
|
expect do
|
102
102
|
described_class.to_percentage(['asdf'])
|
103
|
-
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to
|
103
|
+
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
|
104
104
|
end
|
105
105
|
|
106
106
|
it 'raises argument error for bad float percentage' do
|
107
107
|
expect do
|
108
108
|
described_class.to_percentage(['asdf.0'])
|
109
|
-
end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a
|
109
|
+
end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a percentage))
|
110
110
|
end
|
111
111
|
|
112
112
|
it 'raises argument error for set value that cannot be converted to a set' do
|
data/spec/spec_helper.rb
CHANGED
@@ -22,7 +22,7 @@ Dir[FlipperRoot.join('spec/support/**/*.rb')].sort.each { |f| require f }
|
|
22
22
|
|
23
23
|
RSpec.configure do |config|
|
24
24
|
config.before(:example) do
|
25
|
-
Flipper::
|
25
|
+
Flipper::Poller.reset if defined?(Flipper::Poller)
|
26
26
|
Flipper.unregister_groups
|
27
27
|
Flipper.configuration = nil
|
28
28
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flipper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.26.
|
4
|
+
version: 0.26.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Nunemaker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -35,7 +35,6 @@ files:
|
|
35
35
|
- ".github/dependabot.yml"
|
36
36
|
- ".github/workflows/ci.yml"
|
37
37
|
- ".github/workflows/examples.yml"
|
38
|
-
- ".github/workflows/release.yml"
|
39
38
|
- ".rspec"
|
40
39
|
- ".tool-versions"
|
41
40
|
- CODE_OF_CONDUCT.md
|
@@ -45,6 +44,10 @@ files:
|
|
45
44
|
- LICENSE
|
46
45
|
- README.md
|
47
46
|
- Rakefile
|
47
|
+
- benchmark/enabled_ips.rb
|
48
|
+
- benchmark/enabled_profile.rb
|
49
|
+
- benchmark/instrumentation_ips.rb
|
50
|
+
- benchmark/typecast_ips.rb
|
48
51
|
- docker-compose.yml
|
49
52
|
- docs/DockerCompose.md
|
50
53
|
- docs/README.md
|
@@ -112,6 +115,7 @@ files:
|
|
112
115
|
- lib/flipper/metadata.rb
|
113
116
|
- lib/flipper/middleware/memoizer.rb
|
114
117
|
- lib/flipper/middleware/setup_env.rb
|
118
|
+
- lib/flipper/poller.rb
|
115
119
|
- lib/flipper/railtie.rb
|
116
120
|
- lib/flipper/registry.rb
|
117
121
|
- lib/flipper/spec/shared_adapter_specs.rb
|
@@ -160,6 +164,7 @@ files:
|
|
160
164
|
- spec/flipper/instrumenters/noop_spec.rb
|
161
165
|
- spec/flipper/middleware/memoizer_spec.rb
|
162
166
|
- spec/flipper/middleware/setup_env_spec.rb
|
167
|
+
- spec/flipper/poller_spec.rb
|
163
168
|
- spec/flipper/railtie_spec.rb
|
164
169
|
- spec/flipper/registry_spec.rb
|
165
170
|
- spec/flipper/typecast_spec.rb
|
@@ -239,6 +244,7 @@ test_files:
|
|
239
244
|
- spec/flipper/instrumenters/noop_spec.rb
|
240
245
|
- spec/flipper/middleware/memoizer_spec.rb
|
241
246
|
- spec/flipper/middleware/setup_env_spec.rb
|
247
|
+
- spec/flipper/poller_spec.rb
|
242
248
|
- spec/flipper/railtie_spec.rb
|
243
249
|
- spec/flipper/registry_spec.rb
|
244
250
|
- spec/flipper/typecast_spec.rb
|
@@ -1,44 +0,0 @@
|
|
1
|
-
# https://andrewm.codes/blog/automating-ruby-gem-releases-with-github-actions/
|
2
|
-
name: release
|
3
|
-
|
4
|
-
on:
|
5
|
-
push:
|
6
|
-
branches:
|
7
|
-
- main
|
8
|
-
|
9
|
-
jobs:
|
10
|
-
release-please:
|
11
|
-
runs-on: ubuntu-latest
|
12
|
-
steps:
|
13
|
-
- uses: google-github-actions/release-please-action@v3
|
14
|
-
id: release
|
15
|
-
with:
|
16
|
-
release-type: ruby
|
17
|
-
package-name: flipper
|
18
|
-
bump-minor-pre-major: true
|
19
|
-
version-file: "lib/flipper/version.rb"
|
20
|
-
# Checkout code if release was created
|
21
|
-
- uses: actions/checkout@v3
|
22
|
-
if: ${{ steps.release.outputs.release_created }}
|
23
|
-
# Setup ruby if a release was created
|
24
|
-
- uses: ruby/setup-ruby@v1
|
25
|
-
with:
|
26
|
-
ruby-version: 3.0.0
|
27
|
-
if: ${{ steps.release.outputs.release_created }}
|
28
|
-
# Bundle install
|
29
|
-
- run: bundle install
|
30
|
-
if: ${{ steps.release.outputs.release_created }}
|
31
|
-
# Publish
|
32
|
-
- name: publish gem
|
33
|
-
run: |
|
34
|
-
mkdir -p $HOME/.gem
|
35
|
-
touch $HOME/.gem/credentials
|
36
|
-
chmod 0600 $HOME/.gem/credentials
|
37
|
-
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
38
|
-
gem build *.gemspec
|
39
|
-
gem push *.gem
|
40
|
-
env:
|
41
|
-
# Make sure to update the secret name
|
42
|
-
# if yours isn't named RUBYGEMS_AUTH_TOKEN
|
43
|
-
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
44
|
-
if: ${{ steps.release.outputs.release_created }}
|