unleash 0.1.6 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,8 @@ require 'tmpdir'
3
3
 
4
4
  module Unleash
5
5
  class Configuration
6
- attr_accessor :url,
6
+ attr_accessor \
7
+ :url,
7
8
  :app_name,
8
9
  :environment,
9
10
  :instance_id,
@@ -19,37 +20,12 @@ module Unleash
19
20
  :log_level
20
21
 
21
22
  def initialize(opts = {})
22
- self.app_name = opts[:app_name] || nil
23
- self.environment = opts[:environment] || 'default'
24
- self.url = opts[:url] || nil
25
- self.instance_id = opts[:instance_id] || SecureRandom.uuid
26
-
27
- if opts[:custom_http_headers].is_a?(Hash) || opts[:custom_http_headers].nil?
28
- self.custom_http_headers = opts[:custom_http_headers] || {}
29
- else
30
- raise ArgumentError, "custom_http_headers must be a hash."
31
- end
32
- self.disable_client = opts[:disable_client] || false
33
- self.disable_metrics = opts[:disable_metrics] || false
34
- self.refresh_interval = opts[:refresh_interval] || 15
35
- self.metrics_interval = opts[:metrics_interval] || 10
36
- self.timeout = opts[:timeout] || 30
37
- self.retry_limit = opts[:retry_limit] || 1
38
-
39
- self.backup_file = opts[:backup_file] || nil
23
+ ensure_valid_opts(opts)
24
+ set_defaults
40
25
 
41
- self.logger = opts[:logger] || Logger.new(STDOUT)
42
- self.log_level = opts[:log_level] || Logger::WARN
43
-
44
-
45
- if opts[:logger].nil?
46
- # on default logger, use custom formatter that includes thread_name:
47
- self.logger.formatter = proc do |severity, datetime, progname, msg|
48
- thread_name = (Thread.current[:name] || "Unleash").rjust(16, ' ')
49
- "[#{datetime.iso8601(6)} #{thread_name} #{severity.ljust(5, ' ')}] : #{msg}\n"
50
- end
51
- end
26
+ initialize_default_logger if opts[:logger].nil?
52
27
 
28
+ merge(opts)
53
29
  refresh_backup_file!
54
30
  end
55
31
 
@@ -60,17 +36,15 @@ module Unleash
60
36
  def validate!
61
37
  return if self.disable_client
62
38
 
63
- raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? or self.url.nil?
39
+ raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil?
64
40
  raise ArgumentError, "custom_http_headers must be a hash." unless self.custom_http_headers.is_a?(Hash)
65
41
  end
66
42
 
67
43
  def refresh_backup_file!
68
- if self.backup_file.nil?
69
- self.backup_file = Dir.tmpdir + "/unleash-#{app_name}-repo.json"
70
- end
44
+ self.backup_file = Dir.tmpdir + "/unleash-#{app_name}-repo.json" if self.backup_file.nil?
71
45
  end
72
46
 
73
- def get_http_headers
47
+ def http_headers
74
48
  {
75
49
  'UNLEASH-INSTANCEID' => self.instance_id,
76
50
  'UNLEASH-APPNAME' => self.app_name
@@ -89,5 +63,50 @@ module Unleash
89
63
  self.url + '/client/register'
90
64
  end
91
65
 
66
+ private
67
+
68
+ def ensure_valid_opts(opts)
69
+ unless opts[:custom_http_headers].is_a?(Hash) || opts[:custom_http_headers].nil?
70
+ raise ArgumentError, "custom_http_headers must be a hash."
71
+ end
72
+ end
73
+
74
+ def set_defaults
75
+ self.app_name = nil
76
+ self.environment = 'default'
77
+ self.url = nil
78
+ self.instance_id = SecureRandom.uuid
79
+ self.disable_client = false
80
+ self.disable_metrics = false
81
+ self.refresh_interval = 15
82
+ self.metrics_interval = 10
83
+ self.timeout = 30
84
+ self.retry_limit = 1
85
+ self.backup_file = nil
86
+ self.log_level = Logger::WARN
87
+
88
+ self.custom_http_headers = {}
89
+ end
90
+
91
+ def initialize_default_logger
92
+ self.logger = Logger.new(STDOUT)
93
+
94
+ # on default logger, use custom formatter that includes thread_name:
95
+ self.logger.formatter = proc do |severity, datetime, _progname, msg|
96
+ thread_name = (Thread.current[:name] || "Unleash").rjust(16, ' ')
97
+ "[#{datetime.iso8601(6)} #{thread_name} #{severity.ljust(5, ' ')}] : #{msg}\n"
98
+ end
99
+ end
100
+
101
+ def merge(opts)
102
+ opts.each_pair{ |opt, val| set_option(opt, val) }
103
+ self
104
+ end
105
+
106
+ def set_option(opt, val)
107
+ __send__("#{opt}=", val)
108
+ rescue NoMethodError
109
+ raise ArgumentError, "unknown configuration parameter '#{val}'"
110
+ end
92
111
  end
93
112
  end
@@ -1,23 +1,34 @@
1
1
  module Unleash
2
-
3
2
  class Context
4
3
  attr_accessor :app_name, :environment, :user_id, :session_id, :remote_address, :properties
5
4
 
6
5
  def initialize(params = {})
7
6
  raise ArgumentError, "Unleash::Context must be initialized with a hash." unless params.is_a?(Hash)
8
7
 
9
- self.app_name = params.values_at('appName', :app_name).compact.first || ( !Unleash.configuration.nil? ? Unleash.configuration.app_name : nil )
10
- self.environment = params.values_at('environment', :environment).compact.first || ( !Unleash.configuration.nil? ? Unleash.configuration.environment : 'default' )
11
- self.user_id = params.values_at('userId', :user_id).compact.first || ''
12
- self.session_id = params.values_at('sessionId', :session_id).compact.first || ''
13
- self.remote_address = params.values_at('remoteAddress', :remote_address).compact.first || ''
8
+ self.app_name = value_for('appName', params, Unleash&.configuration&.app_name)
9
+ self.environment = value_for('environment', params, Unleash&.configuration&.environment || 'default')
10
+ self.user_id = value_for('userId', params)
11
+ self.session_id = value_for('sessionId', params)
12
+ self.remote_address = value_for('remoteAddress', params)
14
13
 
15
- properties = params.values_at('properties', :properties).compact.first
14
+ properties = value_for('properties', params)
16
15
  self.properties = properties.is_a?(Hash) ? properties : {}
17
16
  end
18
17
 
19
18
  def to_s
20
19
  "<Context: user_id=#{self.user_id},session_id=#{self.session_id},remote_address=#{self.remote_address},properties=#{self.properties}>"
21
20
  end
21
+
22
+ private
23
+
24
+ # Method to fetch values from hash for two types of keys: string in camelCase and symbol in snake_case
25
+ def value_for(key, params, default_value = '')
26
+ params.values_at(key, underscore(key).to_sym).compact.first || default_value
27
+ end
28
+
29
+ # converts CamelCase to snake_case
30
+ def underscore(camel_cased_word)
31
+ camel_cased_word.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase
32
+ end
22
33
  end
23
34
  end
@@ -8,64 +8,53 @@ module Unleash
8
8
  class FeatureToggle
9
9
  attr_accessor :name, :enabled, :strategies, :variant_definitions
10
10
 
11
- def initialize(params={})
11
+ def initialize(params = {})
12
12
  params = {} if params.nil?
13
13
 
14
- self.name = params.fetch('name', nil)
15
- self.enabled = params.fetch('enabled', false)
16
-
14
+ self.name = params.fetch('name', nil)
15
+ self.enabled = params.fetch('enabled', false)
17
16
  self.strategies = params.fetch('strategies', [])
18
- .select{ |s| ( s.key?('name') && Unleash::STRATEGIES.key?(s['name'].to_sym) ) }
17
+ .select{ |s| s.has_key?('name') && Unleash::STRATEGIES.has_key?(s['name'].to_sym) }
19
18
  .map{ |s| ActivationStrategy.new(s['name'], s['parameters'] || {}) } || []
20
19
 
21
20
  self.variant_definitions = (params.fetch('variants', []) || [])
22
- .select{ |v| v.is_a?(Hash) && v.key?('name') }
23
- .map{ |v|
21
+ .select{ |v| v.is_a?(Hash) && v.has_key?('name') }
22
+ .map do |v|
24
23
  VariantDefinition.new(
25
24
  v.fetch('name', ''),
26
25
  v.fetch('weight', 0),
27
26
  v.fetch('payload', nil),
28
27
  v.fetch('overrides', [])
29
28
  )
30
- } || []
29
+ end || []
31
30
  end
32
31
 
33
32
  def to_s
34
- "<FeatureToggle: name=#{self.name},enabled=#{self.enabled},strategies=#{self.strategies},variant_definitions=#{self.variant_definitions}>"
33
+ "<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
35
34
  end
36
35
 
37
36
  def is_enabled?(context, default_result)
38
- unless ['NilClass', 'Unleash::Context'].include? context.class.name
39
- Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context. Context set to nil."
40
- context = nil
41
- end
42
-
43
37
  result = am_enabled?(context, default_result)
44
38
 
45
39
  choice = result ? :yes : :no
46
40
  Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
47
41
 
48
- return result
42
+ result
49
43
  end
50
44
 
51
45
  def get_variant(context, fallback_variant = disabled_variant)
52
- unless ['NilClass', 'Unleash::Context'].include? context.class.name
53
- Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context. Context set to nil."
54
- context = nil
55
- end
46
+ raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
56
47
 
57
- unless ['Unleash::Variant'].include? fallback_variant.class.name
58
- raise ArgumentError, "Provided fallback_variant is not of the correct type #{fallback_variant.class.name}, please use Unleash::Variant."
59
- end
48
+ context = ensure_valid_context(context)
60
49
 
61
50
  return disabled_variant unless self.enabled && am_enabled?(context, true)
62
- return disabled_variant if get_sum_variant_defs_weights <= 0
51
+ return disabled_variant if sum_variant_defs_weights <= 0
63
52
 
64
53
  variant = variant_from_override_match(context)
65
54
  variant = variant_from_weights(context) if variant.nil?
66
55
 
67
56
  Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
68
- return variant
57
+ variant
69
58
  end
70
59
 
71
60
  private
@@ -75,56 +64,68 @@ module Unleash
75
64
  result =
76
65
  if self.enabled
77
66
  self.strategies.empty? ||
78
- self.strategies.select{ |s|
79
- strategy = Unleash::STRATEGIES.fetch(s.name.to_sym, :unknown)
80
- r = strategy.is_enabled?(s.params, context)
81
- Unleash.logger.debug "Strategy #{s.name} returned #{r} with context: #{context}" #"for params #{s.params} "
82
- r
83
- }.any?
67
+ self.strategies.any?{ |s| strategy_enabled?(s, context) }
84
68
  else
85
69
  default_result
86
70
  end
87
71
 
88
- Unleash.logger.debug "FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} and Strategies combined returned #{result})"
89
- return result
72
+ Unleash.logger.debug "FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} " \
73
+ "and Strategies combined returned #{result})"
74
+
75
+ result
76
+ end
77
+
78
+ def strategy_enabled?(strategy, context)
79
+ r = Unleash::STRATEGIES.fetch(strategy.name.to_sym, :unknown).is_enabled?(strategy.params, context)
80
+ Unleash.logger.debug "Strategy #{strategy.name} returned #{r} with context: #{context}" # "for params #{strategy.params} "
81
+ r
90
82
  end
91
83
 
92
84
  def disabled_variant
93
85
  Unleash::Variant.new(name: 'disabled', enabled: false)
94
86
  end
95
87
 
96
- def get_sum_variant_defs_weights
97
- self.variant_definitions.map{ |v| v.weight }.reduce(0, :+)
88
+ def sum_variant_defs_weights
89
+ self.variant_definitions.map(&:weight).reduce(0, :+)
98
90
  end
99
91
 
100
92
  def variant_salt(context)
101
93
  return context.user_id unless context.user_id.to_s.empty?
102
94
  return context.session_id unless context.session_id.to_s.empty?
103
95
  return context.remote_address unless context.remote_address.to_s.empty?
104
- return SecureRandom.random_number
96
+
97
+ SecureRandom.random_number
105
98
  end
106
99
 
107
100
  def variant_from_override_match(context)
108
- variant = self.variant_definitions.select{ |vd| vd.override_matches_context?(context) }.first
109
-
101
+ variant = self.variant_definitions.find{ |vd| vd.override_matches_context?(context) }
110
102
  return nil if variant.nil?
103
+
111
104
  Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload)
112
105
  end
113
106
 
114
107
  def variant_from_weights(context)
115
- variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), self.name, get_sum_variant_defs_weights())
108
+ variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), self.name, sum_variant_defs_weights)
116
109
  prev_weights = 0
117
110
 
118
111
  variant_definition = self.variant_definitions
119
- .select{ |v|
112
+ .find do |v|
120
113
  res = (prev_weights + v.weight >= variant_weight)
121
114
  prev_weights += v.weight
122
115
  res
123
- }
124
- .first
116
+ end
125
117
  return disabled_variant if variant_definition.nil?
126
118
 
127
119
  Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
128
120
  end
121
+
122
+ def ensure_valid_context(context)
123
+ unless ['NilClass', 'Unleash::Context'].include? context.class.name
124
+ Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, " \
125
+ "please use Unleash::Context. Context set to nil."
126
+ context = nil
127
+ end
128
+ context
129
+ end
129
130
  end
130
131
  end
@@ -1,5 +1,4 @@
1
1
  module Unleash
2
-
3
2
  class Metrics
4
3
  attr_accessor :features
5
4
 
@@ -16,13 +15,13 @@ module Unleash
16
15
  def increment(feature, choice)
17
16
  raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice
18
17
 
19
- self.features[feature] = {yes: 0, no: 0} unless self.features.include? feature
18
+ self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
20
19
  self.features[feature][choice] += 1
21
20
  end
22
21
 
23
22
  def increment_variant(feature, variant)
24
- self.features[feature] = {yes: 0, no: 0} unless self.features.include? feature
25
- self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant'
23
+ self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
24
+ self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant'
26
25
  self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
27
26
  self.features[feature]['variant'][variant] += 1
28
27
  end
@@ -5,7 +5,6 @@ require 'json'
5
5
  require 'time'
6
6
 
7
7
  module Unleash
8
-
9
8
  class MetricsReporter
10
9
  attr_accessor :last_time
11
10
 
@@ -15,7 +14,11 @@ module Unleash
15
14
 
16
15
  def generate_report
17
16
  now = Time.now
18
- start, stop, self.last_time = self.last_time, now, now
17
+
18
+ start = self.last_time
19
+ stop = now
20
+ self.last_time = now
21
+
19
22
  report = {
20
23
  'appName': Unleash.configuration.app_name,
21
24
  'instanceId': Unleash.configuration.instance_id,
@@ -25,37 +28,21 @@ module Unleash
25
28
  'toggles': Unleash.toggle_metrics.features
26
29
  }
27
30
  }
28
-
29
31
  Unleash.toggle_metrics.reset
30
- return report
32
+
33
+ report
31
34
  end
32
35
 
33
36
  def send
34
37
  Unleash.logger.debug "send() Report"
35
38
 
36
- generated_report = self.generate_report()
39
+ response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_url, self.generate_report.to_json)
37
40
 
38
- uri = URI(Unleash.configuration.client_metrics_url)
39
- http = Net::HTTP.new(uri.host, uri.port)
40
- http.use_ssl = true if uri.scheme == 'https'
41
- http.open_timeout = Unleash.configuration.timeout # in seconds
42
- http.read_timeout = Unleash.configuration.timeout # in seconds
43
-
44
- headers = (Unleash.configuration.get_http_headers || {}).dup
45
- headers['Content-Type'] = 'application/json'
46
- request = Net::HTTP::Post.new(uri.request_uri, headers)
47
- request.body = generated_report.to_json
48
-
49
- Unleash.logger.debug "Report to send: #{request.body}"
50
-
51
- response = http.request(request)
52
-
53
- if ['200','202'].include? response.code
41
+ if ['200', '202'].include? response.code
54
42
  Unleash.logger.debug "Report sent to unleash server sucessfully. Server responded with http code #{response.code}"
55
43
  else
56
44
  Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}."
57
45
  end
58
-
59
46
  end
60
47
  end
61
48
  end
@@ -1,5 +1,4 @@
1
1
  module Unleash
2
-
3
2
  class ScheduledExecutor
4
3
  attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread
5
4
 
@@ -15,26 +14,20 @@ module Unleash
15
14
  self.thread = Thread.new do
16
15
  Thread.current[:name] = self.name
17
16
 
17
+ Unleash.logger.debug "thread #{name} loop starting"
18
18
  loop do
19
19
  Unleash.logger.debug "thread #{name} sleeping for #{interval} seconds"
20
20
  sleep interval
21
21
 
22
- Unleash.logger.debug "thread #{name} started"
23
- begin
24
- yield
25
- self.retry_count = 0
26
- rescue Exception => e
27
- self.retry_count += 1
28
- Unleash.logger.error "thread #{name} threw exception #{e.class}:'#{e}' (#{self.retry_count}/#{self.max_exceptions})"
29
- Unleash.logger.error "stacktrace: #{e.backtrace}"
30
- end
22
+ run_blk{ blk.call }
31
23
 
32
- if self.retry_count > self.max_exceptions
33
- Unleash.logger.error "thread #{name} retry_count (#{self.retry_count}) exceeded max_exceptions (#{self.max_exceptions}). Stopping with retries."
24
+ if exceeded_max_exceptions?
25
+ Unleash.logger.error "thread #{name} retry_count (#{self.retry_count}) exceeded " \
26
+ "max_exceptions (#{self.max_exceptions}). Stopping with retries."
34
27
  break
35
28
  end
36
29
  end
37
- Unleash.logger.warn "thread #{name} loop ended"
30
+ Unleash.logger.debug "thread #{name} loop ended"
38
31
  end
39
32
  end
40
33
 
@@ -47,7 +40,27 @@ module Unleash
47
40
  Unleash.logger.warn "thread #{name} will exit!"
48
41
  self.thread.exit
49
42
  self.thread.join if self.running?
43
+ else
44
+ Unleash.logger.info "thread #{name} was already stopped!"
50
45
  end
51
46
  end
47
+
48
+ private
49
+
50
+ def run_blk(&blk)
51
+ Unleash.logger.debug "thread #{name} starting execution"
52
+
53
+ yield(blk)
54
+ self.retry_count = 0
55
+ rescue StandardError => e
56
+ self.retry_count += 1
57
+ Unleash.logger.error "thread #{name} threw exception #{e.class} " \
58
+ " (#{self.retry_count}/#{self.max_exceptions}): '#{e}'"
59
+ Unleash.logger.debug "stacktrace: #{e.backtrace}"
60
+ end
61
+
62
+ def exceeded_max_exceptions?
63
+ self.retry_count > self.max_exceptions
64
+ end
52
65
  end
53
66
  end