feature_flagger 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b54bc71d733eb04b2e52156763557a45f94d9722918b12b09e89a07553b88ee4
4
- data.tar.gz: 5984d522ae2d19f324de342db5810cd9bf351c43a9e60e159f8442209f38575f
3
+ metadata.gz: de6dae8c2c1631e95a47c0a6bb99f5e42c3ca2716f7ac4feb045fbd72a279133
4
+ data.tar.gz: 8619614ec4718f56ca20e1617d398ddb69bd1a81f0412568ed95c2215f7e7c2c
5
5
  SHA512:
6
- metadata.gz: ece7587ed9528ca583e61645bc3e5e9fcb710f37308ebbc8bb278390966e8edec6e8dc0a282fbb5864893623da631a1d4f3bffc0876dd484d3523e8008c90c11
7
- data.tar.gz: 9ccdf791f1c7b6c5f10674753ecfbbfb81b4fa69546057e8061b8c0c269c347999503cb4fc868a215073e70cd6b8cb1382c39bcccfb8c11a0fcd2a346601f82d
6
+ metadata.gz: e930f492fd481b66cd8924b603a4e067872ab2b2d864de8402bfb28e24504d81c5b3149596f0c08c255eaf84b74ca98a82657d3ac2f1ef54c9c538facfcf3428
7
+ data.tar.gz: 851c0d1b9eab08442232ee9c3681b3e90b559ed0fc01e4e631c7e3ec26915731682b393ceb1d5d447d1565c20896fbd138fcd18c7678aabfd2047a5158867250
data/README.md CHANGED
@@ -111,6 +111,13 @@ To clean it up, execute or schedule the rake:
111
111
 
112
112
  $ bundle exec rake feature_flagger:cleanup_removed_rollouts
113
113
 
114
+ ## Upgrading
115
+
116
+ When upgrading from `1.1.x` to `1.2.x` the following command must be executed
117
+ to ensure the data stored in Redis storage is right. Check [#67](https://github.com/ResultadosDigitais/feature_flagger/pull/67) and [#68](https://github.com/ResultadosDigitais/feature_flagger/pull/68) for more info.
118
+
119
+ $ bundle exec rake feature_flagger:migrate_to_resource_keys
120
+
114
121
  ## Contributing
115
122
 
116
123
  Bug reports and pull requests are welcome!
@@ -13,7 +13,15 @@ module FeatureFlagger
13
13
  end
14
14
 
15
15
  def release(feature_key, resource_id)
16
- @storage.add(feature_key, resource_id)
16
+ resource_name = Storage::Keys.extract_resource_name_from_feature_key(
17
+ feature_key
18
+ )
19
+
20
+ @storage.add(feature_key, resource_name, resource_id)
21
+ end
22
+
23
+ def releases(resource_name, resource_id)
24
+ @storage.fetch_releases(resource_name, resource_id, RELEASED_FEATURES)
17
25
  end
18
26
 
19
27
  def release_to_all(feature_key)
@@ -21,7 +29,11 @@ module FeatureFlagger
21
29
  end
22
30
 
23
31
  def unrelease(feature_key, resource_id)
24
- @storage.remove(feature_key, resource_id)
32
+ resource_name = Storage::Keys.extract_resource_name_from_feature_key(
33
+ feature_key
34
+ )
35
+
36
+ @storage.remove(feature_key, resource_name, resource_id)
25
37
  end
26
38
 
27
39
  def unrelease_to_all(feature_key)
@@ -40,8 +52,14 @@ module FeatureFlagger
40
52
  @storage.has_value?(RELEASED_FEATURES, feature_key)
41
53
  end
42
54
 
55
+ # DEPRECATED: this method will be removed from public api on v2.0 version.
56
+ # use instead the feature_keys method.
43
57
  def search_keys(query)
44
58
  @storage.search_keys(query)
45
59
  end
60
+
61
+ def feature_keys
62
+ @storage.feature_keys
63
+ end
46
64
  end
47
65
  end
@@ -0,0 +1,18 @@
1
+ begin
2
+ require 'active_support/core_ext/string/inflections'
3
+ rescue LoadError
4
+ unless ''.respond_to?(:constantize)
5
+ class String
6
+ def constantize
7
+ names = split('::')
8
+ names.shift if names.empty? || names.first.empty?
9
+
10
+ constant = Object
11
+ names.each do |name|
12
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
13
+ end
14
+ constant
15
+ end
16
+ end
17
+ end
18
+ end
@@ -2,8 +2,9 @@ module FeatureFlagger
2
2
  class Manager
3
3
 
4
4
  def self.detached_feature_keys
5
- persisted_features = FeatureFlagger.control.search_keys("*").to_a
5
+ persisted_features = FeatureFlagger.control.feature_keys
6
6
  mapped_feature_keys = FeatureFlagger.config.mapped_feature_keys
7
+
7
8
  persisted_features - mapped_feature_keys
8
9
  end
9
10
 
@@ -26,6 +26,11 @@ module FeatureFlagger
26
26
  FeatureFlagger.control.unrelease(feature.key, id)
27
27
  end
28
28
 
29
+ def releases
30
+ resource_name = self.class.feature_flagger_model_settings.entity_name
31
+ FeatureFlagger.control.releases(resource_name, id)
32
+ end
33
+
29
34
  private
30
35
 
31
36
  def feature_flagger_identifier
@@ -48,6 +53,11 @@ module FeatureFlagger
48
53
  FeatureFlagger.control.release(feature.key, resource_id)
49
54
  end
50
55
 
56
+ def unrelease_id(resource_id, *feature_key)
57
+ feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
58
+ FeatureFlagger.control.unrelease(feature.key, resource_id)
59
+ end
60
+
51
61
  def all_released_ids_for(*feature_key)
52
62
  feature_key.flatten!
53
63
  feature = Feature.new(feature_key, feature_flagger_model_settings.entity_name)
@@ -2,8 +2,8 @@ if defined?(Rails)
2
2
  module FeatureFlagger
3
3
  class Railtie < Rails::Railtie
4
4
  rake_tasks do
5
- load 'tasks/cleanup.rake'
5
+ load 'tasks/feature_flagger.rake'
6
6
  end
7
7
  end
8
8
  end
9
- end
9
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FeatureFlagger
4
+ module Storage
5
+ class FeatureKeysMigration
6
+
7
+ def initialize(from_redis, to_control)
8
+ @from_redis = from_redis
9
+ @to_control = to_control
10
+ end
11
+
12
+ # call migrates features key from the old fashioned to the new
13
+ # format.
14
+ #
15
+ # It must replicate feature keys with changes:
16
+ #
17
+ # from "avenue:traffic_lights" => 42
18
+ # to "avenue:42" => traffic_lights
19
+ def call
20
+ @from_redis.scan_each(match: "*", count: FeatureFlagger::Storage::Redis::SCAN_EACH_BATCH_SIZE) do |redis_key|
21
+ # filter out resource_keys
22
+ next if redis_key.start_with?("#{FeatureFlagger::Storage::Redis::RESOURCE_PREFIX}:")
23
+
24
+ migrate_key(redis_key)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def migrate_key(key)
31
+ return migrate_release_to_all(key) if feature_released_to_all?(key)
32
+
33
+ migrate_release(key)
34
+ end
35
+
36
+ def migrate_release_to_all(key)
37
+ features = @from_redis.smembers(key)
38
+
39
+ features.each do |feature_key|
40
+ @to_control.release_to_all(feature_key)
41
+ end
42
+ end
43
+
44
+ def feature_released_to_all?(key)
45
+ FeatureFlagger::Control::RELEASED_FEATURES == key
46
+ end
47
+
48
+ def migrate_release(key)
49
+ resource_ids = @from_redis.smembers(key)
50
+
51
+ @to_control.release(key, resource_ids)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,21 @@
1
+ module FeatureFlagger
2
+ module Storage
3
+ module Keys
4
+ MINIMUM_VALID_FEATURE_PATH = 2.freeze
5
+
6
+ def self.resource_key(prefix, resource_name, resource_id)
7
+ "#{prefix}:#{resource_name}:#{resource_id}"
8
+ end
9
+
10
+ def self.extract_resource_name_from_feature_key(feature_key)
11
+ feature_paths = feature_key.split(':')
12
+
13
+ raise InvalidResourceNameError if feature_paths.size < MINIMUM_VALID_FEATURE_PATH
14
+
15
+ feature_paths.first
16
+ end
17
+
18
+ class InvalidResourceNameError < StandardError; end
19
+ end
20
+ end
21
+ end
@@ -1,11 +1,13 @@
1
1
  require 'redis'
2
2
  require 'redis-namespace'
3
+ require_relative './keys'
3
4
 
4
5
  module FeatureFlagger
5
6
  module Storage
6
7
  class Redis
7
-
8
8
  DEFAULT_NAMESPACE = :feature_flagger
9
+ RESOURCE_PREFIX = "_r".freeze
10
+ SCAN_EACH_BATCH_SIZE = 1000.freeze
9
11
 
10
12
  def initialize(redis)
11
13
  @redis = redis
@@ -17,39 +19,102 @@ module FeatureFlagger
17
19
  new(ns)
18
20
  end
19
21
 
22
+ def fetch_releases(resource_name, resource_id, global_key)
23
+ resource_key = resource_key(resource_name, resource_id)
24
+ @redis.sunion(resource_key, global_key)
25
+ end
26
+
20
27
  def has_value?(key, value)
21
28
  @redis.sismember(key, value)
22
29
  end
23
30
 
24
- def add(key, value)
25
- @redis.sadd(key, value)
26
- end
31
+ def add(feature_key, resource_name, resource_id)
32
+ resource_key = resource_key(resource_name, resource_id)
27
33
 
28
- def remove(key, value)
29
- @redis.srem(key, value)
34
+ @redis.multi do |redis|
35
+ redis.sadd(feature_key, resource_id)
36
+ redis.sadd(resource_key, feature_key)
37
+ end
30
38
  end
31
39
 
32
- def remove_all(global_key, key)
40
+ def remove(feature_key, resource_name, resource_id)
41
+ resource_key = resource_key(resource_name, resource_id)
42
+
33
43
  @redis.multi do |redis|
34
- redis.srem(global_key, key)
35
- redis.del(key)
44
+ redis.srem(feature_key, resource_id)
45
+ redis.srem(resource_key, feature_key)
36
46
  end
37
47
  end
38
48
 
49
+ def remove_all(global_key, feature_key)
50
+ @redis.srem(global_key, feature_key)
51
+ remove_feature_key_from_resources(feature_key)
52
+ end
53
+
39
54
  def add_all(global_key, key)
40
- @redis.multi do |redis|
41
- redis.sadd(global_key, key)
42
- redis.del(key)
43
- end
55
+ @redis.sadd(global_key, key)
56
+ remove_feature_key_from_resources(key)
44
57
  end
45
58
 
46
59
  def all_values(key)
47
60
  @redis.smembers(key)
48
61
  end
49
62
 
63
+ # DEPRECATED: this method will be removed from public api on v2.0 version.
64
+ # use instead the feature_keys method.
50
65
  def search_keys(query)
51
66
  @redis.scan_each(match: query)
52
67
  end
68
+
69
+ def feature_keys
70
+ feature_keys = []
71
+
72
+ @redis.scan_each(match: "*") do |key|
73
+ # Reject keys related to feature responsible for return
74
+ # released features for a given account.
75
+ next if key.start_with?("#{RESOURCE_PREFIX}:")
76
+
77
+ feature_keys << key
78
+ end
79
+
80
+ feature_keys
81
+ end
82
+
83
+ def synchronize_feature_and_resource
84
+ FeatureFlagger::Storage::FeatureKeysMigration.new(
85
+ @redis,
86
+ FeatureFlagger.control,
87
+ ).call
88
+ end
89
+
90
+ private
91
+
92
+ def resource_key(resource_name, resource_id)
93
+ FeatureFlagger::Storage::Keys.resource_key(
94
+ RESOURCE_PREFIX,
95
+ resource_name,
96
+ resource_id,
97
+ )
98
+ end
99
+
100
+ def remove_feature_key_from_resources(feature_key)
101
+ cursor = 0
102
+ resource_name = feature_key.split(":").first
103
+
104
+ loop do
105
+ cursor, resource_ids = @redis.sscan(feature_key, cursor, count: SCAN_EACH_BATCH_SIZE)
106
+
107
+ @redis.multi do |redis|
108
+ resource_ids.each do |resource_id|
109
+ key = resource_key(resource_name, resource_id)
110
+ redis.srem(key, feature_key)
111
+ redis.srem(feature_key, resource_id)
112
+ end
113
+ end
114
+
115
+ break if cursor == "0"
116
+ end
117
+ end
53
118
  end
54
119
  end
55
120
  end
@@ -1,3 +1,3 @@
1
1
  module FeatureFlagger
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -0,0 +1,41 @@
1
+ namespace :feature_flagger do
2
+ desc "cleaning up keys from storage that are no longer in the rollout.yml file"
3
+ task :cleanup_removed_rollouts => :environment do
4
+ keys = FeatureFlagger::Manager.detached_feature_keys
5
+ puts "Found keys to remove: #{keys}"
6
+ keys.each do |key|
7
+ FeatureFlagger::Manager.cleanup_detached key
8
+ end
9
+ end
10
+
11
+ desc "Synchronizes resource_keys with feature_keys, recommended to apps that installed feature flagger before v.1.2.0"
12
+ task :migrate_to_resource_keys => :environment do
13
+ storage = FeatureFlagger.config.storage
14
+ storage.synchronize_feature_and_resource
15
+ end
16
+
17
+ desc "Release feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:release\[Account,email_marketing:whitelabel,1,2,3,4\]`"
18
+ task :release, [:entity_name, :feature_key] => :environment do |_, args|
19
+ entity = args.entity_name.constantize
20
+ entity_ids = args.extras
21
+ entity.release_id(entity_ids, *args.feature_key.split(':'))
22
+ end
23
+
24
+ desc "Unrelease feature to given identifiers, Usage: `$ bundle exec rake feature_flagger:unrelease\[Account,email_marketing:whitelabel,1,2,3,4\]`"
25
+ task :unrelease, [:entity_name, :feature_key] => :environment do |_, args|
26
+ entity, entity_ids = args.entity_name.constantize, args.extras
27
+ entity.unrelease_id(entity_ids, *args.feature_key.split(':'))
28
+ end
29
+
30
+ desc "Release one feature to all entity ids, Usage: `$ bundle exec rake feature_flagger:release_to_all\[Account,email_marketing:whitelabel\]`"
31
+ task :release_to_all, [:entity_name, :feature_key] => :environment do |_, args|
32
+ entity = args.entity_name.constantize
33
+ entity.release_to_all(*args.feature_key.split(':'))
34
+ end
35
+
36
+ desc "Unrelease one feature to all entity ids, Usage: `$ bundle exec rake feature_flagger:unrelease_to_all\[Account,email_marketing:whitelabel\]`"
37
+ task :unrelease_to_all, [:entity_name, :feature_key] => :environment do |_, args|
38
+ entity = args.entity_name.constantize
39
+ entity.unrelease_to_all(*args.feature_key.split(':'))
40
+ end
41
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feature_flagger
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nando Sousa
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-10-14 00:00:00.000000000 Z
12
+ date: 2020-08-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -43,30 +43,30 @@ dependencies:
43
43
  name: bundler
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - "~>"
46
+ - - ">="
47
47
  - !ruby/object:Gem::Version
48
- version: '2.0'
48
+ version: '0'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - "~>"
53
+ - - ">="
54
54
  - !ruby/object:Gem::Version
55
- version: '2.0'
55
+ version: '0'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: rake
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: '10.0'
62
+ version: '13.0'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '10.0'
69
+ version: '13.0'
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: rspec
72
72
  requirement: !ruby/object:Gem::Requirement
@@ -81,20 +81,34 @@ dependencies:
81
81
  - - "~>"
82
82
  - !ruby/object:Gem::Version
83
83
  version: '3.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: simplecov
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '='
89
+ - !ruby/object:Gem::Version
90
+ version: '0.17'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '='
96
+ - !ruby/object:Gem::Version
97
+ version: '0.17'
84
98
  - !ruby/object:Gem::Dependency
85
99
  name: fakeredis
86
100
  requirement: !ruby/object:Gem::Requirement
87
101
  requirements:
88
102
  - - '='
89
103
  - !ruby/object:Gem::Version
90
- version: 0.7.0
104
+ version: 0.8.0
91
105
  type: :development
92
106
  prerelease: false
93
107
  version_requirements: !ruby/object:Gem::Requirement
94
108
  requirements:
95
109
  - - '='
96
110
  - !ruby/object:Gem::Version
97
- version: 0.7.0
111
+ version: 0.8.0
98
112
  description: Management tool to make it easier rollouting features to customers.
99
113
  email:
100
114
  - nandosousafr@gmail.com
@@ -108,14 +122,17 @@ files:
108
122
  - lib/feature_flagger.rb
109
123
  - lib/feature_flagger/configuration.rb
110
124
  - lib/feature_flagger/control.rb
125
+ - lib/feature_flagger/core_ext.rb
111
126
  - lib/feature_flagger/feature.rb
112
127
  - lib/feature_flagger/manager.rb
113
128
  - lib/feature_flagger/model.rb
114
129
  - lib/feature_flagger/model_settings.rb
115
130
  - lib/feature_flagger/railtie.rb
131
+ - lib/feature_flagger/storage/feature_keys_migration.rb
132
+ - lib/feature_flagger/storage/keys.rb
116
133
  - lib/feature_flagger/storage/redis.rb
117
134
  - lib/feature_flagger/version.rb
118
- - lib/tasks/cleanup.rake
135
+ - lib/tasks/feature_flagger.rake
119
136
  homepage: http://github.com/ResultadosDigitais/feature_flagger
120
137
  licenses:
121
138
  - MIT
@@ -135,8 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
152
  - !ruby/object:Gem::Version
136
153
  version: 2.0.0
137
154
  requirements: []
138
- rubyforge_project:
139
- rubygems_version: 2.7.9
155
+ rubygems_version: 3.1.4
140
156
  signing_key:
141
157
  specification_version: 4
142
158
  summary: Partial release your features.
@@ -1,10 +0,0 @@
1
- namespace :feature_flagger do
2
- desc "cleaning up keys from storage that are no longer in the rollout.yml file"
3
- task :cleanup_removed_rollouts => :environment do
4
- keys = FeatureFlagger::Manager.detached_feature_keys
5
- puts "Found keys to remove: #{keys}"
6
- keys.each do |key|
7
- FeatureFlagger::Manager.cleanup_detached key
8
- end
9
- end
10
- end