rollout 2.4.3 → 2.4.6
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 +5 -5
- data/.circleci/config.yml +95 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +5 -5
- data/Gemfile +3 -1
- data/README.md +18 -6
- data/Rakefile +5 -7
- data/lib/rollout.rb +34 -151
- data/lib/rollout/feature.rb +130 -0
- data/lib/rollout/version.rb +3 -1
- data/rollout.gemspec +27 -23
- data/spec/rollout_spec.rb +7 -2
- data/spec/spec_helper.rb +21 -12
- metadata +39 -42
- data/.document +0 -5
- data/Appraisals +0 -7
- data/Gemfile.lock +0 -56
- data/gemfiles/redis_3.gemfile +0 -13
- data/gemfiles/redis_4.gemfile +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f3e6e9521ed03059b5b673616e985f89231d77d1ca42d8fa7ff00797ef906768
|
|
4
|
+
data.tar.gz: ac66f6f5f0da7e03bbf506d09f62d34e6afd7f1ea09b248b5193d74f3b6a720e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccebf1c2024c332cd921a49b400a7ee8e9a5c0ce95cdf59f5e89fb302bef08684cc04ffae30c952e14ffd5acc97d583871ee590aebe6aa2c519cc416afc17b9f
|
|
7
|
+
data.tar.gz: fe0a400d1f694a6aefb5b1e7d89afb64a8cb28c07a1f510d01655d724bfb97d5b38e7c60e40669aec5fded5581e760549b53564078018cad92c968b6900753ee
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
version: 2.1
|
|
2
|
+
|
|
3
|
+
workflows:
|
|
4
|
+
main:
|
|
5
|
+
jobs:
|
|
6
|
+
- ruby27
|
|
7
|
+
- ruby26
|
|
8
|
+
- ruby25
|
|
9
|
+
- ruby24
|
|
10
|
+
- ruby23
|
|
11
|
+
|
|
12
|
+
executors:
|
|
13
|
+
ruby27:
|
|
14
|
+
docker:
|
|
15
|
+
- image: circleci/ruby:2.7
|
|
16
|
+
- image: circleci/redis:alpine
|
|
17
|
+
ruby26:
|
|
18
|
+
docker:
|
|
19
|
+
- image: circleci/ruby:2.6
|
|
20
|
+
- image: circleci/redis:alpine
|
|
21
|
+
ruby25:
|
|
22
|
+
docker:
|
|
23
|
+
- image: circleci/ruby:2.5
|
|
24
|
+
- image: circleci/redis:alpine
|
|
25
|
+
ruby24:
|
|
26
|
+
docker:
|
|
27
|
+
- image: circleci/ruby:2.4
|
|
28
|
+
- image: circleci/redis:alpine
|
|
29
|
+
ruby23:
|
|
30
|
+
docker:
|
|
31
|
+
- image: circleci/ruby:2.3
|
|
32
|
+
- image: circleci/redis:alpine
|
|
33
|
+
|
|
34
|
+
commands:
|
|
35
|
+
test:
|
|
36
|
+
steps:
|
|
37
|
+
- restore_cache:
|
|
38
|
+
keys:
|
|
39
|
+
- bundler-{{ checksum "Gemfile.lock" }}
|
|
40
|
+
|
|
41
|
+
- run:
|
|
42
|
+
name: Bundle Install
|
|
43
|
+
command: bundle check --path vendor/bundle || bundle install
|
|
44
|
+
|
|
45
|
+
- save_cache:
|
|
46
|
+
key: bundler-{{ checksum "Gemfile.lock" }}
|
|
47
|
+
paths:
|
|
48
|
+
- vendor/bundle
|
|
49
|
+
|
|
50
|
+
- run:
|
|
51
|
+
name: Run rspec
|
|
52
|
+
command: |
|
|
53
|
+
bundle exec rspec --format documentation --format RspecJunitFormatter --out test_results/rspec.xml
|
|
54
|
+
|
|
55
|
+
jobs:
|
|
56
|
+
ruby27:
|
|
57
|
+
executor: ruby27
|
|
58
|
+
steps:
|
|
59
|
+
- checkout
|
|
60
|
+
- test
|
|
61
|
+
|
|
62
|
+
- run:
|
|
63
|
+
name: Report Test Coverage
|
|
64
|
+
command: |
|
|
65
|
+
wget https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 -O cc-test-reporter
|
|
66
|
+
chmod +x cc-test-reporter
|
|
67
|
+
./cc-test-reporter format-coverage -t simplecov -o coverage/codeclimate.json coverage/.resultset.json
|
|
68
|
+
./cc-test-reporter upload-coverage -i coverage/codeclimate.json
|
|
69
|
+
|
|
70
|
+
- store_test_results:
|
|
71
|
+
path: test_results
|
|
72
|
+
|
|
73
|
+
ruby26:
|
|
74
|
+
executor: ruby26
|
|
75
|
+
steps:
|
|
76
|
+
- checkout
|
|
77
|
+
- test
|
|
78
|
+
|
|
79
|
+
ruby25:
|
|
80
|
+
executor: ruby25
|
|
81
|
+
steps:
|
|
82
|
+
- checkout
|
|
83
|
+
- test
|
|
84
|
+
|
|
85
|
+
ruby24:
|
|
86
|
+
executor: ruby24
|
|
87
|
+
steps:
|
|
88
|
+
- checkout
|
|
89
|
+
- test
|
|
90
|
+
|
|
91
|
+
ruby23:
|
|
92
|
+
executor: ruby23
|
|
93
|
+
steps:
|
|
94
|
+
- checkout
|
|
95
|
+
- test
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Fast feature flags based on Redis.
|
|
4
4
|
|
|
5
|
-
[](https://badge.fury.io/rb/rollout)
|
|
6
|
+
[](https://circleci.com/gh/fetlife/rollout)
|
|
7
|
+
[](https://codeclimate.com/github/FetLife/rollout)
|
|
8
|
+
[](https://codeclimate.com/github/FetLife/rollout/coverage)
|
|
9
9
|
|
|
10
10
|
## Install it
|
|
11
11
|
|
|
@@ -20,10 +20,18 @@ Initialize a rollout object. I assign it to a global var.
|
|
|
20
20
|
```ruby
|
|
21
21
|
require 'redis'
|
|
22
22
|
|
|
23
|
-
$redis
|
|
23
|
+
$redis = Redis.new
|
|
24
24
|
$rollout = Rollout.new($redis)
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
or even simpler
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'redis'
|
|
31
|
+
$rollout = Rollout.new(Redis.current) # Will use REDIS_URL env var or default redis url
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
|
|
27
35
|
Update data specific to a feature:
|
|
28
36
|
|
|
29
37
|
```ruby
|
|
@@ -71,6 +79,9 @@ Deactivate groups like this:
|
|
|
71
79
|
$rollout.deactivate_group(:chat, :all)
|
|
72
80
|
```
|
|
73
81
|
|
|
82
|
+
Groups need to be defined every time your app starts. The logic is not persisted
|
|
83
|
+
anywhere.
|
|
84
|
+
|
|
74
85
|
## Specific Users
|
|
75
86
|
|
|
76
87
|
You might want to let a specific user into a beta test or something. If that
|
|
@@ -98,7 +109,7 @@ $rollout.activate_percentage(:chat, 20)
|
|
|
98
109
|
The algorithm for determining which users get let in is this:
|
|
99
110
|
|
|
100
111
|
```ruby
|
|
101
|
-
CRC32(user.id)
|
|
112
|
+
CRC32(user.id) < (2**32 - 1) / 100.0 * percentage
|
|
102
113
|
```
|
|
103
114
|
|
|
104
115
|
So, for 20%, users 0, 1, 10, 11, 20, 21, etc would be allowed in. Those users
|
|
@@ -176,6 +187,7 @@ This example would use the "development:feature:chat:groups" key.
|
|
|
176
187
|
* Python: https://github.com/asenchi/proclaim
|
|
177
188
|
* PHP: https://github.com/opensoft/rollout
|
|
178
189
|
* Clojure: https://github.com/yeller/shoutout
|
|
190
|
+
* Perl: https://metacpan.org/pod/Toggle
|
|
179
191
|
|
|
180
192
|
|
|
181
193
|
## Contributors
|
data/Rakefile
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
require "rspec/core/rake_task"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
require 'rspec/core/rake_task'
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
6
|
+
|
|
7
|
+
task default: :spec
|
data/lib/rollout.rb
CHANGED
|
@@ -1,141 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rollout/feature'
|
|
4
|
+
require 'rollout/version'
|
|
5
|
+
require 'zlib'
|
|
6
|
+
require 'set'
|
|
7
|
+
require 'json'
|
|
5
8
|
|
|
6
9
|
class Rollout
|
|
7
10
|
RAND_BASE = (2**32 - 1) / 100.0
|
|
8
|
-
|
|
9
|
-
class Feature
|
|
10
|
-
attr_accessor :groups, :users, :percentage, :data
|
|
11
|
-
attr_reader :name, :options
|
|
12
|
-
|
|
13
|
-
def initialize(name, string = nil, opts = {})
|
|
14
|
-
@options = opts
|
|
15
|
-
@name = name
|
|
16
|
-
|
|
17
|
-
if string
|
|
18
|
-
raw_percentage,raw_users,raw_groups,raw_data = string.split('|', 4)
|
|
19
|
-
@percentage = raw_percentage.to_f
|
|
20
|
-
@users = users_from_string(raw_users)
|
|
21
|
-
@groups = groups_from_string(raw_groups)
|
|
22
|
-
@data = raw_data.nil? || raw_data.strip.empty? ? {} : JSON.parse(raw_data)
|
|
23
|
-
else
|
|
24
|
-
clear
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def serialize
|
|
29
|
-
"#{@percentage}|#{@users.to_a.join(",")}|#{@groups.to_a.join(",")}|#{serialize_data}"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def add_user(user)
|
|
33
|
-
id = user_id(user)
|
|
34
|
-
@users << id unless @users.include?(id)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def remove_user(user)
|
|
38
|
-
@users.delete(user_id(user))
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def add_group(group)
|
|
42
|
-
@groups << group.to_sym unless @groups.include?(group.to_sym)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def remove_group(group)
|
|
46
|
-
@groups.delete(group.to_sym)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def clear
|
|
50
|
-
@groups = groups_from_string("")
|
|
51
|
-
@users = users_from_string("")
|
|
52
|
-
@percentage = 0
|
|
53
|
-
@data = {}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def active?(rollout, user)
|
|
57
|
-
if user
|
|
58
|
-
id = user_id(user)
|
|
59
|
-
user_in_percentage?(id) ||
|
|
60
|
-
user_in_active_users?(id) ||
|
|
61
|
-
user_in_active_group?(user, rollout)
|
|
62
|
-
else
|
|
63
|
-
@percentage == 100
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def user_in_active_users?(user)
|
|
68
|
-
@users.include?(user_id(user))
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def to_hash
|
|
72
|
-
{
|
|
73
|
-
percentage: @percentage,
|
|
74
|
-
groups: @groups,
|
|
75
|
-
users: @users
|
|
76
|
-
}
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
private
|
|
80
|
-
def user_id(user)
|
|
81
|
-
if user.is_a?(Integer) || user.is_a?(String)
|
|
82
|
-
user.to_s
|
|
83
|
-
else
|
|
84
|
-
user.send(id_user_by).to_s
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def id_user_by
|
|
89
|
-
@options[:id_user_by] || :id
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def user_in_percentage?(user)
|
|
93
|
-
Zlib.crc32(user_id_for_percentage(user)) < RAND_BASE * @percentage
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def user_id_for_percentage(user)
|
|
97
|
-
if @options[:randomize_percentage]
|
|
98
|
-
user_id(user).to_s + @name.to_s
|
|
99
|
-
else
|
|
100
|
-
user_id(user)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def user_in_active_group?(user, rollout)
|
|
105
|
-
@groups.any? do |g|
|
|
106
|
-
rollout.active_in_group?(g, user)
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def serialize_data
|
|
111
|
-
return "" unless @data.is_a? Hash
|
|
112
|
-
|
|
113
|
-
@data.to_json
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def users_from_string(raw_users)
|
|
117
|
-
users = (raw_users || "").split(",").map(&:to_s)
|
|
118
|
-
if @options[:use_sets]
|
|
119
|
-
users.to_set
|
|
120
|
-
else
|
|
121
|
-
users
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def groups_from_string(raw_groups)
|
|
126
|
-
groups = (raw_groups || "").split(",").map(&:to_sym)
|
|
127
|
-
if @options[:use_sets]
|
|
128
|
-
groups.to_set
|
|
129
|
-
else
|
|
130
|
-
groups
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
11
|
|
|
135
12
|
def initialize(storage, opts = {})
|
|
136
13
|
@storage = storage
|
|
137
14
|
@options = opts
|
|
138
|
-
@groups = { all:
|
|
15
|
+
@groups = { all: ->(_user) { true } }
|
|
139
16
|
end
|
|
140
17
|
|
|
141
18
|
def activate(feature)
|
|
@@ -145,15 +22,13 @@ class Rollout
|
|
|
145
22
|
end
|
|
146
23
|
|
|
147
24
|
def deactivate(feature)
|
|
148
|
-
with_feature(feature)
|
|
149
|
-
f.clear
|
|
150
|
-
end
|
|
25
|
+
with_feature(feature, &:clear)
|
|
151
26
|
end
|
|
152
27
|
|
|
153
28
|
def delete(feature)
|
|
154
|
-
features = (@storage.get(features_key) ||
|
|
29
|
+
features = (@storage.get(features_key) || '').split(',')
|
|
155
30
|
features.delete(feature.to_s)
|
|
156
|
-
@storage.set(features_key, features.join(
|
|
31
|
+
@storage.set(features_key, features.join(','))
|
|
157
32
|
@storage.del(key(feature))
|
|
158
33
|
end
|
|
159
34
|
|
|
@@ -193,20 +68,20 @@ class Rollout
|
|
|
193
68
|
|
|
194
69
|
def activate_users(feature, users)
|
|
195
70
|
with_feature(feature) do |f|
|
|
196
|
-
users.each{|user| f.add_user(user)}
|
|
71
|
+
users.each { |user| f.add_user(user) }
|
|
197
72
|
end
|
|
198
73
|
end
|
|
199
74
|
|
|
200
75
|
def deactivate_users(feature, users)
|
|
201
76
|
with_feature(feature) do |f|
|
|
202
|
-
users.each{|user| f.remove_user(user)}
|
|
77
|
+
users.each { |user| f.remove_user(user) }
|
|
203
78
|
end
|
|
204
79
|
end
|
|
205
80
|
|
|
206
81
|
def set_users(feature, users)
|
|
207
82
|
with_feature(feature) do |f|
|
|
208
83
|
f.users = []
|
|
209
|
-
users.each{|user| f.add_user(user)}
|
|
84
|
+
users.each { |user| f.add_user(user) }
|
|
210
85
|
end
|
|
211
86
|
end
|
|
212
87
|
|
|
@@ -242,7 +117,7 @@ class Rollout
|
|
|
242
117
|
|
|
243
118
|
def active_in_group?(group, user)
|
|
244
119
|
f = @groups[group.to_sym]
|
|
245
|
-
f
|
|
120
|
+
f&.call(user)
|
|
246
121
|
end
|
|
247
122
|
|
|
248
123
|
def get(feature)
|
|
@@ -263,29 +138,31 @@ class Rollout
|
|
|
263
138
|
end
|
|
264
139
|
|
|
265
140
|
def multi_get(*features)
|
|
266
|
-
|
|
141
|
+
return [] if features.empty?
|
|
142
|
+
|
|
143
|
+
feature_keys = features.map { |feature| key(feature) }
|
|
267
144
|
@storage.mget(*feature_keys).map.with_index { |string, index| Feature.new(features[index], string, @options) }
|
|
268
145
|
end
|
|
269
146
|
|
|
270
147
|
def features
|
|
271
|
-
(@storage.get(features_key) ||
|
|
148
|
+
(@storage.get(features_key) || '').split(',').map(&:to_sym)
|
|
272
149
|
end
|
|
273
150
|
|
|
274
151
|
def feature_states(user = nil)
|
|
275
|
-
features.each_with_object({}) do |f, hash|
|
|
276
|
-
hash[f] = active?(
|
|
152
|
+
multi_get(*features).each_with_object({}) do |f, hash|
|
|
153
|
+
hash[f.name] = f.active?(self, user)
|
|
277
154
|
end
|
|
278
155
|
end
|
|
279
156
|
|
|
280
157
|
def active_features(user = nil)
|
|
281
|
-
features.select do |f|
|
|
282
|
-
active?(
|
|
283
|
-
end
|
|
158
|
+
multi_get(*features).select do |f|
|
|
159
|
+
f.active?(self, user)
|
|
160
|
+
end.map(&:name)
|
|
284
161
|
end
|
|
285
162
|
|
|
286
163
|
def clear!
|
|
287
164
|
features.each do |feature|
|
|
288
|
-
with_feature(feature
|
|
165
|
+
with_feature(feature, &:clear)
|
|
289
166
|
@storage.del(key(feature))
|
|
290
167
|
end
|
|
291
168
|
|
|
@@ -293,7 +170,13 @@ class Rollout
|
|
|
293
170
|
end
|
|
294
171
|
|
|
295
172
|
def exists?(feature)
|
|
296
|
-
|
|
173
|
+
# since redis-rb v4.2, `#exists?` replaces `#exists` which now returns integer value instead of boolean
|
|
174
|
+
# https://github.com/redis/redis-rb/pull/918
|
|
175
|
+
if @storage.respond_to?(:exists?)
|
|
176
|
+
@storage.exists?(key(feature))
|
|
177
|
+
else
|
|
178
|
+
@storage.exists(key(feature))
|
|
179
|
+
end
|
|
297
180
|
end
|
|
298
181
|
|
|
299
182
|
private
|
|
@@ -303,7 +186,7 @@ class Rollout
|
|
|
303
186
|
end
|
|
304
187
|
|
|
305
188
|
def features_key
|
|
306
|
-
|
|
189
|
+
'feature:__features__'
|
|
307
190
|
end
|
|
308
191
|
|
|
309
192
|
def with_feature(feature)
|
|
@@ -314,6 +197,6 @@ class Rollout
|
|
|
314
197
|
|
|
315
198
|
def save(feature)
|
|
316
199
|
@storage.set(key(feature.name), feature.serialize)
|
|
317
|
-
@storage.set(features_key, (features | [feature.name.to_sym]).join(
|
|
200
|
+
@storage.set(features_key, (features | [feature.name.to_sym]).join(','))
|
|
318
201
|
end
|
|
319
202
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Rollout
|
|
4
|
+
class Feature
|
|
5
|
+
attr_accessor :groups, :users, :percentage, :data
|
|
6
|
+
attr_reader :name, :options
|
|
7
|
+
|
|
8
|
+
def initialize(name, string = nil, opts = {})
|
|
9
|
+
@options = opts
|
|
10
|
+
@name = name
|
|
11
|
+
|
|
12
|
+
if string
|
|
13
|
+
raw_percentage, raw_users, raw_groups, raw_data = string.split('|', 4)
|
|
14
|
+
@percentage = raw_percentage.to_f
|
|
15
|
+
@users = users_from_string(raw_users)
|
|
16
|
+
@groups = groups_from_string(raw_groups)
|
|
17
|
+
@data = raw_data.nil? || raw_data.strip.empty? ? {} : JSON.parse(raw_data)
|
|
18
|
+
else
|
|
19
|
+
clear
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def serialize
|
|
24
|
+
"#{@percentage}|#{@users.to_a.join(',')}|#{@groups.to_a.join(',')}|#{serialize_data}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_user(user)
|
|
28
|
+
id = user_id(user)
|
|
29
|
+
@users << id unless @users.include?(id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def remove_user(user)
|
|
33
|
+
@users.delete(user_id(user))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_group(group)
|
|
37
|
+
@groups << group.to_sym unless @groups.include?(group.to_sym)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def remove_group(group)
|
|
41
|
+
@groups.delete(group.to_sym)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clear
|
|
45
|
+
@groups = groups_from_string('')
|
|
46
|
+
@users = users_from_string('')
|
|
47
|
+
@percentage = 0
|
|
48
|
+
@data = {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def active?(rollout, user)
|
|
52
|
+
if user
|
|
53
|
+
id = user_id(user)
|
|
54
|
+
user_in_percentage?(id) ||
|
|
55
|
+
user_in_active_users?(id) ||
|
|
56
|
+
user_in_active_group?(user, rollout)
|
|
57
|
+
else
|
|
58
|
+
@percentage == 100
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def user_in_active_users?(user)
|
|
63
|
+
@users.include?(user_id(user))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_hash
|
|
67
|
+
{
|
|
68
|
+
percentage: @percentage,
|
|
69
|
+
groups: @groups,
|
|
70
|
+
users: @users
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def user_id(user)
|
|
77
|
+
if user.is_a?(Integer) || user.is_a?(String)
|
|
78
|
+
user.to_s
|
|
79
|
+
else
|
|
80
|
+
user.send(id_user_by).to_s
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def id_user_by
|
|
85
|
+
@options[:id_user_by] || :id
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def user_in_percentage?(user)
|
|
89
|
+
Zlib.crc32(user_id_for_percentage(user)) < RAND_BASE * @percentage
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def user_id_for_percentage(user)
|
|
93
|
+
if @options[:randomize_percentage]
|
|
94
|
+
user_id(user).to_s + @name.to_s
|
|
95
|
+
else
|
|
96
|
+
user_id(user)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def user_in_active_group?(user, rollout)
|
|
101
|
+
@groups.any? do |g|
|
|
102
|
+
rollout.active_in_group?(g, user)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def serialize_data
|
|
107
|
+
return '' unless @data.is_a? Hash
|
|
108
|
+
|
|
109
|
+
@data.to_json
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def users_from_string(raw_users)
|
|
113
|
+
users = (raw_users || '').split(',').map(&:to_s)
|
|
114
|
+
if @options[:use_sets]
|
|
115
|
+
users.to_set
|
|
116
|
+
else
|
|
117
|
+
users
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def groups_from_string(raw_groups)
|
|
122
|
+
groups = (raw_groups || '').split(',').map(&:to_sym)
|
|
123
|
+
if @options[:use_sets]
|
|
124
|
+
groups.to_set
|
|
125
|
+
else
|
|
126
|
+
groups
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/rollout/version.rb
CHANGED
data/rollout.gemspec
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
#
|
|
2
|
-
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
-
require "rollout/version"
|
|
1
|
+
# frozen_string_literal: true
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
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/FetLife/rollout"
|
|
13
|
-
s.license = "MIT"
|
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
|
4
|
+
require 'rollout/version'
|
|
14
5
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = 'rollout'
|
|
8
|
+
spec.version = Rollout::VERSION
|
|
9
|
+
spec.authors = ['James Golick']
|
|
10
|
+
spec.email = ['jamesgolick@gmail.com']
|
|
11
|
+
spec.description = 'Feature flippers with redis.'
|
|
12
|
+
spec.summary = 'Feature flippers with redis.'
|
|
13
|
+
spec.homepage = 'https://github.com/FetLife/rollout'
|
|
14
|
+
spec.license = 'MIT'
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
spec.files = `git ls-files`.split("\n")
|
|
17
|
+
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
18
|
+
spec.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
19
|
+
spec.require_paths = ['lib']
|
|
20
|
+
|
|
21
|
+
spec.required_ruby_version = '>= 2.3'
|
|
22
|
+
|
|
23
|
+
spec.add_dependency 'redis', '~> 4.0'
|
|
24
|
+
|
|
25
|
+
spec.add_development_dependency 'bundler', '>= 1.17'
|
|
26
|
+
spec.add_development_dependency 'pry'
|
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
28
|
+
spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4'
|
|
29
|
+
spec.add_development_dependency 'rubocop', '~> 0.71'
|
|
30
|
+
spec.add_development_dependency 'simplecov', '0.17'
|
|
27
31
|
end
|
data/spec/rollout_spec.rb
CHANGED
|
@@ -2,8 +2,7 @@ require "spec_helper"
|
|
|
2
2
|
|
|
3
3
|
RSpec.describe "Rollout" do
|
|
4
4
|
before do
|
|
5
|
-
@
|
|
6
|
-
@rollout = Rollout.new(@redis)
|
|
5
|
+
@rollout = Rollout.new(Redis.current)
|
|
7
6
|
end
|
|
8
7
|
|
|
9
8
|
describe "when a group is activated" do
|
|
@@ -605,6 +604,12 @@ RSpec.describe "Rollout" do
|
|
|
605
604
|
expect(features[2].percentage).to eq 100
|
|
606
605
|
expect(features.size).to eq 3
|
|
607
606
|
end
|
|
607
|
+
|
|
608
|
+
describe 'when given feature keys is empty' do
|
|
609
|
+
it 'returns empty array' do
|
|
610
|
+
expect(@rollout.multi_get(*[])).to match_array([])
|
|
611
|
+
end
|
|
612
|
+
end
|
|
608
613
|
end
|
|
609
614
|
|
|
610
615
|
describe "#set_feature_data" do
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
require
|
|
5
|
-
require "rspec"
|
|
6
|
-
require ENV["USE_REAL_REDIS"] == "true" ? "redis" : "fakeredis"
|
|
3
|
+
require 'simplecov'
|
|
7
4
|
|
|
8
|
-
SimpleCov.start
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
SimpleCov.start
|
|
6
|
+
|
|
7
|
+
require 'bundler/setup'
|
|
8
|
+
require 'redis'
|
|
9
|
+
require 'rollout'
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
Redis.current = Redis.new(
|
|
12
|
+
host: ENV.fetch('REDIS_HOST', '127.0.0.1'),
|
|
13
|
+
port: ENV.fetch('REDIS_PORT', '6379'),
|
|
14
|
+
db: ENV.fetch('REDIS_DB', '7'),
|
|
15
|
+
)
|
|
15
16
|
|
|
16
17
|
RSpec.configure do |config|
|
|
17
|
-
config.
|
|
18
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
19
|
+
|
|
20
|
+
# config.disable_monkey_patching!
|
|
21
|
+
|
|
22
|
+
config.expect_with :rspec do |c|
|
|
23
|
+
c.syntax = :expect
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
config.before { Redis.current.flushdb }
|
|
18
27
|
end
|
metadata
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rollout
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Golick
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-06-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: redis
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
20
|
-
type: :
|
|
19
|
+
version: '4.0'
|
|
20
|
+
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- - "
|
|
24
|
+
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
26
|
+
version: '4.0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
28
|
+
name: bundler
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '1.17'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
40
|
+
version: '1.17'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
42
|
+
name: pry
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
47
|
+
version: '0'
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
54
|
+
version: '0'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
56
|
+
name: rspec
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
|
-
- - "
|
|
59
|
+
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
61
|
+
version: '3.0'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
|
-
- - "
|
|
66
|
+
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
68
|
+
version: '3.0'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
70
|
+
name: rspec_junit_formatter
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
|
-
- - "
|
|
73
|
+
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0'
|
|
75
|
+
version: '0.4'
|
|
76
76
|
type: :development
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
|
-
- - "
|
|
80
|
+
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0'
|
|
82
|
+
version: '0.4'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
84
|
+
name: rubocop
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
|
-
- - "
|
|
87
|
+
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '0'
|
|
89
|
+
version: '0.71'
|
|
90
90
|
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
|
-
- - "
|
|
94
|
+
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '0'
|
|
96
|
+
version: '0.71'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
98
|
+
name: simplecov
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
|
-
- -
|
|
101
|
+
- - '='
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '0'
|
|
103
|
+
version: '0.17'
|
|
104
104
|
type: :development
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
|
-
- -
|
|
108
|
+
- - '='
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '0'
|
|
110
|
+
version: '0.17'
|
|
111
111
|
description: Feature flippers with redis.
|
|
112
112
|
email:
|
|
113
113
|
- jamesgolick@gmail.com
|
|
@@ -115,19 +115,17 @@ executables: []
|
|
|
115
115
|
extensions: []
|
|
116
116
|
extra_rdoc_files: []
|
|
117
117
|
files:
|
|
118
|
-
- ".
|
|
118
|
+
- ".circleci/config.yml"
|
|
119
119
|
- ".gitignore"
|
|
120
120
|
- ".rspec"
|
|
121
|
+
- ".rubocop.yml"
|
|
121
122
|
- ".travis.yml"
|
|
122
|
-
- Appraisals
|
|
123
123
|
- Gemfile
|
|
124
|
-
- Gemfile.lock
|
|
125
124
|
- LICENSE
|
|
126
125
|
- README.md
|
|
127
126
|
- Rakefile
|
|
128
|
-
- gemfiles/redis_3.gemfile
|
|
129
|
-
- gemfiles/redis_4.gemfile
|
|
130
127
|
- lib/rollout.rb
|
|
128
|
+
- lib/rollout/feature.rb
|
|
131
129
|
- lib/rollout/version.rb
|
|
132
130
|
- rollout.gemspec
|
|
133
131
|
- spec/rollout_spec.rb
|
|
@@ -144,15 +142,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
144
142
|
requirements:
|
|
145
143
|
- - ">="
|
|
146
144
|
- !ruby/object:Gem::Version
|
|
147
|
-
version: '
|
|
145
|
+
version: '2.3'
|
|
148
146
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
147
|
requirements:
|
|
150
148
|
- - ">="
|
|
151
149
|
- !ruby/object:Gem::Version
|
|
152
150
|
version: '0'
|
|
153
151
|
requirements: []
|
|
154
|
-
|
|
155
|
-
rubygems_version: 2.6.12
|
|
152
|
+
rubygems_version: 3.0.3
|
|
156
153
|
signing_key:
|
|
157
154
|
specification_version: 4
|
|
158
155
|
summary: Feature flippers with redis.
|
data/.document
DELETED
data/Appraisals
DELETED
data/Gemfile.lock
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
rollout (2.4.3)
|
|
5
|
-
|
|
6
|
-
GEM
|
|
7
|
-
remote: https://rubygems.org/
|
|
8
|
-
specs:
|
|
9
|
-
appraisal (2.2.0)
|
|
10
|
-
bundler
|
|
11
|
-
rake
|
|
12
|
-
thor (>= 0.14.0)
|
|
13
|
-
codeclimate-test-reporter (1.0.8)
|
|
14
|
-
simplecov (<= 0.13)
|
|
15
|
-
diff-lcs (1.3)
|
|
16
|
-
docile (1.1.5)
|
|
17
|
-
fakeredis (0.6.0)
|
|
18
|
-
redis (~> 3.2)
|
|
19
|
-
json (2.1.0)
|
|
20
|
-
rake (12.0.0)
|
|
21
|
-
redis (3.3.3)
|
|
22
|
-
rspec (3.6.0)
|
|
23
|
-
rspec-core (~> 3.6.0)
|
|
24
|
-
rspec-expectations (~> 3.6.0)
|
|
25
|
-
rspec-mocks (~> 3.6.0)
|
|
26
|
-
rspec-core (3.6.0)
|
|
27
|
-
rspec-support (~> 3.6.0)
|
|
28
|
-
rspec-expectations (3.6.0)
|
|
29
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
30
|
-
rspec-support (~> 3.6.0)
|
|
31
|
-
rspec-mocks (3.6.0)
|
|
32
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
33
|
-
rspec-support (~> 3.6.0)
|
|
34
|
-
rspec-support (3.6.0)
|
|
35
|
-
simplecov (0.13.0)
|
|
36
|
-
docile (~> 1.1.0)
|
|
37
|
-
json (>= 1.8, < 3)
|
|
38
|
-
simplecov-html (~> 0.10.0)
|
|
39
|
-
simplecov-html (0.10.1)
|
|
40
|
-
thor (0.19.4)
|
|
41
|
-
|
|
42
|
-
PLATFORMS
|
|
43
|
-
ruby
|
|
44
|
-
|
|
45
|
-
DEPENDENCIES
|
|
46
|
-
appraisal
|
|
47
|
-
bundler (>= 1.0.0)
|
|
48
|
-
codeclimate-test-reporter
|
|
49
|
-
fakeredis
|
|
50
|
-
redis
|
|
51
|
-
rollout!
|
|
52
|
-
rspec
|
|
53
|
-
simplecov
|
|
54
|
-
|
|
55
|
-
BUNDLED WITH
|
|
56
|
-
1.15.1
|
data/gemfiles/redis_3.gemfile
DELETED