rollout 2.3.0 → 2.4.0
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/.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:
|