flipper 0.12.2 → 0.13.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/Changelog.md +10 -0
- data/Gemfile +1 -0
- data/README.md +2 -2
- data/lib/flipper/adapters/operation_logger.rb +14 -2
- data/lib/flipper/adapters/pstore.rb +5 -2
- data/lib/flipper/adapters/sync.rb +96 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +117 -0
- data/lib/flipper/adapters/sync/interval_synchronizer.rb +53 -0
- data/lib/flipper/adapters/sync/synchronizer.rb +48 -0
- data/lib/flipper/errors.rb +8 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +2 -0
- data/lib/flipper/test/shared_adapter_test.rb +2 -0
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +187 -0
- data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +34 -0
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +35 -0
- data/spec/flipper/adapters/sync_spec.rb +197 -0
- metadata +17 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec6d60d96a353fad27ec23dc10fc62957d0fe5df
|
4
|
+
data.tar.gz: 1c4c0bd098d95eee25f06f745b4e4a8918bbb953
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ec56237a868e32172a0c1a5f25494d1ab13d959101db28dc4db58c8bb994044fa1a8e4907335310c08fc1189fbeb0a61b484808e0ea582785bd3fe2719240c6
|
7
|
+
data.tar.gz: 033d132c8542bda8abd4d716a7debd2a42cc4e35398fa6063abd7f0b7281b00379b212cd373d6fce619624cce10884d9bc323396842c9f00c0413f6f48d949a9
|
data/.rubocop.yml
CHANGED
data/Changelog.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## master
|
2
|
+
|
3
|
+
### Additions/Changes
|
4
|
+
|
5
|
+
* Update PStore adapter to allow setting thread_safe option (https://github.com/jnunemaker/flipper/pull/334).
|
6
|
+
* Update Flipper::UI to Bootstrap 4 (https://github.com/jnunemaker/flipper/pull/336).
|
7
|
+
* Add Flipper::UI configuration to add a banner with customizeable text and background color (https://github.com/jnunemaker/flipper/pull/337).
|
8
|
+
* Add sync adapter (https://github.com/jnunemaker/flipper/pull/341).
|
9
|
+
* Make cloud use sync adapter (https://github.com/jnunemaker/flipper/pull/342). This makes local flipper operations resilient to cloud failures.
|
10
|
+
|
1
11
|
## 0.12.2
|
2
12
|
|
3
13
|
### Additions/Changes
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
![flipper logo](https://raw.githubusercontent.com/jnunemaker/flipper/master/lib/flipper/ui/public/images/logo.png)
|
2
|
-
|
3
1
|
<pre>
|
4
2
|
__
|
5
3
|
_.-~ )
|
@@ -105,4 +103,6 @@ Of course there are more [examples for you to peruse](examples/). You could also
|
|
105
103
|
|---|---|---|
|
106
104
|
| ![@jnunemaker](https://avatars3.githubusercontent.com/u/235?s=64) | [@jnunemaker](https://github.com/jnunemaker) | most things |
|
107
105
|
| ![@alexwheeler](https://avatars3.githubusercontent.com/u/3260042?s=64) | [@alexwheeler](https://github.com/alexwheeler) | api |
|
106
|
+
| ![@thetimbanks](https://avatars1.githubusercontent.com/u/471801?s=64) | [@thetimbanks](https://github.com/thetimbanks) | ui |
|
108
107
|
| ![@lazebny](https://avatars1.githubusercontent.com/u/6276766?s=64) | [@lazebny](https://github.com/lazebny) | docker |
|
108
|
+
|
@@ -8,7 +8,14 @@ module Flipper
|
|
8
8
|
class OperationLogger < SimpleDelegator
|
9
9
|
include ::Flipper::Adapter
|
10
10
|
|
11
|
-
Operation
|
11
|
+
class Operation
|
12
|
+
attr_reader :type, :args
|
13
|
+
|
14
|
+
def initialize(type, args)
|
15
|
+
@type = type
|
16
|
+
@args = args
|
17
|
+
end
|
18
|
+
end
|
12
19
|
|
13
20
|
OperationTypes = [
|
14
21
|
:features,
|
@@ -93,7 +100,12 @@ module Flipper
|
|
93
100
|
|
94
101
|
# Public: Count the number of times a certain operation happened.
|
95
102
|
def count(type)
|
96
|
-
|
103
|
+
type(type).size
|
104
|
+
end
|
105
|
+
|
106
|
+
# Public: Get all operations of a certain type.
|
107
|
+
def type(type)
|
108
|
+
@operations.select { |operation| operation.type == type }
|
97
109
|
end
|
98
110
|
|
99
111
|
# Public: Get the last operation of a certain type.
|
@@ -16,10 +16,13 @@ module Flipper
|
|
16
16
|
# Public: The path to where the file is stored.
|
17
17
|
attr_reader :path
|
18
18
|
|
19
|
+
# Public: PStore's thread_safe option.
|
20
|
+
attr_reader :thread_safe
|
21
|
+
|
19
22
|
# Public
|
20
|
-
def initialize(path = 'flipper.pstore')
|
23
|
+
def initialize(path = 'flipper.pstore', thread_safe = false)
|
21
24
|
@path = path
|
22
|
-
@store = ::PStore.new(path)
|
25
|
+
@store = ::PStore.new(path, thread_safe)
|
23
26
|
@name = :pstore
|
24
27
|
end
|
25
28
|
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "flipper/instrumenters/noop"
|
2
|
+
require "flipper/adapters/sync/synchronizer"
|
3
|
+
require "flipper/adapters/sync/interval_synchronizer"
|
4
|
+
|
5
|
+
module Flipper
|
6
|
+
module Adapters
|
7
|
+
# TODO: Syncing should happen in a background thread on a regular interval
|
8
|
+
# rather than in the main thread only when reads happen.
|
9
|
+
class Sync
|
10
|
+
include ::Flipper::Adapter
|
11
|
+
|
12
|
+
# Public: The name of the adapter.
|
13
|
+
attr_reader :name
|
14
|
+
|
15
|
+
# Public: The synchronizer that will keep the local and remote in sync.
|
16
|
+
attr_reader :synchronizer
|
17
|
+
|
18
|
+
# Public: Build a new sync instance.
|
19
|
+
#
|
20
|
+
# local - The local flipper adapter that should serve reads.
|
21
|
+
# remote - The remote flipper adpater that should serve writes and update
|
22
|
+
# the local on an interval.
|
23
|
+
# interval - The number of milliseconds between syncs from remote to
|
24
|
+
# local. Default value is set in IntervalSynchronizer.
|
25
|
+
def initialize(local, remote, options = {})
|
26
|
+
@name = :sync
|
27
|
+
@local = local
|
28
|
+
@remote = remote
|
29
|
+
@synchronizer = options.fetch(:synchronizer) do
|
30
|
+
instrumenter = options[:instrumenter]
|
31
|
+
sync_options = {}
|
32
|
+
sync_options[:instrumenter] = instrumenter if instrumenter
|
33
|
+
synchronizer = Synchronizer.new(@local, @remote, sync_options)
|
34
|
+
IntervalSynchronizer.new(synchronizer, interval: options[:interval])
|
35
|
+
end
|
36
|
+
sync
|
37
|
+
end
|
38
|
+
|
39
|
+
def features
|
40
|
+
sync
|
41
|
+
@local.features
|
42
|
+
end
|
43
|
+
|
44
|
+
def get(feature)
|
45
|
+
sync
|
46
|
+
@local.get(feature)
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_multi(features)
|
50
|
+
sync
|
51
|
+
@local.get_multi(features)
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_all
|
55
|
+
sync
|
56
|
+
@local.get_all
|
57
|
+
end
|
58
|
+
|
59
|
+
def add(feature)
|
60
|
+
result = @remote.add(feature)
|
61
|
+
@local.add(feature)
|
62
|
+
result
|
63
|
+
end
|
64
|
+
|
65
|
+
def remove(feature)
|
66
|
+
result = @remote.remove(feature)
|
67
|
+
@local.remove(feature)
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
def clear(feature)
|
72
|
+
result = @remote.clear(feature)
|
73
|
+
@local.clear(feature)
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
def enable(feature, gate, thing)
|
78
|
+
result = @remote.enable(feature, gate, thing)
|
79
|
+
@local.enable(feature, gate, thing)
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
def disable(feature, gate, thing)
|
84
|
+
result = @remote.disable(feature, gate, thing)
|
85
|
+
@local.disable(feature, gate, thing)
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def sync
|
92
|
+
@synchronizer.call
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require "flipper/actor"
|
2
|
+
require "flipper/gate_values"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Adapters
|
6
|
+
class Sync
|
7
|
+
# Internal: Given a feature, local gate values and remote gate values,
|
8
|
+
# makes the local equal to the remote.
|
9
|
+
class FeatureSynchronizer
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def_delegator :@local_gate_values, :boolean, :local_boolean
|
13
|
+
def_delegator :@local_gate_values, :actors, :local_actors
|
14
|
+
def_delegator :@local_gate_values, :groups, :local_groups
|
15
|
+
def_delegator :@local_gate_values, :percentage_of_actors,
|
16
|
+
:local_percentage_of_actors
|
17
|
+
def_delegator :@local_gate_values, :percentage_of_time,
|
18
|
+
:local_percentage_of_time
|
19
|
+
|
20
|
+
def_delegator :@remote_gate_values, :boolean, :remote_boolean
|
21
|
+
def_delegator :@remote_gate_values, :actors, :remote_actors
|
22
|
+
def_delegator :@remote_gate_values, :groups, :remote_groups
|
23
|
+
def_delegator :@remote_gate_values, :percentage_of_actors,
|
24
|
+
:remote_percentage_of_actors
|
25
|
+
def_delegator :@remote_gate_values, :percentage_of_time,
|
26
|
+
:remote_percentage_of_time
|
27
|
+
|
28
|
+
def initialize(feature, local_gate_values, remote_gate_values)
|
29
|
+
@feature = feature
|
30
|
+
@local_gate_values = local_gate_values
|
31
|
+
@remote_gate_values = remote_gate_values
|
32
|
+
end
|
33
|
+
|
34
|
+
def call
|
35
|
+
if remote_disabled?
|
36
|
+
return if local_disabled?
|
37
|
+
@feature.disable
|
38
|
+
elsif remote_boolean_enabled?
|
39
|
+
return if local_boolean_enabled?
|
40
|
+
@feature.enable
|
41
|
+
else
|
42
|
+
sync_actors
|
43
|
+
sync_groups
|
44
|
+
sync_percentage_of_actors
|
45
|
+
sync_percentage_of_time
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def sync_actors
|
52
|
+
remote_actors_added = remote_actors - local_actors
|
53
|
+
remote_actors_added.each do |flipper_id|
|
54
|
+
@feature.enable_actor Actor.new(flipper_id)
|
55
|
+
end
|
56
|
+
|
57
|
+
remote_actors_removed = local_actors - remote_actors
|
58
|
+
remote_actors_removed.each do |flipper_id|
|
59
|
+
@feature.disable_actor Actor.new(flipper_id)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def sync_groups
|
64
|
+
remote_groups_added = remote_groups - local_groups
|
65
|
+
remote_groups_added.each do |group_name|
|
66
|
+
@feature.enable_group group_name
|
67
|
+
end
|
68
|
+
|
69
|
+
remote_groups_removed = local_groups - remote_groups
|
70
|
+
remote_groups_removed.each do |group_name|
|
71
|
+
@feature.disable_group group_name
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def sync_percentage_of_actors
|
76
|
+
return if local_percentage_of_actors == remote_percentage_of_actors
|
77
|
+
|
78
|
+
@feature.enable_percentage_of_actors remote_percentage_of_actors
|
79
|
+
end
|
80
|
+
|
81
|
+
def sync_percentage_of_time
|
82
|
+
return if local_percentage_of_time == remote_percentage_of_time
|
83
|
+
|
84
|
+
@feature.enable_percentage_of_time remote_percentage_of_time
|
85
|
+
end
|
86
|
+
|
87
|
+
def default_config
|
88
|
+
@default_config ||= @feature.adapter.default_config
|
89
|
+
end
|
90
|
+
|
91
|
+
def default_gate_values
|
92
|
+
@default_gate_values ||= GateValues.new(default_config)
|
93
|
+
end
|
94
|
+
|
95
|
+
def default_gate_values?(gate_values)
|
96
|
+
gate_values == default_gate_values
|
97
|
+
end
|
98
|
+
|
99
|
+
def local_disabled?
|
100
|
+
default_gate_values? @local_gate_values
|
101
|
+
end
|
102
|
+
|
103
|
+
def remote_disabled?
|
104
|
+
default_gate_values? @remote_gate_values
|
105
|
+
end
|
106
|
+
|
107
|
+
def local_boolean_enabled?
|
108
|
+
local_boolean
|
109
|
+
end
|
110
|
+
|
111
|
+
def remote_boolean_enabled?
|
112
|
+
remote_boolean
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Adapters
|
3
|
+
class Sync
|
4
|
+
# Internal: Wraps a Synchronizer instance and only invokes it every
|
5
|
+
# N milliseconds.
|
6
|
+
class IntervalSynchronizer
|
7
|
+
# Private: Number of milliseconds between syncs (default: 10 seconds).
|
8
|
+
DEFAULT_INTERVAL_MS = 10_000
|
9
|
+
|
10
|
+
# Private
|
11
|
+
def self.now_ms
|
12
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Public: The number of milliseconds between invocations of the
|
16
|
+
# wrapped synchronizer.
|
17
|
+
attr_reader :interval
|
18
|
+
|
19
|
+
# Public: Initializes a new interval synchronizer.
|
20
|
+
#
|
21
|
+
# synchronizer - The Synchronizer to call when the interval has passed.
|
22
|
+
# interval - The Integer number of milliseconds between invocations of
|
23
|
+
# the wrapped synchronizer.
|
24
|
+
def initialize(synchronizer, interval: nil)
|
25
|
+
@synchronizer = synchronizer
|
26
|
+
@interval = interval || DEFAULT_INTERVAL_MS
|
27
|
+
# TODO: add jitter to this so all processes booting at the same time
|
28
|
+
# don't phone home at the same time.
|
29
|
+
@last_sync_at = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def call
|
33
|
+
return unless time_to_sync?
|
34
|
+
|
35
|
+
@last_sync_at = now_ms
|
36
|
+
@synchronizer.call
|
37
|
+
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def time_to_sync?
|
44
|
+
(now_ms - @last_sync_at) >= @interval
|
45
|
+
end
|
46
|
+
|
47
|
+
def now_ms
|
48
|
+
self.class.now_ms
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "flipper/feature"
|
2
|
+
require "flipper/gate_values"
|
3
|
+
require "flipper/instrumenters/noop"
|
4
|
+
require "flipper/adapters/sync/feature_synchronizer"
|
5
|
+
|
6
|
+
module Flipper
|
7
|
+
module Adapters
|
8
|
+
class Sync
|
9
|
+
# Internal: Given a local and remote adapter, it can update the local to
|
10
|
+
# match the remote doing only the necessary enable/disable operations.
|
11
|
+
class Synchronizer
|
12
|
+
def initialize(local, remote, options = {})
|
13
|
+
@local = local
|
14
|
+
@remote = remote
|
15
|
+
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
@instrumenter.instrument("synchronizer_call.flipper") { sync }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def sync
|
25
|
+
local_get_all = @local.get_all
|
26
|
+
# TODO: Move remote get all to background thread to minimize impact to
|
27
|
+
# whatever is happening in main thread.
|
28
|
+
remote_get_all = @remote.get_all
|
29
|
+
|
30
|
+
# Sync all the gate values.
|
31
|
+
remote_get_all.each do |feature_key, remote_gates_hash|
|
32
|
+
feature = Feature.new(feature_key, @local)
|
33
|
+
local_gates_hash = local_get_all[feature_key] || @local.default_config
|
34
|
+
local_gate_values = GateValues.new(local_gates_hash)
|
35
|
+
remote_gate_values = GateValues.new(remote_gates_hash)
|
36
|
+
FeatureSynchronizer.new(feature, local_gate_values, remote_gate_values).call
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add features that are missing
|
40
|
+
features_to_add = remote_get_all.keys - local_get_all.keys
|
41
|
+
features_to_add.each { |key| Feature.new(key, @local).add }
|
42
|
+
rescue => exception
|
43
|
+
@instrumenter.instrument("synchronizer_exception.flipper", exception: exception)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/flipper/errors.rb
CHANGED
@@ -21,4 +21,12 @@ module Flipper
|
|
21
21
|
super(message || default)
|
22
22
|
end
|
23
23
|
end
|
24
|
+
|
25
|
+
# Raised when an invalid value is set to a configuration property
|
26
|
+
class InvalidConfigurationValue < Flipper::Error
|
27
|
+
def initialize(message = nil)
|
28
|
+
default = "Configuration value is not valid."
|
29
|
+
super(message || default)
|
30
|
+
end
|
31
|
+
end
|
24
32
|
end
|
@@ -272,10 +272,12 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
272
272
|
actor = Flipper::Actor.new('Flipper::Actor;22')
|
273
273
|
expect(subject.enable(feature, actor_gate, flipper.actor(actor))).to eq(true)
|
274
274
|
expect(subject.enable(feature, actor_gate, flipper.actor(actor))).to eq(true)
|
275
|
+
expect(subject.get(feature).fetch(:actors)).to eq(Set['Flipper::Actor;22'])
|
275
276
|
end
|
276
277
|
|
277
278
|
it 'can double enable a group without error' do
|
278
279
|
expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true)
|
279
280
|
expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true)
|
281
|
+
expect(subject.get(feature).fetch(:groups)).to eq(Set['admins'])
|
280
282
|
end
|
281
283
|
end
|
@@ -267,11 +267,13 @@ module Flipper
|
|
267
267
|
actor = Flipper::Actor.new('Flipper::Actor;22')
|
268
268
|
assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor))
|
269
269
|
assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor))
|
270
|
+
assert_equal Set['Flipper::Actor;22'], @adapter.get(@feature).fetch(:actors)
|
270
271
|
end
|
271
272
|
|
272
273
|
def test_can_double_enable_a_group_without_error
|
273
274
|
assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins))
|
274
275
|
assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins))
|
276
|
+
assert_equal Set['admins'], @adapter.get(@feature).fetch(:groups)
|
275
277
|
end
|
276
278
|
end
|
277
279
|
end
|
data/lib/flipper/version.rb
CHANGED
@@ -0,0 +1,187 @@
|
|
1
|
+
require "helper"
|
2
|
+
require "flipper/adapters/memory"
|
3
|
+
require "flipper/adapters/operation_logger"
|
4
|
+
require "flipper/adapters/sync/feature_synchronizer"
|
5
|
+
|
6
|
+
RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
|
7
|
+
let(:adapter) do
|
8
|
+
Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
|
9
|
+
end
|
10
|
+
let(:flipper) { Flipper.new(adapter) }
|
11
|
+
let(:feature) { flipper[:search] }
|
12
|
+
|
13
|
+
context "when remote disabled" do
|
14
|
+
let(:remote) { Flipper::GateValues.new({}) }
|
15
|
+
|
16
|
+
it "does nothing if local is disabled" do
|
17
|
+
feature.disable
|
18
|
+
adapter.reset
|
19
|
+
|
20
|
+
described_class.new(feature, feature.gate_values, remote).call
|
21
|
+
|
22
|
+
expect(adapter.get(feature).fetch(:boolean)).to be(nil)
|
23
|
+
expect_no_enable_or_disable
|
24
|
+
end
|
25
|
+
|
26
|
+
it "disables if local is enabled" do
|
27
|
+
feature.enable
|
28
|
+
adapter.reset
|
29
|
+
|
30
|
+
described_class.new(feature, feature.gate_values, remote).call
|
31
|
+
|
32
|
+
expect(adapter.get(feature).fetch(:boolean)).to be(nil)
|
33
|
+
expect_only_disable
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when remote boolean enabled" do
|
38
|
+
let(:remote) { Flipper::GateValues.new(boolean: true) }
|
39
|
+
|
40
|
+
it "does nothing if local boolean enabled" do
|
41
|
+
feature.enable
|
42
|
+
adapter.reset
|
43
|
+
|
44
|
+
described_class.new(feature, feature.gate_values, remote).call
|
45
|
+
expect(feature.boolean_value).to be(true)
|
46
|
+
expect_no_enable_or_disable
|
47
|
+
end
|
48
|
+
|
49
|
+
it "enables if local is disabled" do
|
50
|
+
feature.disable
|
51
|
+
adapter.reset
|
52
|
+
|
53
|
+
described_class.new(feature, feature.gate_values, remote).call
|
54
|
+
expect(feature.boolean_value).to be(true)
|
55
|
+
expect_only_enable
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when conditionally enabled" do
|
60
|
+
it "adds remotely added actors" do
|
61
|
+
remote = Flipper::GateValues.new(actors: Set["1", "2"])
|
62
|
+
feature.enable_actor(Flipper::Actor.new("1"))
|
63
|
+
adapter.reset
|
64
|
+
|
65
|
+
described_class.new(feature, feature.gate_values, remote).call
|
66
|
+
|
67
|
+
expect(feature.actors_value).to eq(Set["1", "2"])
|
68
|
+
expect_only_enable
|
69
|
+
end
|
70
|
+
|
71
|
+
it "removes remotely removed actors" do
|
72
|
+
remote = Flipper::GateValues.new(actors: Set["1"])
|
73
|
+
feature.enable_actor(Flipper::Actor.new("1"))
|
74
|
+
feature.enable_actor(Flipper::Actor.new("2"))
|
75
|
+
adapter.reset
|
76
|
+
|
77
|
+
described_class.new(feature, feature.gate_values, remote).call
|
78
|
+
|
79
|
+
expect(feature.actors_value).to eq(Set["1"])
|
80
|
+
expect_only_disable
|
81
|
+
end
|
82
|
+
|
83
|
+
it "does nothing to actors if in sync" do
|
84
|
+
remote = Flipper::GateValues.new(actors: Set["1"])
|
85
|
+
feature.enable_actor(Flipper::Actor.new("1"))
|
86
|
+
adapter.reset
|
87
|
+
|
88
|
+
described_class.new(feature, feature.gate_values, remote).call
|
89
|
+
|
90
|
+
expect(feature.actors_value).to eq(Set["1"])
|
91
|
+
expect_no_enable_or_disable
|
92
|
+
end
|
93
|
+
|
94
|
+
it "adds remotely added groups" do
|
95
|
+
remote = Flipper::GateValues.new(groups: Set["staff", "early_access"])
|
96
|
+
feature.enable_group(:staff)
|
97
|
+
adapter.reset
|
98
|
+
|
99
|
+
described_class.new(feature, feature.gate_values, remote).call
|
100
|
+
|
101
|
+
expect(feature.groups_value).to eq(Set["staff", "early_access"])
|
102
|
+
expect_only_enable
|
103
|
+
end
|
104
|
+
|
105
|
+
it "removes remotely removed groups" do
|
106
|
+
remote = Flipper::GateValues.new(groups: Set["staff"])
|
107
|
+
feature.enable_group(:staff)
|
108
|
+
feature.enable_group(:early_access)
|
109
|
+
adapter.reset
|
110
|
+
|
111
|
+
described_class.new(feature, feature.gate_values, remote).call
|
112
|
+
|
113
|
+
expect(feature.groups_value).to eq(Set["staff"])
|
114
|
+
expect_only_disable
|
115
|
+
end
|
116
|
+
|
117
|
+
it "does nothing if groups in sync" do
|
118
|
+
remote = Flipper::GateValues.new(groups: Set["staff"])
|
119
|
+
feature.enable_group(:staff)
|
120
|
+
adapter.reset
|
121
|
+
|
122
|
+
described_class.new(feature, feature.gate_values, remote).call
|
123
|
+
|
124
|
+
expect(feature.groups_value).to eq(Set["staff"])
|
125
|
+
expect_no_enable_or_disable
|
126
|
+
end
|
127
|
+
|
128
|
+
it "updates percentage of actors when remote is updated" do
|
129
|
+
remote = Flipper::GateValues.new(percentage_of_actors: 25)
|
130
|
+
feature.enable_percentage_of_actors(10)
|
131
|
+
adapter.reset
|
132
|
+
|
133
|
+
described_class.new(feature, feature.gate_values, remote).call
|
134
|
+
|
135
|
+
expect(feature.percentage_of_actors_value).to be(25)
|
136
|
+
expect_only_enable
|
137
|
+
end
|
138
|
+
|
139
|
+
it "does nothing if local percentage of actors matches remote" do
|
140
|
+
remote = Flipper::GateValues.new(percentage_of_actors: 25)
|
141
|
+
feature.enable_percentage_of_actors(25)
|
142
|
+
adapter.reset
|
143
|
+
|
144
|
+
described_class.new(feature, feature.gate_values, remote).call
|
145
|
+
|
146
|
+
expect(feature.percentage_of_actors_value).to be(25)
|
147
|
+
expect_no_enable_or_disable
|
148
|
+
end
|
149
|
+
|
150
|
+
it "updates percentage of time when remote is updated" do
|
151
|
+
remote = Flipper::GateValues.new(percentage_of_time: 25)
|
152
|
+
feature.enable_percentage_of_time(10)
|
153
|
+
adapter.reset
|
154
|
+
|
155
|
+
described_class.new(feature, feature.gate_values, remote).call
|
156
|
+
|
157
|
+
expect(feature.percentage_of_time_value).to be(25)
|
158
|
+
expect_only_enable
|
159
|
+
end
|
160
|
+
|
161
|
+
it "does nothing if local percentage of time matches remote" do
|
162
|
+
remote = Flipper::GateValues.new(percentage_of_time: 25)
|
163
|
+
feature.enable_percentage_of_time(25)
|
164
|
+
adapter.reset
|
165
|
+
|
166
|
+
described_class.new(feature, feature.gate_values, remote).call
|
167
|
+
|
168
|
+
expect(feature.percentage_of_time_value).to be(25)
|
169
|
+
expect_no_enable_or_disable
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def expect_no_enable_or_disable
|
174
|
+
expect(adapter.count(:enable)).to be(0)
|
175
|
+
expect(adapter.count(:disable)).to be(0)
|
176
|
+
end
|
177
|
+
|
178
|
+
def expect_only_enable
|
179
|
+
expect(adapter.count(:enable)).to be(1)
|
180
|
+
expect(adapter.count(:disable)).to be(0)
|
181
|
+
end
|
182
|
+
|
183
|
+
def expect_only_disable
|
184
|
+
expect(adapter.count(:enable)).to be(0)
|
185
|
+
expect(adapter.count(:disable)).to be(1)
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "helper"
|
2
|
+
require "flipper/adapters/sync/interval_synchronizer"
|
3
|
+
|
4
|
+
RSpec.describe Flipper::Adapters::Sync::IntervalSynchronizer do
|
5
|
+
let(:events) { [] }
|
6
|
+
let(:synchronizer) { -> { events << described_class.now_ms } }
|
7
|
+
let(:interval) { 10 }
|
8
|
+
|
9
|
+
subject { described_class.new(synchronizer, interval: interval) }
|
10
|
+
|
11
|
+
it 'synchronizes on first call' do
|
12
|
+
expect(events.size).to be(0)
|
13
|
+
subject.call
|
14
|
+
expect(events.size).to be(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "only invokes wrapped synchronizer every interval milliseconds" do
|
18
|
+
now = described_class.now_ms
|
19
|
+
subject.call
|
20
|
+
events.clear
|
21
|
+
|
22
|
+
# move time to one millisecond less than last sync + interval
|
23
|
+
1.upto(interval) do |i|
|
24
|
+
allow(described_class).to receive(:now_ms).and_return(now + i - 1)
|
25
|
+
subject.call
|
26
|
+
end
|
27
|
+
expect(events.size).to be(0)
|
28
|
+
|
29
|
+
# move time to last sync + interval
|
30
|
+
allow(described_class).to receive(:now_ms).and_return(now + interval)
|
31
|
+
subject.call
|
32
|
+
expect(events.size).to be(1)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "helper"
|
2
|
+
require "flipper/adapters/memory"
|
3
|
+
require "flipper/instrumenters/memory"
|
4
|
+
require "flipper/adapters/sync/synchronizer"
|
5
|
+
|
6
|
+
RSpec.describe Flipper::Adapters::Sync::Synchronizer do
|
7
|
+
let(:local) { Flipper::Adapters::Memory.new }
|
8
|
+
let(:remote) { Flipper::Adapters::Memory.new }
|
9
|
+
let(:instrumenter) { Flipper::Instrumenters::Memory.new }
|
10
|
+
|
11
|
+
subject { described_class.new(local, remote, instrumenter: instrumenter) }
|
12
|
+
|
13
|
+
it "instruments call" do
|
14
|
+
subject.call
|
15
|
+
events = instrumenter.events.select do |event|
|
16
|
+
event.name == "synchronizer_call.flipper"
|
17
|
+
end
|
18
|
+
expect(events.size).to be(1)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "does not raise, but instruments exceptions for visibility" do
|
22
|
+
exception = StandardError.new
|
23
|
+
expect(remote).to receive(:get_all).and_raise(exception)
|
24
|
+
|
25
|
+
expect { subject.call }.not_to raise_error
|
26
|
+
|
27
|
+
events = instrumenter.events.select do |event|
|
28
|
+
event.name == "synchronizer_exception.flipper"
|
29
|
+
end
|
30
|
+
expect(events.size).to be(1)
|
31
|
+
|
32
|
+
event = events[0]
|
33
|
+
expect(event.payload[:exception]).to eq(exception)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'flipper/adapters/sync'
|
3
|
+
require 'flipper/adapters/memory'
|
4
|
+
require 'flipper/adapters/operation_logger'
|
5
|
+
require 'flipper/spec/shared_adapter_specs'
|
6
|
+
require 'active_support/notifications'
|
7
|
+
|
8
|
+
RSpec.describe Flipper::Adapters::Sync do
|
9
|
+
let(:local_adapter) do
|
10
|
+
Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
|
11
|
+
end
|
12
|
+
let(:remote_adapter) do
|
13
|
+
Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
|
14
|
+
end
|
15
|
+
let(:local) { Flipper.new(local_adapter) }
|
16
|
+
let(:remote) { Flipper.new(remote_adapter) }
|
17
|
+
let(:sync) { Flipper.new(subject) }
|
18
|
+
|
19
|
+
subject do
|
20
|
+
described_class.new(local_adapter, remote_adapter, interval: 1)
|
21
|
+
end
|
22
|
+
|
23
|
+
it_should_behave_like 'a flipper adapter'
|
24
|
+
|
25
|
+
context 'when local has never been synced' do
|
26
|
+
it 'syncs boolean' do
|
27
|
+
remote.enable(:search)
|
28
|
+
expect(sync[:search].boolean_value).to be(true)
|
29
|
+
expect(subject.features.sort).to eq(%w(search))
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'syncs actor' do
|
33
|
+
actor = Flipper::Actor.new("User;1000")
|
34
|
+
remote.enable_actor(:search, actor)
|
35
|
+
expect(sync[:search].actors_value).to eq(Set[actor.flipper_id])
|
36
|
+
expect(subject.features.sort).to eq(%w(search))
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'syncs group' do
|
40
|
+
remote.enable_group(:search, :staff)
|
41
|
+
expect(sync[:search].groups_value).to eq(Set["staff"])
|
42
|
+
expect(subject.features.sort).to eq(%w(search))
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'syncs percentage of actors' do
|
46
|
+
remote.enable_percentage_of_actors(:search, 25)
|
47
|
+
expect(sync[:search].percentage_of_actors_value).to eq(25)
|
48
|
+
expect(subject.features.sort).to eq(%w(search))
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'syncs percentage of time' do
|
52
|
+
remote.enable_percentage_of_time(:search, 15)
|
53
|
+
expect(sync[:search].percentage_of_time_value).to eq(15)
|
54
|
+
expect(subject.features.sort).to eq(%w(search))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'enables boolean locally when remote feature boolean enabled' do
|
59
|
+
remote.disable(:search)
|
60
|
+
local.disable(:search)
|
61
|
+
remote.enable(:search)
|
62
|
+
subject # initialize forces sync
|
63
|
+
expect(local[:search].boolean_value).to be(true)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'disables boolean locally when remote feature disabled' do
|
67
|
+
remote.enable(:search)
|
68
|
+
local.enable(:search)
|
69
|
+
remote.disable(:search)
|
70
|
+
subject # initialize forces sync
|
71
|
+
expect(local[:search].boolean_value).to be(false)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'adds local actor when remote actor is added' do
|
75
|
+
actor = Flipper::Actor.new("User;235")
|
76
|
+
remote.enable_actor(:search, actor)
|
77
|
+
subject # initialize forces sync
|
78
|
+
expect(local[:search].actors_value).to eq(Set[actor.flipper_id])
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'removes local actor when remote actor is removed' do
|
82
|
+
actor = Flipper::Actor.new("User;235")
|
83
|
+
remote.enable_actor(:search, actor)
|
84
|
+
local.enable_actor(:search, actor)
|
85
|
+
remote.disable(:search, actor)
|
86
|
+
subject # initialize forces sync
|
87
|
+
expect(local[:search].actors_value).to eq(Set.new)
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'adds local group when remote group is added' do
|
91
|
+
group = Flipper::Types::Group.new(:staff)
|
92
|
+
remote.enable_group(:search, group)
|
93
|
+
subject # initialize forces sync
|
94
|
+
expect(local[:search].groups_value).to eq(Set["staff"])
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'removes local group when remote group is removed' do
|
98
|
+
group = Flipper::Types::Group.new(:staff)
|
99
|
+
remote.enable_group(:search, group)
|
100
|
+
local.enable_group(:search, group)
|
101
|
+
remote.disable(:search, group)
|
102
|
+
subject # initialize forces sync
|
103
|
+
expect(local[:search].groups_value).to eq(Set.new)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'updates percentage of actors when remote is updated' do
|
107
|
+
remote.enable_percentage_of_actors(:search, 10)
|
108
|
+
local.enable_percentage_of_actors(:search, 10)
|
109
|
+
remote.enable_percentage_of_actors(:search, 15)
|
110
|
+
subject # initialize forces sync
|
111
|
+
expect(local[:search].percentage_of_actors_value).to eq(15)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'updates percentage of time when remote is updated' do
|
115
|
+
remote.enable_percentage_of_time(:search, 10)
|
116
|
+
local.enable_percentage_of_time(:search, 10)
|
117
|
+
remote.enable_percentage_of_time(:search, 15)
|
118
|
+
subject # initialize forces sync
|
119
|
+
expect(local[:search].percentage_of_time_value).to eq(15)
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'when local and remote match' do
|
123
|
+
it 'does not update boolean enabled' do
|
124
|
+
local.enable(:search)
|
125
|
+
remote.enable(:search)
|
126
|
+
local_adapter.reset
|
127
|
+
subject # initialize forces sync
|
128
|
+
expect(local_adapter.count(:enable)).to be(0)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'does not update boolean disabled' do
|
132
|
+
local.disable(:search)
|
133
|
+
remote.disable(:search)
|
134
|
+
local_adapter.reset
|
135
|
+
subject # initialize forces sync
|
136
|
+
expect(local_adapter.count(:disable)).to be(0)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'does not update actors' do
|
140
|
+
actor = Flipper::Actor.new("User;235")
|
141
|
+
local.enable_actor(:search, actor)
|
142
|
+
remote.enable_actor(:search, actor)
|
143
|
+
local_adapter.reset
|
144
|
+
subject # initialize forces sync
|
145
|
+
expect(local_adapter.count(:enable)).to be(0)
|
146
|
+
expect(local_adapter.count(:disable)).to be(0)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'does not update groups' do
|
150
|
+
group = Flipper::Types::Group.new(:staff)
|
151
|
+
local.enable_group(:search, group)
|
152
|
+
remote.enable_group(:search, group)
|
153
|
+
local_adapter.reset
|
154
|
+
subject # initialize forces sync
|
155
|
+
expect(local_adapter.count(:enable)).to be(0)
|
156
|
+
expect(local_adapter.count(:disable)).to be(0)
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'does not update percentage of actors' do
|
160
|
+
local.enable_percentage_of_actors(:search, 10)
|
161
|
+
remote.enable_percentage_of_actors(:search, 10)
|
162
|
+
local_adapter.reset
|
163
|
+
subject # initialize forces sync
|
164
|
+
expect(local_adapter.count(:enable)).to be(0)
|
165
|
+
expect(local_adapter.count(:disable)).to be(0)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'does not update percentage of time' do
|
169
|
+
local.enable_percentage_of_time(:search, 10)
|
170
|
+
remote.enable_percentage_of_time(:search, 10)
|
171
|
+
local_adapter.reset
|
172
|
+
subject # initialize forces sync
|
173
|
+
expect(local_adapter.count(:enable)).to be(0)
|
174
|
+
expect(local_adapter.count(:disable)).to be(0)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'synchronizes for #features' do
|
179
|
+
expect(subject).to receive(:sync)
|
180
|
+
subject.features
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'synchronizes for #get' do
|
184
|
+
expect(subject).to receive(:sync)
|
185
|
+
subject.get sync[:search]
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'synchronizes for #get_multi' do
|
189
|
+
expect(subject).to receive(:sync)
|
190
|
+
subject.get_multi [sync[:search]]
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'synchronizes for #get_all' do
|
194
|
+
expect(subject).to receive(:sync)
|
195
|
+
subject.get_all
|
196
|
+
end
|
197
|
+
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.
|
4
|
+
version: 0.13.0.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Nunemaker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-03-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Feature flipper is the act of enabling/disabling features in your application,
|
14
14
|
ideally without re-deploying or changing anything in your code base. Flipper makes
|
@@ -67,6 +67,10 @@ files:
|
|
67
67
|
- lib/flipper/adapters/operation_logger.rb
|
68
68
|
- lib/flipper/adapters/pstore.rb
|
69
69
|
- lib/flipper/adapters/read_only.rb
|
70
|
+
- lib/flipper/adapters/sync.rb
|
71
|
+
- lib/flipper/adapters/sync/feature_synchronizer.rb
|
72
|
+
- lib/flipper/adapters/sync/interval_synchronizer.rb
|
73
|
+
- lib/flipper/adapters/sync/synchronizer.rb
|
70
74
|
- lib/flipper/configuration.rb
|
71
75
|
- lib/flipper/dsl.rb
|
72
76
|
- lib/flipper/errors.rb
|
@@ -109,6 +113,10 @@ files:
|
|
109
113
|
- spec/flipper/adapters/operation_logger_spec.rb
|
110
114
|
- spec/flipper/adapters/pstore_spec.rb
|
111
115
|
- spec/flipper/adapters/read_only_spec.rb
|
116
|
+
- spec/flipper/adapters/sync/feature_synchronizer_spec.rb
|
117
|
+
- spec/flipper/adapters/sync/interval_synchronizer_spec.rb
|
118
|
+
- spec/flipper/adapters/sync/synchronizer_spec.rb
|
119
|
+
- spec/flipper/adapters/sync_spec.rb
|
112
120
|
- spec/flipper/configuration_spec.rb
|
113
121
|
- spec/flipper/dsl_spec.rb
|
114
122
|
- spec/flipper/feature_check_context_spec.rb
|
@@ -158,12 +166,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
158
166
|
version: '0'
|
159
167
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
168
|
requirements:
|
161
|
-
- - "
|
169
|
+
- - ">"
|
162
170
|
- !ruby/object:Gem::Version
|
163
|
-
version:
|
171
|
+
version: 1.3.1
|
164
172
|
requirements: []
|
165
173
|
rubyforge_project:
|
166
|
-
rubygems_version: 2.
|
174
|
+
rubygems_version: 2.6.14
|
167
175
|
signing_key:
|
168
176
|
specification_version: 4
|
169
177
|
summary: Feature flipper for ANYTHING
|
@@ -178,6 +186,10 @@ test_files:
|
|
178
186
|
- spec/flipper/adapters/operation_logger_spec.rb
|
179
187
|
- spec/flipper/adapters/pstore_spec.rb
|
180
188
|
- spec/flipper/adapters/read_only_spec.rb
|
189
|
+
- spec/flipper/adapters/sync/feature_synchronizer_spec.rb
|
190
|
+
- spec/flipper/adapters/sync/interval_synchronizer_spec.rb
|
191
|
+
- spec/flipper/adapters/sync/synchronizer_spec.rb
|
192
|
+
- spec/flipper/adapters/sync_spec.rb
|
181
193
|
- spec/flipper/configuration_spec.rb
|
182
194
|
- spec/flipper/dsl_spec.rb
|
183
195
|
- spec/flipper/feature_check_context_spec.rb
|