Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
jcagarcia committed Oct 26, 2023
1 parent 11d4c3a commit 0f81ed0
Show file tree
Hide file tree
Showing 16 changed files with 1,072 additions and 555 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All changes to `rollout-redis` will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2023-10-xx

### Added
- `#with_old_rollout_gem_compatibility` method for allowing working with Feature Flags that were stored by the old `rollout` gem.
- `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.
- Add a new parameter when performing `#activate` method for providing a specific degrade configuration for the feature flag that is being activated.
- Add a new parameter when performing `rollout:on` rake task for providing a specific degrade configuration for the feature flag that is being activated.
- You can implement now your own `Rollout::Notifications::Channels::Channel` in case the ones offered by the `gem` are not enough.
- New notification `extended_time` can be configured when using the `#with_notifications` method in order to notify to the different channels the feature flags that are active for a long time period.

## [1.0.0] - 2023-10-25

Expand Down
49 changes: 41 additions & 8 deletions MIGRATING_FROM_ROLLOUT_GEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

In this guide you can find the most important changes and differences between the old rollout gem and this gem.

- [Deprecations](#deprecations)
- [New methods](#new-methods-)
- [Important Changes](#important-changes-)
- [Keys format and stored data has been changed](#keys-format-and-stored-data-has-been-changed-)
- [Migrating Feature Flags](#migrating-feature-flags)
- [Make the gem compatible](#make-the-gem-compatible)

## Deprecations

In order to simplify the features of the first versions of the gem, the following capabilities have been removed:
Expand All @@ -13,26 +20,24 @@ In order to simplify the features of the first versions of the gem, the followin

This is the complete list of methods that have been removed: `#groups`, `#delete`, `#set`, `#activate_group`, `#deactivate_group`, `#activate_user`, `#deactivate_user`, `#activate_users`, `#deactivate_users`, `#set_users`, `#define_group`, `#user_in_active_users?`, `#inactive?`, `#deactivate_percentage`, `active_in_group?`, `#get`, `#set_feature_data`, `#clear_feature_data`, `#multi_get`, `#feature_states`, `#active_features`, `#clear!`, `#exists?`, `#with_feature`.

If you consider some of these methods or features 👆 should be added again to the gem, please, open an issue and we will evaluate it.
If you consider some of these 👆 methods or features should be added again to the gem, please, open an discussion and we will evaluate it.

## New methods 🎁

New methods has been added: `#with_cache`, `#with_degrade`, `#with_feature_flag`, `#clean_cache`.
New methods has been added: `#with_cache`, `#with_degrade`, `#with_feature_flag`, `#clean_cache`, `#with_notifications`.

## Important changes 🚨

### Keys format and stored data has been changed 🔑

The old [rollout](https://github.com/fetlife/rollout) gem is storing the features flags using `feature:#{name}` as key format. The stored value for each feature flag is a string with this format: `percentage|users|groups|data`. This an example of a current flag stored in redis:
The old [rollout](https://github.com/fetlife/rollout) gem is storing the features flags using `feature:#{name}` as key format. The stored value for each feature flag is a string with this format: `percentage|users|groups|data`. This an example of a feature flag stored in redis by the **old** `rollout` gem:

```
Key: "feature:my-feature-flag"
Value: "100|||{}"
```

We have decided to store the data of each feature flag in a more understandable way, so as we don't want to collision with your current stored feature flags this new gem is using `feature-rollout-redis:#{name}` as namespace for storing the new feature flag names in redis.

_NOTE_: This mean that any of your current active feature flags will be taken into consideration!!!
We have decided to store the data of each feature flag in a more understandable way so, as we don't want to collision with your current stored feature flags, our new gem is using `feature-rollout-redis:#{name}` as namespace for storing the new feature flag names in redis.

Also, the stored information has been changed and now is a JSON:

Expand All @@ -44,14 +49,42 @@ Also, the stored information has been changed and now is a JSON:
}
```

This mean that any of your current active feature flags will NOT be taken into consideration when asking if `#active?`!!!

If you want to keep working with your current feature flags, you have two options:

#### Migrating feature flags

In order to facilitate the migration, we are offering a method for easily move from the old format to the new one.
If you decide to fully go with the new `rollout-redis` gem structure, we are offering a method for easily move from the old format to the new one.

```ruby
@rollout.migrate_from_rollout_format
```

This method will NOT remove your old stored feature flags. It will just perform a migration.

Take into consideration that as we are removing users and groups capabilities, you will lost that information after performing the migration. If you want to keep that information, we encourage you to build your own method/script for performing the migration.
Also, we are offering a rake task for performing this method in an easy way before start using the new gem.

```shell
bundle exec rake rollout:migrate_from_rollout_format
```

Take into consideration that as we are removing users and groups capabilities, you will lost that information after performing the migration. If you want to keep that information, we encourage you to build your own method/script for performing the migration.

#### Make the gem compatible

However, if you want to keep your old feature flags stored in your Redis, you can configure the `Rollout` instance for checking them. So when using the `#active?` method or the `#with_feature_flag` merhod, if the feature flag does not exist using the new key format, we will check the old format and if exists:

1. We will parse the content to match the new format
2. If enabled, we will automatically store it in the new format to do a gradual migration.
3. We will return if the requested feature flag is active or not.

For doing that, you can use the `#with_old_rollout_gem_compatibility` instance method when creating the rollout instance:

```ruby
@rollout ||= Rollout.new(@storage)
.with_cache
.with_degrade
.with_old_rollout_gem_compatibility(auto_migration: true)
```

29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Topics covered in this README:
- [Sending Notifications](#sending-notifications)
- [Rake tasks](#rake-tasks)
- [Migrating from rollout gem](#migrating-from-rollout-gem-)
- [Compabile payloads and keys](#compatible-payloads-and-keys)
- [Changelog](#changelog)
- [Contributing](#contributing)

Expand Down Expand Up @@ -143,14 +144,26 @@ In the case that you need to clear the cache at any point, you can make use of t

### Auto-deactivating flags

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.
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.

```ruby
@rollout ||= Rollout.new(redis)
.with_cache
.with_degrade(min: 100, threshold: 0.1)
```

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):

```ruby
@rollout.activate('FEATURE_FLAG_NAME', 100, degrade: { min: 500, threshold: 0.2 })
```

The same configuration 👆 is available when using the rake task for activating the feature flag. Check [Rake tasks](#rake-tasks) section.

```shell
bundle exec rake rollout:on[FEATURE_FLAG_NAME,100,500,0.2]
```

So now, instead of using the `active?` method, you need to wrap your new code under the `with_feature` method.

```ruby
Expand Down Expand Up @@ -220,6 +233,10 @@ email_channel = Rollout::Notifications::Channels::Email.new(
)
```

##### Custom channel

TODO

## Rake tasks

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:
Expand Down Expand Up @@ -260,10 +277,20 @@ For listing all the stored feature flags, do:
bundle exec rake rollout:list
```

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:

```shell
bundle exec rake rollout:migrate_from_rollout_format
```

## Migrating from rollout gem 🚨

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.

### Compatible payloads and keys

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.

## Changelog

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).
Expand Down
90 changes: 80 additions & 10 deletions lib/rollout.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def initialize(storage)
@storage = storage
@cache_enabled = false
@degrade_enabled = false
@old_gem_compatibility_enabled = false
@auto_migrate_from_old_format = false
end

def with_cache(expires_in: 300)
Expand Down Expand Up @@ -54,8 +56,22 @@ def with_notifications(status_change:[], degrade:[])
self
end

def activate(feature_name, percentage=100)
def with_old_rollout_gem_compatibility(auto_migration: false)
@old_gem_compatibility_enabled = true
@auto_migrate_from_old_format = auto_migration

self
end

def activate(feature_name, percentage=100, degrade: nil)
data = { percentage: percentage }
data.merge!({
degrade: {
min: degrade[:min] || 0,
threshold: degrade[:threshold] || 0
}
}) if degrade

feature = Feature.new(feature_name, data)
result = save(feature) == "OK"

Expand Down Expand Up @@ -84,12 +100,19 @@ def deactivate(feature_name)
end

def active?(feature_name, determinator = nil)
feature = get(feature_name, determinator)
feature = get(feature_name)
if feature.nil? && @old_gem_compatibility_enabled
feature = get_with_old_format(feature_name)
if feature && @auto_migrate_from_old_format
activate(feature_name, feature.percentage)
end
end

return false unless feature

active = feature.active?(determinator)

if active && @degrade_enabled
if active && degrade_enabled?(feature)
feature.add_request
save(feature)
end
Expand All @@ -99,9 +122,11 @@ def active?(feature_name, determinator = nil)

def with_feature_flag(feature_name, determinator = nil, &block)
yield if active?(feature_name, determinator)
rescue Rollout::Error => e
raise
rescue => e
feature = get(feature_name, determinator)
if @degrade_enabled && feature
feature = get(feature_name)
if feature && degrade_enabled?(feature)
feature.add_error
save(feature)

Expand Down Expand Up @@ -147,7 +172,7 @@ def migrate_from_rollout_format

@storage.set(new_key, new_data)

puts "Migrated key: #{old_key.gsub('feature:', '')} to #{new_key.gsub('feature-rollout-redis:', '')} with data #{new_data}"
puts "Migrated redis key from #{old_key} to #{new_key}. Migrating data from '#{old_data}' to '#{new_data}'."

if percentage > 0
@status_change_notifier&.notify(new_key.gsub('feature-rollout-redis:', ''), :activated, percentage)
Expand All @@ -158,7 +183,7 @@ def migrate_from_rollout_format

private

def get(feature_name, determinator = nil)
def get(feature_name)
feature = from_redis(feature_name)
return unless feature

Expand All @@ -175,6 +200,15 @@ def get(feature_name, determinator = nil)
cached_feature
end

def get_with_old_format(feature_name)
feature = from_redis_with_old_format(feature_name)
return unless feature

feature
rescue ::Redis::BaseError => e
raise Rollout::Error.new(e)
end

def save(feature)
@storage.set(key(feature.name), feature.data.to_json)
end
Expand Down Expand Up @@ -218,22 +252,58 @@ def from_redis(feature_name)
Feature.new(feature_name, JSON.parse(data, symbolize_names: true))
end

def from_redis_with_old_format(feature_name)
old_data = @storage.get(old_key(feature_name))
return unless old_data

percentage = old_data.split('|')[0].to_i

new_data = {
percentage: percentage,
requests: 0,
errors: 0
}

Feature.new(feature_name, new_data)
end

def expired?(timestamp)
Time.now.to_i - timestamp > @cache_time
end

def degraded?(feature)
return false if !@degrade_enabled
return false if feature.requests < @degrade_min
return false if !degrade_enabled?(feature)

if feature.degrade
degrade_min = feature.degrade[:min]
degrade_threshold = feature.degrade[:threshold]
else
degrade_min = @degrade_min
degrade_threshold = @degrade_threshold
end

feature.errors > @degrade_threshold * feature.requests
return false if feature.requests < degrade_min

feature.errors > degrade_threshold * feature.requests
end

def degrade_enabled?(feature)
@degrade_enabled || !feature.degrade.nil?
end

def key(name)
"#{key_prefix}:#{name}"
end

def old_key(name)
"#{old_key_prefix}:#{name}"
end

def key_prefix
"feature-rollout-redis"
end

def old_key_prefix
"feature"
end
end
11 changes: 9 additions & 2 deletions lib/rollout/feature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class Rollout
class Feature
attr_accessor :percentage
attr_accessor :percentage, :degrade
attr_reader :name, :data

RAND_BASE = (2**32 - 1) / 100.0
Expand All @@ -13,6 +13,7 @@ def initialize(name, data={})
@name = name
@data = data
@percentage = @data[:percentage]
@degrade = @data[:degrade]
end

def active?(determinator=nil)
Expand Down Expand Up @@ -48,14 +49,20 @@ def errors
end

def to_h
{
h = {
name: @name,
percentage: @percentage,
data: {
requests: requests,
errors: errors
}
}

h.merge!({
degrade: @degrade
}) if @degrade

h
end

private
Expand Down
Loading

0 comments on commit 0f81ed0

Please sign in to comment.