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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09b02bf812a6d2bf771a7d9b3d299113d6600c43d88516e40ff43d724c6681b9'
4
- data.tar.gz: d18a8829c7ded0e56c2c9f43da26a73e2011d28a7aee15a9011d6246bcc5a166
3
+ metadata.gz: d3d7a7f17af2164aaeaefcea4ebd6def9fa5a9d29af982e5ed0ad05f2ee4a3a8
4
+ data.tar.gz: f9b9ff1cd3a1d45fa46695150d705dfff5489937baeaff082718fca7221e90f0
5
5
  SHA512:
6
- metadata.gz: 8da06a204a6133487f43c52c013485476391b1ff8d2d5680eda8ef9f419aa6b516a63a27fbae3627f0b747b7515fff2347b0594802252ab8b05f78a8b9988154
7
- data.tar.gz: 2850a4238642e603504ea4034bd036d09649d319127d99eae4afbe2903f099c7af6d6cc49e50248bfff4b01354982ce93b4b8e58468fdbf7c37dae6801bd0f10
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 your feature flag automatically when a threshold of erros is reached, you can enable the degrade feature using the `with_degrade` method.
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 deactivating the feature flag if the threshold of errors is reached. All the managed or captured errors inside the wrapped code will not be taken into consideration.
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. Once you clone the repository, you can use the `Make` commands to build the project.
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
@@ -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,15 @@
1
+ class Rollout
2
+ module Notifications
3
+ module Notifiers
4
+ class Base
5
+ def initialize(channels)
6
+ if channels.respond_to?(:first)
7
+ @channels = channels
8
+ else
9
+ @channels = [channels]
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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
- activated = rollout.activate(args.feature, args.percentage.to_i)
7
+ percentage = args.percentage.to_i
9
8
  else
10
- activated = rollout.activate(args.feature)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rollout
4
- VERSION = '0.3.1'
4
+ VERSION = '1.1.0'
5
5
  end
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 activate(feature_name, percentage=100)
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
- @cache[feature_name] = {
40
- feature: feature,
41
- timestamp: Time.now.to_i
42
- } if @cache_enabled
43
- save(feature) == "OK"
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, determinator)
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 && @degrade_enabled
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, determinator)
72
- if @degrade_enabled && feature
134
+ feature = get(feature_name)
135
+ if feature && degrade_enabled?(feature)
73
136
  feature.add_error
74
137
  save(feature)
75
138
 
76
- deactivate(feature_name) if degraded?(feature)
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: #{old_key} to #{new_key} with data #{new_data}"
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, determinator = nil)
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 !@degrade_enabled
175
- return false if feature.requests < @degrade_min
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
- feature.errors > @degrade_threshold * feature.requests
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
@@ -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.3.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-24 00:00:00.000000000 Z
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