rolloutbr 2.0.1br

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 ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MTAxOWIyZTk5ZWM1YzFlMzkzNzFhMDJmOGUzZDUwOWY2MGIyY2NkYw==
5
+ data.tar.gz: !binary |-
6
+ NTQ3MGYxNTEwM2FlM2ZjYmYzZWY2OGY5YjlmZGY5NmMxODQ1Nzg3Zg==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MTM5YmUwNTFkZGY2M2FiZjg5YWRiNjM1ZTlkNWU2NmNkN2U1YTdlYzkwYTll
10
+ N2Q2NWU1ZTU5ODdjMzE5MWJjNTBjYjEwZDA3YzZiZGU5MTY5ZGUzZGQwMDMz
11
+ Y2UzOTlmN2RjZjNlNWViMjhkN2QwODQ5NTgyMTgyNTU0MjkyMzg=
12
+ data.tar.gz: !binary |-
13
+ YzQ1MzFhOTllNzBlNWRhOWE4NjVmZmM4YjM3ODFiZDQ1MzYzYjRhODNmYjZk
14
+ YjFkMWRkOTI0NzdjYmUyZTQ5YmI3ZWUxZTUzN2VjYWY3NjM5MTE3NWNjNTE1
15
+ YjAwZWMzNjIzYWFkYTlmYzE3OTVmNzFhMmZiNDZiMDVkN2E2Yzg=
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ *.gem
21
+
22
+ ## PROJECT::SPECIFIC
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,40 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rollout (2.0.0a)
5
+ redis
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ bourne (1.0)
11
+ mocha (= 0.9.8)
12
+ diff-lcs (1.1.3)
13
+ git (1.2.5)
14
+ jeweler (1.6.4)
15
+ bundler (~> 1.0)
16
+ git (>= 1.2.5)
17
+ rake
18
+ mocha (0.9.8)
19
+ rake
20
+ rake (0.9.2.2)
21
+ redis (3.0.2)
22
+ rspec (2.10.0)
23
+ rspec-core (~> 2.10.0)
24
+ rspec-expectations (~> 2.10.0)
25
+ rspec-mocks (~> 2.10.0)
26
+ rspec-core (2.10.1)
27
+ rspec-expectations (2.10.0)
28
+ diff-lcs (~> 1.1.3)
29
+ rspec-mocks (2.10.1)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ bourne (= 1.0)
36
+ bundler (>= 1.0.0)
37
+ jeweler (~> 1.6.4)
38
+ mocha (= 0.9.8)
39
+ rollout!
40
+ rspec (~> 2.10.0)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 James Golick
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,119 @@
1
+ = rollout
2
+
3
+ Feature flippers.
4
+
5
+ == MAKE SURE TO READ THIS: 2.0 Changes and Migration Path
6
+
7
+ As of rollout-2.x, only one key is used per feature for performance reasons. The data format is <tt>percentage|user_id,user_id,...|group,_group...</tt>. This has the effect of making concurrent feature modifications unsafe, but in practice, I doubt this will actually be a problem.
8
+
9
+ This also has the effect of rollout no longer being dependent on redis. Just give it something that responds to <tt>set(key,value)</tt> and <tt>get(key)</tt>.
10
+
11
+ If you have been using the 1.x format, you can initialize Rollout with <tt>:migrate => true</tt> and it'll do its best to automatically migrate your old features to the new format. There will be some performance impact, but it should be limited and short-lived since each feature only needs to get migrated once.
12
+
13
+ == Rollout::Legacy
14
+
15
+ If you'd prefer to continue to use the old layout in redis, <tt>Rollout::Legacy</tt> is a copy and paste of the old code :-).
16
+
17
+ == Install it
18
+
19
+ gem install rollout
20
+
21
+ == How it works
22
+
23
+ Initialize a rollout object. I assign it to a global var.
24
+
25
+ $redis = Redis.new
26
+ $rollout = Rollout.new($redis)
27
+
28
+ Check whether a feature is active for a particular user:
29
+
30
+ $rollout.active?(:chat, User.first) # => true/false
31
+
32
+ Check whether a feature is active globally:
33
+
34
+ $rollout.active?(:chat)
35
+
36
+ You can activate features using a number of different mechanisms.
37
+
38
+ == Groups
39
+
40
+ Rollout ships with one group by default: "all", which does exactly what it sounds like.
41
+
42
+ You can activate the all group for the chat feature like this:
43
+
44
+ $rollout.activate_group(:chat, :all)
45
+
46
+ You might also want to define your own groups. We have one for our caretakers:
47
+
48
+ $rollout.define_group(:caretakers) do |user|
49
+ user.caretaker?
50
+ end
51
+
52
+ You can activate multiple groups per feature.
53
+
54
+ Deactivate groups like this:
55
+
56
+ $rollout.deactivate_group(:chat, :all)
57
+
58
+ == Specific Users
59
+
60
+ You might want to let a specific user into a beta test or something. If that user isn't part of an existing group, you can let them in specifically:
61
+
62
+ $rollout.activate_user(:chat, @user)
63
+
64
+ Deactivate them like this:
65
+
66
+ $rollout.deactivate_user(:chat, @user)
67
+
68
+ == User Percentages
69
+
70
+ If you're rolling out a new feature, you might want to test the waters by slowly enabling it for a percentage of your users.
71
+
72
+ $rollout.activate_percentage(:chat, 20)
73
+
74
+ The algorithm for determining which users get let in is this:
75
+
76
+ user.id % 10 < percentage / 10
77
+
78
+ So, for 20%, users 0, 1, 10, 11, 20, 21, etc would be allowed in. Those users would remain in as the percentage increases.
79
+
80
+ Deactivate all percentages like this:
81
+
82
+ $rollout.deactivate_percentage(:chat)
83
+
84
+ _Note that activating a feature for 100% of users will also make it active "globally". That is when calling Rollout#active? without a user object._
85
+
86
+ == Feature is broken
87
+
88
+ Deactivate everybody at once:
89
+
90
+ $rollout.deactivate(:chat)
91
+
92
+ For many of our features, we keep track of error rates using redis, and deactivate them automatically when a threshold is reached to prevent service failures from cascading. See http://github.com/jamesgolick/degrade for the failure detection code.
93
+
94
+ == Namespacing
95
+
96
+ Rollout separates its keys from other keys in the data store using the "feature" keyspace.
97
+
98
+ If you're using redis, you can namespace keys further to support multiple environments by using the http://github.com/defunkt/redis-namespace gem.
99
+
100
+ $ns = Redis::Namespace.new(Rails.env, :redis => $redis)
101
+ $rollout = Rollout.new($ns)
102
+ $rollout.activate_group(:chat, :all)
103
+
104
+ This example would use the "development:feature:chat:groups" key.
105
+
106
+ == misc/check_rollout.rb
107
+
108
+ In our infrastructure, rollout obviously allows us to progressively enable new features but we also use it to automatically disable features and services that break or fail to prevent them from causing cascading failures and wiping out our entire system.
109
+
110
+ When a feature reaches "maturity" - in other words, expected to be at 100% rollout all the time - we use check_rollout.rb to setup nagios alerts on the rollouts so that we get paged if one of them gets disabled.
111
+
112
+
113
+ == Implementations in other languages
114
+
115
+ * Python: http://github.com/asenchi/proclaim
116
+
117
+ == Copyright
118
+
119
+ Copyright (c) 2010 James Golick, BitLove, Inc. See LICENSE for details.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.1.0
@@ -0,0 +1,134 @@
1
+ class Rollout
2
+ class Legacy
3
+ def initialize(redis)
4
+ @redis = redis
5
+ @groups = {"all" => lambda { |user| true }}
6
+ end
7
+
8
+ def activate_globally(feature)
9
+ @redis.sadd(global_key, feature)
10
+ end
11
+
12
+ def deactivate_globally(feature)
13
+ @redis.srem(global_key, feature)
14
+ end
15
+
16
+ def activate_group(feature, group)
17
+ @redis.sadd(group_key(feature), group)
18
+ end
19
+
20
+ def deactivate_group(feature, group)
21
+ @redis.srem(group_key(feature), group)
22
+ end
23
+
24
+ def deactivate_all(feature)
25
+ @redis.del(group_key(feature))
26
+ @redis.del(user_key(feature))
27
+ @redis.del(percentage_key(feature))
28
+ deactivate_globally(feature)
29
+ end
30
+
31
+ def activate_user(feature, user)
32
+ @redis.sadd(user_key(feature), user.id)
33
+ end
34
+
35
+ def deactivate_user(feature, user)
36
+ @redis.srem(user_key(feature), user.id)
37
+ end
38
+
39
+ def define_group(group, &block)
40
+ @groups[group.to_s] = block
41
+ end
42
+
43
+ def active?(feature, user = nil)
44
+ if user
45
+ active_globally?(feature) ||
46
+ user_in_active_group?(feature, user) ||
47
+ user_active?(feature, user) ||
48
+ user_within_active_percentage?(feature, user)
49
+ else
50
+ active_globally?(feature)
51
+ end
52
+ end
53
+
54
+ def activate_percentage(feature, percentage)
55
+ @redis.set(percentage_key(feature), percentage)
56
+ end
57
+
58
+ def deactivate_percentage(feature)
59
+ @redis.del(percentage_key(feature))
60
+ end
61
+
62
+ def info(feature = nil)
63
+ if feature
64
+ {
65
+ :percentage => (active_percentage(feature) || 0).to_i,
66
+ :groups => active_groups(feature).map { |g| g.to_sym },
67
+ :users => active_user_ids(feature),
68
+ :global => active_global_features
69
+ }
70
+ else
71
+ {
72
+ :global => active_global_features
73
+ }
74
+ end
75
+ end
76
+
77
+ private
78
+ def key(name)
79
+ "feature:#{name}"
80
+ end
81
+
82
+ def group_key(name)
83
+ "#{key(name)}:groups"
84
+ end
85
+
86
+ def user_key(name)
87
+ "#{key(name)}:users"
88
+ end
89
+
90
+ def percentage_key(name)
91
+ "#{key(name)}:percentage"
92
+ end
93
+
94
+ def global_key
95
+ "feature:__global__"
96
+ end
97
+
98
+ def active_groups(feature)
99
+ @redis.smembers(group_key(feature)) || []
100
+ end
101
+
102
+ def active_user_ids(feature)
103
+ @redis.smembers(user_key(feature)).map { |id| id.to_i }
104
+ end
105
+
106
+ def active_global_features
107
+ (@redis.smembers(global_key) || []).map(&:to_sym)
108
+ end
109
+
110
+ def active_percentage(feature)
111
+ @redis.get(percentage_key(feature))
112
+ end
113
+
114
+ def active_globally?(feature)
115
+ @redis.sismember(global_key, feature)
116
+ end
117
+
118
+ def user_in_active_group?(feature, user)
119
+ active_groups(feature).any? do |group|
120
+ @groups.key?(group) && @groups[group].call(user)
121
+ end
122
+ end
123
+
124
+ def user_active?(feature, user)
125
+ @redis.sismember(user_key(feature), user.id)
126
+ end
127
+
128
+ def user_within_active_percentage?(feature, user)
129
+ percentage = active_percentage(feature)
130
+ return false if percentage.nil?
131
+ user.id % 100 < percentage.to_i
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,3 @@
1
+ class Rollout
2
+ VERSION = "2.0.1br"
3
+ end
data/lib/rollout.rb ADDED
@@ -0,0 +1,211 @@
1
+ require "rollout/legacy"
2
+ require "zlib"
3
+
4
+ class Rollout
5
+ class Feature
6
+ attr_reader :name, :groups, :users, :percentage
7
+ attr_writer :percentage, :groups, :users
8
+
9
+ def initialize(name, string = nil)
10
+ @name = name
11
+ if string
12
+ raw_percentage,raw_users,raw_groups = string.split("|")
13
+ @percentage = raw_percentage.to_i
14
+ @users = (raw_users || "").split(",").map(&:to_s)
15
+ @groups = (raw_groups || "").split(",").map(&:to_sym)
16
+ else
17
+ clear
18
+ end
19
+ end
20
+
21
+ def serialize
22
+ "#{@percentage}|#{@users.join(",")}|#{@groups.join(",")}"
23
+ end
24
+
25
+ def add_user(user)
26
+ @users << user.id.to_s unless @users.include?(user.id.to_s)
27
+ end
28
+
29
+ def remove_user(user)
30
+ @users.delete(user.id.to_s)
31
+ end
32
+
33
+ def add_group(group)
34
+ @groups << group.to_sym unless @groups.include?(group.to_sym)
35
+ end
36
+
37
+ def remove_group(group)
38
+ @groups.delete(group.to_sym)
39
+ end
40
+
41
+ def clear
42
+ @groups = []
43
+ @users = []
44
+ @percentage = 0
45
+ end
46
+
47
+ def active?(rollout, user)
48
+ if user.nil?
49
+ @percentage == 100
50
+ else
51
+ user_in_percentage?(user) ||
52
+ user_in_active_users?(user) ||
53
+ user_in_active_group?(user, rollout)
54
+ end
55
+ end
56
+
57
+ def to_hash
58
+ {:percentage => @percentage,
59
+ :groups => @groups,
60
+ :users => @users}
61
+ end
62
+
63
+ private
64
+ def user_in_percentage?(user)
65
+ Zlib.crc32(user.id.to_s) % 100 < @percentage
66
+ end
67
+
68
+ def user_in_active_users?(user)
69
+ @users.include?(user.id.to_s)
70
+ end
71
+
72
+ def user_in_active_group?(user, rollout)
73
+ @groups.any? do |g|
74
+ rollout.active_in_group?(g, user)
75
+ end
76
+ end
77
+ end
78
+
79
+ @@storage_hash = {}
80
+ @@cleared = Time.now
81
+ puts @@cleared
82
+
83
+ def initialize(storage, opts = {})
84
+ @storage = storage
85
+ @groups = {:all => lambda { |user| true }}
86
+ @legacy = Legacy.new(@storage) if opts[:migrate]
87
+ end
88
+
89
+ def activate(feature)
90
+ with_feature(feature) do |f|
91
+ f.percentage = 100
92
+ end
93
+ end
94
+
95
+ def deactivate(feature)
96
+ with_feature(feature) do |f|
97
+ f.clear
98
+ end
99
+ end
100
+
101
+ def activate_group(feature, group)
102
+ with_feature(feature) do |f|
103
+ f.add_group(group)
104
+ end
105
+ end
106
+
107
+ def deactivate_group(feature, group)
108
+ with_feature(feature) do |f|
109
+ f.remove_group(group)
110
+ end
111
+ end
112
+
113
+ def activate_user(feature, user)
114
+ with_feature(feature) do |f|
115
+ f.add_user(user)
116
+ end
117
+ end
118
+
119
+ def deactivate_user(feature, user)
120
+ with_feature(feature) do |f|
121
+ f.remove_user(user)
122
+ end
123
+ end
124
+
125
+ def define_group(group, &block)
126
+ @groups[group.to_sym] = block
127
+ end
128
+
129
+ def active?(feature, user = nil)
130
+ feature = get(feature)
131
+ feature.active?(self, user)
132
+ end
133
+
134
+ def activate_percentage(feature, percentage)
135
+ with_feature(feature) do |f|
136
+ f.percentage = percentage
137
+ end
138
+ end
139
+
140
+ def deactivate_percentage(feature)
141
+ with_feature(feature) do |f|
142
+ f.percentage = 0
143
+ end
144
+ end
145
+
146
+ def active_in_group?(group, user)
147
+ f = @groups[group.to_sym]
148
+ f && f.call(user)
149
+ end
150
+
151
+ def period
152
+ @period ||= 60
153
+ end
154
+
155
+ def get(feature, force=false)
156
+ puts "force: #{force}"
157
+ puts "!@@cleared: #{!@@cleared}"
158
+ puts "Time.now: #{Time.now}"
159
+ puts "@@cleared + period: #{@@cleared + period}"
160
+ puts "Comp: #{Time.now >= (@@cleared + period)}"
161
+ if force || !@@cleared || Time.now >= (@@cleared + period)
162
+ puts "forced"
163
+ @@storage_hash = {}
164
+ @@cleared = Time.now
165
+ end
166
+ if @@storage_hash[feature].nil?
167
+ puts "nil"
168
+ @@storage_hash[feature] = @storage.get(key(feature)) || '0||'
169
+ end
170
+ string = @@storage_hash[feature]
171
+ if string || !migrate?
172
+ Feature.new(feature, string)
173
+ else
174
+ info = @legacy.info(feature)
175
+ f = Feature.new(feature)
176
+ f.percentage = info[:percentage]
177
+ f.groups = info[:groups].map { |g| g.to_sym }
178
+ f.users = info[:users].map { |u| u.to_s }
179
+ save(f)
180
+ f
181
+ end
182
+ end
183
+
184
+ def features
185
+ (@storage.get(features_key) || "").split(",").map(&:to_sym)
186
+ end
187
+
188
+ private
189
+ def key(name)
190
+ "feature:#{name}"
191
+ end
192
+
193
+ def features_key
194
+ "feature:__features__"
195
+ end
196
+
197
+ def with_feature(feature)
198
+ f = get(feature)
199
+ yield(f)
200
+ save(f)
201
+ end
202
+
203
+ def save(feature)
204
+ @storage.set(key(feature.name), feature.serialize)
205
+ @storage.set(features_key, (features | [feature.name]).join(","))
206
+ end
207
+
208
+ def migrate?
209
+ @legacy
210
+ end
211
+ end
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "yajl"
5
+ require "open-uri"
6
+
7
+ output = Yajl::Parser.parse(open(ARGV[0]))
8
+ percentage = output["percentage"].to_i
9
+
10
+ puts Yajl::Encoder.encode(output)
11
+
12
+ if percentage == 100
13
+ exit(0)
14
+ else
15
+ exit(2)
16
+ end
data/rollout.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rollout/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rolloutbr"
7
+ s.version = Rollout::VERSION
8
+ s.authors = ["James Golick"]
9
+ s.email = ["jamesgolick@gmail.com"]
10
+ s.description = "Feature flippers with redis."
11
+ s.summary = "Feature flippers with redis."
12
+ s.homepage = "https://github.com/jamesgolick/rollout"
13
+
14
+ s.require_paths = ["lib"]
15
+
16
+ s.rubyforge_project = "rollout"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ # specify any dependencies here; for example:
24
+ s.add_development_dependency "rspec", "~> 2.10.0"
25
+ s.add_development_dependency "bundler", ">= 1.0.0"
26
+ s.add_development_dependency "jeweler", "~> 1.6.4"
27
+ s.add_development_dependency "bourne", "1.0"
28
+ s.add_development_dependency "mocha", "0.9.8"
29
+
30
+ s.add_runtime_dependency "redis"
31
+ end
@@ -0,0 +1,222 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Rollout::Legacy" do
4
+ before do
5
+ @redis = Redis.new
6
+ @rollout = Rollout::Legacy.new(@redis)
7
+ end
8
+
9
+ describe "when a group is activated" do
10
+ before do
11
+ @rollout.define_group(:fivesonly) { |user| user.id == 5 }
12
+ @rollout.activate_group(:chat, :fivesonly)
13
+ end
14
+
15
+ it "the feature is active for users for which the block evaluates to true" do
16
+ @rollout.should be_active(:chat, stub(:id => 5))
17
+ end
18
+
19
+ it "is not active for users for which the block evaluates to false" do
20
+ @rollout.should_not be_active(:chat, stub(:id => 1))
21
+ end
22
+
23
+ it "is not active if a group is found in Redis but not defined in Rollout" do
24
+ @rollout.activate_group(:chat, :fake)
25
+ @rollout.should_not be_active(:chat, stub(:id => 1))
26
+ end
27
+ end
28
+
29
+ describe "the default all group" do
30
+ before do
31
+ @rollout.activate_group(:chat, :all)
32
+ end
33
+
34
+ it "evaluates to true no matter what" do
35
+ @rollout.should be_active(:chat, stub(:id => 0))
36
+ end
37
+ end
38
+
39
+ describe "deactivating a group" do
40
+ before do
41
+ @rollout.define_group(:fivesonly) { |user| user.id == 5 }
42
+ @rollout.activate_group(:chat, :all)
43
+ @rollout.activate_group(:chat, :fivesonly)
44
+ @rollout.deactivate_group(:chat, :all)
45
+ end
46
+
47
+ it "deactivates the rules for that group" do
48
+ @rollout.should_not be_active(:chat, stub(:id => 10))
49
+ end
50
+
51
+ it "leaves the other groups active" do
52
+ @rollout.should be_active(:chat, stub(:id => 5))
53
+ end
54
+ end
55
+
56
+ describe "deactivating a feature completely" do
57
+ before do
58
+ @rollout.define_group(:fivesonly) { |user| user.id == 5 }
59
+ @rollout.activate_group(:chat, :all)
60
+ @rollout.activate_group(:chat, :fivesonly)
61
+ @rollout.activate_user(:chat, stub(:id => 51))
62
+ @rollout.activate_percentage(:chat, 100)
63
+ @rollout.activate_globally(:chat)
64
+ @rollout.deactivate_all(:chat)
65
+ end
66
+
67
+ it "removes all of the groups" do
68
+ @rollout.should_not be_active(:chat, stub(:id => 0))
69
+ end
70
+
71
+ it "removes all of the users" do
72
+ @rollout.should_not be_active(:chat, stub(:id => 51))
73
+ end
74
+
75
+ it "removes the percentage" do
76
+ @rollout.should_not be_active(:chat, stub(:id => 24))
77
+ end
78
+
79
+ it "removes globally" do
80
+ @rollout.should_not be_active(:chat)
81
+ end
82
+ end
83
+
84
+ describe "activating a specific user" do
85
+ before do
86
+ @rollout.activate_user(:chat, stub(:id => 42))
87
+ end
88
+
89
+ it "is active for that user" do
90
+ @rollout.should be_active(:chat, stub(:id => 42))
91
+ end
92
+
93
+ it "remains inactive for other users" do
94
+ @rollout.should_not be_active(:chat, stub(:id => 24))
95
+ end
96
+ end
97
+
98
+ describe "deactivating a specific user" do
99
+ before do
100
+ @rollout.activate_user(:chat, stub(:id => 42))
101
+ @rollout.activate_user(:chat, stub(:id => 24))
102
+ @rollout.deactivate_user(:chat, stub(:id => 42))
103
+ end
104
+
105
+ it "that user should no longer be active" do
106
+ @rollout.should_not be_active(:chat, stub(:id => 42))
107
+ end
108
+
109
+ it "remains active for other active users" do
110
+ @rollout.should be_active(:chat, stub(:id => 24))
111
+ end
112
+ end
113
+
114
+ describe "activating a feature globally" do
115
+ before do
116
+ @rollout.activate_globally(:chat)
117
+ end
118
+
119
+ it "activates the feature" do
120
+ @rollout.should be_active(:chat)
121
+ end
122
+ end
123
+
124
+ describe "activating a feature for a percentage of users" do
125
+ before do
126
+ @rollout.activate_percentage(:chat, 20)
127
+ end
128
+
129
+ it "activates the feature for that percentage of the users" do
130
+ (1..120).select { |id| @rollout.active?(:chat, stub(:id => id)) }.length.should == 39
131
+ end
132
+ end
133
+
134
+ describe "activating a feature for a percentage of users" do
135
+ before do
136
+ @rollout.activate_percentage(:chat, 20)
137
+ end
138
+
139
+ it "activates the feature for that percentage of the users" do
140
+ (1..200).select { |id| @rollout.active?(:chat, stub(:id => id)) }.length.should == 40
141
+ end
142
+ end
143
+
144
+ describe "activating a feature for a percentage of users" do
145
+ before do
146
+ @rollout.activate_percentage(:chat, 5)
147
+ end
148
+
149
+ it "activates the feature for that percentage of the users" do
150
+ (1..100).select { |id| @rollout.active?(:chat, stub(:id => id)) }.length.should == 5
151
+ end
152
+ end
153
+
154
+
155
+ describe "deactivating the percentage of users" do
156
+ before do
157
+ @rollout.activate_percentage(:chat, 100)
158
+ @rollout.deactivate_percentage(:chat)
159
+ end
160
+
161
+ it "becomes inactivate for all users" do
162
+ @rollout.should_not be_active(:chat, stub(:id => 24))
163
+ end
164
+ end
165
+
166
+ describe "deactivating the feature globally" do
167
+ before do
168
+ @rollout.activate_globally(:chat)
169
+ @rollout.deactivate_globally(:chat)
170
+ end
171
+
172
+ it "becomes inactivate" do
173
+ @rollout.should_not be_active(:chat)
174
+ end
175
+ end
176
+
177
+ describe "#info" do
178
+ context "global features" do
179
+ let(:features) { [:signup, :chat, :table] }
180
+
181
+ before do
182
+ features.each do |f|
183
+ @rollout.activate_globally(f)
184
+ end
185
+ end
186
+
187
+ it "returns all global features" do
188
+ @rollout.info.should eq({ :global => features.reverse })
189
+ end
190
+ end
191
+
192
+ describe "with a percentage set" do
193
+ before do
194
+ @rollout.activate_percentage(:chat, 10)
195
+ @rollout.activate_group(:chat, :caretakers)
196
+ @rollout.activate_group(:chat, :greeters)
197
+ @rollout.activate_globally(:signup)
198
+ @rollout.activate_user(:chat, stub(:id => 42))
199
+ end
200
+
201
+ it "returns info about all the activations" do
202
+ @rollout.info(:chat).should == {
203
+ :percentage => 10,
204
+ :groups => [:greeters, :caretakers],
205
+ :users => [42],
206
+ :global => [:signup]
207
+ }
208
+ end
209
+ end
210
+
211
+ describe "without a percentage set" do
212
+ it "defaults to 0" do
213
+ @rollout.info(:chat).should == {
214
+ :percentage => 0,
215
+ :groups => [],
216
+ :users => [],
217
+ :global => []
218
+ }
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,274 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Rollout" do
4
+ before do
5
+ @redis = Redis.new
6
+ @rollout = Rollout.new(@redis)
7
+ end
8
+
9
+ describe "when a group is activated" do
10
+ before do
11
+ @rollout.define_group(:fivesonly) { |user| user.id == 5 }
12
+ @rollout.activate_group(:chat, :fivesonly)
13
+ end
14
+
15
+ it "the feature is active for users for which the block evaluates to true" do
16
+ @rollout.should be_active(:chat, stub(:id => 5))
17
+ end
18
+
19
+ it "is not active for users for which the block evaluates to false" do
20
+ @rollout.should_not be_active(:chat, stub(:id => 1))
21
+ end
22
+
23
+ it "is not active if a group is found in Redis but not defined in Rollout" do
24
+ @rollout.activate_group(:chat, :fake)
25
+ @rollout.should_not be_active(:chat, stub(:id => 1))
26
+ end
27
+ end
28
+
29
+ describe "the default all group" do
30
+ before do
31
+ @rollout.activate_group(:chat, :all)
32
+ end
33
+
34
+ it "evaluates to true no matter what" do
35
+ @rollout.should be_active(:chat, stub(:id => 0))
36
+ end
37
+ end
38
+
39
+ describe "deactivating a group" do
40
+ before do
41
+ @rollout.define_group(:fivesonly) { |user| user.id == 5 }
42
+ @rollout.activate_group(:chat, :all)
43
+ @rollout.activate_group(:chat, :some)
44
+ @rollout.activate_group(:chat, :fivesonly)
45
+ @rollout.deactivate_group(:chat, :all)
46
+ @rollout.deactivate_group(:chat, "some")
47
+ end
48
+
49
+ it "deactivates the rules for that group" do
50
+ @rollout.should_not be_active(:chat, stub(:id => 10))
51
+ end
52
+
53
+ it "leaves the other groups active" do
54
+ @rollout.get(:chat).groups.should == [:fivesonly]
55
+ end
56
+ end
57
+
58
+ describe "deactivating a feature completely" do
59
+ before do
60
+ @rollout.define_group(:fivesonly) { |user| user.id == 5 }
61
+ @rollout.activate_group(:chat, :all)
62
+ @rollout.activate_group(:chat, :fivesonly)
63
+ @rollout.activate_user(:chat, stub(:id => 51))
64
+ @rollout.activate_percentage(:chat, 100)
65
+ @rollout.activate(:chat)
66
+ @rollout.deactivate(:chat)
67
+ end
68
+
69
+ it "removes all of the groups" do
70
+ @rollout.should_not be_active(:chat, stub(:id => 0))
71
+ end
72
+
73
+ it "removes all of the users" do
74
+ @rollout.should_not be_active(:chat, stub(:id => 51))
75
+ end
76
+
77
+ it "removes the percentage" do
78
+ @rollout.should_not be_active(:chat, stub(:id => 24))
79
+ end
80
+
81
+ it "removes globally" do
82
+ @rollout.should_not be_active(:chat)
83
+ end
84
+ end
85
+
86
+ describe "activating a specific user" do
87
+ before do
88
+ @rollout.activate_user(:chat, stub(:id => 42))
89
+ end
90
+
91
+ it "is active for that user" do
92
+ @rollout.should be_active(:chat, stub(:id => 42))
93
+ end
94
+
95
+ it "remains inactive for other users" do
96
+ @rollout.should_not be_active(:chat, stub(:id => 24))
97
+ end
98
+ end
99
+
100
+ describe "activating a specific user with a string id" do
101
+ before do
102
+ @rollout.activate_user(:chat, stub(:id => 'user-72'))
103
+ end
104
+
105
+ it "is active for that user" do
106
+ @rollout.should be_active(:chat, stub(:id => 'user-72'))
107
+ end
108
+
109
+ it "remains inactive for other users" do
110
+ @rollout.should_not be_active(:chat, stub(:id => 'user-12'))
111
+ end
112
+ end
113
+
114
+ describe "deactivating a specific user" do
115
+ before do
116
+ @rollout.activate_user(:chat, stub(:id => 42))
117
+ @rollout.activate_user(:chat, stub(:id => 4242))
118
+ @rollout.activate_user(:chat, stub(:id => 24))
119
+ @rollout.deactivate_user(:chat, stub(:id => 42))
120
+ @rollout.deactivate_user(:chat, stub(:id => "4242"))
121
+ end
122
+
123
+ it "that user should no longer be active" do
124
+ @rollout.should_not be_active(:chat, stub(:id => 42))
125
+ end
126
+
127
+ it "remains active for other active users" do
128
+ @rollout.get(:chat).users.should == %w(24)
129
+ end
130
+ end
131
+
132
+ describe "activating a feature globally" do
133
+ before do
134
+ @rollout.activate(:chat)
135
+ end
136
+
137
+ it "activates the feature" do
138
+ @rollout.should be_active(:chat)
139
+ end
140
+ end
141
+
142
+ describe "activating a feature for a percentage of users" do
143
+ before do
144
+ @rollout.activate_percentage(:chat, 20)
145
+ end
146
+
147
+ it "activates the feature for that percentage of the users" do
148
+ (1..120).select { |id| @rollout.active?(:chat, stub(:id => id)) }.length.should be_within(1).of(20)
149
+ end
150
+ end
151
+
152
+ describe "activating a feature for a percentage of users" do
153
+ before do
154
+ @rollout.activate_percentage(:chat, 20)
155
+ end
156
+
157
+ it "activates the feature for that percentage of the users" do
158
+ (1..200).select { |id| @rollout.active?(:chat, stub(:id => id)) }.length.should be_within(5).of(40)
159
+ end
160
+ end
161
+
162
+ describe "activating a feature for a percentage of users" do
163
+ before do
164
+ @rollout.activate_percentage(:chat, 5)
165
+ end
166
+
167
+ it "activates the feature for that percentage of the users" do
168
+ (1..100).select { |id| @rollout.active?(:chat, stub(:id => id)) }.length.should be_within(2).of(5)
169
+ end
170
+ end
171
+
172
+ describe "activating a feature for a group as a string" do
173
+ before do
174
+ @rollout.define_group(:admins) { |user| user.id == 5 }
175
+ @rollout.activate_group(:chat, 'admins')
176
+ end
177
+
178
+ it "the feature is active for users for which the block evaluates to true" do
179
+ @rollout.should be_active(:chat, stub(:id => 5))
180
+ end
181
+
182
+ it "is not active for users for which the block evaluates to false" do
183
+ @rollout.should_not be_active(:chat, stub(:id => 1))
184
+ end
185
+ end
186
+
187
+ describe "deactivating the percentage of users" do
188
+ before do
189
+ @rollout.activate_percentage(:chat, 100)
190
+ @rollout.deactivate_percentage(:chat)
191
+ end
192
+
193
+ it "becomes inactivate for all users" do
194
+ @rollout.should_not be_active(:chat, stub(:id => 24))
195
+ end
196
+ end
197
+
198
+ describe "deactivating the feature globally" do
199
+ before do
200
+ @rollout.activate(:chat)
201
+ @rollout.deactivate(:chat)
202
+ end
203
+
204
+ it "becomes inactivate" do
205
+ @rollout.should_not be_active(:chat)
206
+ end
207
+ end
208
+
209
+ describe "keeps a list of features" do
210
+ it "saves the feature" do
211
+ @rollout.activate(:chat)
212
+ @rollout.features.should be_include(:chat)
213
+ end
214
+
215
+ it "does not contain doubles" do
216
+ @rollout.activate(:chat)
217
+ @rollout.activate(:chat)
218
+ @rollout.features.size.should == 1
219
+ end
220
+ end
221
+
222
+ describe "#get" do
223
+ before do
224
+ @rollout.activate_percentage(:chat, 10)
225
+ @rollout.activate_group(:chat, :caretakers)
226
+ @rollout.activate_group(:chat, :greeters)
227
+ @rollout.activate(:signup)
228
+ @rollout.activate_user(:chat, stub(:id => 42))
229
+ end
230
+
231
+ it "returns the feature object" do
232
+ feature = @rollout.get(:chat)
233
+ feature.groups.should == [:caretakers, :greeters]
234
+ feature.percentage.should == 10
235
+ feature.users.should == %w(42)
236
+ feature.to_hash.should == {
237
+ :groups => [:caretakers, :greeters],
238
+ :percentage => 10,
239
+ :users => %w(42)
240
+ }
241
+
242
+ feature = @rollout.get(:signup)
243
+ feature.groups.should be_empty
244
+ feature.users.should be_empty
245
+ feature.percentage.should == 100
246
+ end
247
+ end
248
+
249
+ describe "migration mode" do
250
+ before do
251
+ @legacy = Rollout::Legacy.new(@redis)
252
+ @legacy.activate_percentage(:chat, 12)
253
+ @legacy.activate_user(:chat, stub(:id => 42))
254
+ @legacy.activate_user(:chat, stub(:id => 24))
255
+ @legacy.activate_group(:chat, :dope_people)
256
+ @rollout = Rollout.new(@redis, :migrate => true)
257
+ end
258
+
259
+ it "imports the settings from the legacy rollout once" do
260
+ @rollout.get(:chat).to_hash.should == {
261
+ :percentage => 12,
262
+ :users => %w(24 42),
263
+ :groups => [:dope_people]
264
+ }
265
+ @legacy.deactivate_all(:chat)
266
+ @rollout.get(:chat).to_hash.should == {
267
+ :percentage => 12,
268
+ :users => %w(24 42),
269
+ :groups => [:dope_people]
270
+ }
271
+ @redis.get("feature:chat").should_not be_nil
272
+ end
273
+ end
274
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'rollout'
4
+ require 'rspec'
5
+ require 'bourne'
6
+ require 'redis'
7
+
8
+ RSpec.configure do |config|
9
+ config.mock_with :mocha
10
+ config.before { Redis.new.flushdb }
11
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rolloutbr
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.1br
5
+ platform: ruby
6
+ authors:
7
+ - James Golick
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-02-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 2.10.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 2.10.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: jeweler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 1.6.4
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.6.4
55
+ - !ruby/object:Gem::Dependency
56
+ name: bourne
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mocha
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.9.8
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.9.8
83
+ - !ruby/object:Gem::Dependency
84
+ name: redis
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Feature flippers with redis.
98
+ email:
99
+ - jamesgolick@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - .document
105
+ - .gitignore
106
+ - Gemfile
107
+ - Gemfile.lock
108
+ - LICENSE
109
+ - README.rdoc
110
+ - VERSION
111
+ - lib/rollout.rb
112
+ - lib/rollout/legacy.rb
113
+ - lib/rollout/version.rb
114
+ - misc/check_rollout.rb
115
+ - rollout.gemspec
116
+ - spec/legacy_spec.rb
117
+ - spec/rollout_spec.rb
118
+ - spec/spec.opts
119
+ - spec/spec_helper.rb
120
+ homepage: https://github.com/jamesgolick/rollout
121
+ licenses: []
122
+ metadata: {}
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ! '>'
135
+ - !ruby/object:Gem::Version
136
+ version: 1.3.1
137
+ requirements: []
138
+ rubyforge_project: rollout
139
+ rubygems_version: 2.0.0
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: Feature flippers with redis.
143
+ test_files:
144
+ - spec/legacy_spec.rb
145
+ - spec/rollout_spec.rb
146
+ - spec/spec.opts
147
+ - spec/spec_helper.rb