unleash 0.1.6 → 3.2.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/.gitignore +1 -1
- data/.rubocop.yml +53 -5
- data/.travis.yml +12 -8
- data/README.md +4 -6
- data/Rakefile +1 -1
- data/bin/unleash-client +21 -17
- data/examples/simple.rb +4 -4
- data/lib/unleash.rb +7 -8
- data/lib/unleash/client.rb +49 -57
- data/lib/unleash/configuration.rb +54 -35
- data/lib/unleash/context.rb +18 -7
- data/lib/unleash/feature_toggle.rb +42 -41
- data/lib/unleash/metrics.rb +3 -4
- data/lib/unleash/metrics_reporter.rb +9 -22
- data/lib/unleash/scheduled_executor.rb +26 -13
- data/lib/unleash/strategy/application_hostname.rb +2 -2
- data/lib/unleash/strategy/base.rb +2 -2
- data/lib/unleash/strategy/default.rb +1 -1
- data/lib/unleash/strategy/gradual_rollout_random.rb +1 -1
- data/lib/unleash/strategy/gradual_rollout_sessionid.rb +2 -2
- data/lib/unleash/strategy/gradual_rollout_userid.rb +2 -2
- data/lib/unleash/strategy/remote_address.rb +1 -1
- data/lib/unleash/strategy/user_with_id.rb +1 -1
- data/lib/unleash/toggle_fetcher.rb +19 -36
- data/lib/unleash/util/http.rb +48 -0
- data/lib/unleash/variant.rb +2 -5
- data/lib/unleash/variant_definition.rb +1 -2
- data/lib/unleash/version.rb +1 -1
- data/unleash-client.gemspec +9 -9
- metadata +26 -11
- data/TODO.md +0 -37
@@ -3,7 +3,8 @@ require 'tmpdir'
|
|
3
3
|
|
4
4
|
module Unleash
|
5
5
|
class Configuration
|
6
|
-
attr_accessor
|
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
|
-
|
23
|
-
|
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
|
-
|
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?
|
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
|
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
|
data/lib/unleash/context.rb
CHANGED
@@ -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 =
|
10
|
-
self.environment =
|
11
|
-
self.user_id
|
12
|
-
self.session_id
|
13
|
-
self.remote_address =
|
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 =
|
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
|
15
|
-
self.enabled
|
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|
|
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.
|
23
|
-
.map
|
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=#{
|
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
|
-
|
42
|
+
result
|
49
43
|
end
|
50
44
|
|
51
45
|
def get_variant(context, fallback_variant = disabled_variant)
|
52
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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}
|
89
|
-
|
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
|
97
|
-
self.variant_definitions.map
|
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
|
-
|
96
|
+
|
97
|
+
SecureRandom.random_number
|
105
98
|
end
|
106
99
|
|
107
100
|
def variant_from_override_match(context)
|
108
|
-
variant = self.variant_definitions.
|
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,
|
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
|
-
.
|
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
|
data/lib/unleash/metrics.rb
CHANGED
@@ -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'] = {}
|
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
|
-
|
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
|
-
|
32
|
+
|
33
|
+
report
|
31
34
|
end
|
32
35
|
|
33
36
|
def send
|
34
37
|
Unleash.logger.debug "send() Report"
|
35
38
|
|
36
|
-
|
39
|
+
response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_url, self.generate_report.to_json)
|
37
40
|
|
38
|
-
|
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
|
-
|
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
|
33
|
-
Unleash.logger.error "thread #{name} retry_count (#{self.retry_count}) exceeded
|
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.
|
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
|