flipper 0.26.0 → 0.26.2
Sign up to get free protection for your applications and to get access to all the features.
- 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 }}
|