rollout 2.4.3 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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