abstract_feature_branch 1.5.0 → 1.6.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: 0240cf2db5b90b0e267e2655cdc6492dc601983959d5438f6e8489ab2eb2c93b
4
- data.tar.gz: 17a136533fdc8dc4332428d302581aa154463272a0a3dff94ca43d3a076a85b7
3
+ metadata.gz: 6bb188fdad68361367ec4bb43a495957ccd7eaa3c89355b5a0d2812055dbef07
4
+ data.tar.gz: 26b576a9a85f5eadac24f46d39086d5d871036b1c8e6b3cfef5ca7b26202373a
5
5
  SHA512:
6
- metadata.gz: cf380b5f6c9fd78ea192887bad5628a8632e26ade45636ee4ee10028efa69e3413c92d19748349e21421bed4666ac72c7b88a3ab8979be97621075a4abc36f0e
7
- data.tar.gz: 441c92f87e112691be34ee9b703e039a7a368b6a7e4d0e082242892dc14907472e3892d3b81a95378eee8a70197e12b31daff631e017772a00d60b0ffce28e24
6
+ metadata.gz: 265ad74dc47a31670c8088b119c3b1966d895e91eb451d3ff5074a594f29e4f682b26ddd87606c8b17d1c783f082fcc78170a290af6935482f2286ce3b487918
7
+ data.tar.gz: 3b782bfaf2efe15b87707060c22da8995761c8e8267b40a2e02cb43a492ead632a403423d02a7ae7a93ce4999be6af7169e0cb5d7c39ad1f1e537f9d27309584
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.6.0
4
+
5
+ - Support Ruby 3.3 - Ruby 1.9.1
6
+ - Thread-Safe Support for Multi-Threaded Usage of `feature_enabled?` and `feature_branch` (fixes issue with multi-threaded usage of `feature_enabled?` causing a `merge` method invocation error due to a `features[environment]` `nil` value that should have been a `Hash` value instead)
7
+
8
+ ## 1.5.1
9
+
10
+ - `AbstractFeatureBranch.toggled_features_for_scope(scope)` API method that returns toggled features for a scope (String)
11
+ - `AbstractFeatureBranch.scopes_for_feature(feature)` API method that returns scopes for a (scoped) feature (String or Symbol)
12
+
3
13
  ## 1.5.0
4
14
 
5
15
  - Generalize "Per-User Feature Enablement" as "Scoped Feature Enablement" (using `scoped` value instead of `per_user` value) to scope features by any scope IDs (e.g. entity IDs or value objects)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Abstract Feature Branch 1.5.0
1
+ # Abstract Feature Branch 1.6.0
2
2
  [![Gem Version](https://badge.fury.io/rb/abstract_feature_branch.png)](http://badge.fury.io/rb/abstract_feature_branch)
3
3
  [![Build Status](https://api.travis-ci.org/AndyObtiva/abstract_feature_branch.png?branch=master)](https://travis-ci.org/AndyObtiva/abstract_feature_branch)
4
4
  [![Coverage Status](https://coveralls.io/repos/AndyObtiva/abstract_feature_branch/badge.png?branch=master)](https://coveralls.io/r/AndyObtiva/abstract_feature_branch?branch=master)
@@ -23,11 +23,9 @@ context-specific feature files if needed.
23
23
 
24
24
  [`abstract_feature_branch`](https://rubygems.org/gems/abstract_feature_branch) is one of the simplest and most minimalistic "Feature Flags" Ruby gems out there as it enables you to get started very quickly by simply leveraging YAML files without having to set up a data store if you do not need it (albeit, you also have the option to use [Redis](https://redis.com) as a very fast in-memory data store).
25
25
 
26
-
27
-
28
26
  Requirements
29
27
  ------------
30
- - Ruby (between `~> 3.1.0` and `~> 1.8.7`)
28
+ - Ruby (between `~> 3.3.0` and `~> 1.9.1`)
31
29
  - [Optional] Rails (between `~> 7.0` and `~> 2.0`)
32
30
  - [Optional] Redis Server (between `~> 7.0` and `~> 2.0`)
33
31
  - [Optional] Redis client gem (between `~> 5.0` and `~> 3.0`)
@@ -38,8 +36,8 @@ Setup
38
36
  ### Rails Application Use
39
37
 
40
38
  1. Configure Rubygem
41
- - With `rails` between `~> 7.0` and `~> 2.0`: Add the following to Gemfile <pre>gem 'abstract_feature_branch', '~> 1.5.0'</pre>
42
- - With `rails` `~> 2.0` only: Add the following to config/environment.rb <pre>config.gem 'abstract_feature_branch', :version => '1.5.0'</pre>
39
+ - With `rails` between `~> 7.0` and `~> 2.0`: Add the following to Gemfile <pre>gem 'abstract_feature_branch', '~> 1.6.0'</pre>
40
+ - With `rails` `~> 2.0` only: Add the following to config/environment.rb <pre>config.gem 'abstract_feature_branch', :version => '1.6.0'</pre>
43
41
  2. Generate <code>config/initializers/abstract_feature_branch.rb</code>, <code>lib/tasks/abstract_feature_branch.rake</code>, <code>config/features.yml</code> and <code>config/features.local.yml</code> in your Rails app directory by running <pre>rails g abstract_feature_branch:install</pre>
44
42
  3. [Optional] Generate <code>config/features/[context_path].yml</code> in your Rails app directory by running <pre>rails g abstract_feature_branch:context context_path</pre> (more details under [**instructions**](#instructions))
45
43
  4. [Optional] Customize configuration in <code>config/initializers/abstract_feature_branch.rb</code> (can be useful for changing location of feature files in Rails application, configuring Redis with a Redis or ConnectionPool instance to use for overrides, and [scoped feature enablement](#scoped-feature-enablement) (e.g. per-user), or troubleshooting a specific Rails environment feature configuration)
@@ -48,7 +46,7 @@ Setup
48
46
 
49
47
  ### Ruby Application General Use
50
48
 
51
- 1. <pre>gem install abstract_feature_branch -v 1.5.0</pre>
49
+ 1. <pre>gem install abstract_feature_branch -v 1.6.0</pre>
52
50
  2. Add code <code>require 'abstract_feature_branch'</code>
53
51
  3. Create <code>config/features.yml</code> under <code>AbstractFeatureBranch.application_root</code> and fill it with content similar to that of the sample <code>config/features.yml</code> mentioned under [**instructions**](#instructions).
54
52
  4. [Optional] Create <code>config/features.local.yml</code> under <code>AbstractFeatureBranch.application_root</code> (more details under [**instructions**](#instructions))
@@ -114,7 +112,9 @@ multi-line logic:
114
112
  single-line logic:
115
113
  > feature_branch(:feature1) { # perform logic }
116
114
 
117
- Note that <code>feature_branch</code> returns nil and does not execute the block if the feature is disabled or non-existent.
115
+ Note that <code>feature_branch</code> returns `nil` and does not execute the block if the feature is disabled or non-existent.
116
+
117
+ `feature_branch` supports multi-threaded code (i.e. usage from multiple parallel threads, as possible in JRuby).
118
118
 
119
119
  - Imperatively check if a feature is enabled or not:
120
120
 
@@ -124,7 +124,9 @@ Note that <code>feature_branch</code> returns nil and does not execute the block
124
124
  > # perform alternate logic
125
125
  > end
126
126
 
127
- Note that <code>feature_enabled?</code> returns false if the feature is disabled and nil if the feature is non-existent (practically the same effect, but nil can sometimes be useful to detect if a feature is referenced).
127
+ Note that <code>feature_enabled?</code> returns `false` if the feature is disabled and `nil` if the feature is non-existent (practically the same effect, but nil can sometimes be useful to detect if a feature is referenced).
128
+
129
+ `feature_enabled?` supports multi-threaded code (i.e. usage from multiple parallel threads, as possible in JRuby).
128
130
 
129
131
  - List all configured features for a particular environment:
130
132
 
@@ -138,10 +140,10 @@ It is possible to restrict enablement of features per specific users (or per ent
138
140
  1. Use <code>toggle_features_for_scope</code> in Ruby code to enable features per scope ID (e.g. entity ID, comma-separated compound ID, JSON string, or value object), which must be a String or a value that is safely-convertable to a String like Integer (e.g. email address or database ID). This loads Redis client gem into memory and stores scoped feature configuration in Redis.
139
141
  In the example below, current_user is a method that provides the current signed in user (e.g. using Rails [Devise] (https://github.com/plataformatec/devise) library).
140
142
 
141
- > scope_id = current_user.email
142
- > AbstractFeatureBranch.toggle_features_for_scope(scope_id, :feature1 => true, :feature2 => false, :feature3 => true, :feature5 => true)
143
+ > scope = current_user.email
144
+ > AbstractFeatureBranch.toggle_features_for_scope(scope, :feature1 => true, :feature2 => false, :feature3 => true, :feature5 => true)
143
145
 
144
- Use alternate version of <code>feature_branch</code> and <code>feature_enabled?</code> passing extra <code>scope_id</code> argument
146
+ Use alternate version of <code>feature_branch</code> and <code>feature_enabled?</code> passing extra <code>scope</code> argument
145
147
 
146
148
  Examples:
147
149
 
@@ -171,6 +173,14 @@ If a feature is enabled as <code>true</code> or disabled as <code>false</code> i
171
173
  like features.local.yml or environment variable overrides), then it overrides toggled scoped feature restrictions, becoming
172
174
  enabled or disabled globally.
173
175
 
176
+ API
177
+ ---
178
+
179
+ `AbstractFeatureBranch.toggle_features_for_scope(scope, feature1: true, feature2: false, ...)`: API method that toggles features (Strings or Symbols) for a scope
180
+
181
+ `AbstractFeatureBranch.toggled_features_for_scope(scope)`: API method that returns toggled features for a scope (String)
182
+
183
+ `AbstractFeatureBranch.scopes_for_feature(feature)`: API method that returns scopes for a (scoped) feature (String or Symbol)
174
184
 
175
185
  Recommendations
176
186
  ---------------
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.0
1
+ 1.6.0
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: abstract_feature_branch 1.5.0 ruby lib
5
+ # stub: abstract_feature_branch 1.6.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "abstract_feature_branch".freeze
9
- s.version = "1.5.0"
9
+ s.version = "1.6.0".freeze
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2023-02-12"
14
+ s.date = "2024-01-22"
15
15
  s.description = "abstract_feature_branch is a Ruby gem that provides a unique variation on the Branch by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin Fowler to enhance team productivity and improve software fault tolerance.\n\nIt provides the ability to wrap blocks of code with an abstract feature branch name, and then specify in a configuration file which features to be switched on or off.\n\nThe goal is to build out upcoming features in the same source code repository branch (i.e. Continuous Integration and Trunk-Based Development), regardless of whether all are completed by the next release date or not, thus increasing team productivity by preventing integration delays. Developers then disable in-progress features until they are ready to be switched on in production, yet enable them locally and in staging environments for in-progress testing.\n\nThis gives developers the added benefit of being able to switch a feature off after release should big problems arise for a high risk feature.\n\nabstract_feature_branch additionally supports Domain Driven Design's pattern of Bounded Contexts by allowing developers to configure context-specific feature files if needed.\n\nabstract_feature_branch is one of the simplest and most minimalistic \"Feature Flags\" Ruby gems out there as it enables you to get started very quickly by simply leveraging YAML files without having to set up a data store if you do not need it (albeit, you also have the option to use Redis as a very fast in-memory data store).\n".freeze
16
16
  s.extra_rdoc_files = [
17
17
  "CHANGELOG.md",
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
27
27
  "lib/abstract_feature_branch.rb",
28
28
  "lib/abstract_feature_branch/configuration.rb",
29
29
  "lib/abstract_feature_branch/file_beautifier.rb",
30
+ "lib/abstract_feature_branch/memoizable.rb",
30
31
  "lib/abstract_feature_branch/redis/connection_pool_to_redis_adapter.rb",
31
32
  "lib/ext/feature_branch.rb",
32
33
  "lib/generators/abstract_feature_branch/context_generator.rb",
@@ -40,29 +41,17 @@ Gem::Specification.new do |s|
40
41
  s.homepage = "http://github.com/AndyObtiva/abstract_feature_branch".freeze
41
42
  s.licenses = ["MIT".freeze]
42
43
  s.post_install_message = "\nRails-only post-install instructions:\n\n1) Run the following command to generate the Rails initializer and basic feature files:\n\nrails g abstract_feature_branch:install\n\n2) Optionally, you may run this command to generate feature files per context:\n\nrails g abstract_feature_branch:context context_path\n \n3) Optionally, install Redis server with [Homebrew](https://brew.sh/) by running:\n\nbrew install redis\n\n4) Optionally, install redis client gem (required with Redis server) by adding the following line to Gemfile above abstract_feature_branch:\n\ngem 'redis', '~> 5.0.5'\n\nAfterwards, run:\n\nbundle\n\n5) Optionally, customize configuration in config/initializers/abstract_feature_branch.rb\n\n(can be useful for changing location of feature files in Rails application,\nconfiguring Redis with a Redis or ConnectionPool instance to use for overrides and scoped feature enablement (e.g. per-user),\nand/or troubleshooting specific Rails environment feature configurations)\n\n".freeze
43
- s.rubygems_version = "3.3.6".freeze
44
+ s.rubygems_version = "3.5.3".freeze
44
45
  s.summary = "abstract_feature_branch is a Ruby gem that provides a variation on the Branch by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin Fowler (aka Feature Flags) to enable Continuous Integration and Trunk-Based Development.".freeze
45
46
 
46
- if s.respond_to? :specification_version then
47
- s.specification_version = 4
48
- end
47
+ s.specification_version = 4
49
48
 
50
- if s.respond_to? :add_runtime_dependency then
51
- s.add_runtime_dependency(%q<deep_merge>.freeze, [">= 1.0.0", "< 2.0.0"])
52
- s.add_development_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
53
- s.add_development_dependency(%q<bundler>.freeze, [">= 2.1.4"])
54
- s.add_development_dependency(%q<rspec>.freeze, ["= 2.14.1"])
55
- s.add_development_dependency(%q<rdoc>.freeze, ["= 5.1.0"])
56
- s.add_development_dependency(%q<psych>.freeze, ["= 3.3.4"])
57
- s.add_development_dependency(%q<rake-tui>.freeze, ["~> 0.2"])
58
- else
59
- s.add_dependency(%q<deep_merge>.freeze, [">= 1.0.0", "< 2.0.0"])
60
- s.add_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
61
- s.add_dependency(%q<bundler>.freeze, [">= 2.1.4"])
62
- s.add_dependency(%q<rspec>.freeze, ["= 2.14.1"])
63
- s.add_dependency(%q<rdoc>.freeze, ["= 5.1.0"])
64
- s.add_dependency(%q<psych>.freeze, ["= 3.3.4"])
65
- s.add_dependency(%q<rake-tui>.freeze, ["~> 0.2"])
66
- end
49
+ s.add_runtime_dependency(%q<deep_merge>.freeze, [">= 1.0.0".freeze, "< 2.0.0".freeze])
50
+ s.add_development_dependency(%q<jeweler>.freeze, ["~> 2.3.9".freeze])
51
+ s.add_development_dependency(%q<bundler>.freeze, [">= 2.1.4".freeze])
52
+ s.add_development_dependency(%q<rspec>.freeze, ["~> 3.12.0".freeze])
53
+ s.add_development_dependency(%q<rdoc>.freeze, ["= 5.1.0".freeze])
54
+ s.add_development_dependency(%q<psych>.freeze, ["= 3.3.4".freeze])
55
+ s.add_development_dependency(%q<rake-tui>.freeze, ["~> 0.2".freeze])
67
56
  end
68
57
 
@@ -1,9 +1,20 @@
1
+ require 'abstract_feature_branch/memoizable'
1
2
  require 'abstract_feature_branch/redis/connection_pool_to_redis_adapter'
2
3
 
3
4
  module AbstractFeatureBranch
4
5
  class Configuration
6
+ include Memoizable
7
+
8
+ MUTEX = {
9
+ '@application_root': Mutex.new,
10
+ '@application_environment': Mutex.new,
11
+ '@logger': Mutex.new,
12
+ '@cacheable': Mutex.new,
13
+ '@feature_store_live_fetching': Mutex.new,
14
+ }
15
+
5
16
  def application_root
6
- @application_root ||= initialize_application_root
17
+ memoize_thread_safe(:@application_root, :initialize_application_root)
7
18
  end
8
19
  def application_root=(path)
9
20
  @application_root = path
@@ -12,7 +23,7 @@ module AbstractFeatureBranch
12
23
  self.application_root = defined?(Rails) ? Rails.root : '.'
13
24
  end
14
25
  def application_environment
15
- @application_environment ||= initialize_application_environment
26
+ memoize_thread_safe(:@application_environment, :initialize_application_environment)
16
27
  end
17
28
  def application_environment=(environment)
18
29
  @application_environment = environment
@@ -21,7 +32,7 @@ module AbstractFeatureBranch
21
32
  self.application_environment = defined?(Rails) ? Rails.env.to_s : ENV['APP_ENV'] || 'development'
22
33
  end
23
34
  def logger
24
- @logger ||= initialize_logger
35
+ memoize_thread_safe(:@logger, :initialize_logger)
25
36
  end
26
37
  def logger=(logger)
27
38
  @logger = logger
@@ -30,7 +41,7 @@ module AbstractFeatureBranch
30
41
  self.logger = defined?(Rails) && Rails.logger ? Rails.logger : Logger.new(STDOUT)
31
42
  end
32
43
  def cacheable
33
- @cacheable ||= initialize_cacheable
44
+ memoize_thread_safe(:@cacheable, :initialize_cacheable)
34
45
  end
35
46
  def cacheable=(cacheable)
36
47
  @cacheable = cacheable
@@ -64,8 +75,7 @@ module AbstractFeatureBranch
64
75
  alias user_features_storage= feature_store=
65
76
 
66
77
  def feature_store_live_fetching
67
- initialize_feature_store_live_fetching if @feature_store_live_fetching.nil?
68
- @feature_store_live_fetching
78
+ memoize_thread_safe(:@feature_store_live_fetching, :initialize_feature_store_live_fetching)
69
79
  end
70
80
  alias feature_store_live_fetching? feature_store_live_fetching
71
81
 
@@ -0,0 +1,23 @@
1
+ module AbstractFeatureBranch
2
+ module Memoizable
3
+ private
4
+
5
+ # memoizes a variable thread-safe
6
+ # expects a MUTEX constant on the class including this moddule, which pre-initializes
7
+ # mutexes at class definition time
8
+ # Example:
9
+ # MUTEX = { '@varname' => Mutex.new }
10
+ def memoize_thread_safe(variable, variable_build_method_name = nil, &variable_builder)
11
+ variable_builder ||= method(variable_build_method_name)
12
+ if instance_variable_get(variable).nil?
13
+ mutex_hash = self.is_a?(Module) ? self::MUTEX : self.class::MUTEX
14
+ mutex_hash[variable].synchronize do
15
+ if instance_variable_get(variable).nil?
16
+ instance_variable_set(variable, variable_builder.call)
17
+ end
18
+ end
19
+ end
20
+ instance_variable_get(variable)
21
+ end
22
+ end
23
+ end
@@ -15,13 +15,28 @@ require 'forwardable'
15
15
 
16
16
  $LOAD_PATH.unshift(File.dirname(__FILE__))
17
17
 
18
+ require 'abstract_feature_branch/memoizable'
18
19
  require 'abstract_feature_branch/configuration'
19
20
 
20
21
  module AbstractFeatureBranch
22
+ extend Memoizable
23
+
21
24
  ENV_FEATURE_PREFIX = "abstract_feature_branch_"
22
25
  REDIS_HKEY = "abstract_feature_branch"
23
26
  VALUE_SCOPED = 'scoped'
24
27
  SCOPED_SPECIAL_VALUES = [VALUE_SCOPED, 'per_user', 'per-user', 'per user']
28
+ MUTEX = {
29
+ '@configuration': Mutex.new,
30
+ '@redis_overrides': Mutex.new,
31
+ '@environment_variable_overrides': Mutex.new,
32
+ '@local_features': Mutex.new,
33
+ '@features': Mutex.new,
34
+ '@environment_features': Mutex.new,
35
+ '@redis_scoped_features': Mutex.new,
36
+ 'environment_features': Mutex.new,
37
+ 'load_application_features': Mutex.new,
38
+ 'unload_application_features': Mutex.new,
39
+ }
25
40
 
26
41
  class << self
27
42
  extend Forwardable
@@ -31,11 +46,11 @@ module AbstractFeatureBranch
31
46
  :feature_store_live_fetching, :feature_store_live_fetching=
32
47
 
33
48
  def configuration
34
- @configuration ||= Configuration.new
49
+ memoize_thread_safe(:@configuration) { Configuration.new }
35
50
  end
36
51
 
37
52
  def redis_overrides
38
- @redis_overrides ||= load_redis_overrides
53
+ memoize_thread_safe(:@redis_overrides, :load_redis_overrides)
39
54
  end
40
55
  def load_redis_overrides
41
56
  return (@redis_overrides = {}) if feature_store.nil?
@@ -51,14 +66,14 @@ module AbstractFeatureBranch
51
66
  end
52
67
 
53
68
  def environment_variable_overrides
54
- @environment_variable_overrides ||= load_environment_variable_overrides
69
+ memoize_thread_safe(:@environment_variable_overrides, :load_environment_variable_overrides)
55
70
  end
56
71
  def load_environment_variable_overrides
57
72
  @environment_variable_overrides = featureize_keys(downcase_keys(booleanize_values(select_feature_keys(ENV))))
58
73
  end
59
74
 
60
75
  def local_features
61
- @local_features ||= load_local_features
76
+ memoize_thread_safe(:@local_features, :load_local_features)
62
77
  end
63
78
  def load_local_features
64
79
  @local_features = {}
@@ -66,7 +81,7 @@ module AbstractFeatureBranch
66
81
  end
67
82
 
68
83
  def features
69
- @features ||= load_features
84
+ memoize_thread_safe(:@features, :load_features)
70
85
  end
71
86
  def load_features
72
87
  @features = {}
@@ -75,8 +90,15 @@ module AbstractFeatureBranch
75
90
 
76
91
  # performance optimization via caching of feature values resolved through environment variable overrides and local features
77
92
  def environment_features(environment)
78
- @environment_features ||= {}
79
- @environment_features[environment] ||= load_environment_features(environment)
93
+ if environment_features_for_all_environments[environment].nil?
94
+ MUTEX[:environment_features].synchronize do
95
+ if environment_features_for_all_environments[environment].nil?
96
+ environment_features_for_all_environments[environment] = load_environment_features(environment)
97
+ end
98
+ @unload_application_features = nil
99
+ end
100
+ end
101
+ environment_features_for_all_environments[environment]
80
102
  end
81
103
  def load_environment_features(environment)
82
104
  @environment_features ||= {}
@@ -88,8 +110,12 @@ module AbstractFeatureBranch
88
110
  merge(redis_overrides)
89
111
  end
90
112
 
113
+ def environment_features_for_all_environments
114
+ memoize_thread_safe(:@environment_features) { {} }
115
+ end
116
+
91
117
  def redis_scoped_features
92
- @redis_scoped_features ||= load_redis_scoped_features
118
+ memoize_thread_safe(:@redis_scoped_features, :load_redis_scoped_features)
93
119
  end
94
120
  def load_redis_scoped_features
95
121
  @redis_scoped_features = {}
@@ -101,10 +127,7 @@ module AbstractFeatureBranch
101
127
  normalized_feature_name = feature.to_s.downcase
102
128
  @redis_scoped_features[normalized_feature_name] ||= []
103
129
  begin
104
- scoped_feature_scope_ids = AbstractFeatureBranch.
105
- feature_store.
106
- smembers("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}")
107
- @redis_scoped_features[normalized_feature_name] += scoped_feature_scope_ids
130
+ @redis_scoped_features[normalized_feature_name] += scopes_for_feature(normalized_feature_name)
108
131
  rescue Exception => error
109
132
  AbstractFeatureBranch.logger.error "AbstractFeatureBranch encountered an error in retrieving Per-User values for feature \"#{normalized_feature_name}\"! Defaulting to no values...\n\nError: #{error.full_message}\n\n"
110
133
  nil
@@ -117,27 +140,44 @@ module AbstractFeatureBranch
117
140
  end
118
141
 
119
142
  def application_features
120
- unload_application_features unless cacheable?
143
+ unload_application_features if !cacheable?
121
144
  environment_features(application_environment)
122
145
  end
123
146
  def load_application_features
124
- AbstractFeatureBranch.load_redis_overrides
125
- AbstractFeatureBranch.load_environment_variable_overrides
126
- AbstractFeatureBranch.load_features
127
- AbstractFeatureBranch.load_local_features
128
- AbstractFeatureBranch.load_environment_features(application_environment)
129
- AbstractFeatureBranch.load_redis_scoped_features
147
+ if @load_application_features.nil?
148
+ MUTEX[:load_application_features].synchronize do
149
+ if @load_application_features.nil?
150
+ AbstractFeatureBranch.load_redis_overrides
151
+ AbstractFeatureBranch.load_environment_variable_overrides
152
+ AbstractFeatureBranch.load_features
153
+ AbstractFeatureBranch.load_local_features
154
+ AbstractFeatureBranch.load_environment_features(application_environment)
155
+ AbstractFeatureBranch.load_redis_scoped_features
156
+ @unload_application_features = nil
157
+ @load_application_features = true
158
+ end
159
+ end
160
+ end
130
161
  end
131
162
  def unload_application_features
132
- @redis_overrides = nil
133
- @environment_variable_overrides = nil
134
- @features = nil
135
- @local_features = nil
136
- @environment_features = nil
137
- @redis_scoped_features = nil
163
+ if @unload_application_features.nil?
164
+ MUTEX[:unload_application_features].synchronize do
165
+ if @unload_application_features.nil?
166
+ @redis_overrides = nil
167
+ @environment_variable_overrides = nil
168
+ @features = nil
169
+ @local_features = nil
170
+ @environment_features = nil
171
+ @redis_scoped_features = nil
172
+ @load_application_features = nil
173
+ @unload_application_features = true
174
+ end
175
+ end
176
+ end
138
177
  end
139
178
 
140
179
  def cacheable?
180
+ # TODO Make thread-safe
141
181
  value = downcase_keys(cacheable)[application_environment]
142
182
  value = (application_environment != 'development') if value.nil?
143
183
  value
@@ -192,25 +232,42 @@ module AbstractFeatureBranch
192
232
  end
193
233
  end
194
234
 
195
- def toggle_features_for_scope(scope_id, features)
235
+ def toggle_features_for_scope(scope, features)
196
236
  features.each do |name, value|
197
237
  if value
198
- feature_store.sadd("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", scope_id)
238
+ feature_store.sadd("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", scope)
199
239
  else
200
- feature_store.srem("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", scope_id)
240
+ feature_store.srem("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", scope)
201
241
  end
202
242
  end
203
243
  end
204
244
  alias toggle_features_for_user toggle_features_for_scope
205
-
245
+
246
+ def toggled_features_for_scope(scope)
247
+ AbstractFeatureBranch.feature_store.keys.select do |key|
248
+ key.start_with?(AbstractFeatureBranch::ENV_FEATURE_PREFIX)
249
+ end.map do |key|
250
+ feature = key.sub(AbstractFeatureBranch::ENV_FEATURE_PREFIX, '')
251
+ end.select do |feature|
252
+ scopes_for_feature(feature).include?(scope.to_s)
253
+ end
254
+ end
255
+
256
+ def scopes_for_feature(feature)
257
+ normalized_feature_name = feature.to_s.downcase
258
+ AbstractFeatureBranch.
259
+ feature_store.
260
+ smembers("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}")
261
+ end
262
+
206
263
  private
207
-
264
+
208
265
  def load_specific_features(features_hash, extension)
209
266
  Dir.glob(File.join(application_root, 'config', 'features', '**', "*#{extension}")).each do |feature_configuration_file|
210
267
  features_hash.deep_merge!(downcase_feature_hash_keys(YAML.load_file(feature_configuration_file)))
211
268
  end
212
269
  main_local_features_file = File.join(application_root, 'config', "features#{extension}")
213
- features_hash.deep_merge!(downcase_feature_hash_keys(YAML.load_file(main_local_features_file))) if File.exists?(main_local_features_file)
270
+ features_hash.deep_merge!(downcase_feature_hash_keys(YAML.load_file(main_local_features_file))) if File.exist?(main_local_features_file)
214
271
  features_hash
215
272
  end
216
273
 
@@ -1,20 +1,20 @@
1
1
  class Object
2
2
  class << self
3
3
  raise 'Abstract feature branch conflicts with another Ruby library having Object::feature_branch' if respond_to?(:feature_branch)
4
- def feature_branch(feature_name, scope_id = nil, &feature_work)
5
- if feature_enabled?(feature_name, scope_id)
4
+ def feature_branch(feature_name, scope = nil, &feature_work)
5
+ if feature_enabled?(feature_name, scope)
6
6
  feature_work.call
7
7
  end
8
8
  end
9
9
 
10
10
  raise 'Abstract feature branch conflicts with another Ruby library having Object::feature_enabled?' if respond_to?(:feature_enabled?)
11
- def feature_enabled?(feature_name, scope_id = nil)
11
+ def feature_enabled?(feature_name, scope = nil)
12
12
  normalized_feature_name = feature_name.to_s.downcase
13
13
 
14
14
  redis_override_value = feature_enabled_reddis_override_value(normalized_feature_name)
15
15
  value = !redis_override_value.nil? ? redis_override_value : AbstractFeatureBranch.application_features[normalized_feature_name]
16
16
  if AbstractFeatureBranch.scoped_value?(value)
17
- value = !scope_id.nil? && feature_enabled_scoped_value(feature_name, scope_id)
17
+ value = !scope.nil? && feature_enabled_scoped_value(feature_name, scope)
18
18
  end
19
19
  value
20
20
  end
@@ -34,27 +34,27 @@ class Object
34
34
  end
35
35
 
36
36
  raise 'Abstract feature branch conflicts with another Ruby library having Object::feature_enabled_scoped_value' if Object.new.respond_to?(:feature_enabled_scoped_value, true)
37
- def feature_enabled_scoped_value(normalized_feature_name, scope_id)
37
+ def feature_enabled_scoped_value(normalized_feature_name, scope)
38
38
  if AbstractFeatureBranch.configuration.feature_store_live_fetching?
39
39
  begin
40
- AbstractFeatureBranch.feature_store.sismember("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}", scope_id)
40
+ AbstractFeatureBranch.feature_store.sismember("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}", scope)
41
41
  rescue Exception => error
42
- AbstractFeatureBranch.logger.error "AbstractFeatureBranch encountered an error in retrieving Per-User value for feature \"#{normalized_feature_name}\" and scope_id #{scope_id}! Defaulting to nil value...\n\nError: #{error.full_message}\n\n"
42
+ AbstractFeatureBranch.logger.error "AbstractFeatureBranch encountered an error in retrieving Per-User value for feature \"#{normalized_feature_name}\" and scope #{scope}! Defaulting to nil value...\n\nError: #{error.full_message}\n\n"
43
43
  nil
44
44
  end
45
45
  else
46
- AbstractFeatureBranch.redis_scoped_features[normalized_feature_name]&.include?(scope_id.to_s)
46
+ AbstractFeatureBranch.redis_scoped_features[normalized_feature_name]&.include?(scope.to_s)
47
47
  end
48
48
  end
49
49
  end
50
50
 
51
51
  raise 'Abstract feature branch conflicts with another Ruby library having Object#feature_branch' if Object.new.respond_to?(:feature_branch)
52
- def feature_branch(feature_name, scope_id = nil, &feature_work)
53
- Object.feature_branch(feature_name.to_s, scope_id, &feature_work)
52
+ def feature_branch(feature_name, scope = nil, &feature_work)
53
+ Object.feature_branch(feature_name.to_s, scope, &feature_work)
54
54
  end
55
55
 
56
56
  raise 'Abstract feature branch conflicts with another Ruby library having Object#feature_enabled?' if Object.new.respond_to?(:feature_enabled?)
57
- def feature_enabled?(feature_name, scope_id = nil)
58
- Object.feature_enabled?(feature_name.to_s, scope_id)
57
+ def feature_enabled?(feature_name, scope = nil)
58
+ Object.feature_enabled?(feature_name.to_s, scope)
59
59
  end
60
60
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abstract_feature_branch
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-12 00:00:00.000000000 Z
11
+ date: 2024-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
@@ -62,16 +62,16 @@ dependencies:
62
62
  name: rspec
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
- - - '='
65
+ - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: 2.14.1
67
+ version: 3.12.0
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
- - - '='
72
+ - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: 2.14.1
74
+ version: 3.12.0
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: rdoc
77
77
  requirement: !ruby/object:Gem::Requirement
@@ -142,6 +142,7 @@ files:
142
142
  - lib/abstract_feature_branch.rb
143
143
  - lib/abstract_feature_branch/configuration.rb
144
144
  - lib/abstract_feature_branch/file_beautifier.rb
145
+ - lib/abstract_feature_branch/memoizable.rb
145
146
  - lib/abstract_feature_branch/redis/connection_pool_to_redis_adapter.rb
146
147
  - lib/ext/feature_branch.rb
147
148
  - lib/generators/abstract_feature_branch/context_generator.rb
@@ -181,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
182
  - !ruby/object:Gem::Version
182
183
  version: '0'
183
184
  requirements: []
184
- rubygems_version: 3.3.6
185
+ rubygems_version: 3.5.3
185
186
  signing_key:
186
187
  specification_version: 4
187
188
  summary: abstract_feature_branch is a Ruby gem that provides a variation on the Branch