flipper 0.12.2 → 0.13.0.beta1
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/.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
|
-

|
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://github.com/jnunemaker) | most things |
|
107
105
|
|  | [@alexwheeler](https://github.com/alexwheeler) | api |
|
106
|
+
|  | [@thetimbanks](https://github.com/thetimbanks) | ui |
|
108
107
|
|  | [@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
|