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.
- checksums.yaml +4 -4
- data/.github/workflows/add-to-project.yml +14 -0
- data/.github/workflows/pull_request.yml +79 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +114 -6
- data/README.md +263 -27
- data/bin/unleash-client +15 -5
- data/examples/bootstrap.rb +51 -0
- data/examples/default-toggles.json +42 -0
- data/examples/simple.rb +5 -4
- data/lib/unleash/bootstrap/configuration.rb +25 -0
- data/lib/unleash/bootstrap/handler.rb +22 -0
- data/lib/unleash/bootstrap/provider/base.rb +14 -0
- data/lib/unleash/bootstrap/provider/from_file.rb +14 -0
- data/lib/unleash/bootstrap/provider/from_url.rb +19 -0
- data/lib/unleash/client.rb +27 -17
- data/lib/unleash/configuration.rb +41 -19
- data/lib/unleash/constraint.rb +88 -10
- data/lib/unleash/context.rb +3 -2
- data/lib/unleash/feature_toggle.rb +26 -19
- data/lib/unleash/metrics_reporter.rb +18 -4
- data/lib/unleash/scheduled_executor.rb +5 -2
- data/lib/unleash/strategy/application_hostname.rb +1 -0
- data/lib/unleash/strategy/flexible_rollout.rb +5 -5
- data/lib/unleash/strategy/remote_address.rb +17 -1
- data/lib/unleash/toggle_fetcher.rb +51 -42
- data/lib/unleash/util/http.rb +7 -6
- data/lib/unleash/variant_definition.rb +5 -4
- data/lib/unleash/version.rb +1 -1
- data/lib/unleash.rb +0 -5
- data/unleash-client.gemspec +3 -2
- metadata +35 -13
- data/.travis.yml +0 -15
@@ -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
|
data/lib/unleash/client.rb
CHANGED
@@ -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,
|
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
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
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 =
|
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 #{
|
64
|
-
return fallback_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
|
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
|
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.
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
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(
|
55
|
+
}.merge!(generate_custom_http_headers)
|
52
56
|
end
|
53
57
|
|
54
|
-
def
|
55
|
-
self.
|
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
|
59
|
-
self.
|
64
|
+
def client_metrics_uri
|
65
|
+
URI("#{self.url_stripped_of_slash}/client/metrics")
|
60
66
|
end
|
61
67
|
|
62
|
-
def
|
63
|
-
self.
|
68
|
+
def client_register_uri
|
69
|
+
URI("#{self.url_stripped_of_slash}/client/register")
|
64
70
|
end
|
65
71
|
|
66
|
-
|
72
|
+
def url_stripped_of_slash
|
73
|
+
self.url.delete_suffix '/'
|
74
|
+
end
|
67
75
|
|
68
|
-
def
|
69
|
-
|
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 =
|
82
|
-
self.metrics_interval =
|
90
|
+
self.refresh_interval = 10
|
91
|
+
self.metrics_interval = 60
|
83
92
|
self.timeout = 30
|
84
|
-
self.retry_limit =
|
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
|
data/lib/unleash/constraint.rb
CHANGED
@@ -1,26 +1,104 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
1
3
|
module Unleash
|
2
4
|
class Constraint
|
3
|
-
attr_accessor :context_name, :operator, :
|
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
|
-
|
25
|
+
LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
|
6
26
|
|
7
|
-
def initialize(context_name, operator,
|
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:" +
|
10
|
-
|
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.
|
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?
|
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
|
-
|
98
|
+
v.map!(&:upcase) if self.case_insensitive
|
99
|
+
context_value.upcase! if self.case_insensitive
|
22
100
|
|
23
|
-
operator
|
101
|
+
OPERATORS[self.operator].call(context_value, v)
|
24
102
|
end
|
25
103
|
end
|
26
104
|
end
|
data/lib/unleash/context.rb
CHANGED
@@ -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
|
27
|
-
result = am_enabled?(context
|
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
|
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
|
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
|
-
|
68
|
+
false
|
62
69
|
end
|
63
70
|
|
64
|
-
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled}
|
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
|
37
|
-
Unleash.logger.debug "
|
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.
|
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
|
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"
|
@@ -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
|
-
|
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
|
-
|
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
|