rollout-redis 1.0.0 → 1.1.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 +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
|