rollout-redis 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +57 -1
- data/lib/rollout/feature.rb +9 -2
- data/lib/rollout/notifications/channels/email.rb +0 -4
- data/lib/rollout/notifications/channels/slack.rb +0 -4
- data/lib/rollout/notifications/notifiers/degrade.rb +7 -4
- data/lib/rollout/notifications/notifiers/status_change.rb +7 -4
- data/lib/rollout/tasks/rollout.rake +18 -4
- data/lib/rollout/version.rb +1 -1
- data/lib/rollout.rb +86 -10
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d3d7a7f17af2164aaeaefcea4ebd6def9fa5a9d29af982e5ed0ad05f2ee4a3a8
|
4
|
+
data.tar.gz: f9b9ff1cd3a1d45fa46695150d705dfff5489937baeaff082718fca7221e90f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 016c6a7f22c74586628b600bada44d95a3c5934b40c973811e539de286136282d039e1dedc991d6678275b4b25a67a9b3c8139b8403d607ebc78ec27657545d7
|
7
|
+
data.tar.gz: e959cd3d1d9a3a9bbd727b68fef633a30b6e066ecfa0d708bc9dc27db199a6ab9285e2a9709b2696d02af14565055230c02fb85305917f4854454345a7dab005
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,14 @@ All changes to `rollout-redis` will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## [1.1.0] - 2023-10-28
|
8
|
+
|
9
|
+
### Added
|
10
|
+
- `#with_old_rollout_gem_compatibility` method for allowing working with Feature Flags that were stored by the old `rollout` gem.
|
11
|
+
- `rollout:migrate_from_rollout_format` rake task for performing a migration of the feature flags stored by the old `rollout` gem to the new `rollout-redis` format.
|
12
|
+
- Add a new parameter when performing `#activate` method for providing a specific degrade configuration for the feature flag that is being activated.
|
13
|
+
- Add a new parameter when performing `rollout:on` rake task for providing a specific degrade configuration for the feature flag that is being activated.
|
14
|
+
- You can implement now your own `Rollout::Notifications::Channels::Channel` in case the ones offered by the `gem` are not enough.
|
7
15
|
|
8
16
|
## [1.0.0] - 2023-10-25
|
9
17
|
|
data/README.md
CHANGED
@@ -17,6 +17,7 @@ Topics covered in this README:
|
|
17
17
|
- [Sending Notifications](#sending-notifications)
|
18
18
|
- [Rake tasks](#rake-tasks)
|
19
19
|
- [Migrating from rollout gem](#migrating-from-rollout-gem-)
|
20
|
+
- [Compatible payloads and keys](#compatible-payloads-and-keys)
|
20
21
|
- [Changelog](#changelog)
|
21
22
|
- [Contributing](#contributing)
|
22
23
|
|
@@ -143,7 +144,7 @@ In the case that you need to clear the cache at any point, you can make use of t
|
|
143
144
|
|
144
145
|
### Auto-deactivating flags
|
145
146
|
|
146
|
-
If you want to allow the gem to deactivate
|
147
|
+
If you want to allow the gem to deactivate all the feature flags automatically when a threshold of errors is reached, you can enable the degrade feature using the `with_degrade` method.
|
147
148
|
|
148
149
|
```ruby
|
149
150
|
@rollout ||= Rollout.new(redis)
|
@@ -151,6 +152,18 @@ If you want to allow the gem to deactivate your feature flag automatically when
|
|
151
152
|
.with_degrade(min: 100, threshold: 0.1)
|
152
153
|
```
|
153
154
|
|
155
|
+
However, if you just want to activate the degradation of an specific feature flag, you need to provide the following information when activating the feature flag (note that now the percentage is a mandatory parameter if you want to pass the degrade options):
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
@rollout.activate('FEATURE_FLAG_NAME', 100, degrade: { min: 500, threshold: 0.2 })
|
159
|
+
```
|
160
|
+
|
161
|
+
The same configuration 👆 is available when using the rake task for activating the feature flag. Check [Rake tasks](#rake-tasks) section.
|
162
|
+
|
163
|
+
```shell
|
164
|
+
bundle exec rake rollout:on[FEATURE_FLAG_NAME,100,500,0.2]
|
165
|
+
```
|
166
|
+
|
154
167
|
So now, instead of using the `active?` method, you need to wrap your new code under the `with_feature` method.
|
155
168
|
|
156
169
|
```ruby
|
@@ -220,6 +233,39 @@ email_channel = Rollout::Notifications::Channels::Email.new(
|
|
220
233
|
)
|
221
234
|
```
|
222
235
|
|
236
|
+
##### Custom channel
|
237
|
+
|
238
|
+
If you want to send the notifications using a different channel not offered by this gem, you can implement your own class with a `publish` method.
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
require 'rollout'
|
242
|
+
|
243
|
+
module YourApp
|
244
|
+
class YourCustomChannel
|
245
|
+
def initialize()
|
246
|
+
# provide here whatever you need for configuring your channel
|
247
|
+
end
|
248
|
+
|
249
|
+
def publish(text)
|
250
|
+
# Implement the way you want to publish the notification
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
After implementing it, you can pass it to the list of configured channels
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
your_channel = YourApp::YourCustomChannel.new(url: 'wadus', param2: 'foo')
|
260
|
+
@rollout ||= Rollout.new(redis)
|
261
|
+
.with_cache
|
262
|
+
.with_degrade(min: 100, threshold: 0.1)
|
263
|
+
.with_notifications(
|
264
|
+
status_change: [your_channel],
|
265
|
+
degrade: [your_channel]
|
266
|
+
)
|
267
|
+
```
|
268
|
+
|
223
269
|
## Rake tasks
|
224
270
|
|
225
271
|
In order to have access to the rollout rakes, you have to load manually the task definitions. For doing so load the rollout rake task:
|
@@ -260,10 +306,20 @@ For listing all the stored feature flags, do:
|
|
260
306
|
bundle exec rake rollout:list
|
261
307
|
```
|
262
308
|
|
309
|
+
For migrating feature flags stored using the old `rollout` gem format (check [migration guide](https://github.com/jcagarcia/rollout-redis/blob/main/MIGRATING_FROM_ROLLOUT_GEM.md)), do:
|
310
|
+
|
311
|
+
```shell
|
312
|
+
bundle exec rake rollout:migrate_from_rollout_format
|
313
|
+
```
|
314
|
+
|
263
315
|
## Migrating from rollout gem 🚨
|
264
316
|
|
265
317
|
If you are currently using the unmaintained [rollout](https://github.com/fetlife/rollout) gem, you should consider checking this [migration guide](https://github.com/jcagarcia/rollout-redis/blob/main/MIGRATING_FROM_ROLLOUT_GEM.md) for start using the new `rollout-redis` gem.
|
266
318
|
|
319
|
+
### Compatible payloads and keys
|
320
|
+
|
321
|
+
You can use the `.with_old_rollout_gem_compatibility` instance method for making the `rollout-redis` gem work as the the discontinued [rollout](https://github.com/fetlife/rollout) gem in terms of redis storage and format storage. Check the [migration guide](https://github.com/jcagarcia/rollout-redis/blob/main/MIGRATING_FROM_ROLLOUT_GEM.md) for more information.
|
322
|
+
|
267
323
|
## Changelog
|
268
324
|
|
269
325
|
If you're interested in seeing the changes and bug fixes between each version of `rollout-redis`, read the [Changelog](https://github.com/jcagarcia/rollout-redis/blob/main/CHANGELOG.md).
|
data/lib/rollout/feature.rb
CHANGED
@@ -4,7 +4,7 @@ require 'zlib'
|
|
4
4
|
|
5
5
|
class Rollout
|
6
6
|
class Feature
|
7
|
-
attr_accessor :percentage
|
7
|
+
attr_accessor :percentage, :degrade
|
8
8
|
attr_reader :name, :data
|
9
9
|
|
10
10
|
RAND_BASE = (2**32 - 1) / 100.0
|
@@ -13,6 +13,7 @@ class Rollout
|
|
13
13
|
@name = name
|
14
14
|
@data = data
|
15
15
|
@percentage = @data[:percentage]
|
16
|
+
@degrade = @data[:degrade]
|
16
17
|
end
|
17
18
|
|
18
19
|
def active?(determinator=nil)
|
@@ -48,7 +49,7 @@ class Rollout
|
|
48
49
|
end
|
49
50
|
|
50
51
|
def to_h
|
51
|
-
{
|
52
|
+
h = {
|
52
53
|
name: @name,
|
53
54
|
percentage: @percentage,
|
54
55
|
data: {
|
@@ -56,6 +57,12 @@ class Rollout
|
|
56
57
|
errors: errors
|
57
58
|
}
|
58
59
|
}
|
60
|
+
|
61
|
+
h.merge!({
|
62
|
+
degrade: @degrade
|
63
|
+
}) if @degrade
|
64
|
+
|
65
|
+
h
|
59
66
|
end
|
60
67
|
|
61
68
|
private
|
@@ -10,19 +10,22 @@ class Rollout
|
|
10
10
|
|
11
11
|
def notify(feature_name, requests, errors)
|
12
12
|
@channels.each do |c|
|
13
|
-
|
14
|
-
|
13
|
+
if c.class == Rollout::Notifications::Channels::Email
|
14
|
+
publish_to_email_channel(c, feature_name, requests, errors)
|
15
|
+
else
|
16
|
+
publish_to_channel(c, feature_name, requests, errors)
|
17
|
+
end
|
15
18
|
end
|
16
19
|
end
|
17
20
|
|
18
21
|
private
|
19
22
|
|
20
|
-
def
|
23
|
+
def publish_to_channel(c, feature_name, requests, errors)
|
21
24
|
text = "Feature flag '#{feature_name}' has been degraded after #{requests} requests and #{errors} errors"
|
22
25
|
c.publish(text)
|
23
26
|
end
|
24
27
|
|
25
|
-
def
|
28
|
+
def publish_to_email_channel(c, feature_name, requests, errors)
|
26
29
|
subject = 'Feature flag has been automatically deactivated!'
|
27
30
|
content = "Feature flag '#{feature_name}' has been degraded after #{requests} requests and #{errors} errors"
|
28
31
|
c.publish(subject, content)
|
@@ -10,14 +10,17 @@ class Rollout
|
|
10
10
|
|
11
11
|
def notify(feature_name, new_status, new_percentage=nil)
|
12
12
|
@channels.each do |c|
|
13
|
-
|
14
|
-
|
13
|
+
if c.class == Rollout::Notifications::Channels::Email
|
14
|
+
publish_to_email_channel(c, feature_name, new_status, new_percentage)
|
15
|
+
else
|
16
|
+
publish_to_channel(c, feature_name, new_status, new_percentage)
|
17
|
+
end
|
15
18
|
end
|
16
19
|
end
|
17
20
|
|
18
21
|
private
|
19
22
|
|
20
|
-
def
|
23
|
+
def publish_to_channel(c, feature_name, new_status, new_percentage)
|
21
24
|
if new_status == :activated
|
22
25
|
text = "Feature flag '#{feature_name}' has been activated with percentage #{new_percentage}!"
|
23
26
|
elsif new_status == :deactivated
|
@@ -26,7 +29,7 @@ class Rollout
|
|
26
29
|
c.publish(text)
|
27
30
|
end
|
28
31
|
|
29
|
-
def
|
32
|
+
def publish_to_email_channel(c, feature_name, new_status, new_percentage)
|
30
33
|
if new_status == :activated
|
31
34
|
subject = 'Feature flag has been activated!'
|
32
35
|
content = "Feature flag '#{feature_name}' has been activated with percentage #{new_percentage}!"
|
@@ -1,13 +1,20 @@
|
|
1
1
|
|
2
2
|
namespace :rollout do
|
3
3
|
desc "Activate a feature"
|
4
|
-
task :on, [:feature, :percentage] => :environment do |task, args|
|
4
|
+
task :on, [:feature, :percentage, :degrade_min, :degrade_threshold] => :environment do |task, args|
|
5
5
|
if args.feature
|
6
|
-
puts "Activating feature #{args.feature}..."
|
7
6
|
if args.percentage
|
8
|
-
|
7
|
+
percentage = args.percentage.to_i
|
9
8
|
else
|
10
|
-
|
9
|
+
percentage = 100
|
10
|
+
end
|
11
|
+
|
12
|
+
if args.degrade_min && args.degrade_threshold
|
13
|
+
puts "Activating feature #{args.feature} at #{percentage}% (degrade config set to a min of #{args.degrade_min} requests and a threshold of error of #{args.degrade_threshold.to_f*100}%)..."
|
14
|
+
activated = rollout.activate(args.feature, percentage, degrade: { min: args.degrade_min.to_i, threshold: args.degrade_threshold.to_f})
|
15
|
+
else
|
16
|
+
puts "Activating feature #{args.feature} at #{percentage}%..."
|
17
|
+
activated = rollout.activate(args.feature, percentage)
|
11
18
|
end
|
12
19
|
|
13
20
|
if activated
|
@@ -43,6 +50,13 @@ namespace :rollout do
|
|
43
50
|
end
|
44
51
|
end
|
45
52
|
|
53
|
+
desc "Migrate stored feature flags to the new format without removing the old information"
|
54
|
+
task migrate_from_rollout_format: :environment do
|
55
|
+
puts "Starting the migration..."
|
56
|
+
rollout.migrate_from_rollout_format
|
57
|
+
puts "Migration has finished!"
|
58
|
+
end
|
59
|
+
|
46
60
|
private
|
47
61
|
|
48
62
|
def rollout
|
data/lib/rollout/version.rb
CHANGED
data/lib/rollout.rb
CHANGED
@@ -21,6 +21,8 @@ class Rollout
|
|
21
21
|
@storage = storage
|
22
22
|
@cache_enabled = false
|
23
23
|
@degrade_enabled = false
|
24
|
+
@old_gem_compatibility_enabled = false
|
25
|
+
@auto_migrate_from_old_format = false
|
24
26
|
end
|
25
27
|
|
26
28
|
def with_cache(expires_in: 300)
|
@@ -44,18 +46,38 @@ class Rollout
|
|
44
46
|
degrade_channels = degrade
|
45
47
|
|
46
48
|
if !status_change_channels.empty?
|
49
|
+
status_change_channels.each do |c|
|
50
|
+
raise Rollout::Error.new("Channel #{c.class.name} does not implement `publish` method") unless c.respond_to?(:publish)
|
51
|
+
end
|
47
52
|
@status_change_notifier = Notifications::Notifiers::StatusChange.new(status_change_channels)
|
48
53
|
end
|
49
54
|
|
50
55
|
if !degrade_channels.empty?
|
56
|
+
degrade_channels.each do |c|
|
57
|
+
raise Rollout::Error.new("Channel #{c.class.name} does not implement `publish` method") unless c.respond_to?(:publish)
|
58
|
+
end
|
51
59
|
@degrade_notifier = Notifications::Notifiers::Degrade.new(degrade_channels)
|
52
60
|
end
|
53
61
|
|
54
62
|
self
|
55
63
|
end
|
56
64
|
|
57
|
-
def
|
65
|
+
def with_old_rollout_gem_compatibility(auto_migration: false)
|
66
|
+
@old_gem_compatibility_enabled = true
|
67
|
+
@auto_migrate_from_old_format = auto_migration
|
68
|
+
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def activate(feature_name, percentage=100, degrade: nil)
|
58
73
|
data = { percentage: percentage }
|
74
|
+
data.merge!({
|
75
|
+
degrade: {
|
76
|
+
min: degrade[:min] || 0,
|
77
|
+
threshold: degrade[:threshold] || 0
|
78
|
+
}
|
79
|
+
}) if degrade
|
80
|
+
|
59
81
|
feature = Feature.new(feature_name, data)
|
60
82
|
result = save(feature) == "OK"
|
61
83
|
|
@@ -84,12 +106,19 @@ class Rollout
|
|
84
106
|
end
|
85
107
|
|
86
108
|
def active?(feature_name, determinator = nil)
|
87
|
-
feature = get(feature_name
|
109
|
+
feature = get(feature_name)
|
110
|
+
if feature.nil? && @old_gem_compatibility_enabled
|
111
|
+
feature = get_with_old_format(feature_name)
|
112
|
+
if feature && @auto_migrate_from_old_format
|
113
|
+
activate(feature_name, feature.percentage)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
88
117
|
return false unless feature
|
89
118
|
|
90
119
|
active = feature.active?(determinator)
|
91
120
|
|
92
|
-
if active &&
|
121
|
+
if active && degrade_enabled?(feature)
|
93
122
|
feature.add_request
|
94
123
|
save(feature)
|
95
124
|
end
|
@@ -99,9 +128,11 @@ class Rollout
|
|
99
128
|
|
100
129
|
def with_feature_flag(feature_name, determinator = nil, &block)
|
101
130
|
yield if active?(feature_name, determinator)
|
131
|
+
rescue Rollout::Error => e
|
132
|
+
raise
|
102
133
|
rescue => e
|
103
|
-
feature = get(feature_name
|
104
|
-
if
|
134
|
+
feature = get(feature_name)
|
135
|
+
if feature && degrade_enabled?(feature)
|
105
136
|
feature.add_error
|
106
137
|
save(feature)
|
107
138
|
|
@@ -147,7 +178,7 @@ class Rollout
|
|
147
178
|
|
148
179
|
@storage.set(new_key, new_data)
|
149
180
|
|
150
|
-
puts "Migrated key
|
181
|
+
puts "Migrated redis key from #{old_key} to #{new_key}. Migrating data from '#{old_data}' to '#{new_data}'."
|
151
182
|
|
152
183
|
if percentage > 0
|
153
184
|
@status_change_notifier&.notify(new_key.gsub('feature-rollout-redis:', ''), :activated, percentage)
|
@@ -158,7 +189,7 @@ class Rollout
|
|
158
189
|
|
159
190
|
private
|
160
191
|
|
161
|
-
def get(feature_name
|
192
|
+
def get(feature_name)
|
162
193
|
feature = from_redis(feature_name)
|
163
194
|
return unless feature
|
164
195
|
|
@@ -175,6 +206,15 @@ class Rollout
|
|
175
206
|
cached_feature
|
176
207
|
end
|
177
208
|
|
209
|
+
def get_with_old_format(feature_name)
|
210
|
+
feature = from_redis_with_old_format(feature_name)
|
211
|
+
return unless feature
|
212
|
+
|
213
|
+
feature
|
214
|
+
rescue ::Redis::BaseError => e
|
215
|
+
raise Rollout::Error.new(e)
|
216
|
+
end
|
217
|
+
|
178
218
|
def save(feature)
|
179
219
|
@storage.set(key(feature.name), feature.data.to_json)
|
180
220
|
end
|
@@ -218,22 +258,58 @@ class Rollout
|
|
218
258
|
Feature.new(feature_name, JSON.parse(data, symbolize_names: true))
|
219
259
|
end
|
220
260
|
|
261
|
+
def from_redis_with_old_format(feature_name)
|
262
|
+
old_data = @storage.get(old_key(feature_name))
|
263
|
+
return unless old_data
|
264
|
+
|
265
|
+
percentage = old_data.split('|')[0].to_i
|
266
|
+
|
267
|
+
new_data = {
|
268
|
+
percentage: percentage,
|
269
|
+
requests: 0,
|
270
|
+
errors: 0
|
271
|
+
}
|
272
|
+
|
273
|
+
Feature.new(feature_name, new_data)
|
274
|
+
end
|
275
|
+
|
221
276
|
def expired?(timestamp)
|
222
277
|
Time.now.to_i - timestamp > @cache_time
|
223
278
|
end
|
224
279
|
|
225
280
|
def degraded?(feature)
|
226
|
-
return false if
|
227
|
-
|
281
|
+
return false if !degrade_enabled?(feature)
|
282
|
+
|
283
|
+
if feature.degrade
|
284
|
+
degrade_min = feature.degrade[:min]
|
285
|
+
degrade_threshold = feature.degrade[:threshold]
|
286
|
+
else
|
287
|
+
degrade_min = @degrade_min
|
288
|
+
degrade_threshold = @degrade_threshold
|
289
|
+
end
|
228
290
|
|
229
|
-
|
291
|
+
return false if feature.requests < degrade_min
|
292
|
+
|
293
|
+
feature.errors > degrade_threshold * feature.requests
|
294
|
+
end
|
295
|
+
|
296
|
+
def degrade_enabled?(feature)
|
297
|
+
@degrade_enabled || !feature.degrade.nil?
|
230
298
|
end
|
231
299
|
|
232
300
|
def key(name)
|
233
301
|
"#{key_prefix}:#{name}"
|
234
302
|
end
|
235
303
|
|
304
|
+
def old_key(name)
|
305
|
+
"#{old_key_prefix}:#{name}"
|
306
|
+
end
|
307
|
+
|
236
308
|
def key_prefix
|
237
309
|
"feature-rollout-redis"
|
238
310
|
end
|
311
|
+
|
312
|
+
def old_key_prefix
|
313
|
+
"feature"
|
314
|
+
end
|
239
315
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rollout-redis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Juan Carlos García
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-10-
|
11
|
+
date: 2023-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|