rollout 1.2.0 → 2.0.0a

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,6 +1,18 @@
1
1
  = rollout
2
2
 
3
- Conditionally roll out features with redis.
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 `percentage|user_id,user_id,...|group,_group...`. 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 `set(key,value)` and `get(key)`
10
+
11
+ If you have been using the 1.x format, you can initialize Rollout with `:migrate => true` 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, `Rollout::Legacy` is a copy and paste of the old code :-).
4
16
 
5
17
  == Install it
6
18
 
@@ -17,6 +29,10 @@ Check whether a feature is active for a particular user:
17
29
 
18
30
  $rollout.active?(:chat, User.first) # => true/false
19
31
 
32
+ Check whether a feature is active globally:
33
+
34
+ $rollout.active?(:chat)
35
+
20
36
  You can activate features using a number of different mechanisms.
21
37
 
22
38
  == Groups
@@ -51,7 +67,7 @@ Deactivate them like this:
51
67
 
52
68
  == User Percentages
53
69
 
54
- If you're rolling out a new feature, you might want to test the waters by slowly letting in a percentage of your users.
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.
55
71
 
56
72
  $rollout.activate_percentage(:chat, 20)
57
73
 
@@ -65,19 +81,21 @@ Deactivate all percentages like this:
65
81
 
66
82
  $rollout.deactivate_percentage(:chat)
67
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
+
68
86
  == Feature is broken
69
87
 
70
88
  Deactivate everybody at once:
71
89
 
72
- $rollout.deactivate_all
90
+ $rollout.deactivate(:chat)
73
91
 
74
- For some of our less stable features, we are actually measuring the error rate using redis, and deactivating them automatically when it raises above a certain threshold. It's pretty cool. See http://github.com/jamesgolick/degrade for the failure detection code.
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.
75
93
 
76
94
  == Namespacing
77
95
 
78
- Rollout separates its keys from other keys on the Redis server using the "feature" keyspace.
96
+ Rollout separates its keys from other keys in the data store using the "feature" keyspace.
79
97
 
80
- You can namespace keys further to support multiple environments by using the http://github.com/defunkt/redis-namespace gem.
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.
81
99
 
82
100
  $ns = Redis::Namespace.new(Rails.env, :redis => $redis)
83
101
  $rollout = Rollout.new($ns)
@@ -85,20 +103,17 @@ You can namespace keys further to support multiple environments by using the htt
85
103
 
86
104
  This example would use the "development:feature:chat:groups" key.
87
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
+
88
113
  == Implementations in other languages
89
114
 
90
115
  * Python: http://github.com/asenchi/proclaim
91
116
 
92
- == Note on Patches/Pull Requests
93
-
94
- * Fork the project.
95
- * Make your feature addition or bug fix.
96
- * Add tests for it. This is important so I don't break it in a
97
- future version unintentionally.
98
- * Commit, do not mess with rakefile, version, or history.
99
- (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
100
- * Send me a pull request. Bonus points for topic branches.
101
-
102
117
  == Copyright
103
118
 
104
- Copyright (c) 2010 James Golick, Protose, Inc. See LICENSE for details.
119
+ Copyright (c) 2010 James Golick, BitLove, Inc. See LICENSE for details.
data/lib/rollout.rb CHANGED
@@ -1,132 +1,188 @@
1
+ require "rollout/legacy"
2
+
1
3
  class Rollout
2
- def initialize(redis)
3
- @redis = redis
4
- @groups = {"all" => lambda { |user| true }}
4
+ class Feature
5
+ attr_reader :name, :groups, :users, :percentage
6
+ attr_writer :percentage, :groups, :users
7
+
8
+ def initialize(name, string = nil)
9
+ @name = name
10
+ if string
11
+ raw_percentage,raw_users,raw_groups = string.split("|")
12
+ @percentage = raw_percentage.to_i
13
+ @users = (raw_users || "").split(",").map(&:to_i)
14
+ @groups = (raw_groups || "").split(",").map(&:to_sym)
15
+ else
16
+ clear
17
+ end
18
+ end
19
+
20
+ def serialize
21
+ "#{@percentage}|#{@users.join(",")}|#{@groups.join(",")}"
22
+ end
23
+
24
+ def add_user(user)
25
+ @users << user.id unless @users.include?(user.id)
26
+ end
27
+
28
+ def remove_user(user)
29
+ @users.delete(user.id)
30
+ end
31
+
32
+ def add_group(group)
33
+ @groups << group unless @groups.include?(group)
34
+ end
35
+
36
+ def remove_group(group)
37
+ @groups.delete(group)
38
+ end
39
+
40
+ def clear
41
+ @groups = []
42
+ @users = []
43
+ @percentage = 0
44
+ end
45
+
46
+ def active?(rollout, user)
47
+ if user.nil?
48
+ @percentage == 100
49
+ else
50
+ user_in_percentage?(user) ||
51
+ user_in_active_users?(user) ||
52
+ user_in_active_group?(user, rollout)
53
+ end
54
+ end
55
+
56
+ def to_hash
57
+ {:percentage => @percentage,
58
+ :groups => @groups,
59
+ :users => @users}
60
+ end
61
+
62
+ private
63
+ def user_in_percentage?(user)
64
+ user.id % 100 < @percentage
65
+ end
66
+
67
+ def user_in_active_users?(user)
68
+ @users.include?(user.id)
69
+ end
70
+
71
+ def user_in_active_group?(user, rollout)
72
+ @groups.any? do |g|
73
+ rollout.active_in_group?(g, user)
74
+ end
75
+ end
5
76
  end
6
77
 
7
- def activate_globally(feature)
8
- @redis.sadd(global_key, feature)
78
+ def initialize(storage, opts = {})
79
+ @storage = storage
80
+ @groups = {:all => lambda { |user| true }}
81
+ @legacy = Legacy.new(@storage) if opts[:migrate]
9
82
  end
10
83
 
11
- def deactivate_globally(feature)
12
- @redis.srem(global_key, feature)
84
+ def activate(feature)
85
+ with_feature(feature) do |f|
86
+ f.percentage = 100
87
+ end
13
88
  end
14
89
 
15
- def activate_group(feature, group)
16
- @redis.sadd(group_key(feature), group)
90
+ def deactivate(feature)
91
+ with_feature(feature) do |f|
92
+ f.clear
93
+ end
17
94
  end
18
95
 
19
- def deactivate_group(feature, group)
20
- @redis.srem(group_key(feature), group)
96
+ def activate_group(feature, group)
97
+ with_feature(feature) do |f|
98
+ f.add_group(group)
99
+ end
21
100
  end
22
101
 
23
- def deactivate_all(feature)
24
- @redis.del(group_key(feature))
25
- @redis.del(user_key(feature))
26
- @redis.del(percentage_key(feature))
27
- deactivate_globally(feature)
102
+ def deactivate_group(feature, group)
103
+ with_feature(feature) do |f|
104
+ f.remove_group(group)
105
+ end
28
106
  end
29
107
 
30
108
  def activate_user(feature, user)
31
- @redis.sadd(user_key(feature), user.id)
109
+ with_feature(feature) do |f|
110
+ f.add_user(user)
111
+ end
32
112
  end
33
113
 
34
114
  def deactivate_user(feature, user)
35
- @redis.srem(user_key(feature), user.id)
115
+ with_feature(feature) do |f|
116
+ f.remove_user(user)
117
+ end
36
118
  end
37
119
 
38
120
  def define_group(group, &block)
39
- @groups[group.to_s] = block
121
+ @groups[group] = block
40
122
  end
41
123
 
42
124
  def active?(feature, user = nil)
43
- if user
44
- active_globally?(feature) ||
45
- user_in_active_group?(feature, user) ||
46
- user_active?(feature, user) ||
47
- user_within_active_percentage?(feature, user)
48
- else
49
- active_globally?(feature)
50
- end
125
+ feature = get(feature)
126
+ feature.active?(self, user)
51
127
  end
52
128
 
53
129
  def activate_percentage(feature, percentage)
54
- @redis.set(percentage_key(feature), percentage)
130
+ with_feature(feature) do |f|
131
+ f.percentage = percentage
132
+ end
55
133
  end
56
134
 
57
135
  def deactivate_percentage(feature)
58
- @redis.del(percentage_key(feature))
136
+ with_feature(feature) do |f|
137
+ f.percentage = 0
138
+ end
139
+ end
140
+
141
+ def active_in_group?(group, user)
142
+ f = @groups[group]
143
+ f && f.call(user)
59
144
  end
60
145
 
61
- def info(feature = nil)
62
- if feature
63
- {
64
- :percentage => (active_percentage(feature) || 0).to_i,
65
- :groups => active_groups(feature).map { |g| g.to_sym },
66
- :users => active_user_ids(feature),
67
- :global => active_global_features
68
- }
146
+ def get(feature)
147
+ string = @storage.get(key(feature))
148
+ if string || !migrate?
149
+ Feature.new(feature, string)
69
150
  else
70
- {
71
- :global => active_global_features
72
- }
151
+ info = @legacy.info(feature)
152
+ f = Feature.new(feature)
153
+ f.percentage = info[:percentage]
154
+ f.groups = info[:groups]
155
+ f.users = info[:users]
156
+ save(f)
157
+ f
73
158
  end
74
159
  end
75
160
 
161
+ def features
162
+ (@storage.get(features_key) || "").split(",").map(&:to_sym)
163
+ end
164
+
76
165
  private
77
166
  def key(name)
78
167
  "feature:#{name}"
79
168
  end
80
169
 
81
- def group_key(name)
82
- "#{key(name)}:groups"
83
- end
84
-
85
- def user_key(name)
86
- "#{key(name)}:users"
170
+ def features_key
171
+ "feature:__features__"
87
172
  end
88
173
 
89
- def percentage_key(name)
90
- "#{key(name)}:percentage"
91
- end
92
-
93
- def global_key
94
- "feature:__global__"
95
- end
96
-
97
- def active_groups(feature)
98
- @redis.smembers(group_key(feature)) || []
99
- end
100
-
101
- def active_user_ids(feature)
102
- @redis.smembers(user_key(feature)).map { |id| id.to_i }
103
- end
104
-
105
- def active_global_features
106
- (@redis.smembers(global_key) || []).map(&:to_sym)
107
- end
108
-
109
- def active_percentage(feature)
110
- @redis.get(percentage_key(feature))
111
- end
112
-
113
- def active_globally?(feature)
114
- @redis.sismember(global_key, feature)
115
- end
116
-
117
- def user_in_active_group?(feature, user)
118
- active_groups(feature).any? do |group|
119
- @groups.key?(group) && @groups[group].call(user)
120
- end
174
+ def with_feature(feature)
175
+ f = get(feature)
176
+ yield(f)
177
+ save(f)
121
178
  end
122
179
 
123
- def user_active?(feature, user)
124
- @redis.sismember(user_key(feature), user.id)
180
+ def save(feature)
181
+ @storage.set(key(feature.name), feature.serialize)
182
+ @storage.set(features_key, (features + [feature.name]).join(","))
125
183
  end
126
184
 
127
- def user_within_active_percentage?(feature, user)
128
- percentage = active_percentage(feature)
129
- return false if percentage.nil?
130
- user.id % 100 < percentage.to_i
185
+ def migrate?
186
+ @legacy
131
187
  end
132
188
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  class Rollout
2
- VERSION = "1.2.0"
2
+ VERSION = "2.0.0a"
3
3
  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
@@ -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
data/spec/rollout_spec.rb CHANGED
@@ -60,8 +60,8 @@ describe "Rollout" do
60
60
  @rollout.activate_group(:chat, :fivesonly)
61
61
  @rollout.activate_user(:chat, stub(:id => 51))
62
62
  @rollout.activate_percentage(:chat, 100)
63
- @rollout.activate_globally(:chat)
64
- @rollout.deactivate_all(:chat)
63
+ @rollout.activate(:chat)
64
+ @rollout.deactivate(:chat)
65
65
  end
66
66
 
67
67
  it "removes all of the groups" do
@@ -113,7 +113,7 @@ describe "Rollout" do
113
113
 
114
114
  describe "activating a feature globally" do
115
115
  before do
116
- @rollout.activate_globally(:chat)
116
+ @rollout.activate(:chat)
117
117
  end
118
118
 
119
119
  it "activates the feature" do
@@ -165,8 +165,8 @@ describe "Rollout" do
165
165
 
166
166
  describe "deactivating the feature globally" do
167
167
  before do
168
- @rollout.activate_globally(:chat)
169
- @rollout.deactivate_globally(:chat)
168
+ @rollout.activate(:chat)
169
+ @rollout.deactivate(:chat)
170
170
  end
171
171
 
172
172
  it "becomes inactivate" do
@@ -174,49 +174,61 @@ describe "Rollout" do
174
174
  end
175
175
  end
176
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 "the percentage defaults to 0" do
213
- @rollout.info(:chat).should == {
214
- :percentage => 0,
215
- :groups => [],
216
- :users => [],
217
- :global => []
218
- }
219
- end
177
+ it "keeps a list of features" do
178
+ @rollout.activate(:chat)
179
+ @rollout.features.should be_include(:chat)
180
+ end
181
+
182
+ describe "#get" do
183
+ before do
184
+ @rollout.activate_percentage(:chat, 10)
185
+ @rollout.activate_group(:chat, :caretakers)
186
+ @rollout.activate_group(:chat, :greeters)
187
+ @rollout.activate(:signup)
188
+ @rollout.activate_user(:chat, stub(:id => 42))
189
+ end
190
+
191
+ it "returns the feature object" do
192
+ feature = @rollout.get(:chat)
193
+ feature.groups.should == [:caretakers, :greeters]
194
+ feature.percentage.should == 10
195
+ feature.users.should == [42]
196
+ feature.to_hash.should == {
197
+ :groups => [:caretakers, :greeters],
198
+ :percentage => 10,
199
+ :users => [42]
200
+ }
201
+
202
+ feature = @rollout.get(:signup)
203
+ feature.groups.should be_empty
204
+ feature.users.should be_empty
205
+ feature.percentage.should == 100
206
+ end
207
+ end
208
+
209
+ describe "migration mode" do
210
+ before do
211
+ @legacy = Rollout::Legacy.new(@redis)
212
+ @legacy.activate_percentage(:chat, 12)
213
+ @legacy.activate_user(:chat, stub(:id => 42))
214
+ @legacy.activate_user(:chat, stub(:id => 24))
215
+ @legacy.activate_group(:chat, :dope_people)
216
+ @rollout = Rollout.new(@redis, :migrate => true)
217
+ end
218
+
219
+ it "imports the settings from the legacy rollout once" do
220
+ @rollout.get(:chat).to_hash.should == {
221
+ :percentage => 12,
222
+ :users => [24, 42],
223
+ :groups => [:dope_people]
224
+ }
225
+ @legacy.deactivate_all(:chat)
226
+ @rollout.get(:chat).to_hash.should == {
227
+ :percentage => 12,
228
+ :users => [24, 42],
229
+ :groups => [:dope_people]
230
+ }
231
+ @redis.get("feature:chat").should_not be_nil
220
232
  end
221
233
  end
222
234
  end
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rollout
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
5
- prerelease:
4
+ version: 2.0.0a
5
+ prerelease: 5
6
6
  platform: ruby
7
7
  authors:
8
8
  - James Golick
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-14 00:00:00.000000000 Z
12
+ date: 2012-11-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &70171348875940 !ruby/object:Gem::Requirement
16
+ requirement: &70240073640980 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 2.10.0
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70171348875940
24
+ version_requirements: *70240073640980
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bundler
27
- requirement: &70171348875080 !ruby/object:Gem::Requirement
27
+ requirement: &70240073640480 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 1.0.0
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70171348875080
35
+ version_requirements: *70240073640480
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: jeweler
38
- requirement: &70171348874140 !ruby/object:Gem::Requirement
38
+ requirement: &70240073640020 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 1.6.4
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70171348874140
46
+ version_requirements: *70240073640020
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bourne
49
- requirement: &70171348872540 !ruby/object:Gem::Requirement
49
+ requirement: &70240073639560 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - =
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '1.0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70171348872540
57
+ version_requirements: *70240073639560
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: mocha
60
- requirement: &70171348871700 !ruby/object:Gem::Requirement
60
+ requirement: &70240073639100 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - =
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 0.9.8
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70171348871700
68
+ version_requirements: *70240073639100
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: redis
71
- requirement: &70171348870700 !ruby/object:Gem::Requirement
71
+ requirement: &70240073638720 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,7 +76,7 @@ dependencies:
76
76
  version: '0'
77
77
  type: :runtime
78
78
  prerelease: false
79
- version_requirements: *70171348870700
79
+ version_requirements: *70240073638720
80
80
  description: Feature flippers with redis.
81
81
  email:
82
82
  - jamesgolick@gmail.com
@@ -92,8 +92,11 @@ files:
92
92
  - README.rdoc
93
93
  - VERSION
94
94
  - lib/rollout.rb
95
+ - lib/rollout/legacy.rb
95
96
  - lib/rollout/version.rb
97
+ - misc/check_rollout.rb
96
98
  - rollout.gemspec
99
+ - spec/legacy_spec.rb
97
100
  - spec/rollout_spec.rb
98
101
  - spec/spec.opts
99
102
  - spec/spec_helper.rb
@@ -112,9 +115,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
112
115
  required_rubygems_version: !ruby/object:Gem::Requirement
113
116
  none: false
114
117
  requirements:
115
- - - ! '>='
118
+ - - ! '>'
116
119
  - !ruby/object:Gem::Version
117
- version: '0'
120
+ version: 1.3.1
118
121
  requirements: []
119
122
  rubyforge_project: rollout
120
123
  rubygems_version: 1.8.10
@@ -122,6 +125,8 @@ signing_key:
122
125
  specification_version: 3
123
126
  summary: Feature flippers with redis.
124
127
  test_files:
128
+ - spec/legacy_spec.rb
125
129
  - spec/rollout_spec.rb
126
130
  - spec/spec.opts
127
131
  - spec/spec_helper.rb
132
+ has_rdoc: