Skip to content

Commit

Permalink
wip - variants
Browse files Browse the repository at this point in the history
  • Loading branch information
rarruda committed Jan 25, 2019
1 parent e514e23 commit 5281e96
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 70 deletions.
9 changes: 8 additions & 1 deletion bin/unleash-client
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require 'unleash/context'
options = {
variant: false,
verbose: false,
quiet: false,
url: 'http://localhost:4242',
demo: false,
disable_metrics: true,
Expand All @@ -25,6 +26,10 @@ OptionParser.new do |opts|
options[:verbose] = v
end

opts.on("-q", "--quiet", "Quiet mode, minimum output only") do |v|
options[:quiet] = v
end

opts.on("-uURL", "--url=URL", "URL base for the Unleash feature toggle service") do |u|
options[:url] = u
end
Expand All @@ -50,12 +55,13 @@ end.parse!
feature_name = ARGV.shift
raise 'feature_name is required. see --help for usage.' unless feature_name

options[:verbose] = false if options[:quiet]

@unleash = Unleash::Client.new(
url: options[:url],
app_name: 'unleash-client-ruby-cli',
disable_metrics: options[:metrics],
log_level: options[:verbose] ? Logger::DEBUG : Logger::WARN,
log_level: log_level = options[:quiet] ? Logger::ERROR : (options[:verbose] ? Logger::DEBUG : Logger::WARN),
)

context_params = ARGV.map{ |e| e.split("=")}.map{|k,v| [k.to_sym, v]}.to_h
Expand Down Expand Up @@ -93,4 +99,5 @@ else
end
end


@unleash.shutdown
56 changes: 32 additions & 24 deletions lib/unleash/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,44 +62,48 @@ def is_enabled?(feature, context = nil, default_value = false)
return toggle_result
end

# def if_enabled(feature, context = nil, default_value = false, &blk)
# yield if is_enabled?(feature, context, default_value)
# end

# enabled? is a more ruby idiomatic method name than is_enabled?
alias_method :enabled?, :is_enabled?

# execute a code block (passed as a parameter), if is_enabled? is true.
def if_enabled(feature, context = nil, default_value = false, &blk)
yield if is_enabled?(feature, context, default_value)
end

def get_variant(feature, context = nil, default_value = false)
Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
#
def get_variant(feature, context = nil, fallback_variant = false)

if Unleash.configuration.disable_client
Unleash.logger.warn "unleash_client is disabled! Always returning #{default_value} for feature #{feature}!"
return default_value
end
empty_variant = Unleash::FeatureToggle.disabled_variant
#disabled_variant = Variant.new('disabled', false, nil)
Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"

toggle_as_hash = Unleash.toggles.select{ |toggle| toggle['name'] == feature }.first if Unleash.toggles
if Unleash.configuration.disable_client
Unleash.logger.warn "unleash_client is disabled! Always returning #{default_variant} for feature #{feature}!"
return fallback_variant || empty_variant
end

if toggle_as_hash.nil?
Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found"
return default_value
end
toggle_as_hash = Unleash.toggles.select{ |toggle| toggle['name'] == feature }.first if Unleash.toggles

# check if is_enabled here too!
if toggle_as_hash.nil?
Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found"
return fallback_variant || empty_variant
end

toggle = Unleash::FeatureToggle.new(toggle_as_hash)
# check if is_enabled here too!

variant = toggle.get_variant(context, default_value)
toggle = Unleash::FeatureToggle.new(toggle_as_hash)

if variant.nil?
Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
return default_value
end
variant = toggle.get_variant(context, fallback_variant)

if variant.nil?
Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
return fallback_variant || empty_variant
end


#TODO:README: name, payload, enabled (bool)
#TODO:README: name, payload, enabled (bool)

return variant
return variant
end


Expand All @@ -123,6 +127,10 @@ def shutdown!
end

private
def default_fallback_variant
Variant.new('disabled', false, nil)
end

def info
return {
'appName': Unleash.configuration.app_name,
Expand Down
2 changes: 1 addition & 1 deletion lib/unleash/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def fetch(params, camelcase_key, default_ret = '')
return default_ret unless params.is_a?(Hash)
return default_ret unless camelcase_key.is_a?(String) or camelcase_key.is_a?(Symbol)

params.fetch(camelcase_key, nil) || params.fetch(snake_sym(camelcase_key), nil) || default_ret
params.values_at(camelcase_key, snake_sym(camelcase_key)).compact.first || default_ret
end

# transform CamelCase to snake_case and make it a sym, if it is a string
Expand Down
91 changes: 63 additions & 28 deletions lib/unleash/feature_toggle.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
require 'unleash/activation_strategy'
require 'unleash/variant_definition'
require 'unleash/variant'
require 'unleash/strategy/util'

module Unleash
class FeatureToggle
attr_accessor :name, :enabled, :strategies, :variants
attr_accessor :name, :enabled, :strategies, :variant_definitions

def initialize(params={})
params = {} if params.nil?
Expand All @@ -14,69 +15,103 @@ def initialize(params={})

self.strategies = params.fetch('strategies', [])
.select{|s| ( s.key?('name') && Unleash::STRATEGIES.key?(s['name'].to_sym) ) }
.map{|s| ActivationStrategy.new(s['name'], s['parameters'])} || []
.map{|s| ActivationStrategy.new(s['name'], s['parameters'] || {})} || []

self.variants = (params.fetch('variants', []) || [])
self.variant_definitions = (params.fetch('variants', []) || [])
.select{|v| v.is_a?(Hash) && v.key?('name') }
.map{|v| Variant.new(v.fetch('name', ''), v.fetch('weight', 0), v.fetch('payload', nil))} || []
.map{|v| VariantDefinition.new(v.fetch('name', ''), v.fetch('weight', 0), v.fetch('payload', nil))} || []

Unleash.logger.debug "FeatureToggle params: #{params}"
# Unleash.logger.debug "strategies: #{self.strategies}"
end

def to_s
"<FeatureToggle: name=#{self.name},enabled=#{self.enabled},strategies=#{self.strategies},variants=#{self.variants}>"
"<FeatureToggle: name=#{self.name},enabled=#{self.enabled},strategies=#{self.strategies},variant_definitions=#{self.variant_definitions}>"
end

def is_enabled?(context, default_result)
if not ['NilClass', 'Unleash::Context'].include? context.class.name
Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context. Context set to nil."
context = nil
end

result = self.enabled && ( self.strategies.select{ |s|
strategy = Unleash::STRATEGIES.fetch(s.name.to_sym, :unknown)
r = strategy.is_enabled?(s.params, context)
Unleash.logger.debug "Strategy #{s.name} returned #{r} with context: #{context}" #"for params #{s.params} "
r
}.any? || self.strategies.empty? )
result ||= default_result

Unleash.logger.debug "FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} and Strategies combined returned #{result})"
result = am_enabled?(context, default_result)

choice = result ? :yes : :no
Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics

return result
end

def get_variant(context, default_result)
#feature, context, fallback_variant
# raise on invalid fallback_variant

def get_variant(context, fallback_variant = disabled_variant()) # default_payload
if not ['NilClass', 'Unleash::Context'].include? context.class.name
Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context. Context set to nil."
context = nil
end

# TODO/FIXME: issues with metrics here:
return nil unless self.enabled && self.is_enabled?(context, default_result)
if fallback_variant.class.name != 'Unleash::Variant'
puts fallback_variant
raise "Provided fallback_variant is not of the correct type #{fallback_variant.class.name}, please use Unleash::Variant."
end


return disabled_variant() unless self.enabled && am_enabled?(context, true)
return disabled_variant() if get_sum_variant_defs_weights <= 0

# TODO/FIXME: context has empty strings as defaul:
hash_salt = context.user_id || context.session_id || context.remote_address
req_weight = Unleash::Strategy::Util.get_normalized_number(self.name, context.user_id)
req_weight = Unleash::Strategy::Util.get_number(self.name, context.user_id, get_sum_variant_defs_weights())
prev_weights = 0

Unleash.logger.debug "req_weight: #{req_weight}"
Unleash.logger.debug "All valid variants: #{self.variants}"
variant_list = self.variants
Unleash.logger.debug "All valid variant_definitions: #{self.variant_definitions}"
variant_definition_list = self.variant_definitions
.select{ |v|
Unleash.logger.debug "req_weight: #{req_weight} < v.weight: #{v.weight} + prev_weights: #{prev_weights} for v:#{v}"
res = (req_weight < v.weight + prev_weights)
prev_weights += v.weight
res
}
return disabled_variant() if variant_definition_list.size == 0

Unleash.logger.debug "after select list of valid variants: #{variant_definition_list}"
variant_definition = variant_definition_list.first

# ap variant_definition

variant = Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
Unleash.logger.debug "finally selected variant_definition: #{variant_definition}"
Unleash.logger.debug "finally selected variant: #{variant}"

Unleash.logger.debug "after select list of valid variants: #{variant_list}"
Unleash.logger.debug "finally selected variant: #{variant_list.first}"
Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics

return variant
end

private
# only check if it is enabled, do not do metrics
def am_enabled?(context, default_result)
if not ['NilClass', 'Unleash::Context'].include? context.class.name
Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context. Context set to nil."
context = nil
end

result = self.enabled && ( self.strategies.select{ |s|
strategy = Unleash::STRATEGIES.fetch(s.name.to_sym, :unknown)
r = strategy.is_enabled?(s.params, context)
Unleash.logger.debug "Strategy #{s.name} returned #{r} with context: #{context}" #"for params #{s.params} "
r
}.any? || self.strategies.empty? )
result ||= default_result

Unleash.logger.debug "FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} and Strategies combined returned #{result})"
return result
end

def disabled_variant
Unleash::Variant.new(name: 'disabled', enabled: false)
end

return variant_list.first
def get_sum_variant_defs_weights
self.variant_definitions.map{ |v| v.weight }.reduce(0, :+)
end
end
end
11 changes: 10 additions & 1 deletion lib/unleash/metrics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module Unleash
class Metrics
attr_accessor :features

# NOTE: no mutexes for features

def initialize
self.features = {}
end
Expand All @@ -18,8 +20,15 @@ def increment(feature, choice)
self.features[feature][choice] += 1
end

def increment_variant(feature, variant)
self.features[feature] = {yes: 0, no: 0} unless self.features.include? feature
self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant'
self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
self.features[feature]['variant'][variant] += 1
end

def reset
self.features = {}
end
end
end
end
7 changes: 6 additions & 1 deletion lib/unleash/strategy/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ module Util

# convert the two strings () into a number between 1 and 100
def get_normalized_number(identifier, group_id)
MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % NORMALIZER + 1
# MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % NORMALIZER + 1
get_number(identifier, group_id, NORMALIZER)
end

def get_number(identifier, group_id, base = 100)
MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % base + 1
end
end
end
Expand Down
27 changes: 21 additions & 6 deletions lib/unleash/variant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,31 @@

module Unleash
class Variant
attr_accessor :name, :weight, :payload
attr_accessor :name, :enabled, :payload

def initialize(name, weight, payload)
self.name = name
self.weight = weight
self.payload = payload
# def initialize(name, enabled = false, payload = nil)
# self.name = name
# self.enabled = enabled
# self.payload = payload
# end

def initialize(params = {})
raise ArgumentError, "Variant initializer requires a hash." unless params.is_a?(Hash)

self.name = params.values_at('name', :name).compact.first
self.enabled = params.values_at('enabled', :enabled).compact.first || false
self.payload = params.values_at('payload', :payload).compact.first

raise ArgumentError, "Variant initializer requires a hash." if self.name.nil?
end

def to_s
"<Variant: name=#{self.name},weight=#{self.weight},payload=#{self.payload}>"
"<Variant: name=#{self.name},enabled=#{self.enabled},payload=#{self.payload}>"
end

def ==(v)
self.name == v.name && self.enabled == v.enabled && self.payload == v.payload
end

end
end
17 changes: 17 additions & 0 deletions lib/unleash/variant_definition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@


module Unleash
class VariantDefinition
attr_accessor :name, :weight, :payload

def initialize(name, weight = 0, payload = nil)
self.name = name
self.weight = weight
self.payload = payload
end

def to_s
"<Variant: name=#{self.name},weight=#{self.weight},payload=#{self.payload}>"
end
end
end
Loading

0 comments on commit 5281e96

Please sign in to comment.