unleash 3.2.2 → 4.3.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.
@@ -0,0 +1,14 @@
1
+ require 'unleash/bootstrap/provider/base'
2
+
3
+ module Unleash
4
+ module Bootstrap
5
+ module Provider
6
+ class FromFile < Base
7
+ # @param file_path [String]
8
+ def self.read(file_path)
9
+ File.read(file_path)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ require 'unleash/bootstrap/provider/base'
2
+
3
+ module Unleash
4
+ module Bootstrap
5
+ module Provider
6
+ class FromUrl < Base
7
+ # @param url [String]
8
+ # @param headers [Hash, nil] HTTP headers to use. If not set, the unleash client SDK ones will be used.
9
+ def self.read(url, headers = nil)
10
+ response = Unleash::Util::Http.get(URI.parse(url), nil, headers)
11
+
12
+ return nil if response.code != '200'
13
+
14
+ response.body
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -18,8 +18,9 @@ module Unleash
18
18
  Unleash.logger = Unleash.configuration.logger.clone
19
19
  Unleash.logger.level = Unleash.configuration.log_level
20
20
 
21
+ Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
21
22
  if Unleash.configuration.disable_client
22
- Unleash.logger.warn "Unleash::Client is disabled! Will only return default results!"
23
+ Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!"
23
24
  return
24
25
  end
25
26
 
@@ -28,13 +29,14 @@ module Unleash
28
29
  start_metrics unless Unleash.configuration.disable_metrics
29
30
  end
30
31
 
31
- def is_enabled?(feature, context = nil, default_value = false)
32
+ def is_enabled?(feature, context = nil, default_value_param = false, &fallback_blk)
32
33
  Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}"
33
34
 
34
- if Unleash.configuration.disable_client
35
- Unleash.logger.warn "unleash_client is disabled! Always returning #{default_value} for feature #{feature}!"
36
- return default_value
37
- end
35
+ default_value = if block_given?
36
+ default_value_param || !!fallback_blk.call(feature, context)
37
+ else
38
+ default_value_param
39
+ end
38
40
 
39
41
  toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
40
42
 
@@ -45,7 +47,7 @@ module Unleash
45
47
 
46
48
  toggle = Unleash::FeatureToggle.new(toggle_as_hash)
47
49
 
48
- toggle.is_enabled?(context, default_value)
50
+ toggle.is_enabled?(context)
49
51
  end
50
52
 
51
53
  # enabled? is a more ruby idiomatic method name than is_enabled?
@@ -56,19 +58,19 @@ module Unleash
56
58
  yield(blk) if is_enabled?(feature, context, default_value)
57
59
  end
58
60
 
59
- def get_variant(feature, context = nil, fallback_variant = nil)
61
+ def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant)
60
62
  Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
61
63
 
62
64
  if Unleash.configuration.disable_client
63
- Unleash.logger.debug "unleash_client is disabled! Always returning #{default_variant} for feature #{feature}!"
64
- return fallback_variant || Unleash::FeatureToggle.disabled_variant
65
+ Unleash.logger.debug "unleash_client is disabled! Always returning #{fallback_variant} for feature #{feature}!"
66
+ return fallback_variant
65
67
  end
66
68
 
67
69
  toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
68
70
 
69
71
  if toggle_as_hash.nil?
70
72
  Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found"
71
- return fallback_variant || Unleash::FeatureToggle.disabled_variant
73
+ return fallback_variant
72
74
  end
73
75
 
74
76
  toggle = Unleash::FeatureToggle.new(toggle_as_hash)
@@ -76,7 +78,7 @@ module Unleash
76
78
 
77
79
  if variant.nil?
78
80
  Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
79
- return fallback_variant || Unleash::FeatureToggle.disabled_variant
81
+ return fallback_variant
80
82
  end
81
83
 
82
84
  # TODO: Add to README: name, payload, enabled (bool)
@@ -88,7 +90,7 @@ module Unleash
88
90
  def shutdown
89
91
  unless Unleash.configuration.disable_client
90
92
  Unleash.toggle_fetcher.save!
91
- Unleash.reporter.send unless Unleash.configuration.disable_metrics
93
+ Unleash.reporter.post unless Unleash.configuration.disable_metrics
92
94
  shutdown!
93
95
  end
94
96
  end
@@ -115,11 +117,11 @@ module Unleash
115
117
  end
116
118
 
117
119
  def start_toggle_fetcher
118
- Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
119
120
  self.fetcher_scheduled_executor = Unleash::ScheduledExecutor.new(
120
121
  'ToggleFetcher',
121
122
  Unleash.configuration.refresh_interval,
122
- Unleash.configuration.retry_limit
123
+ Unleash.configuration.retry_limit,
124
+ first_fetch_is_eager
123
125
  )
124
126
  self.fetcher_scheduled_executor.run do
125
127
  Unleash.toggle_fetcher.fetch
@@ -135,7 +137,7 @@ module Unleash
135
137
  Unleash.configuration.retry_limit
136
138
  )
137
139
  self.metrics_scheduled_executor.run do
138
- Unleash.reporter.send
140
+ Unleash.reporter.post
139
141
  end
140
142
  end
141
143
 
@@ -144,12 +146,20 @@ module Unleash
144
146
 
145
147
  # Send the request, if possible
146
148
  begin
147
- response = Unleash::Util::Http.post(Unleash.configuration.client_register_url, info.to_json)
149
+ response = Unleash::Util::Http.post(Unleash.configuration.client_register_uri, info.to_json)
148
150
  rescue StandardError => e
149
151
  Unleash.logger.error "unable to register client with unleash server due to exception #{e.class}:'#{e}'."
150
152
  Unleash.logger.error "stacktrace: #{e.backtrace}"
151
153
  end
152
154
  Unleash.logger.debug "client registered: #{response}"
153
155
  end
156
+
157
+ def disabled_variant
158
+ @disabled_variant ||= Unleash::FeatureToggle.disabled_variant
159
+ end
160
+
161
+ def first_fetch_is_eager
162
+ Unleash.configuration.use_bootstrap?
163
+ end
154
164
  end
155
165
  end
@@ -1,5 +1,6 @@
1
1
  require 'securerandom'
2
2
  require 'tmpdir'
3
+ require 'unleash/bootstrap/configuration'
3
4
 
4
5
  module Unleash
5
6
  class Configuration
@@ -8,6 +9,7 @@ module Unleash
8
9
  :app_name,
9
10
  :environment,
10
11
  :instance_id,
12
+ :project_name,
11
13
  :custom_http_headers,
12
14
  :disable_client,
13
15
  :disable_metrics,
@@ -17,10 +19,11 @@ module Unleash
17
19
  :metrics_interval,
18
20
  :backup_file,
19
21
  :logger,
20
- :log_level
22
+ :log_level,
23
+ :bootstrap_config
21
24
 
22
25
  def initialize(opts = {})
23
- ensure_valid_opts(opts)
26
+ validate_custom_http_headers!(opts[:custom_http_headers]) if opts.has_key?(:custom_http_headers)
24
27
  set_defaults
25
28
 
26
29
  initialize_default_logger if opts[:logger].nil?
@@ -37,53 +40,60 @@ module Unleash
37
40
  return if self.disable_client
38
41
 
39
42
  raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil?
40
- raise ArgumentError, "custom_http_headers must be a hash." unless self.custom_http_headers.is_a?(Hash)
43
+
44
+ validate_custom_http_headers!(self.custom_http_headers)
41
45
  end
42
46
 
43
47
  def refresh_backup_file!
44
- self.backup_file = Dir.tmpdir + "/unleash-#{app_name}-repo.json" if self.backup_file.nil?
48
+ self.backup_file = File.join(Dir.tmpdir, "unleash-#{app_name}-repo.json")
45
49
  end
46
50
 
47
51
  def http_headers
48
52
  {
49
53
  'UNLEASH-INSTANCEID' => self.instance_id,
50
54
  'UNLEASH-APPNAME' => self.app_name
51
- }.merge(custom_http_headers.dup)
55
+ }.merge!(generate_custom_http_headers)
52
56
  end
53
57
 
54
- def fetch_toggles_url
55
- self.url + '/client/features'
58
+ def fetch_toggles_uri
59
+ uri = URI("#{self.url_stripped_of_slash}/client/features")
60
+ uri.query = "project=#{self.project_name}" unless self.project_name.nil?
61
+ uri
56
62
  end
57
63
 
58
- def client_metrics_url
59
- self.url + '/client/metrics'
64
+ def client_metrics_uri
65
+ URI("#{self.url_stripped_of_slash}/client/metrics")
60
66
  end
61
67
 
62
- def client_register_url
63
- self.url + '/client/register'
68
+ def client_register_uri
69
+ URI("#{self.url_stripped_of_slash}/client/register")
64
70
  end
65
71
 
66
- private
72
+ def url_stripped_of_slash
73
+ self.url.delete_suffix '/'
74
+ end
67
75
 
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
76
+ def use_bootstrap?
77
+ self.bootstrap_config&.valid?
72
78
  end
73
79
 
80
+ private
81
+
74
82
  def set_defaults
75
83
  self.app_name = nil
76
84
  self.environment = 'default'
77
85
  self.url = nil
78
86
  self.instance_id = SecureRandom.uuid
87
+ self.project_name = nil
79
88
  self.disable_client = false
80
89
  self.disable_metrics = false
81
- self.refresh_interval = 15
82
- self.metrics_interval = 10
90
+ self.refresh_interval = 10
91
+ self.metrics_interval = 60
83
92
  self.timeout = 30
84
- self.retry_limit = 1
93
+ self.retry_limit = Float::INFINITY
85
94
  self.backup_file = nil
86
95
  self.log_level = Logger::WARN
96
+ self.bootstrap_config = nil
87
97
 
88
98
  self.custom_http_headers = {}
89
99
  end
@@ -103,6 +113,18 @@ module Unleash
103
113
  self
104
114
  end
105
115
 
116
+ def validate_custom_http_headers!(custom_http_headers)
117
+ return if custom_http_headers.is_a?(Hash) || custom_http_headers.respond_to?(:call)
118
+
119
+ raise ArgumentError, "custom_http_headers must be a Hash or a Proc."
120
+ end
121
+
122
+ def generate_custom_http_headers
123
+ return self.custom_http_headers.call if self.custom_http_headers.respond_to?(:call)
124
+
125
+ self.custom_http_headers
126
+ end
127
+
106
128
  def set_option(opt, val)
107
129
  __send__("#{opt}=", val)
108
130
  rescue NoMethodError
@@ -1,26 +1,104 @@
1
+ require 'date'
2
+
1
3
  module Unleash
2
4
  class Constraint
3
- attr_accessor :context_name, :operator, :values
5
+ attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
6
+
7
+ OPERATORS = {
8
+ IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
9
+ NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
10
+ STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
11
+ STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
12
+ STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
13
+ NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
14
+ NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
15
+ NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
16
+ NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
17
+ NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
18
+ DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
19
+ DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
20
+ SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
21
+ SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
22
+ SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }
23
+ }.freeze
4
24
 
5
- VALID_OPERATORS = ['IN', 'NOT_IN'].freeze
25
+ LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
6
26
 
7
- def initialize(context_name, operator, values = [])
27
+ def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
8
28
  raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
9
- raise ArgumentError, "operator does not hold a valid value:" + VALID_OPERATORS unless VALID_OPERATORS.include? operator
10
- raise ArgumentError, "values does not hold an Array" unless values.is_a?(Array)
29
+ raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
30
+
31
+ self.validate_constraint_value_type(operator.to_sym, value)
11
32
 
12
33
  self.context_name = context_name
13
- self.operator = operator
14
- self.values = values
34
+ self.operator = operator.to_sym
35
+ self.value = value
36
+ self.inverted = !!inverted
37
+ self.case_insensitive = !!case_insensitive
15
38
  end
16
39
 
17
40
  def matches_context?(context)
18
- Unleash.logger.debug "Unleash::Constraint matches_context? values: #{self.values} context.get_by_name(#{self.context_name})" \
41
+ Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" \
19
42
  " #{context.get_by_name(self.context_name)} "
43
+ match = matches_constraint?(context)
44
+ self.inverted ? !match : match
45
+ rescue KeyError
46
+ Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
47
+ found on the context"
48
+ false
49
+ end
50
+
51
+ def self.on_valid_date(val1, val2)
52
+ val1 = DateTime.parse(val1)
53
+ val2 = DateTime.parse(val2)
54
+ yield(val1, val2)
55
+ rescue ArgumentError
56
+ Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
57
+ or constraint_value (#{val2}) into a date. Returning false!"
58
+ false
59
+ end
60
+
61
+ def self.on_valid_float(val1, val2)
62
+ val1 = Float(val1)
63
+ val2 = Float(val2)
64
+ yield(val1, val2)
65
+ rescue ArgumentError
66
+ Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
67
+ or constraint_value (#{val2}) into a number. Returning false!"
68
+ false
69
+ end
70
+
71
+ def self.on_valid_version(val1, val2)
72
+ val1 = Gem::Version.new(val1)
73
+ val2 = Gem::Version.new(val2)
74
+ yield(val1, val2)
75
+ rescue ArgumentError
76
+ Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
77
+ or constraint_value (#{val2}) into a version. Return false!"
78
+ false
79
+ end
80
+
81
+ # This should be a private method but for some reason this fails on Ruby 2.5
82
+ def validate_constraint_value_type(operator, value)
83
+ raise ArgumentError, "context_name is not an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
84
+ raise ArgumentError, "context_name is not a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
85
+ end
86
+
87
+ private
88
+
89
+ def matches_constraint?(context)
90
+ unless OPERATORS.include?(self.operator)
91
+ Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
92
+ false
93
+ end
94
+
95
+ v = self.value.dup
96
+ context_value = context.get_by_name(self.context_name)
20
97
 
21
- is_included = self.values.include? context.get_by_name(self.context_name)
98
+ v.map!(&:upcase) if self.case_insensitive
99
+ context_value.upcase! if self.case_insensitive
22
100
 
23
- operator == 'IN' ? is_included : !is_included
101
+ OPERATORS[self.operator].call(context_value, v)
24
102
  end
25
103
  end
26
104
  end
@@ -1,6 +1,6 @@
1
1
  module Unleash
2
2
  class Context
3
- ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address].freeze
3
+ ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address, :current_time].freeze
4
4
 
5
5
  attr_accessor(*[ATTRS, :properties].flatten)
6
6
 
@@ -12,6 +12,7 @@ module Unleash
12
12
  self.user_id = value_for('userId', params)
13
13
  self.session_id = value_for('sessionId', params)
14
14
  self.remote_address = value_for('remoteAddress', params)
15
+ self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
15
16
 
16
17
  properties = value_for('properties', params)
17
18
  self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
@@ -28,7 +29,7 @@ module Unleash
28
29
  if ATTRS.include? normalized_name
29
30
  self.send(normalized_name)
30
31
  else
31
- self.properties.fetch(normalized_name)
32
+ self.properties.fetch(normalized_name, nil) || self.properties.fetch(name.to_sym)
32
33
  end
33
34
  end
34
35
 
@@ -23,8 +23,8 @@ module Unleash
23
23
  "<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
24
24
  end
25
25
 
26
- def is_enabled?(context, default_result)
27
- result = am_enabled?(context, default_result)
26
+ def is_enabled?(context)
27
+ result = am_enabled?(context)
28
28
 
29
29
  choice = result ? :yes : :no
30
30
  Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
@@ -32,25 +32,32 @@ module Unleash
32
32
  result
33
33
  end
34
34
 
35
- def get_variant(context, fallback_variant = disabled_variant)
35
+ def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant)
36
36
  raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
37
37
 
38
38
  context = ensure_valid_context(context)
39
39
 
40
- return disabled_variant unless self.enabled && am_enabled?(context, true)
41
- return disabled_variant if sum_variant_defs_weights <= 0
40
+ return Unleash::FeatureToggle.disabled_variant unless self.enabled && am_enabled?(context)
41
+ return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
42
42
 
43
- variant = variant_from_override_match(context)
44
- variant = variant_from_weights(context) if variant.nil?
43
+ variant = variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
45
44
 
46
45
  Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
47
46
  variant
48
47
  end
49
48
 
49
+ def self.disabled_variant
50
+ Unleash::Variant.new(name: 'disabled', enabled: false)
51
+ end
52
+
50
53
  private
51
54
 
55
+ def resolve_stickiness
56
+ self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
57
+ end
58
+
52
59
  # only check if it is enabled, do not do metrics
53
- def am_enabled?(context, default_result)
60
+ def am_enabled?(context)
54
61
  result =
55
62
  if self.enabled
56
63
  self.strategies.empty? ||
@@ -58,10 +65,10 @@ module Unleash
58
65
  strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
59
66
  end
60
67
  else
61
- default_result
68
+ false
62
69
  end
63
70
 
64
- Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} " \
71
+ Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \
65
72
  "and Strategies combined with contraints returned #{result})"
66
73
 
67
74
  result
@@ -77,15 +84,12 @@ module Unleash
77
84
  strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
78
85
  end
79
86
 
80
- def disabled_variant
81
- Unleash::Variant.new(name: 'disabled', enabled: false)
82
- end
83
-
84
87
  def sum_variant_defs_weights
85
88
  self.variant_definitions.map(&:weight).reduce(0, :+)
86
89
  end
87
90
 
88
- def variant_salt(context)
91
+ def variant_salt(context, stickiness = "default")
92
+ return context.get_by_name(stickiness) unless stickiness == "default"
89
93
  return context.user_id unless context.user_id.to_s.empty?
90
94
  return context.session_id unless context.session_id.to_s.empty?
91
95
  return context.remote_address unless context.remote_address.to_s.empty?
@@ -100,8 +104,8 @@ module Unleash
100
104
  Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload)
101
105
  end
102
106
 
103
- def variant_from_weights(context)
104
- variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), self.name, sum_variant_defs_weights)
107
+ def variant_from_weights(context, stickiness)
108
+ variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights)
105
109
  prev_weights = 0
106
110
 
107
111
  variant_definition = self.variant_definitions
@@ -110,7 +114,7 @@ module Unleash
110
114
  prev_weights += v.weight
111
115
  res
112
116
  end
113
- return disabled_variant if variant_definition.nil?
117
+ return self.disabled_variant if variant_definition.nil?
114
118
 
115
119
  Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
116
120
  end
@@ -135,7 +139,9 @@ module Unleash
135
139
  Constraint.new(
136
140
  c.fetch('contextName'),
137
141
  c.fetch('operator'),
138
- c.fetch('values')
142
+ c.fetch('values', nil) || c.fetch('value', nil),
143
+ inverted: c.fetch('inverted', false),
144
+ case_insensitive: c.fetch('caseInsensitive', false)
139
145
  )
140
146
  end
141
147
  )
@@ -150,6 +156,7 @@ module Unleash
150
156
  v.fetch('name', ''),
151
157
  v.fetch('weight', 0),
152
158
  v.fetch('payload', nil),
159
+ v.fetch('stickiness', nil),
153
160
  v.fetch('overrides', [])
154
161
  )
155
162
  end || []
@@ -6,6 +6,8 @@ require 'time'
6
6
 
7
7
  module Unleash
8
8
  class MetricsReporter
9
+ LONGEST_WITHOUT_A_REPORT = 600
10
+
9
11
  attr_accessor :last_time
10
12
 
11
13
  def initialize
@@ -33,16 +35,28 @@ module Unleash
33
35
  report
34
36
  end
35
37
 
36
- def send
37
- Unleash.logger.debug "send() Report"
38
+ def post
39
+ Unleash.logger.debug "post() Report"
40
+
41
+ if bucket_empty? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes...
42
+ Unleash.logger.debug "Report not posted to server, as it would have been empty. (and has been empty for up to 10 min)"
43
+
44
+ return
45
+ end
38
46
 
39
- response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_url, self.generate_report.to_json)
47
+ response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, self.generate_report.to_json)
40
48
 
41
49
  if ['200', '202'].include? response.code
42
- Unleash.logger.debug "Report sent to unleash server sucessfully. Server responded with http code #{response.code}"
50
+ Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}"
43
51
  else
44
52
  Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}."
45
53
  end
46
54
  end
55
+
56
+ private
57
+
58
+ def bucket_empty?
59
+ Unleash.toggle_metrics.features.empty?
60
+ end
47
61
  end
48
62
  end
@@ -1,19 +1,22 @@
1
1
  module Unleash
2
2
  class ScheduledExecutor
3
- attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread
3
+ attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread, :immediate_execution
4
4
 
5
- def initialize(name, interval, max_exceptions = 5)
5
+ def initialize(name, interval, max_exceptions = 5, immediate_execution = false)
6
6
  self.name = name || ''
7
7
  self.interval = interval
8
8
  self.max_exceptions = max_exceptions
9
9
  self.retry_count = 0
10
10
  self.thread = nil
11
+ self.immediate_execution = immediate_execution
11
12
  end
12
13
 
13
14
  def run(&blk)
14
15
  self.thread = Thread.new do
15
16
  Thread.current[:name] = self.name
16
17
 
18
+ run_blk{ blk.call } if self.immediate_execution
19
+
17
20
  Unleash.logger.debug "thread #{name} loop starting"
18
21
  loop do
19
22
  Unleash.logger.debug "thread #{name} sleeping for #{interval} seconds"
@@ -4,6 +4,7 @@ module Unleash
4
4
  module Strategy
5
5
  class ApplicationHostname < Base
6
6
  attr_accessor :hostname
7
+
7
8
  PARAM = 'hostnames'.freeze
8
9
 
9
10
  def initialize
@@ -38,16 +38,16 @@ module Unleash
38
38
 
39
39
  def resolve_stickiness(stickiness, context)
40
40
  case stickiness
41
- when 'userId'
42
- context.user_id
43
- when 'sessionId'
44
- context.session_id
45
41
  when 'random'
46
42
  random
47
43
  when 'default'
48
44
  context.user_id || context.session_id || random
49
45
  else
50
- nil
46
+ begin
47
+ context.get_by_name(stickiness)
48
+ rescue KeyError
49
+ nil
50
+ end
51
51
  end
52
52
  end
53
53
  end
@@ -13,7 +13,23 @@ module Unleash
13
13
  return false unless params.fetch(PARAM, nil).is_a? String
14
14
  return false unless context.class.name == 'Unleash::Context'
15
15
 
16
- params[PARAM].split(',').map(&:strip).include?(context.remote_address)
16
+ remote_address = ipaddr_or_nil_from_str(context.remote_address)
17
+
18
+ params[PARAM]
19
+ .split(',')
20
+ .map(&:strip)
21
+ .map{ |ipblock| ipaddr_or_nil_from_str(ipblock) }
22
+ .compact
23
+ .map{ |ipb| ipb.include? remote_address }
24
+ .any?
25
+ end
26
+
27
+ private
28
+
29
+ def ipaddr_or_nil_from_str(ip)
30
+ IPAddr.new(ip)
31
+ rescue StandardError
32
+ nil
17
33
  end
18
34
  end
19
35
  end