rollout-redis 0.3.0 → 1.0.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 +11 -0
- data/README.md +76 -7
- data/lib/rollout/notifications/channels/email.rb +31 -0
- data/lib/rollout/notifications/channels/slack.rb +45 -0
- data/lib/rollout/notifications/notifiers/base.rb +15 -0
- data/lib/rollout/notifications/notifiers/degrade.rb +33 -0
- data/lib/rollout/notifications/notifiers/status_change.rb +42 -0
- data/lib/rollout/version.rb +1 -1
- data/lib/rollout.rb +62 -10
- 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: 8e4931c09772c7b9e3b1c3042d9912b49cbe8e3d07e6062d1238c6ac9842dd3c
|
4
|
+
data.tar.gz: fbc9b0e1f80851d1960038169ddce0e93b813097979a8b12c2c82f8fec916ce2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d543d5e22d3519838ae6b9c15fbd6a982db066ef7ffe8792945d98bf636a4a34c23294fe02618b4484c6f3d3ee5625fb600a8e3942fc843fb393fdcc701e299
|
7
|
+
data.tar.gz: 8a79720ea90eed5001b67e6394e0ae26e19893e70478166aef179da9e5848c266f736fd1a69c962fa162a309fb510df5265cf14a134a15fb43081c6dd5b87af7
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,17 @@ 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
7
|
|
8
|
+
## [1.0.0] - 2023-10-25
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- `#with_notifications` method for allowing to send notifications when some different event occurs.
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
- 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.
|
15
|
+
|
16
|
+
## [0.3.1] - 2023-10-24
|
17
|
+
- 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.
|
18
|
+
|
8
19
|
## [0.3.0] - 2023-10-24
|
9
20
|
|
10
21
|
### Added
|
data/README.md
CHANGED
@@ -9,13 +9,14 @@ Based on the discontinued [rollout](https://github.com/fetlife/rollout) project,
|
|
9
9
|
Topics covered in this README:
|
10
10
|
|
11
11
|
- [Install it](#install-it)
|
12
|
-
- [Quick Start](#quick-start
|
13
|
-
- [Advanced features](#advanced-features
|
12
|
+
- [Quick Start](#quick-start-)
|
13
|
+
- [Advanced features](#advanced-features-)
|
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
|
-
- [Migrating from rollout gem](#migrating-from-rollout-gem
|
19
|
+
- [Migrating from rollout gem](#migrating-from-rollout-gem-)
|
19
20
|
- [Changelog](#changelog)
|
20
21
|
- [Contributing](#contributing)
|
21
22
|
|
@@ -142,12 +143,12 @@ In the case that you need to clear the cache at any point, you can make use of t
|
|
142
143
|
|
143
144
|
### Auto-deactivating flags
|
144
145
|
|
145
|
-
If you want to allow the gem to deactivate your feature flag automatically when a threshold of
|
146
|
+
If you want to allow the gem to deactivate your feature flag automatically when a threshold of errors is reached, you can enable the degrade feature using the `with_degrade` method.
|
146
147
|
|
147
148
|
```ruby
|
148
149
|
@rollout ||= Rollout.new(redis)
|
149
150
|
.with_cache
|
150
|
-
.with_degrade(
|
151
|
+
.with_degrade(min: 100, threshold: 0.1)
|
151
152
|
```
|
152
153
|
|
153
154
|
So now, instead of using the `active?` method, you need to wrap your new code under the `with_feature` method.
|
@@ -158,7 +159,66 @@ So now, instead of using the `active?` method, you need to wrap your new code un
|
|
158
159
|
end
|
159
160
|
```
|
160
161
|
|
161
|
-
When any unexpected error appears during the wrapped code execution, the Rollout gem will take it into account for automatically
|
162
|
+
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.
|
163
|
+
|
164
|
+
_NOTE_: All the managed or captured errors inside the wrapped code will not be taken into consideration for degrading the feature flag.
|
165
|
+
|
166
|
+
### Sending notifications
|
167
|
+
|
168
|
+
`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:
|
169
|
+
|
170
|
+
- **status_change**: This notification is triggered when a feature flag is activated or deactivated using the `rollout-redis` gem.
|
171
|
+
- **degrade**: This notification is triggered when a feature flag is automatically degraded because the threshold of errors is reached
|
172
|
+
- The instance must be configured for automatically degrading using the `with_degrade` instance method.
|
173
|
+
|
174
|
+
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.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
@rollout ||= Rollout.new(redis)
|
178
|
+
.with_cache
|
179
|
+
.with_degrade(min: 100, threshold: 0.1)
|
180
|
+
.with_notifications(
|
181
|
+
status_change: [slack_channel],
|
182
|
+
degrade: [slack_channel, email_channel]
|
183
|
+
)
|
184
|
+
```
|
185
|
+
|
186
|
+
#### Defining the channels
|
187
|
+
|
188
|
+
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.
|
189
|
+
|
190
|
+
##### Slack Channel
|
191
|
+
|
192
|
+
Allows you to send notifications using a slack webhook.
|
193
|
+
|
194
|
+
The first thing to do is to setup an incoming webhook service integration. You can do this from your services page.
|
195
|
+
|
196
|
+
After that, you can provide the obtained webhook url when instantiating the Slack channel.
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
require 'rollout'
|
200
|
+
|
201
|
+
slack_channel = Rollout::Notifications::Channels::Slack.new(
|
202
|
+
webhook_url: ENV.fetch('SLACK_COMPANY_WEBHOOK_URL'),
|
203
|
+
channel: '#feature-flags-notifications',
|
204
|
+
username: 'rollout-redis'
|
205
|
+
)
|
206
|
+
```
|
207
|
+
|
208
|
+
##### Email Channel
|
209
|
+
|
210
|
+
Allows you to send notifications via email.
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
require 'rollout'
|
214
|
+
|
215
|
+
email_channel = Rollout::Notifications::Channels::Email.new(
|
216
|
+
smtp_host: ENV.fetch('SMTP_HOST'),
|
217
|
+
smtp_port: ENV.fetch('SMTP_PORT'),
|
218
|
+
from: 'no-reply@rollout-redis.com',
|
219
|
+
to: 'developers@yourcompany.com'
|
220
|
+
)
|
221
|
+
```
|
162
222
|
|
163
223
|
## Rake tasks
|
164
224
|
|
@@ -170,6 +230,13 @@ require 'rollout'
|
|
170
230
|
load 'rollout/tasks/rollout.rake'
|
171
231
|
```
|
172
232
|
|
233
|
+
Also, for using the rake tasks, you must set the following env variables
|
234
|
+
|
235
|
+
```shell
|
236
|
+
ROLLOUT_REDIS_HOST=localhost
|
237
|
+
ROLLOUT_REDIS_PORT=6379
|
238
|
+
```
|
239
|
+
|
173
240
|
### Usage
|
174
241
|
|
175
242
|
To activate/deactivate features, execute the following rake tasks:
|
@@ -219,7 +286,9 @@ We welcome and appreciate contributions from the open-source community. Before y
|
|
219
286
|
|
220
287
|
### Development
|
221
288
|
|
222
|
-
This project is dockerized
|
289
|
+
This project is dockerized, so be sure you have docker installed in your machine.
|
290
|
+
|
291
|
+
Once you clone the repository, you can use the `Make` commands to build the project.
|
223
292
|
|
224
293
|
```shell
|
225
294
|
make build
|
@@ -0,0 +1,31 @@
|
|
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
|
+
|
25
|
+
def type
|
26
|
+
:email
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,45 @@
|
|
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
|
+
def type
|
31
|
+
:slack
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def slack_notifier
|
37
|
+
@notifier ||= ::Slack::Notifier.new @webhook_url do
|
38
|
+
defaults channel: @channel,
|
39
|
+
username: @username
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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
|
+
publish_for_slack_channel(c, feature_name, requests, errors) if c.type == :slack
|
14
|
+
publish_for_email_channel(c, feature_name, requests, errors) if c.type == :email
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def publish_for_slack_channel(c, feature_name, requests, errors)
|
21
|
+
text = "Feature flag '#{feature_name}' has been degraded after #{requests} requests and #{errors} errors"
|
22
|
+
c.publish(text)
|
23
|
+
end
|
24
|
+
|
25
|
+
def publish_for_email_channel(c, feature_name, requests, errors)
|
26
|
+
subject = 'Feature flag has been automatically deactivated!'
|
27
|
+
content = "Feature flag '#{feature_name}' has been degraded after #{requests} requests and #{errors} errors"
|
28
|
+
c.publish(subject, content)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,42 @@
|
|
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
|
+
publish_for_slack_channel(c, feature_name, new_status, new_percentage) if c.type == :slack
|
14
|
+
publish_for_email_channel(c, feature_name, new_status, new_percentage) if c.type == :email
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def publish_for_slack_channel(c, feature_name, new_status, new_percentage)
|
21
|
+
if new_status == :activated
|
22
|
+
text = "Feature flag '#{feature_name}' has been activated with percentage #{new_percentage}!"
|
23
|
+
elsif new_status == :deactivated
|
24
|
+
text = "Feature flag '#{feature_name}' has been deactivated and deleted!"
|
25
|
+
end
|
26
|
+
c.publish(text)
|
27
|
+
end
|
28
|
+
|
29
|
+
def publish_for_email_channel(c, feature_name, new_status, new_percentage)
|
30
|
+
if new_status == :activated
|
31
|
+
subject = 'Feature flag has been activated!'
|
32
|
+
content = "Feature flag '#{feature_name}' has been activated with percentage #{new_percentage}!"
|
33
|
+
elsif new_status == :deactivated
|
34
|
+
subject = 'Feature flag has been deactivated!'
|
35
|
+
content = "Feature flag '#{feature_name}' has been deactivated and deleted!"
|
36
|
+
end
|
37
|
+
c.publish(subject, content)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
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
|
@@ -33,14 +39,36 @@ class Rollout
|
|
33
39
|
self
|
34
40
|
end
|
35
41
|
|
42
|
+
def with_notifications(status_change:[], degrade:[])
|
43
|
+
status_change_channels = status_change
|
44
|
+
degrade_channels = degrade
|
45
|
+
|
46
|
+
if !status_change_channels.empty?
|
47
|
+
@status_change_notifier = Notifications::Notifiers::StatusChange.new(status_change_channels)
|
48
|
+
end
|
49
|
+
|
50
|
+
if !degrade_channels.empty?
|
51
|
+
@degrade_notifier = Notifications::Notifiers::Degrade.new(degrade_channels)
|
52
|
+
end
|
53
|
+
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
36
57
|
def activate(feature_name, percentage=100)
|
37
58
|
data = { percentage: percentage }
|
38
59
|
feature = Feature.new(feature_name, data)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
60
|
+
result = save(feature) == "OK"
|
61
|
+
|
62
|
+
if result
|
63
|
+
@cache[feature_name] = {
|
64
|
+
feature: feature,
|
65
|
+
timestamp: Time.now.to_i
|
66
|
+
} if @cache_enabled
|
67
|
+
|
68
|
+
@status_change_notifier&.notify(feature_name, :activated, percentage)
|
69
|
+
end
|
70
|
+
|
71
|
+
result
|
44
72
|
end
|
45
73
|
|
46
74
|
def activate_percentage(feature_name, percentage)
|
@@ -48,7 +76,11 @@ class Rollout
|
|
48
76
|
end
|
49
77
|
|
50
78
|
def deactivate(feature_name)
|
51
|
-
del(feature_name)
|
79
|
+
result = del(feature_name)
|
80
|
+
|
81
|
+
@status_change_notifier&.notify(feature_name, :deactivated)
|
82
|
+
|
83
|
+
result
|
52
84
|
end
|
53
85
|
|
54
86
|
def active?(feature_name, determinator = nil)
|
@@ -73,7 +105,7 @@ class Rollout
|
|
73
105
|
feature.add_error
|
74
106
|
save(feature)
|
75
107
|
|
76
|
-
|
108
|
+
degrade(feature_name) if degraded?(feature)
|
77
109
|
end
|
78
110
|
raise e
|
79
111
|
end
|
@@ -115,7 +147,11 @@ class Rollout
|
|
115
147
|
|
116
148
|
@storage.set(new_key, new_data)
|
117
149
|
|
118
|
-
puts "Migrated key: #{old_key} to #{new_key} with data #{new_data}"
|
150
|
+
puts "Migrated key: #{old_key.gsub('feature:', '')} to #{new_key.gsub('feature-rollout-redis:', '')} with data #{new_data}"
|
151
|
+
|
152
|
+
if percentage > 0
|
153
|
+
@status_change_notifier&.notify(new_key.gsub('feature-rollout-redis:', ''), :activated, percentage)
|
154
|
+
end
|
119
155
|
end
|
120
156
|
end
|
121
157
|
end
|
@@ -147,6 +183,22 @@ class Rollout
|
|
147
183
|
@storage.del(key(feature_name)) == 1
|
148
184
|
end
|
149
185
|
|
186
|
+
def degrade(feature_name)
|
187
|
+
feature = get(feature_name)
|
188
|
+
data_with_degrade = feature.data.merge({
|
189
|
+
percentage: 0,
|
190
|
+
degraded: true,
|
191
|
+
degraded_at: Time.now
|
192
|
+
})
|
193
|
+
result = @storage.set(key(feature.name), data_with_degrade.to_json) == "OK"
|
194
|
+
|
195
|
+
if result
|
196
|
+
@degrade_notifier.notify(feature_name, feature.requests, feature.errors)
|
197
|
+
end
|
198
|
+
|
199
|
+
result
|
200
|
+
end
|
201
|
+
|
150
202
|
def from_cache(feature_name)
|
151
203
|
return nil unless @cache_enabled
|
152
204
|
|
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: 0.
|
4
|
+
version: 1.0.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-25 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
|