rollout-redis 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e4931c09772c7b9e3b1c3042d9912b49cbe8e3d07e6062d1238c6ac9842dd3c
4
- data.tar.gz: fbc9b0e1f80851d1960038169ddce0e93b813097979a8b12c2c82f8fec916ce2
3
+ metadata.gz: d3d7a7f17af2164aaeaefcea4ebd6def9fa5a9d29af982e5ed0ad05f2ee4a3a8
4
+ data.tar.gz: f9b9ff1cd3a1d45fa46695150d705dfff5489937baeaff082718fca7221e90f0
5
5
  SHA512:
6
- metadata.gz: 8d543d5e22d3519838ae6b9c15fbd6a982db066ef7ffe8792945d98bf636a4a34c23294fe02618b4484c6f3d3ee5625fb600a8e3942fc843fb393fdcc701e299
7
- data.tar.gz: 8a79720ea90eed5001b67e6394e0ae26e19893e70478166aef179da9e5848c266f736fd1a69c962fa162a309fb510df5265cf14a134a15fb43081c6dd5b87af7
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 your feature flag automatically when a threshold of errors 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.
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).
@@ -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
@@ -21,10 +21,6 @@ class Rollout
21
21
  mail.delivery_method :smtp, address: @smtp_host, port: @smtp_port
22
22
  mail.deliver
23
23
  end
24
-
25
- def type
26
- :email
27
- end
28
24
  end
29
25
  end
30
26
  end
@@ -27,10 +27,6 @@ class Rollout
27
27
  end
28
28
  end
29
29
 
30
- def type
31
- :slack
32
- end
33
-
34
30
  private
35
31
 
36
32
  def slack_notifier
@@ -10,19 +10,22 @@ class Rollout
10
10
 
11
11
  def notify(feature_name, requests, errors)
12
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
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 publish_for_slack_channel(c, feature_name, requests, errors)
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 publish_for_email_channel(c, feature_name, requests, errors)
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
- 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
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 publish_for_slack_channel(c, feature_name, new_status, new_percentage)
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 publish_for_email_channel(c, feature_name, new_status, new_percentage)
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
- 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 = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
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 activate(feature_name, percentage=100)
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, 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
+
88
117
  return false unless feature
89
118
 
90
119
  active = feature.active?(determinator)
91
120
 
92
- if active && @degrade_enabled
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, determinator)
104
- if @degrade_enabled && feature
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: #{old_key.gsub('feature:', '')} to #{new_key.gsub('feature-rollout-redis:', '')} with data #{new_data}"
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, determinator = nil)
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 !@degrade_enabled
227
- 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
228
290
 
229
- 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?
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.0.0
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-25 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