unleash 0.1.6 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|