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 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