rollout 2.4.3 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c11b1c464e66f315ad7ee924ea7408a5c4bf2a6f
4
- data.tar.gz: 16da6d26818429e62e4783533df17d9022380a46
2
+ SHA256:
3
+ metadata.gz: 37e56080ba30fb08aaa7e718e22fae3cab0745e84a91537e4e5520c2843a63a2
4
+ data.tar.gz: 2b2f8c5a6e7a3ddd8275d818b4a168e7573204c2e259e323ca7a2ea9996062d9
5
5
  SHA512:
6
- metadata.gz: 4b7b1416e6ba291b67dbce4b5d67db30bbe494fbb31791f6435efe3280607abf17d64cf96210bbe113c6c9948ca8d50c25c0547b9870d0f652033b69cf412b00
7
- data.tar.gz: 19e0b2bd04907cfe2f1b400ccb047b8320b35dddf85f1b76d607b5491340f0b3a63a20fdb2f30af138579a8e016accc4aa88549d32c4136c8a003fe2997ea318
6
+ metadata.gz: a31905463a16ca7413566460c9a693ab5b9063e74547c05aafe2cf40d5e8f8d83ecb9e85316dd48c3e6b5e5b63311ac222343b485228a695ff5b72f8b0d82a2c
7
+ data.tar.gz: 33fc542bdfe0def2b13a1b3697ba59c1e5f83e813a7c67740d94bf0a9725e17003fdc3fecfa923874883730dde589e0906b5d49ab19ff49c36943e259c28f134
@@ -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
@@ -19,5 +19,6 @@ rdoc
19
19
  pkg
20
20
  *.gem
21
21
  gemfiles/*.lock
22
+ .rspec_status
22
23
 
23
24
  ## PROJECT::SPECIFIC
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'spec/**/*'
4
+
5
+ Metrics/LineLength:
6
+ Max: 120
7
+ Metrics/MethodLength:
8
+ Max: 20
9
+
10
+ Style/TrailingCommaInArguments:
11
+ EnforcedStyleForMultiline: comma
data/.travis.yml CHANGED
@@ -4,13 +4,13 @@ sudo: false
4
4
  services:
5
5
  - redis-server
6
6
  rvm:
7
- - 2.4.1
8
- - 2.3.1
7
+ - 2.6
8
+ - 2.5
9
+ - 2.4
10
+ - 2.3
9
11
  - 2.2
10
12
  - 2.1
11
- - 2.0.0
12
- - 1.9.3
13
- - jruby-19mode
13
+ - 2.0
14
14
  env:
15
15
  - USE_REAL_REDIS=true
16
16
  gemfile:
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
- source "https://rubygems.org"
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
2
4
 
3
5
  gemspec
data/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  Fast feature flags based on Redis.
4
4
 
5
- [![Build Status](https://travis-ci.org/fetlife/rollout.svg?branch=master)](https://travis-ci.org/fetlife/rollout)
6
- [![Code Climate](https://codeclimate.com/github/FetLife/rollout/badges/gpa.svg)](https://codeclimate.com/github/fetlife/rollout)
7
- [![Test Coverage](https://codeclimate.com/github/FetLife/rollout/badges/coverage.svg)](https://codeclimate.com/github/fetlife/rollout/coverage)
8
- [![Dependency Status](https://gemnasium.com/FetLife/rollout.svg)](https://gemnasium.com/fetlife/rollout)
5
+ [![Gem Version](https://badge.fury.io/rb/rollout.svg)](https://badge.fury.io/rb/rollout)
6
+ [![CircleCI](https://circleci.com/gh/fetlife/rollout.svg?style=svg)](https://circleci.com/gh/fetlife/rollout)
7
+ [![Code Climate](https://codeclimate.com/github/FetLife/rollout/badges/gpa.svg)](https://codeclimate.com/github/FetLife/rollout)
8
+ [![Test Coverage](https://codeclimate.com/github/FetLife/rollout/badges/coverage.svg)](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 = Redis.new
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) % 100_000 < percentage * 1_000
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
- begin
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
3
2
 
4
- RSpec::Core::RakeTask.new(:spec)
3
+ require 'rspec/core/rake_task'
5
4
 
6
- task default: :spec
7
- rescue LoadError
8
- # no rspec available
9
- end
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
data/lib/rollout.rb CHANGED
@@ -1,141 +1,30 @@
1
- require "rollout/version"
2
- require "zlib"
3
- require "set"
4
- require "json"
1
+ # frozen_string_literal: true
5
2
 
6
- class Rollout
7
- 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
3
+ require 'rollout/feature'
4
+ require 'rollout/logging'
5
+ require 'rollout/version'
6
+ require 'zlib'
7
+ require 'set'
8
+ require 'json'
9
+ require 'observer'
95
10
 
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
11
+ class Rollout
12
+ include Observable
115
13
 
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
14
+ RAND_BASE = (2**32 - 1) / 100.0
124
15
 
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
16
+ attr_reader :options, :storage
134
17
 
135
18
  def initialize(storage, opts = {})
136
19
  @storage = storage
137
20
  @options = opts
138
- @groups = { all: lambda { |user| true } }
21
+ @groups = { all: ->(_user) { true } }
22
+
23
+ extend(Logging) if opts[:logging]
24
+ end
25
+
26
+ def groups
27
+ @groups.keys
139
28
  end
140
29
 
141
30
  def activate(feature)
@@ -145,16 +34,18 @@ class Rollout
145
34
  end
146
35
 
147
36
  def deactivate(feature)
148
- with_feature(feature) do |f|
149
- f.clear
150
- end
37
+ with_feature(feature, &:clear)
151
38
  end
152
39
 
153
40
  def delete(feature)
154
- features = (@storage.get(features_key) || "").split(",")
41
+ features = (@storage.get(features_key) || '').split(',')
155
42
  features.delete(feature.to_s)
156
- @storage.set(features_key, features.join(","))
43
+ @storage.set(features_key, features.join(','))
157
44
  @storage.del(key(feature))
45
+
46
+ if respond_to?(:logging)
47
+ logging.delete(feature)
48
+ end
158
49
  end
159
50
 
160
51
  def set(feature, desired_state)
@@ -193,20 +84,20 @@ class Rollout
193
84
 
194
85
  def activate_users(feature, users)
195
86
  with_feature(feature) do |f|
196
- users.each{|user| f.add_user(user)}
87
+ users.each { |user| f.add_user(user) }
197
88
  end
198
89
  end
199
90
 
200
91
  def deactivate_users(feature, users)
201
92
  with_feature(feature) do |f|
202
- users.each{|user| f.remove_user(user)}
93
+ users.each { |user| f.remove_user(user) }
203
94
  end
204
95
  end
205
96
 
206
97
  def set_users(feature, users)
207
98
  with_feature(feature) do |f|
208
99
  f.users = []
209
- users.each{|user| f.add_user(user)}
100
+ users.each { |user| f.add_user(user) }
210
101
  end
211
102
  end
212
103
 
@@ -242,7 +133,7 @@ class Rollout
242
133
 
243
134
  def active_in_group?(group, user)
244
135
  f = @groups[group.to_sym]
245
- f && f.call(user)
136
+ f&.call(user)
246
137
  end
247
138
 
248
139
  def get(feature)
@@ -263,29 +154,31 @@ class Rollout
263
154
  end
264
155
 
265
156
  def multi_get(*features)
266
- feature_keys = features.map{ |feature| key(feature) }
157
+ return [] if features.empty?
158
+
159
+ feature_keys = features.map { |feature| key(feature) }
267
160
  @storage.mget(*feature_keys).map.with_index { |string, index| Feature.new(features[index], string, @options) }
268
161
  end
269
162
 
270
163
  def features
271
- (@storage.get(features_key) || "").split(",").map(&:to_sym)
164
+ (@storage.get(features_key) || '').split(',').map(&:to_sym)
272
165
  end
273
166
 
274
167
  def feature_states(user = nil)
275
- features.each_with_object({}) do |f, hash|
276
- hash[f] = active?(f, user)
168
+ multi_get(*features).each_with_object({}) do |f, hash|
169
+ hash[f.name] = f.active?(self, user)
277
170
  end
278
171
  end
279
172
 
280
173
  def active_features(user = nil)
281
- features.select do |f|
282
- active?(f, user)
283
- end
174
+ multi_get(*features).select do |f|
175
+ f.active?(self, user)
176
+ end.map(&:name)
284
177
  end
285
178
 
286
179
  def clear!
287
180
  features.each do |feature|
288
- with_feature(feature) { |f| f.clear }
181
+ with_feature(feature, &:clear)
289
182
  @storage.del(key(feature))
290
183
  end
291
184
 
@@ -293,7 +186,28 @@ class Rollout
293
186
  end
294
187
 
295
188
  def exists?(feature)
296
- @storage.exists(key(feature))
189
+ # since redis-rb v4.2, `#exists?` replaces `#exists` which now returns integer value instead of boolean
190
+ # https://github.com/redis/redis-rb/pull/918
191
+ if @storage.respond_to?(:exists?)
192
+ @storage.exists?(key(feature))
193
+ else
194
+ @storage.exists(key(feature))
195
+ end
196
+ end
197
+
198
+ def with_feature(feature)
199
+ f = get(feature)
200
+
201
+ if count_observers > 0
202
+ before = Marshal.load(Marshal.dump(f))
203
+ yield(f)
204
+ save(f)
205
+ changed
206
+ notify_observers(:update, before, f)
207
+ else
208
+ yield(f)
209
+ save(f)
210
+ end
297
211
  end
298
212
 
299
213
  private
@@ -303,17 +217,11 @@ class Rollout
303
217
  end
304
218
 
305
219
  def features_key
306
- "feature:__features__"
307
- end
308
-
309
- def with_feature(feature)
310
- f = get(feature)
311
- yield(f)
312
- save(f)
220
+ 'feature:__features__'
313
221
  end
314
222
 
315
223
  def save(feature)
316
224
  @storage.set(key(feature.name), feature.serialize)
317
- @storage.set(features_key, (features | [feature.name.to_sym]).join(","))
225
+ @storage.set(features_key, (features | [feature.name.to_sym]).join(','))
318
226
  end
319
227
  end