sidekiq 5.0.1 → 5.2.9
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sidekiq might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.circleci/config.yml +61 -0
- data/.github/issue_template.md +3 -1
- data/.gitignore +2 -0
- data/.travis.yml +6 -13
- data/COMM-LICENSE +11 -9
- data/Changes.md +136 -1
- data/Ent-Changes.md +46 -3
- data/Gemfile +14 -20
- data/LICENSE +1 -1
- data/Pro-4.0-Upgrade.md +35 -0
- data/Pro-Changes.md +125 -0
- data/README.md +5 -3
- data/Rakefile +2 -5
- data/bin/sidekiqctl +13 -92
- data/bin/sidekiqload +2 -2
- data/lib/sidekiq.rb +24 -15
- data/lib/sidekiq/api.rb +83 -37
- data/lib/sidekiq/cli.rb +106 -76
- data/lib/sidekiq/client.rb +36 -33
- data/lib/sidekiq/ctl.rb +221 -0
- data/lib/sidekiq/delay.rb +23 -2
- data/lib/sidekiq/exception_handler.rb +2 -4
- data/lib/sidekiq/fetch.rb +1 -1
- data/lib/sidekiq/job_logger.rb +4 -3
- data/lib/sidekiq/job_retry.rb +51 -24
- data/lib/sidekiq/launcher.rb +18 -12
- data/lib/sidekiq/logging.rb +9 -5
- data/lib/sidekiq/manager.rb +5 -6
- data/lib/sidekiq/middleware/server/active_record.rb +2 -1
- data/lib/sidekiq/processor.rb +85 -48
- data/lib/sidekiq/rails.rb +7 -0
- data/lib/sidekiq/redis_connection.rb +40 -4
- data/lib/sidekiq/scheduled.rb +35 -8
- data/lib/sidekiq/testing.rb +4 -4
- data/lib/sidekiq/util.rb +5 -1
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web.rb +4 -4
- data/lib/sidekiq/web/action.rb +2 -2
- data/lib/sidekiq/web/application.rb +24 -2
- data/lib/sidekiq/web/helpers.rb +18 -8
- data/lib/sidekiq/web/router.rb +10 -10
- data/lib/sidekiq/worker.rb +39 -22
- data/sidekiq.gemspec +6 -17
- data/web/assets/javascripts/application.js +0 -0
- data/web/assets/javascripts/dashboard.js +15 -5
- data/web/assets/stylesheets/application.css +35 -2
- data/web/assets/stylesheets/bootstrap.css +2 -2
- data/web/locales/ar.yml +1 -0
- data/web/locales/en.yml +2 -0
- data/web/locales/es.yml +4 -3
- data/web/locales/ja.yml +5 -3
- data/web/views/_footer.erb +3 -0
- data/web/views/_nav.erb +3 -17
- data/web/views/layout.erb +1 -1
- data/web/views/queue.erb +1 -0
- data/web/views/queues.erb +2 -0
- data/web/views/retries.erb +4 -0
- metadata +20 -156
data/lib/sidekiq/ctl.rb
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'sidekiq/api'
|
5
|
+
|
6
|
+
class Sidekiq::Ctl
|
7
|
+
DEFAULT_KILL_TIMEOUT = 10
|
8
|
+
CMD = File.basename($0)
|
9
|
+
|
10
|
+
attr_reader :stage, :pidfile, :kill_timeout
|
11
|
+
|
12
|
+
def self.print_usage
|
13
|
+
puts "#{CMD} - control Sidekiq from the command line."
|
14
|
+
puts
|
15
|
+
puts "Usage: #{CMD} quiet <pidfile> <kill_timeout>"
|
16
|
+
puts " #{CMD} stop <pidfile> <kill_timeout>"
|
17
|
+
puts " #{CMD} status <section>"
|
18
|
+
puts
|
19
|
+
puts " <pidfile> is path to a pidfile"
|
20
|
+
puts " <kill_timeout> is number of seconds to wait until Sidekiq exits"
|
21
|
+
puts " (default: #{Sidekiq::Ctl::DEFAULT_KILL_TIMEOUT}), after which Sidekiq will be KILL'd"
|
22
|
+
puts
|
23
|
+
puts " <section> (optional) view a specific section of the status output"
|
24
|
+
puts " Valid sections are: #{Sidekiq::Ctl::Status::VALID_SECTIONS.join(', ')}"
|
25
|
+
puts
|
26
|
+
puts "Be sure to set the kill_timeout LONGER than Sidekiq's -t timeout. If you want"
|
27
|
+
puts "to wait 60 seconds for jobs to finish, use `sidekiq -t 60` and `sidekiqctl stop"
|
28
|
+
puts " path_to_pidfile 61`"
|
29
|
+
puts
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(stage, pidfile, timeout)
|
33
|
+
@stage = stage
|
34
|
+
@pidfile = pidfile
|
35
|
+
@kill_timeout = timeout
|
36
|
+
|
37
|
+
done('No pidfile given', :error) if !pidfile
|
38
|
+
done("Pidfile #{pidfile} does not exist", :warn) if !File.exist?(pidfile)
|
39
|
+
done('Invalid pidfile content', :error) if pid == 0
|
40
|
+
|
41
|
+
fetch_process
|
42
|
+
|
43
|
+
begin
|
44
|
+
send(stage)
|
45
|
+
rescue NoMethodError
|
46
|
+
done "Invalid command: #{stage}", :error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch_process
|
51
|
+
Process.kill(0, pid)
|
52
|
+
rescue Errno::ESRCH
|
53
|
+
done "Process doesn't exist", :error
|
54
|
+
# We were not allowed to send a signal, but the process must have existed
|
55
|
+
# when Process.kill() was called.
|
56
|
+
rescue Errno::EPERM
|
57
|
+
return pid
|
58
|
+
end
|
59
|
+
|
60
|
+
def done(msg, error = nil)
|
61
|
+
puts msg
|
62
|
+
exit(exit_signal(error))
|
63
|
+
end
|
64
|
+
|
65
|
+
def exit_signal(error)
|
66
|
+
(error == :error) ? 1 : 0
|
67
|
+
end
|
68
|
+
|
69
|
+
def pid
|
70
|
+
@pid ||= File.read(pidfile).to_i
|
71
|
+
end
|
72
|
+
|
73
|
+
def quiet
|
74
|
+
`kill -TSTP #{pid}`
|
75
|
+
end
|
76
|
+
|
77
|
+
def stop
|
78
|
+
`kill -TERM #{pid}`
|
79
|
+
kill_timeout.times do
|
80
|
+
begin
|
81
|
+
Process.kill(0, pid)
|
82
|
+
rescue Errno::ESRCH
|
83
|
+
FileUtils.rm_f pidfile
|
84
|
+
done 'Sidekiq shut down gracefully.'
|
85
|
+
rescue Errno::EPERM
|
86
|
+
done 'Not permitted to shut down Sidekiq.'
|
87
|
+
end
|
88
|
+
sleep 1
|
89
|
+
end
|
90
|
+
`kill -9 #{pid}`
|
91
|
+
FileUtils.rm_f pidfile
|
92
|
+
done 'Sidekiq shut down forcefully.'
|
93
|
+
end
|
94
|
+
alias_method :shutdown, :stop
|
95
|
+
|
96
|
+
class Status
|
97
|
+
VALID_SECTIONS = %w[all version overview processes queues]
|
98
|
+
def display(section = nil)
|
99
|
+
section ||= 'all'
|
100
|
+
unless VALID_SECTIONS.include? section
|
101
|
+
puts "I don't know how to check the status of '#{section}'!"
|
102
|
+
puts "Try one of these: #{VALID_SECTIONS.join(', ')}"
|
103
|
+
return
|
104
|
+
end
|
105
|
+
send(section)
|
106
|
+
rescue StandardError => e
|
107
|
+
puts "Couldn't get status: #{e}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def all
|
111
|
+
version
|
112
|
+
puts
|
113
|
+
overview
|
114
|
+
puts
|
115
|
+
processes
|
116
|
+
puts
|
117
|
+
queues
|
118
|
+
end
|
119
|
+
|
120
|
+
def version
|
121
|
+
puts "Sidekiq #{Sidekiq::VERSION}"
|
122
|
+
puts Time.now
|
123
|
+
end
|
124
|
+
|
125
|
+
def overview
|
126
|
+
puts '---- Overview ----'
|
127
|
+
puts " Processed: #{delimit stats.processed}"
|
128
|
+
puts " Failed: #{delimit stats.failed}"
|
129
|
+
puts " Busy: #{delimit stats.workers_size}"
|
130
|
+
puts " Enqueued: #{delimit stats.enqueued}"
|
131
|
+
puts " Retries: #{delimit stats.retry_size}"
|
132
|
+
puts " Scheduled: #{delimit stats.scheduled_size}"
|
133
|
+
puts " Dead: #{delimit stats.dead_size}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def processes
|
137
|
+
puts "---- Processes (#{process_set.size}) ----"
|
138
|
+
process_set.each_with_index do |process, index|
|
139
|
+
puts "#{process['identity']} #{tags_for(process)}"
|
140
|
+
puts " Started: #{Time.at(process['started_at'])} (#{time_ago(process['started_at'])})"
|
141
|
+
puts " Threads: #{process['concurrency']} (#{process['busy']} busy)"
|
142
|
+
puts " Queues: #{split_multiline(process['queues'].sort, pad: 11)}"
|
143
|
+
puts '' unless (index+1) == process_set.size
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
COL_PAD = 2
|
148
|
+
def queues
|
149
|
+
puts "---- Queues (#{queue_data.size}) ----"
|
150
|
+
columns = {
|
151
|
+
name: [:ljust, (['name'] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
|
152
|
+
size: [:rjust, (['size'] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
|
153
|
+
latency: [:rjust, (['latency'] + queue_data.map(&:latency)).map(&:length).max + COL_PAD]
|
154
|
+
}
|
155
|
+
columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
|
156
|
+
puts
|
157
|
+
queue_data.each do |q|
|
158
|
+
columns.each do |col, (dir, width)|
|
159
|
+
print q.send(col).public_send(dir, width)
|
160
|
+
end
|
161
|
+
puts
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def delimit(number)
|
168
|
+
number.to_s.reverse.scan(/.{1,3}/).join(',').reverse
|
169
|
+
end
|
170
|
+
|
171
|
+
def split_multiline(values, opts = {})
|
172
|
+
return 'none' unless values
|
173
|
+
pad = opts[:pad] || 0
|
174
|
+
max_length = opts[:max_length] || (80 - pad)
|
175
|
+
out = []
|
176
|
+
line = ''
|
177
|
+
values.each do |value|
|
178
|
+
if (line.length + value.length) > max_length
|
179
|
+
out << line
|
180
|
+
line = ' ' * pad
|
181
|
+
end
|
182
|
+
line << value + ', '
|
183
|
+
end
|
184
|
+
out << line[0..-3]
|
185
|
+
out.join("\n")
|
186
|
+
end
|
187
|
+
|
188
|
+
def tags_for(process)
|
189
|
+
tags = [
|
190
|
+
process['tag'],
|
191
|
+
process['labels'],
|
192
|
+
(process['quiet'] == 'true' ? 'quiet' : nil)
|
193
|
+
].flatten.compact
|
194
|
+
tags.any? ? "[#{tags.join('] [')}]" : nil
|
195
|
+
end
|
196
|
+
|
197
|
+
def time_ago(timestamp)
|
198
|
+
seconds = Time.now - Time.at(timestamp)
|
199
|
+
return 'just now' if seconds < 60
|
200
|
+
return 'a minute ago' if seconds < 120
|
201
|
+
return "#{seconds.floor / 60} minutes ago" if seconds < 3600
|
202
|
+
return 'an hour ago' if seconds < 7200
|
203
|
+
"#{seconds.floor / 60 / 60} hours ago"
|
204
|
+
end
|
205
|
+
|
206
|
+
QUEUE_STRUCT = Struct.new(:name, :size, :latency)
|
207
|
+
def queue_data
|
208
|
+
@queue_data ||= Sidekiq::Queue.all.map do |q|
|
209
|
+
QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf('%#.2f', q.latency))
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def process_set
|
214
|
+
@process_set ||= Sidekiq::ProcessSet.new
|
215
|
+
end
|
216
|
+
|
217
|
+
def stats
|
218
|
+
@stats ||= Sidekiq::Stats.new
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
data/lib/sidekiq/delay.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Sidekiq
|
2
3
|
module Extensions
|
3
4
|
|
4
5
|
def self.enable_delay!
|
5
6
|
if defined?(::ActiveSupport)
|
7
|
+
require 'sidekiq/extensions/active_record'
|
8
|
+
require 'sidekiq/extensions/action_mailer'
|
9
|
+
|
10
|
+
# Need to patch Psych so it can autoload classes whose names are serialized
|
11
|
+
# in the delayed YAML.
|
12
|
+
Psych::Visitors::ToRuby.prepend(Sidekiq::Extensions::PsychAutoload)
|
13
|
+
|
6
14
|
ActiveSupport.on_load(:active_record) do
|
7
|
-
require 'sidekiq/extensions/active_record'
|
8
15
|
include Sidekiq::Extensions::ActiveRecord
|
9
16
|
end
|
10
17
|
ActiveSupport.on_load(:action_mailer) do
|
11
|
-
require 'sidekiq/extensions/action_mailer'
|
12
18
|
extend Sidekiq::Extensions::ActionMailer
|
13
19
|
end
|
14
20
|
end
|
@@ -17,5 +23,20 @@ module Sidekiq
|
|
17
23
|
Module.__send__(:include, Sidekiq::Extensions::Klass)
|
18
24
|
end
|
19
25
|
|
26
|
+
module PsychAutoload
|
27
|
+
def resolve_class(klass_name)
|
28
|
+
return nil if !klass_name || klass_name.empty?
|
29
|
+
# constantize
|
30
|
+
names = klass_name.split('::')
|
31
|
+
names.shift if names.empty? || names.first.empty?
|
32
|
+
|
33
|
+
names.inject(Object) do |constant, name|
|
34
|
+
constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
35
|
+
end
|
36
|
+
rescue NameError
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
20
40
|
end
|
21
41
|
end
|
42
|
+
|
@@ -7,11 +7,10 @@ module Sidekiq
|
|
7
7
|
class Logger
|
8
8
|
def call(ex, ctxHash)
|
9
9
|
Sidekiq.logger.warn(Sidekiq.dump_json(ctxHash)) if !ctxHash.empty?
|
10
|
-
Sidekiq.logger.warn
|
11
|
-
Sidekiq.logger.warn
|
10
|
+
Sidekiq.logger.warn("#{ex.class.name}: #{ex.message}")
|
11
|
+
Sidekiq.logger.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
|
12
12
|
end
|
13
13
|
|
14
|
-
# Set up default handler which just logs the error
|
15
14
|
Sidekiq.error_handlers << Sidekiq::ExceptionHandler::Logger.new
|
16
15
|
end
|
17
16
|
|
@@ -26,6 +25,5 @@ module Sidekiq
|
|
26
25
|
end
|
27
26
|
end
|
28
27
|
end
|
29
|
-
|
30
28
|
end
|
31
29
|
end
|
data/lib/sidekiq/fetch.rb
CHANGED
data/lib/sidekiq/job_logger.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Sidekiq
|
2
3
|
class JobLogger
|
3
4
|
|
4
5
|
def call(item, queue)
|
5
|
-
start =
|
6
|
-
logger.info("start"
|
6
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
7
|
+
logger.info("start")
|
7
8
|
yield
|
8
9
|
logger.info("done: #{elapsed(start)} sec")
|
9
10
|
rescue Exception
|
@@ -14,7 +15,7 @@ module Sidekiq
|
|
14
15
|
private
|
15
16
|
|
16
17
|
def elapsed(start)
|
17
|
-
(
|
18
|
+
(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start).round(3)
|
18
19
|
end
|
19
20
|
|
20
21
|
def logger
|
data/lib/sidekiq/job_retry.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'sidekiq/scheduled'
|
2
3
|
require 'sidekiq/api'
|
3
4
|
|
@@ -55,7 +56,8 @@ module Sidekiq
|
|
55
56
|
# end
|
56
57
|
#
|
57
58
|
class JobRetry
|
58
|
-
class
|
59
|
+
class Handled < ::RuntimeError; end
|
60
|
+
class Skip < Handled; end
|
59
61
|
|
60
62
|
include Sidekiq::Util
|
61
63
|
|
@@ -70,7 +72,7 @@ module Sidekiq
|
|
70
72
|
# require the worker to be instantiated.
|
71
73
|
def global(msg, queue)
|
72
74
|
yield
|
73
|
-
rescue
|
75
|
+
rescue Handled => ex
|
74
76
|
raise ex
|
75
77
|
rescue Sidekiq::Shutdown => ey
|
76
78
|
# ignore, will be pushed back onto queue during hard_shutdown
|
@@ -79,9 +81,19 @@ module Sidekiq
|
|
79
81
|
# ignore, will be pushed back onto queue during hard_shutdown
|
80
82
|
raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
|
81
83
|
|
82
|
-
|
83
|
-
|
84
|
-
|
84
|
+
if msg['retry']
|
85
|
+
attempt_retry(nil, msg, queue, e)
|
86
|
+
else
|
87
|
+
Sidekiq.death_handlers.each do |handler|
|
88
|
+
begin
|
89
|
+
handler.call(msg, e)
|
90
|
+
rescue => handler_ex
|
91
|
+
handle_exception(handler_ex, { context: "Error calling death handler", job: msg })
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
raise Handled
|
85
97
|
end
|
86
98
|
|
87
99
|
|
@@ -95,7 +107,7 @@ module Sidekiq
|
|
95
107
|
# calling the handle_exception handlers.
|
96
108
|
def local(worker, msg, queue)
|
97
109
|
yield
|
98
|
-
rescue
|
110
|
+
rescue Handled => ex
|
99
111
|
raise ex
|
100
112
|
rescue Sidekiq::Shutdown => ey
|
101
113
|
# ignore, will be pushed back onto queue during hard_shutdown
|
@@ -129,9 +141,7 @@ module Sidekiq
|
|
129
141
|
queue
|
130
142
|
end
|
131
143
|
|
132
|
-
|
133
|
-
# that won't convert to JSON.
|
134
|
-
m = exception.message.to_s[0, 10_000]
|
144
|
+
m = exception_message(exception)
|
135
145
|
if m.respond_to?(:scrub!)
|
136
146
|
m.force_encoding("utf-8")
|
137
147
|
m.scrub!
|
@@ -157,7 +167,8 @@ module Sidekiq
|
|
157
167
|
|
158
168
|
if count < max_retry_attempts
|
159
169
|
delay = delay_for(worker, count, exception)
|
160
|
-
|
170
|
+
# Logging here can break retries if the logging device raises ENOSPC #3979
|
171
|
+
#logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
|
161
172
|
retry_at = Time.now.to_f + delay
|
162
173
|
payload = Sidekiq.dump_json(msg)
|
163
174
|
Sidekiq.redis do |conn|
|
@@ -170,28 +181,28 @@ module Sidekiq
|
|
170
181
|
end
|
171
182
|
|
172
183
|
def retries_exhausted(worker, msg, exception)
|
173
|
-
logger.debug { "Retries exhausted for job" }
|
174
184
|
begin
|
175
|
-
block = worker && worker.sidekiq_retries_exhausted_block
|
185
|
+
block = worker && worker.sidekiq_retries_exhausted_block
|
176
186
|
block.call(msg, exception) if block
|
177
187
|
rescue => e
|
178
|
-
handle_exception(e, { context: "Error calling retries_exhausted
|
188
|
+
handle_exception(e, { context: "Error calling retries_exhausted", job: msg })
|
189
|
+
end
|
190
|
+
|
191
|
+
Sidekiq.death_handlers.each do |handler|
|
192
|
+
begin
|
193
|
+
handler.call(msg, exception)
|
194
|
+
rescue => e
|
195
|
+
handle_exception(e, { context: "Error calling death handler", job: msg })
|
196
|
+
end
|
179
197
|
end
|
180
198
|
|
181
199
|
send_to_morgue(msg) unless msg['dead'] == false
|
182
200
|
end
|
183
201
|
|
184
202
|
def send_to_morgue(msg)
|
185
|
-
|
203
|
+
logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
|
186
204
|
payload = Sidekiq.dump_json(msg)
|
187
|
-
|
188
|
-
Sidekiq.redis do |conn|
|
189
|
-
conn.multi do
|
190
|
-
conn.zadd('dead', now, payload)
|
191
|
-
conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
|
192
|
-
conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
|
193
|
-
end
|
194
|
-
end
|
205
|
+
DeadSet.new.kill(payload, notify_failure: false)
|
195
206
|
end
|
196
207
|
|
197
208
|
def retry_attempts_from(msg_retry, default)
|
@@ -203,7 +214,11 @@ module Sidekiq
|
|
203
214
|
end
|
204
215
|
|
205
216
|
def delay_for(worker, count, exception)
|
206
|
-
worker && worker.sidekiq_retry_in_block
|
217
|
+
if worker && worker.sidekiq_retry_in_block
|
218
|
+
custom_retry_in = retry_in(worker, count, exception).to_i
|
219
|
+
return custom_retry_in if custom_retry_in > 0
|
220
|
+
end
|
221
|
+
seconds_to_delay(count)
|
207
222
|
end
|
208
223
|
|
209
224
|
# delayed_job uses the same basic formula
|
@@ -213,7 +228,7 @@ module Sidekiq
|
|
213
228
|
|
214
229
|
def retry_in(worker, count, exception)
|
215
230
|
begin
|
216
|
-
worker.sidekiq_retry_in_block.call(count, exception)
|
231
|
+
worker.sidekiq_retry_in_block.call(count, exception)
|
217
232
|
rescue Exception => e
|
218
233
|
handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
|
219
234
|
nil
|
@@ -231,5 +246,17 @@ module Sidekiq
|
|
231
246
|
exception_caused_by_shutdown?(e.cause, checked_causes)
|
232
247
|
end
|
233
248
|
|
249
|
+
# Extract message from exception.
|
250
|
+
# Set a default if the message raises an error
|
251
|
+
def exception_message(exception)
|
252
|
+
begin
|
253
|
+
# App code can stuff all sorts of crazy binary data into the error message
|
254
|
+
# that won't convert to JSON.
|
255
|
+
exception.message.to_s[0, 10_000]
|
256
|
+
rescue
|
257
|
+
"!!! ERROR MESSAGE THREW AN ERROR !!!".dup
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
234
261
|
end
|
235
262
|
end
|