rollout-redis 0.3.1 → 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 +19 -0
- data/README.md +121 -3
- data/lib/rollout/feature.rb +9 -2
- data/lib/rollout/notifications/channels/email.rb +27 -0
- data/lib/rollout/notifications/channels/slack.rb +41 -0
- data/lib/rollout/notifications/notifiers/base.rb +15 -0
- data/lib/rollout/notifications/notifiers/degrade.rb +36 -0
- data/lib/rollout/notifications/notifiers/status_change.rb +45 -0
- data/lib/rollout/tasks/rollout.rake +18 -4
- data/lib/rollout/version.rb +1 -1
- data/lib/rollout.rb +147 -19
- data/rollout-redis.gemspec +2 -0
- metadata +35 -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,25 @@ 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.
|
15
|
+
|
16
|
+
## [1.0.0] - 2023-10-25
|
17
|
+
|
18
|
+
### Added
|
19
|
+
- `#with_notifications` method for allowing to send notifications when some different event occurs.
|
20
|
+
|
21
|
+
### Changed
|
22
|
+
- When the threshold of errors is reached when using the `with_degrade` feature, instead of deleting the feature flag from the redis, we are marking it now as degraded, moving the activation percentage to 0% and adding some useful information to the feature flag stored data.
|
23
|
+
|
24
|
+
## [0.3.1] - 2023-10-24
|
25
|
+
- Same as 0.3.0. When testing GitHub actions for moving to first release `1.0.0` it deployed a new version of the gem by error.
|
7
26
|
|
8
27
|
## [0.3.0] - 2023-10-24
|
9
28
|
|
data/README.md
CHANGED
@@ -14,8 +14,10 @@ Topics covered in this README:
|
|
14
14
|
- [Gradual activation based on percentages](#gradual-activation-based-on-percentages)
|
15
15
|
- [Caching Feature Flags](#caching-feature-flags)
|
16
16
|
- [Auto-deactivating flags](#auto-deactivating-flags)
|
17
|
+
- [Sending Notifications](#sending-notifications)
|
17
18
|
- [Rake tasks](#rake-tasks)
|
18
19
|
- [Migrating from rollout gem](#migrating-from-rollout-gem-)
|
20
|
+
- [Compatible payloads and keys](#compatible-payloads-and-keys)
|
19
21
|
- [Changelog](#changelog)
|
20
22
|
- [Contributing](#contributing)
|
21
23
|
|
@@ -142,7 +144,7 @@ In the case that you need to clear the cache at any point, you can make use of t
|
|
142
144
|
|
143
145
|
### Auto-deactivating flags
|
144
146
|
|
145
|
-
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.
|
146
148
|
|
147
149
|
```ruby
|
148
150
|
@rollout ||= Rollout.new(redis)
|
@@ -150,6 +152,18 @@ If you want to allow the gem to deactivate your feature flag automatically when
|
|
150
152
|
.with_degrade(min: 100, threshold: 0.1)
|
151
153
|
```
|
152
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
|
+
|
153
167
|
So now, instead of using the `active?` method, you need to wrap your new code under the `with_feature` method.
|
154
168
|
|
155
169
|
```ruby
|
@@ -158,7 +172,99 @@ So now, instead of using the `active?` method, you need to wrap your new code un
|
|
158
172
|
end
|
159
173
|
```
|
160
174
|
|
161
|
-
When any unexpected error appears during the wrapped code execution, the Rollout gem will take it into account for automatically
|
175
|
+
When any unexpected error appears during the wrapped code execution, the Rollout gem will take it into account for automatically degrading the feature flag if the threshold of errors is reached. The feature flag will not be removed from the redis, but it will change its percentage to 0 and it will be marked as degraded.
|
176
|
+
|
177
|
+
_NOTE_: All the managed or captured errors inside the wrapped code will not be taken into consideration for degrading the feature flag.
|
178
|
+
|
179
|
+
### Sending notifications
|
180
|
+
|
181
|
+
`rollout-redis` gem can send different notifications to your development team. For enabling this feature, you just need to use the `with_notifications` instance method providing the channels where you want to publish each of the different events that can occur:
|
182
|
+
|
183
|
+
- **status_change**: This notification is triggered when a feature flag is activated or deactivated using the `rollout-redis` gem.
|
184
|
+
- **degrade**: This notification is triggered when a feature flag is automatically degraded because the threshold of errors is reached
|
185
|
+
- The instance must be configured for automatically degrading using the `with_degrade` instance method.
|
186
|
+
|
187
|
+
You must provide at least one [channel](#defining-the-channels) as a parameter if you want to enable the notifications for that specific event. If no channels provided, the notifications will not be sent.
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
@rollout ||= Rollout.new(redis)
|
191
|
+
.with_cache
|
192
|
+
.with_degrade(min: 100, threshold: 0.1)
|
193
|
+
.with_notifications(
|
194
|
+
status_change: [slack_channel],
|
195
|
+
degrade: [slack_channel, email_channel]
|
196
|
+
)
|
197
|
+
```
|
198
|
+
|
199
|
+
#### Defining the channels
|
200
|
+
|
201
|
+
When enabling a notification, you can provide the different channels where the notification should be published. `rollout-redis` gem offers different channels that can be configured.
|
202
|
+
|
203
|
+
##### Slack Channel
|
204
|
+
|
205
|
+
Allows you to send notifications using a slack webhook.
|
206
|
+
|
207
|
+
The first thing to do is to setup an incoming webhook service integration. You can do this from your services page.
|
208
|
+
|
209
|
+
After that, you can provide the obtained webhook url when instantiating the Slack channel.
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
require 'rollout'
|
213
|
+
|
214
|
+
slack_channel = Rollout::Notifications::Channels::Slack.new(
|
215
|
+
webhook_url: ENV.fetch('SLACK_COMPANY_WEBHOOK_URL'),
|
216
|
+
channel: '#feature-flags-notifications',
|
217
|
+
username: 'rollout-redis'
|
218
|
+
)
|
219
|
+
```
|
220
|
+
|
221
|
+
##### Email Channel
|
222
|
+
|
223
|
+
Allows you to send notifications via email.
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
require 'rollout'
|
227
|
+
|
228
|
+
email_channel = Rollout::Notifications::Channels::Email.new(
|
229
|
+
smtp_host: ENV.fetch('SMTP_HOST'),
|
230
|
+
smtp_port: ENV.fetch('SMTP_PORT'),
|
231
|
+
from: 'no-reply@rollout-redis.com',
|
232
|
+
to: 'developers@yourcompany.com'
|
233
|
+
)
|
234
|
+
```
|
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
|
+
```
|
162
268
|
|
163
269
|
## Rake tasks
|
164
270
|
|
@@ -200,10 +306,20 @@ For listing all the stored feature flags, do:
|
|
200
306
|
bundle exec rake rollout:list
|
201
307
|
```
|
202
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
|
+
|
203
315
|
## Migrating from rollout gem 🚨
|
204
316
|
|
205
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.
|
206
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
|
+
|
207
323
|
## Changelog
|
208
324
|
|
209
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).
|
@@ -226,7 +342,9 @@ We welcome and appreciate contributions from the open-source community. Before y
|
|
226
342
|
|
227
343
|
### Development
|
228
344
|
|
229
|
-
This project is dockerized
|
345
|
+
This project is dockerized, so be sure you have docker installed in your machine.
|
346
|
+
|
347
|
+
Once you clone the repository, you can use the `Make` commands to build the project.
|
230
348
|
|
231
349
|
```shell
|
232
350
|
make build
|
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
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
class Rollout
|
4
|
+
module Notifications
|
5
|
+
module Channels
|
6
|
+
class Email
|
7
|
+
def initialize(smtp_host:, smtp_port:, from:'no-reply@rollout-redis.com', to:)
|
8
|
+
@smtp_host = smtp_host
|
9
|
+
@smtp_port = smtp_port
|
10
|
+
@from = from
|
11
|
+
@to = to
|
12
|
+
end
|
13
|
+
|
14
|
+
def publish(subject, body)
|
15
|
+
mail = Mail.new do
|
16
|
+
subject subject
|
17
|
+
body body
|
18
|
+
end
|
19
|
+
mail.smtp_envelope_from = @from
|
20
|
+
mail.smtp_envelope_to = @to
|
21
|
+
mail.delivery_method :smtp, address: @smtp_host, port: @smtp_port
|
22
|
+
mail.deliver
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'slack-notifier'
|
2
|
+
|
3
|
+
class Rollout
|
4
|
+
module Notifications
|
5
|
+
module Channels
|
6
|
+
class Slack
|
7
|
+
def initialize(webhook_url:, channel:, username:'rollout-redis')
|
8
|
+
@webhook_url = webhook_url
|
9
|
+
@channel = channel
|
10
|
+
@username = username
|
11
|
+
end
|
12
|
+
|
13
|
+
def publish(text)
|
14
|
+
begin
|
15
|
+
blocks = [
|
16
|
+
{
|
17
|
+
"type": "section",
|
18
|
+
"text": {
|
19
|
+
"type": "mrkdwn",
|
20
|
+
"text": text
|
21
|
+
}
|
22
|
+
}
|
23
|
+
]
|
24
|
+
slack_notifier.post(blocks: blocks)
|
25
|
+
rescue => e
|
26
|
+
puts "[ERROR] Error sending notification to slack webhook. Error => #{e}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def slack_notifier
|
33
|
+
@notifier ||= ::Slack::Notifier.new @webhook_url do
|
34
|
+
defaults channel: @channel,
|
35
|
+
username: @username
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
class Rollout
|
4
|
+
module Notifications
|
5
|
+
module Notifiers
|
6
|
+
class Degrade < Base
|
7
|
+
def initialize(channels)
|
8
|
+
super(channels)
|
9
|
+
end
|
10
|
+
|
11
|
+
def notify(feature_name, requests, errors)
|
12
|
+
@channels.each do |c|
|
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
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def publish_to_channel(c, feature_name, requests, errors)
|
24
|
+
text = "Feature flag '#{feature_name}' has been degraded after #{requests} requests and #{errors} errors"
|
25
|
+
c.publish(text)
|
26
|
+
end
|
27
|
+
|
28
|
+
def publish_to_email_channel(c, feature_name, requests, errors)
|
29
|
+
subject = 'Feature flag has been automatically deactivated!'
|
30
|
+
content = "Feature flag '#{feature_name}' has been degraded after #{requests} requests and #{errors} errors"
|
31
|
+
c.publish(subject, content)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
class Rollout
|
4
|
+
module Notifications
|
5
|
+
module Notifiers
|
6
|
+
class StatusChange < Base
|
7
|
+
def initialize(channels)
|
8
|
+
super(channels)
|
9
|
+
end
|
10
|
+
|
11
|
+
def notify(feature_name, new_status, new_percentage=nil)
|
12
|
+
@channels.each do |c|
|
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
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def publish_to_channel(c, feature_name, new_status, new_percentage)
|
24
|
+
if new_status == :activated
|
25
|
+
text = "Feature flag '#{feature_name}' has been activated with percentage #{new_percentage}!"
|
26
|
+
elsif new_status == :deactivated
|
27
|
+
text = "Feature flag '#{feature_name}' has been deactivated and deleted!"
|
28
|
+
end
|
29
|
+
c.publish(text)
|
30
|
+
end
|
31
|
+
|
32
|
+
def publish_to_email_channel(c, feature_name, new_status, new_percentage)
|
33
|
+
if new_status == :activated
|
34
|
+
subject = 'Feature flag has been activated!'
|
35
|
+
content = "Feature flag '#{feature_name}' has been activated with percentage #{new_percentage}!"
|
36
|
+
elsif new_status == :deactivated
|
37
|
+
subject = 'Feature flag has been deactivated!'
|
38
|
+
content = "Feature flag '#{feature_name}' has been deactivated and deleted!"
|
39
|
+
end
|
40
|
+
c.publish(subject, content)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -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
@@ -1,10 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'rollout/feature'
|
4
|
-
require 'rollout/version'
|
5
3
|
require 'redis'
|
6
4
|
require 'json'
|
7
5
|
|
6
|
+
require 'rollout/feature'
|
7
|
+
require 'rollout/notifications/channels/email'
|
8
|
+
require 'rollout/notifications/channels/slack'
|
9
|
+
require 'rollout/notifications/notifiers/degrade'
|
10
|
+
require 'rollout/notifications/notifiers/status_change'
|
11
|
+
require 'rollout/version'
|
12
|
+
|
13
|
+
|
8
14
|
class Rollout
|
9
15
|
|
10
16
|
class Error < StandardError; end
|
@@ -15,6 +21,8 @@ class Rollout
|
|
15
21
|
@storage = storage
|
16
22
|
@cache_enabled = false
|
17
23
|
@degrade_enabled = false
|
24
|
+
@old_gem_compatibility_enabled = false
|
25
|
+
@auto_migrate_from_old_format = false
|
18
26
|
end
|
19
27
|
|
20
28
|
def with_cache(expires_in: 300)
|
@@ -33,14 +41,56 @@ class Rollout
|
|
33
41
|
self
|
34
42
|
end
|
35
43
|
|
36
|
-
def
|
44
|
+
def with_notifications(status_change:[], degrade:[])
|
45
|
+
status_change_channels = status_change
|
46
|
+
degrade_channels = degrade
|
47
|
+
|
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
|
52
|
+
@status_change_notifier = Notifications::Notifiers::StatusChange.new(status_change_channels)
|
53
|
+
end
|
54
|
+
|
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
|
59
|
+
@degrade_notifier = Notifications::Notifiers::Degrade.new(degrade_channels)
|
60
|
+
end
|
61
|
+
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
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)
|
37
73
|
data = { percentage: percentage }
|
74
|
+
data.merge!({
|
75
|
+
degrade: {
|
76
|
+
min: degrade[:min] || 0,
|
77
|
+
threshold: degrade[:threshold] || 0
|
78
|
+
}
|
79
|
+
}) if degrade
|
80
|
+
|
38
81
|
feature = Feature.new(feature_name, data)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
82
|
+
result = save(feature) == "OK"
|
83
|
+
|
84
|
+
if result
|
85
|
+
@cache[feature_name] = {
|
86
|
+
feature: feature,
|
87
|
+
timestamp: Time.now.to_i
|
88
|
+
} if @cache_enabled
|
89
|
+
|
90
|
+
@status_change_notifier&.notify(feature_name, :activated, percentage)
|
91
|
+
end
|
92
|
+
|
93
|
+
result
|
44
94
|
end
|
45
95
|
|
46
96
|
def activate_percentage(feature_name, percentage)
|
@@ -48,16 +98,27 @@ class Rollout
|
|
48
98
|
end
|
49
99
|
|
50
100
|
def deactivate(feature_name)
|
51
|
-
del(feature_name)
|
101
|
+
result = del(feature_name)
|
102
|
+
|
103
|
+
@status_change_notifier&.notify(feature_name, :deactivated)
|
104
|
+
|
105
|
+
result
|
52
106
|
end
|
53
107
|
|
54
108
|
def active?(feature_name, determinator = nil)
|
55
|
-
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
|
+
|
56
117
|
return false unless feature
|
57
118
|
|
58
119
|
active = feature.active?(determinator)
|
59
120
|
|
60
|
-
if active &&
|
121
|
+
if active && degrade_enabled?(feature)
|
61
122
|
feature.add_request
|
62
123
|
save(feature)
|
63
124
|
end
|
@@ -67,13 +128,15 @@ class Rollout
|
|
67
128
|
|
68
129
|
def with_feature_flag(feature_name, determinator = nil, &block)
|
69
130
|
yield if active?(feature_name, determinator)
|
131
|
+
rescue Rollout::Error => e
|
132
|
+
raise
|
70
133
|
rescue => e
|
71
|
-
feature = get(feature_name
|
72
|
-
if
|
134
|
+
feature = get(feature_name)
|
135
|
+
if feature && degrade_enabled?(feature)
|
73
136
|
feature.add_error
|
74
137
|
save(feature)
|
75
138
|
|
76
|
-
|
139
|
+
degrade(feature_name) if degraded?(feature)
|
77
140
|
end
|
78
141
|
raise e
|
79
142
|
end
|
@@ -115,14 +178,18 @@ class Rollout
|
|
115
178
|
|
116
179
|
@storage.set(new_key, new_data)
|
117
180
|
|
118
|
-
puts "Migrated key
|
181
|
+
puts "Migrated redis key from #{old_key} to #{new_key}. Migrating data from '#{old_data}' to '#{new_data}'."
|
182
|
+
|
183
|
+
if percentage > 0
|
184
|
+
@status_change_notifier&.notify(new_key.gsub('feature-rollout-redis:', ''), :activated, percentage)
|
185
|
+
end
|
119
186
|
end
|
120
187
|
end
|
121
188
|
end
|
122
189
|
|
123
190
|
private
|
124
191
|
|
125
|
-
def get(feature_name
|
192
|
+
def get(feature_name)
|
126
193
|
feature = from_redis(feature_name)
|
127
194
|
return unless feature
|
128
195
|
|
@@ -139,6 +206,15 @@ class Rollout
|
|
139
206
|
cached_feature
|
140
207
|
end
|
141
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
|
+
|
142
218
|
def save(feature)
|
143
219
|
@storage.set(key(feature.name), feature.data.to_json)
|
144
220
|
end
|
@@ -147,6 +223,22 @@ class Rollout
|
|
147
223
|
@storage.del(key(feature_name)) == 1
|
148
224
|
end
|
149
225
|
|
226
|
+
def degrade(feature_name)
|
227
|
+
feature = get(feature_name)
|
228
|
+
data_with_degrade = feature.data.merge({
|
229
|
+
percentage: 0,
|
230
|
+
degraded: true,
|
231
|
+
degraded_at: Time.now
|
232
|
+
})
|
233
|
+
result = @storage.set(key(feature.name), data_with_degrade.to_json) == "OK"
|
234
|
+
|
235
|
+
if result
|
236
|
+
@degrade_notifier.notify(feature_name, feature.requests, feature.errors)
|
237
|
+
end
|
238
|
+
|
239
|
+
result
|
240
|
+
end
|
241
|
+
|
150
242
|
def from_cache(feature_name)
|
151
243
|
return nil unless @cache_enabled
|
152
244
|
|
@@ -166,22 +258,58 @@ class Rollout
|
|
166
258
|
Feature.new(feature_name, JSON.parse(data, symbolize_names: true))
|
167
259
|
end
|
168
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
|
+
|
169
276
|
def expired?(timestamp)
|
170
277
|
Time.now.to_i - timestamp > @cache_time
|
171
278
|
end
|
172
279
|
|
173
280
|
def degraded?(feature)
|
174
|
-
return false if
|
175
|
-
|
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
|
176
290
|
|
177
|
-
|
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?
|
178
298
|
end
|
179
299
|
|
180
300
|
def key(name)
|
181
301
|
"#{key_prefix}:#{name}"
|
182
302
|
end
|
183
303
|
|
304
|
+
def old_key(name)
|
305
|
+
"#{old_key_prefix}:#{name}"
|
306
|
+
end
|
307
|
+
|
184
308
|
def key_prefix
|
185
309
|
"feature-rollout-redis"
|
186
310
|
end
|
311
|
+
|
312
|
+
def old_key_prefix
|
313
|
+
"feature"
|
314
|
+
end
|
187
315
|
end
|
data/rollout-redis.gemspec
CHANGED
@@ -23,6 +23,8 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.required_ruby_version = '>= 2.3'
|
24
24
|
|
25
25
|
spec.add_runtime_dependency 'redis', '>= 4.0', '<= 5'
|
26
|
+
spec.add_runtime_dependency 'slack-notifier', '~> 2.4'
|
27
|
+
spec.add_runtime_dependency 'mail', '~> 2.8'
|
26
28
|
|
27
29
|
spec.add_development_dependency 'bundler', '>= 2.4'
|
28
30
|
spec.add_development_dependency 'rspec', '~> 3.12'
|
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:
|
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
|
@@ -30,6 +30,34 @@ dependencies:
|
|
30
30
|
- - "<="
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '5'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: slack-notifier
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.4'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.4'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: mail
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.8'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.8'
|
33
61
|
- !ruby/object:Gem::Dependency
|
34
62
|
name: bundler
|
35
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,6 +126,11 @@ files:
|
|
98
126
|
- Rakefile
|
99
127
|
- lib/rollout.rb
|
100
128
|
- lib/rollout/feature.rb
|
129
|
+
- lib/rollout/notifications/channels/email.rb
|
130
|
+
- lib/rollout/notifications/channels/slack.rb
|
131
|
+
- lib/rollout/notifications/notifiers/base.rb
|
132
|
+
- lib/rollout/notifications/notifiers/degrade.rb
|
133
|
+
- lib/rollout/notifications/notifiers/status_change.rb
|
101
134
|
- lib/rollout/tasks/rollout.rake
|
102
135
|
- lib/rollout/version.rb
|
103
136
|
- rollout-redis.gemspec
|