rollout 2.3.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.md +8 -2
- data/lib/rollout.rb +53 -9
- data/lib/rollout/version.rb +1 -1
- data/spec/rollout_spec.rb +175 -7
- metadata +2 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 531e50e9914dd1ea745969469ae49b805fe583b7
|
4
|
+
data.tar.gz: 2c25094aa9935d7ce97263f0829072bcb728dffc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87983bb4671e964ea46e5e7c85f98b4a9e9254707c2a4775c7a24998a2480dc0faa07a67acb0f90c2cdab8a84530ffd41417fca763cfd9c4308060179f8ece78
|
7
|
+
data.tar.gz: fa7c1d4f2c9fc0202d0272f0750cf0ea9087a6bb375e692631bc9c4b2ea3c6e52e649a601a0c2bcef4549e38d7fcd5d683fd08711805a77db757b86d6531bab3
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -10,7 +10,7 @@ Feature flippers.
|
|
10
10
|
## MAKE SURE TO READ THIS: 2.X Changes and Migration Path
|
11
11
|
|
12
12
|
As of rollout-2.x, only one key is used per feature for performance reasons.
|
13
|
-
The
|
13
|
+
The serialized format is `percentage|user_id,user_id,...|group,_group...|data_json`. This has
|
14
14
|
the effect of making concurrent feature modifications unsafe, but in practice,
|
15
15
|
I doubt this will actually be a problem.
|
16
16
|
|
@@ -35,6 +35,12 @@ $redis = Redis.new
|
|
35
35
|
$rollout = Rollout.new($redis)
|
36
36
|
```
|
37
37
|
|
38
|
+
Update data specific to a feature:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
@rollout.set_feature_data(:chat, description: 'foo', release_date: 'bar', whatever: 'baz')
|
42
|
+
```
|
43
|
+
|
38
44
|
Check whether a feature is active for a particular user:
|
39
45
|
|
40
46
|
```ruby
|
@@ -103,7 +109,7 @@ $rollout.activate_percentage(:chat, 20)
|
|
103
109
|
The algorithm for determining which users get let in is this:
|
104
110
|
|
105
111
|
```ruby
|
106
|
-
CRC32(user.id) %
|
112
|
+
CRC32(user.id) % 100_000 < percentage * 1_000
|
107
113
|
```
|
108
114
|
|
109
115
|
So, for 20%, users 0, 1, 10, 11, 20, 21, etc would be allowed in. Those users
|
data/lib/rollout.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
require "rollout/version"
|
2
2
|
require "zlib"
|
3
3
|
require "set"
|
4
|
+
require "json"
|
4
5
|
|
5
6
|
class Rollout
|
6
7
|
class Feature
|
7
|
-
attr_accessor :groups, :users, :percentage
|
8
|
+
attr_accessor :groups, :users, :percentage, :data
|
8
9
|
attr_reader :name, :options
|
9
10
|
|
10
11
|
def initialize(name, string = nil, opts = {})
|
@@ -12,17 +13,18 @@ class Rollout
|
|
12
13
|
@name = name
|
13
14
|
|
14
15
|
if string
|
15
|
-
raw_percentage,raw_users,raw_groups = string.split(
|
16
|
+
raw_percentage,raw_users,raw_groups,raw_data = string.split('|', 4)
|
16
17
|
@percentage = raw_percentage.to_f
|
17
|
-
@users = (raw_users
|
18
|
-
@groups = (raw_groups
|
18
|
+
@users = users_from_string(raw_users)
|
19
|
+
@groups = groups_from_string(raw_groups)
|
20
|
+
@data = raw_data.nil? || raw_data.strip.empty? ? {} : JSON.parse(raw_data)
|
19
21
|
else
|
20
22
|
clear
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
24
26
|
def serialize
|
25
|
-
"#{@percentage}|#{@users.to_a.join(",")}|#{@groups.to_a.join(",")}"
|
27
|
+
"#{@percentage}|#{@users.to_a.join(",")}|#{@groups.to_a.join(",")}|#{serialize_data}"
|
26
28
|
end
|
27
29
|
|
28
30
|
def add_user(user)
|
@@ -43,9 +45,10 @@ class Rollout
|
|
43
45
|
end
|
44
46
|
|
45
47
|
def clear
|
46
|
-
@groups =
|
47
|
-
@users =
|
48
|
+
@groups = groups_from_string("")
|
49
|
+
@users = users_from_string("")
|
48
50
|
@percentage = 0
|
51
|
+
@data = {}
|
49
52
|
end
|
50
53
|
|
51
54
|
def active?(rollout, user)
|
@@ -101,6 +104,30 @@ class Rollout
|
|
101
104
|
rollout.active_in_group?(g, user)
|
102
105
|
end
|
103
106
|
end
|
107
|
+
|
108
|
+
def serialize_data
|
109
|
+
return "" unless @data.is_a? Hash
|
110
|
+
|
111
|
+
@data.to_json
|
112
|
+
end
|
113
|
+
|
114
|
+
def users_from_string(raw_users)
|
115
|
+
users = (raw_users || "").split(",").map(&:to_s)
|
116
|
+
if @options[:use_sets]
|
117
|
+
users.to_set
|
118
|
+
else
|
119
|
+
users
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def groups_from_string(raw_groups)
|
124
|
+
groups = (raw_groups || "").split(",").map(&:to_sym)
|
125
|
+
if @options[:use_sets]
|
126
|
+
groups.to_set
|
127
|
+
else
|
128
|
+
groups
|
129
|
+
end
|
130
|
+
end
|
104
131
|
end
|
105
132
|
|
106
133
|
def initialize(storage, opts = {})
|
@@ -122,8 +149,8 @@ class Rollout
|
|
122
149
|
end
|
123
150
|
|
124
151
|
def delete(feature)
|
125
|
-
features = (@storage.get(features_key) || "").split(",")
|
126
|
-
features.delete(feature)
|
152
|
+
features = (@storage.get(features_key) || "").split(",")
|
153
|
+
features.delete(feature.to_s)
|
127
154
|
@storage.set(features_key, features.join(","))
|
128
155
|
@storage.del(key(feature))
|
129
156
|
end
|
@@ -214,6 +241,23 @@ class Rollout
|
|
214
241
|
Feature.new(feature, string, @options)
|
215
242
|
end
|
216
243
|
|
244
|
+
def set_feature_data(feature, data)
|
245
|
+
with_feature(feature) do |f|
|
246
|
+
f.data.merge!(data) if data.is_a? Hash
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def clear_feature_data(feature)
|
251
|
+
with_feature(feature) do |f|
|
252
|
+
f.data = {}
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def multi_get(*features)
|
257
|
+
feature_keys = features.map{ |feature| key(feature) }
|
258
|
+
@storage.mget(*feature_keys).map.with_index { |string, index| Feature.new(features[index], string, @options) }
|
259
|
+
end
|
260
|
+
|
217
261
|
def features
|
218
262
|
(@storage.get(features_key) || "").split(",").map(&:to_sym)
|
219
263
|
end
|
data/lib/rollout/version.rb
CHANGED
data/spec/rollout_spec.rb
CHANGED
@@ -51,6 +51,12 @@ RSpec.describe "Rollout" do
|
|
51
51
|
end
|
52
52
|
|
53
53
|
it "leaves the other groups active" do
|
54
|
+
expect(@rollout.get(:chat).groups).to eq [:fivesonly]
|
55
|
+
end
|
56
|
+
|
57
|
+
it "leaves the other groups active using sets" do
|
58
|
+
@options = @rollout.instance_variable_get("@options")
|
59
|
+
@options[:use_sets] = true
|
54
60
|
expect(@rollout.get(:chat).groups).to eq [:fivesonly].to_set
|
55
61
|
end
|
56
62
|
end
|
@@ -169,6 +175,15 @@ RSpec.describe "Rollout" do
|
|
169
175
|
end
|
170
176
|
|
171
177
|
it "remains active for other active users" do
|
178
|
+
@options = @rollout.instance_variable_get("@options")
|
179
|
+
@options[:use_sets] = false
|
180
|
+
expect(@rollout.get(:chat).users).to eq %w(24)
|
181
|
+
end
|
182
|
+
|
183
|
+
it "remains active for other active users using sets" do
|
184
|
+
@options = @rollout.instance_variable_get("@options")
|
185
|
+
@options[:use_sets] = true
|
186
|
+
|
172
187
|
expect(@rollout.get(:chat).users).to eq %w(24).to_set
|
173
188
|
end
|
174
189
|
end
|
@@ -219,6 +234,10 @@ RSpec.describe "Rollout" do
|
|
219
234
|
it "activates the feature" do
|
220
235
|
expect(@rollout).to be_active(:chat)
|
221
236
|
end
|
237
|
+
|
238
|
+
it "sets @data to empty hash" do
|
239
|
+
expect(@rollout.get(:chat).data).to eq({})
|
240
|
+
end
|
222
241
|
end
|
223
242
|
|
224
243
|
describe "activating a feature for a percentage of users" do
|
@@ -344,6 +363,14 @@ RSpec.describe "Rollout" do
|
|
344
363
|
@rollout.set(:chat, true)
|
345
364
|
end
|
346
365
|
|
366
|
+
context "when feature was passed as string" do
|
367
|
+
it "should be removed from features list" do
|
368
|
+
expect(@rollout.features.size).to eq 1
|
369
|
+
@rollout.delete('chat')
|
370
|
+
expect(@rollout.features.size).to eq 0
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
347
374
|
it "should be removed from features list" do
|
348
375
|
expect(@rollout.features.size).to eq 1
|
349
376
|
@rollout.delete(:chat)
|
@@ -386,6 +413,26 @@ RSpec.describe "Rollout" do
|
|
386
413
|
end
|
387
414
|
|
388
415
|
it "returns the feature object" do
|
416
|
+
feature = @rollout.get(:chat)
|
417
|
+
expect(feature.groups).to eq [:caretakers, :greeters]
|
418
|
+
expect(feature.percentage).to eq 10
|
419
|
+
expect(feature.users).to eq %w(42)
|
420
|
+
expect(feature.to_hash).to eq(
|
421
|
+
groups: [:caretakers, :greeters],
|
422
|
+
percentage: 10,
|
423
|
+
users: %w(42)
|
424
|
+
)
|
425
|
+
|
426
|
+
feature = @rollout.get(:signup)
|
427
|
+
expect(feature.groups).to be_empty
|
428
|
+
expect(feature.users).to be_empty
|
429
|
+
expect(feature.percentage).to eq(100)
|
430
|
+
end
|
431
|
+
|
432
|
+
it "returns the feature objects using sets" do
|
433
|
+
@options = @rollout.instance_variable_get("@options")
|
434
|
+
@options[:use_sets] = true
|
435
|
+
|
389
436
|
feature = @rollout.get(:chat)
|
390
437
|
expect(feature.groups).to eq [:caretakers, :greeters].to_set
|
391
438
|
expect(feature.percentage).to eq 10
|
@@ -413,6 +460,18 @@ RSpec.describe "Rollout" do
|
|
413
460
|
end
|
414
461
|
|
415
462
|
it "each feature is cleared" do
|
463
|
+
features.each do |feature|
|
464
|
+
expect(@rollout.get(feature).to_hash).to eq(
|
465
|
+
percentage: 0,
|
466
|
+
users: [],
|
467
|
+
groups: []
|
468
|
+
)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
it "each feature is cleared with sets" do
|
473
|
+
@options = @rollout.instance_variable_get("@options")
|
474
|
+
@options[:use_sets] = true
|
416
475
|
features.each do |feature|
|
417
476
|
expect(@rollout.get(feature).to_hash).to eq(
|
418
477
|
percentage: 0,
|
@@ -515,18 +574,127 @@ RSpec.describe "Rollout" do
|
|
515
574
|
expect(@rollout.user_in_active_users?(:chat, "5")).to eq(false)
|
516
575
|
end
|
517
576
|
end
|
518
|
-
end
|
519
577
|
|
520
|
-
describe "
|
521
|
-
|
522
|
-
|
523
|
-
|
578
|
+
describe "#multi_get" do
|
579
|
+
before do
|
580
|
+
@rollout.activate_percentage(:chat, 10)
|
581
|
+
@rollout.activate_group(:chat, :caretakers)
|
582
|
+
@rollout.activate_group(:videos, :greeters)
|
583
|
+
@rollout.activate(:signup)
|
584
|
+
@rollout.activate_user(:photos, double(id: 42))
|
585
|
+
end
|
586
|
+
|
587
|
+
it "returns an array of features" do
|
588
|
+
features = @rollout.multi_get(:chat, :videos, :signup)
|
589
|
+
expect(features[0].name).to eq :chat
|
590
|
+
expect(features[0].groups).to eq [:caretakers]
|
591
|
+
expect(features[0].percentage).to eq 10
|
592
|
+
expect(features[1].name).to eq :videos
|
593
|
+
expect(features[1].groups).to eq [:greeters]
|
594
|
+
expect(features[2].name).to eq :signup
|
595
|
+
expect(features[2].percentage).to eq 100
|
596
|
+
expect(features.size).to eq 3
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
describe "#set_feature_data" do
|
601
|
+
before do
|
602
|
+
@rollout.set_feature_data(:chat, description: 'foo', release_date: 'bar')
|
603
|
+
end
|
604
|
+
|
605
|
+
it 'sets the data attribute on feature' do
|
606
|
+
expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
607
|
+
end
|
608
|
+
|
609
|
+
it 'updates a data attribute' do
|
610
|
+
@rollout.set_feature_data(:chat, description: 'baz')
|
611
|
+
expect(@rollout.get(:chat).data).to include('description' => 'baz', 'release_date' => 'bar')
|
612
|
+
end
|
613
|
+
|
614
|
+
it 'only sets data on specified feature' do
|
615
|
+
@rollout.set_feature_data(:talk, image_url: 'kittens.png')
|
616
|
+
expect(@rollout.get(:chat).data).not_to include('image_url' => 'kittens.png')
|
617
|
+
expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
618
|
+
end
|
619
|
+
|
620
|
+
it 'does not modify @data if param is nil' do
|
621
|
+
expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
622
|
+
@rollout.set_feature_data(:chat, nil)
|
623
|
+
expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
624
|
+
end
|
625
|
+
|
626
|
+
it 'does not modify @data if param is empty string' do
|
627
|
+
expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
628
|
+
@rollout.set_feature_data(:chat, " ")
|
629
|
+
expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
630
|
+
end
|
631
|
+
|
632
|
+
it 'properly parses data when it contains a |' do
|
633
|
+
user = double("User", id: 8)
|
634
|
+
@rollout.activate_user(:chat, user)
|
635
|
+
@rollout.set_feature_data(:chat, "|call||text|" => "a|bunch|of|stuff")
|
636
|
+
expect(@rollout.get(:chat).data).to include("|call||text|" => "a|bunch|of|stuff")
|
637
|
+
expect(@rollout.active?(:chat, user)).to be true
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
describe "#clear_feature_data" do
|
642
|
+
it 'resets data to empty string' do
|
643
|
+
@rollout.set_feature_data(:chat, description: 'foo')
|
644
|
+
expect(@rollout.get(:chat).data).to include('description' => 'foo')
|
645
|
+
@rollout.clear_feature_data(:chat)
|
646
|
+
expect(@rollout.get(:chat).data).to eq({})
|
647
|
+
end
|
524
648
|
end
|
649
|
+
end
|
525
650
|
|
651
|
+
describe "Rollout::Feature" do
|
526
652
|
describe "#add_user" do
|
527
653
|
it "ids a user using id_user_by" do
|
528
|
-
@
|
529
|
-
|
654
|
+
user = double("User", email: "test@test.com")
|
655
|
+
feature = Rollout::Feature.new(:chat, nil, id_user_by: :email)
|
656
|
+
feature.add_user(user)
|
657
|
+
expect(user).to have_received :email
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
describe "#initialize" do
|
662
|
+
describe "when string does not exist" do
|
663
|
+
it 'clears feature attributes when string is not given' do
|
664
|
+
feature = Rollout::Feature.new(:chat)
|
665
|
+
expect(feature.groups).to be_empty
|
666
|
+
expect(feature.users).to be_empty
|
667
|
+
expect(feature.percentage).to eq 0
|
668
|
+
expect(feature.data).to eq({})
|
669
|
+
end
|
670
|
+
|
671
|
+
it 'clears feature attributes when string is nil' do
|
672
|
+
feature = Rollout::Feature.new(:chat, nil)
|
673
|
+
expect(feature.groups).to be_empty
|
674
|
+
expect(feature.users).to be_empty
|
675
|
+
expect(feature.percentage).to eq 0
|
676
|
+
expect(feature.data).to eq({})
|
677
|
+
end
|
678
|
+
|
679
|
+
it 'clears feature attributes when string is empty string' do
|
680
|
+
feature = Rollout::Feature.new(:chat, "")
|
681
|
+
expect(feature.groups).to be_empty
|
682
|
+
expect(feature.users).to be_empty
|
683
|
+
expect(feature.percentage).to eq 0
|
684
|
+
expect(feature.data).to eq({})
|
685
|
+
end
|
686
|
+
|
687
|
+
describe "when there is no data" do
|
688
|
+
it 'sets @data to empty hash' do
|
689
|
+
feature = Rollout::Feature.new(:chat, "0||")
|
690
|
+
expect(feature.data).to eq({})
|
691
|
+
end
|
692
|
+
|
693
|
+
it 'sets @data to empty hash' do
|
694
|
+
feature = Rollout::Feature.new(:chat, "||| ")
|
695
|
+
expect(feature.data).to eq({})
|
696
|
+
end
|
697
|
+
end
|
530
698
|
end
|
531
699
|
end
|
532
700
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rollout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Golick
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -126,4 +126,3 @@ summary: Feature flippers with redis.
|
|
126
126
|
test_files:
|
127
127
|
- spec/rollout_spec.rb
|
128
128
|
- spec/spec_helper.rb
|
129
|
-
has_rdoc:
|