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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 965343e60248399aab50206cf8f7186575780659
4
- data.tar.gz: 6cdcc01506fc5513c8cd29ac27665aaa298afa78
3
+ metadata.gz: ec6d60d96a353fad27ec23dc10fc62957d0fe5df
4
+ data.tar.gz: 1c4c0bd098d95eee25f06f745b4e4a8918bbb953
5
5
  SHA512:
6
- metadata.gz: f8bdbd7c2357b312d0ba05308319b02ef49e200fa051e349f632122da85b17f655bbdb8138c48bf2c605838bc6fcfd92c86870b17d29615213a850ace1238d5a
7
- data.tar.gz: 1fee068e18c2a5f1a328abc1c80cc0d6eb29ddb1eb89499958eb6938be87cb088b9e00f4919fef1b5d11d562634862d1b83fbcc75ff0834393b9350deb216aea
6
+ metadata.gz: 0ec56237a868e32172a0c1a5f25494d1ab13d959101db28dc4db58c8bb994044fa1a8e4907335310c08fc1189fbeb0a61b484808e0ea582785bd3fe2719240c6
7
+ data.tar.gz: 033d132c8542bda8abd4d716a7debd2a42cc4e35398fa6063abd7f0b7281b00379b212cd373d6fce619624cce10884d9bc323396842c9f00c0413f6f48d949a9
@@ -27,6 +27,12 @@ Style/NumericLiterals:
27
27
  Style/StringLiterals:
28
28
  Enabled: false
29
29
 
30
+ Style/GuardClause:
31
+ Enabled: false
32
+
33
+ Style/IfUnlessModifier:
34
+ Enabled: false
35
+
30
36
  Metrics/LineLength:
31
37
  Max: 100
32
38
  Exclude:
@@ -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
@@ -24,6 +24,7 @@ gem 'test-unit', '~> 3.0'
24
24
 
25
25
  group(:guard) do
26
26
  gem 'guard', '~> 2.12.5'
27
+ gem 'guard-rubocop', '~> 1.3.0'
27
28
  gem 'guard-rspec', '~> 4.5.0'
28
29
  gem 'guard-bundler', '~> 2.1.0'
29
30
  gem 'guard-coffeescript', '~> 2.0.1'
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 = Struct.new(:type, :args)
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
- @operations.select { |operation| operation.type == type }.size
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.12.2'.freeze
2
+ VERSION = '0.13.0.beta1'.freeze
3
3
  end
@@ -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.12.2
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-01-22 00:00:00.000000000 Z
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: '0'
171
+ version: 1.3.1
164
172
  requirements: []
165
173
  rubyforge_project:
166
- rubygems_version: 2.5.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