sidekiq 6.2.2 → 8.1.5
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/Changes.md +726 -11
- data/LICENSE.txt +9 -0
- data/README.md +70 -39
- data/bin/kiq +17 -0
- data/bin/lint-herb +13 -0
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiq +4 -9
- data/bin/sidekiqload +214 -115
- data/bin/sidekiqmon +4 -1
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
- data/lib/generators/sidekiq/job_generator.rb +71 -0
- data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +3 -3
- data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
- data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
- data/lib/sidekiq/api.rb +729 -264
- data/lib/sidekiq/capsule.rb +135 -0
- data/lib/sidekiq/cli.rb +124 -100
- data/lib/sidekiq/client.rb +153 -106
- data/lib/sidekiq/component.rb +132 -0
- data/lib/sidekiq/config.rb +320 -0
- data/lib/sidekiq/deploy.rb +64 -0
- data/lib/sidekiq/embedded.rb +64 -0
- data/lib/sidekiq/fetch.rb +27 -26
- data/lib/sidekiq/iterable_job.rb +56 -0
- data/lib/sidekiq/job/interrupt_handler.rb +24 -0
- data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
- data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
- data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
- data/lib/sidekiq/job/iterable.rb +322 -0
- data/lib/sidekiq/job.rb +397 -5
- data/lib/sidekiq/job_logger.rb +23 -32
- data/lib/sidekiq/job_retry.rb +141 -68
- data/lib/sidekiq/job_util.rb +113 -0
- data/lib/sidekiq/launcher.rb +122 -98
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +27 -106
- data/lib/sidekiq/manager.rb +41 -43
- data/lib/sidekiq/metrics/query.rb +184 -0
- data/lib/sidekiq/metrics/shared.rb +109 -0
- data/lib/sidekiq/metrics/tracking.rb +153 -0
- data/lib/sidekiq/middleware/chain.rb +96 -51
- data/lib/sidekiq/middleware/current_attributes.rb +120 -0
- data/lib/sidekiq/middleware/i18n.rb +8 -4
- data/lib/sidekiq/middleware/modules.rb +23 -0
- data/lib/sidekiq/monitor.rb +16 -6
- data/lib/sidekiq/paginator.rb +37 -10
- data/lib/sidekiq/processor.rb +105 -87
- data/lib/sidekiq/profiler.rb +73 -0
- data/lib/sidekiq/rails.rb +49 -36
- data/lib/sidekiq/redis_client_adapter.rb +117 -0
- data/lib/sidekiq/redis_connection.rb +55 -86
- data/lib/sidekiq/ring_buffer.rb +32 -0
- data/lib/sidekiq/scheduled.rb +106 -50
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -342
- data/lib/sidekiq/transaction_aware_client.rb +59 -0
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
- data/lib/sidekiq/tui/tabs/busy.rb +118 -0
- data/lib/sidekiq/tui/tabs/dead.rb +19 -0
- data/lib/sidekiq/tui/tabs/home.rb +144 -0
- data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
- data/lib/sidekiq/tui/tabs/queues.rb +95 -0
- data/lib/sidekiq/tui/tabs/retries.rb +19 -0
- data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
- data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
- data/lib/sidekiq/tui/tabs.rb +15 -0
- data/lib/sidekiq/tui.rb +382 -0
- data/lib/sidekiq/version.rb +6 -1
- data/lib/sidekiq/web/action.rb +149 -64
- data/lib/sidekiq/web/application.rb +376 -268
- data/lib/sidekiq/web/config.rb +117 -0
- data/lib/sidekiq/web/helpers.rb +213 -87
- data/lib/sidekiq/web/router.rb +61 -74
- data/lib/sidekiq/web.rb +71 -100
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +95 -196
- data/sidekiq.gemspec +14 -11
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +171 -57
- data/web/assets/javascripts/base-charts.js +120 -0
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +194 -0
- data/web/assets/javascripts/dashboard.js +41 -274
- data/web/assets/javascripts/metrics.js +280 -0
- data/web/assets/stylesheets/style.css +776 -0
- data/web/locales/ar.yml +72 -70
- data/web/locales/cs.yml +64 -62
- data/web/locales/da.yml +62 -53
- data/web/locales/de.yml +67 -65
- data/web/locales/el.yml +45 -24
- data/web/locales/en.yml +93 -69
- data/web/locales/es.yml +91 -68
- data/web/locales/fa.yml +67 -65
- data/web/locales/fr.yml +82 -67
- data/web/locales/gd.yml +110 -0
- data/web/locales/he.yml +67 -64
- data/web/locales/hi.yml +61 -59
- data/web/locales/it.yml +94 -54
- data/web/locales/ja.yml +74 -68
- data/web/locales/ko.yml +54 -52
- data/web/locales/lt.yml +68 -66
- data/web/locales/nb.yml +63 -61
- data/web/locales/nl.yml +54 -52
- data/web/locales/pl.yml +47 -45
- data/web/locales/{pt-br.yml → pt-BR.yml} +85 -56
- data/web/locales/pt.yml +53 -51
- data/web/locales/ru.yml +69 -66
- data/web/locales/sv.yml +55 -53
- data/web/locales/ta.yml +62 -60
- data/web/locales/tr.yml +102 -0
- data/web/locales/uk.yml +87 -61
- data/web/locales/ur.yml +66 -64
- data/web/locales/vi.yml +69 -67
- data/web/locales/zh-CN.yml +107 -0
- data/web/locales/{zh-tw.yml → zh-TW.yml} +44 -9
- data/web/views/_footer.html.erb +32 -0
- data/web/views/_job_info.html.erb +115 -0
- data/web/views/_metrics_period_select.html.erb +15 -0
- data/web/views/_nav.html.erb +45 -0
- data/web/views/_paging.html.erb +26 -0
- data/web/views/_poll_link.html.erb +4 -0
- data/web/views/_summary.html.erb +40 -0
- data/web/views/busy.html.erb +151 -0
- data/web/views/dashboard.html.erb +104 -0
- data/web/views/dead.html.erb +38 -0
- data/web/views/filtering.html.erb +6 -0
- data/web/views/layout.html.erb +26 -0
- data/web/views/metrics.html.erb +85 -0
- data/web/views/metrics_for_job.html.erb +58 -0
- data/web/views/morgue.html.erb +69 -0
- data/web/views/profiles.html.erb +43 -0
- data/web/views/queue.html.erb +57 -0
- data/web/views/queues.html.erb +46 -0
- data/web/views/retries.html.erb +77 -0
- data/web/views/retry.html.erb +39 -0
- data/web/views/scheduled.html.erb +64 -0
- data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
- metadata +130 -61
- data/LICENSE +0 -9
- data/lib/generators/sidekiq/worker_generator.rb +0 -57
- data/lib/sidekiq/delay.rb +0 -41
- data/lib/sidekiq/exception_handler.rb +0 -27
- data/lib/sidekiq/extensions/action_mailer.rb +0 -48
- data/lib/sidekiq/extensions/active_record.rb +0 -43
- data/lib/sidekiq/extensions/class_methods.rb +0 -43
- data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
- data/lib/sidekiq/util.rb +0 -95
- data/lib/sidekiq/web/csrf_protection.rb +0 -180
- data/lib/sidekiq/worker.rb +0 -244
- data/web/assets/stylesheets/application-dark.css +0 -147
- data/web/assets/stylesheets/application-rtl.css +0 -246
- data/web/assets/stylesheets/application.css +0 -1053
- data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
- data/web/assets/stylesheets/bootstrap.css +0 -5
- data/web/locales/zh-cn.yml +0 -68
- data/web/views/_footer.erb +0 -20
- data/web/views/_job_info.erb +0 -89
- data/web/views/_nav.erb +0 -52
- data/web/views/_paging.erb +0 -23
- data/web/views/_poll_link.erb +0 -7
- data/web/views/_status.erb +0 -4
- data/web/views/_summary.erb +0 -40
- data/web/views/busy.erb +0 -132
- data/web/views/dashboard.erb +0 -83
- data/web/views/dead.erb +0 -34
- data/web/views/layout.erb +0 -42
- data/web/views/morgue.erb +0 -78
- data/web/views/queue.erb +0 -55
- data/web/views/queues.erb +0 -38
- data/web/views/retries.erb +0 -83
- data/web/views/retry.erb +0 -34
- data/web/views/scheduled.erb +0 -57
data/lib/sidekiq/job_retry.rb
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "sidekiq/scheduled"
|
|
4
|
-
require "sidekiq/api"
|
|
5
|
-
|
|
6
3
|
require "zlib"
|
|
7
|
-
require "
|
|
4
|
+
require "sidekiq/component"
|
|
8
5
|
|
|
9
6
|
module Sidekiq
|
|
10
7
|
##
|
|
@@ -25,18 +22,19 @@ module Sidekiq
|
|
|
25
22
|
#
|
|
26
23
|
# A job looks like:
|
|
27
24
|
#
|
|
28
|
-
# { 'class' => '
|
|
25
|
+
# { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => true }
|
|
29
26
|
#
|
|
30
27
|
# The 'retry' option also accepts a number (in place of 'true'):
|
|
31
28
|
#
|
|
32
|
-
# { 'class' => '
|
|
29
|
+
# { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => 5 }
|
|
33
30
|
#
|
|
34
31
|
# The job will be retried this number of times before giving up. (If simply
|
|
35
32
|
# 'true', Sidekiq retries 25 times)
|
|
36
33
|
#
|
|
37
|
-
#
|
|
34
|
+
# Relevant options for job retries:
|
|
38
35
|
#
|
|
39
|
-
# * 'queue' - the queue
|
|
36
|
+
# * 'queue' - the queue for the initial job
|
|
37
|
+
# * 'retry_queue' - if job retries should be pushed to a different (e.g. lower priority) queue
|
|
40
38
|
# * 'retry_count' - number of times we've retried so far.
|
|
41
39
|
# * 'error_message' - the message from the exception
|
|
42
40
|
# * 'error_class' - the exception class
|
|
@@ -50,31 +48,39 @@ module Sidekiq
|
|
|
50
48
|
# The default number of retries is 25 which works out to about 3 weeks
|
|
51
49
|
# You can change the default maximum number of retries in your initializer:
|
|
52
50
|
#
|
|
53
|
-
# Sidekiq.
|
|
51
|
+
# Sidekiq.default_configuration[:max_retries] = 7
|
|
54
52
|
#
|
|
55
|
-
# or limit the number of retries for a particular
|
|
53
|
+
# or limit the number of retries for a particular job and send retries to
|
|
54
|
+
# a low priority queue with:
|
|
56
55
|
#
|
|
57
|
-
# class
|
|
58
|
-
# include Sidekiq::
|
|
59
|
-
# sidekiq_options :
|
|
56
|
+
# class MyJob
|
|
57
|
+
# include Sidekiq::Job
|
|
58
|
+
# sidekiq_options retry: 10, retry_queue: 'low'
|
|
60
59
|
# end
|
|
61
60
|
#
|
|
62
61
|
class JobRetry
|
|
62
|
+
# Handled means the job failed but has been dealt with
|
|
63
|
+
# (by creating a retry, rescheduling it, etc). It still
|
|
64
|
+
# needs to be logged and dispatched to error_handlers.
|
|
63
65
|
class Handled < ::RuntimeError; end
|
|
64
66
|
|
|
67
|
+
# Skip means the job failed but Sidekiq does not need to
|
|
68
|
+
# create a retry, log it or send to error_handlers.
|
|
65
69
|
class Skip < Handled; end
|
|
66
70
|
|
|
67
|
-
include Sidekiq::
|
|
71
|
+
include Sidekiq::Component
|
|
68
72
|
|
|
69
73
|
DEFAULT_MAX_RETRY_ATTEMPTS = 25
|
|
70
74
|
|
|
71
|
-
def initialize(
|
|
72
|
-
@
|
|
75
|
+
def initialize(capsule)
|
|
76
|
+
@config = @capsule = capsule
|
|
77
|
+
@max_retries = Sidekiq.default_configuration[:max_retries] || DEFAULT_MAX_RETRY_ATTEMPTS
|
|
78
|
+
@backtrace_cleaner = Sidekiq.default_configuration[:backtrace_cleaner]
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
# The global retry handler requires only the barest of data.
|
|
76
82
|
# We want to be able to retry as much as possible so we don't
|
|
77
|
-
# require the
|
|
83
|
+
# require the job to be instantiated.
|
|
78
84
|
def global(jobstr, queue)
|
|
79
85
|
yield
|
|
80
86
|
rescue Handled => ex
|
|
@@ -88,9 +94,9 @@ module Sidekiq
|
|
|
88
94
|
|
|
89
95
|
msg = Sidekiq.load_json(jobstr)
|
|
90
96
|
if msg["retry"]
|
|
91
|
-
|
|
97
|
+
process_retry(nil, msg, queue, e)
|
|
92
98
|
else
|
|
93
|
-
|
|
99
|
+
@capsule.config.death_handlers.each do |handler|
|
|
94
100
|
handler.call(msg, e)
|
|
95
101
|
rescue => handler_ex
|
|
96
102
|
handle_exception(handler_ex, {context: "Error calling death handler", job: msg})
|
|
@@ -101,14 +107,14 @@ module Sidekiq
|
|
|
101
107
|
end
|
|
102
108
|
|
|
103
109
|
# The local retry support means that any errors that occur within
|
|
104
|
-
# this block can be associated with the given
|
|
110
|
+
# this block can be associated with the given job instance.
|
|
105
111
|
# This is required to support the `sidekiq_retries_exhausted` block.
|
|
106
112
|
#
|
|
107
113
|
# Note that any exception from the block is wrapped in the Skip
|
|
108
114
|
# exception so the global block does not reprocess the error. The
|
|
109
115
|
# Skip exception is unwrapped within Sidekiq::Processor#process before
|
|
110
116
|
# calling the handle_exception handlers.
|
|
111
|
-
def local(
|
|
117
|
+
def local(jobinst, jobstr, queue)
|
|
112
118
|
yield
|
|
113
119
|
rescue Handled => ex
|
|
114
120
|
raise ex
|
|
@@ -121,88 +127,171 @@ module Sidekiq
|
|
|
121
127
|
|
|
122
128
|
msg = Sidekiq.load_json(jobstr)
|
|
123
129
|
if msg["retry"].nil?
|
|
124
|
-
msg["retry"] =
|
|
130
|
+
msg["retry"] = jobinst.class.get_sidekiq_options["retry"]
|
|
125
131
|
end
|
|
126
132
|
|
|
127
133
|
raise e unless msg["retry"]
|
|
128
|
-
|
|
134
|
+
process_retry(jobinst, msg, queue, e)
|
|
129
135
|
# We've handled this error associated with this job, don't
|
|
130
136
|
# need to handle it at the global level
|
|
131
|
-
raise
|
|
137
|
+
raise Handled
|
|
132
138
|
end
|
|
133
139
|
|
|
134
140
|
private
|
|
135
141
|
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
def now_ms
|
|
143
|
+
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Note that +jobinst+ can be nil here if an error is raised before we can
|
|
147
|
+
# instantiate the job instance. All access must be guarded and
|
|
138
148
|
# best effort.
|
|
139
|
-
def
|
|
149
|
+
def process_retry(jobinst, msg, queue, exception)
|
|
140
150
|
max_retry_attempts = retry_attempts_from(msg["retry"], @max_retries)
|
|
141
151
|
|
|
142
152
|
msg["queue"] = (msg["retry_queue"] || queue)
|
|
143
153
|
|
|
144
154
|
m = exception_message(exception)
|
|
145
155
|
if m.respond_to?(:scrub!)
|
|
146
|
-
m.force_encoding(
|
|
156
|
+
m.force_encoding(Encoding::UTF_8)
|
|
147
157
|
m.scrub!
|
|
148
158
|
end
|
|
149
159
|
|
|
150
160
|
msg["error_message"] = m
|
|
151
161
|
msg["error_class"] = exception.class.name
|
|
152
162
|
count = if msg["retry_count"]
|
|
153
|
-
msg["retried_at"] =
|
|
163
|
+
msg["retried_at"] = now_ms
|
|
154
164
|
msg["retry_count"] += 1
|
|
155
165
|
else
|
|
156
|
-
msg["failed_at"] =
|
|
166
|
+
msg["failed_at"] = now_ms
|
|
157
167
|
msg["retry_count"] = 0
|
|
158
168
|
end
|
|
159
169
|
|
|
160
170
|
if msg["backtrace"]
|
|
171
|
+
backtrace = @backtrace_cleaner.call(exception.backtrace)
|
|
161
172
|
lines = if msg["backtrace"] == true
|
|
162
|
-
|
|
173
|
+
backtrace
|
|
163
174
|
else
|
|
164
|
-
|
|
175
|
+
backtrace[0...msg["backtrace"].to_i]
|
|
165
176
|
end
|
|
166
177
|
|
|
167
178
|
msg["error_backtrace"] = compress_backtrace(lines)
|
|
168
179
|
end
|
|
169
180
|
|
|
170
|
-
if
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
# retry_for and retry are mutually exclusive - if retry_for is set,
|
|
182
|
+
# we exclusively use duration-based retry logic and ignore count-based logic
|
|
183
|
+
rf = msg["retry_for"]
|
|
184
|
+
if rf
|
|
185
|
+
return retries_exhausted(jobinst, msg, exception) if (time_for(msg["failed_at"]) + rf) < Time.now
|
|
186
|
+
elsif count >= max_retry_attempts
|
|
187
|
+
return retries_exhausted(jobinst, msg, exception)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
strategy, delay = delay_for(jobinst, count, exception, msg)
|
|
191
|
+
case strategy
|
|
192
|
+
when :discard
|
|
193
|
+
msg["discarded_at"] = now_ms
|
|
194
|
+
|
|
195
|
+
return run_death_handlers(msg, exception)
|
|
196
|
+
when :kill
|
|
197
|
+
return retries_exhausted(jobinst, msg, exception)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Logging here can break retries if the logging device raises ENOSPC #3979
|
|
201
|
+
# logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
|
|
202
|
+
jitter = rand(10 * (count + 1))
|
|
203
|
+
retry_at = Time.now.to_f + delay + jitter
|
|
204
|
+
payload = Sidekiq.dump_json(msg)
|
|
205
|
+
redis do |conn|
|
|
206
|
+
conn.zadd("retry", retry_at.to_s, payload)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def time_for(item)
|
|
211
|
+
if item.is_a?(Float)
|
|
212
|
+
Time.at(item)
|
|
179
213
|
else
|
|
180
|
-
|
|
181
|
-
retries_exhausted(worker, msg, exception)
|
|
214
|
+
Time.at(item / 1000, item % 1000)
|
|
182
215
|
end
|
|
183
216
|
end
|
|
184
217
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
218
|
+
# returns (strategy, seconds)
|
|
219
|
+
def delay_for(jobinst, count, exception, msg)
|
|
220
|
+
rv = begin
|
|
221
|
+
# sidekiq_retry_in can return two different things:
|
|
222
|
+
# 1. When to retry next, as an integer of seconds
|
|
223
|
+
# 2. A symbol which re-routes the job elsewhere, e.g. :discard, :kill, :default
|
|
224
|
+
block = jobinst&.sidekiq_retry_in_block
|
|
225
|
+
|
|
226
|
+
# the sidekiq_retry_in_block can be defined in a wrapped class (ActiveJob for instance)
|
|
227
|
+
unless msg["wrapped"].nil?
|
|
228
|
+
wrapped = Object.const_get(msg["wrapped"])
|
|
229
|
+
block = wrapped.respond_to?(:sidekiq_retry_in_block) ? wrapped.sidekiq_retry_in_block : nil
|
|
230
|
+
end
|
|
231
|
+
block&.call(count, exception, msg)
|
|
232
|
+
rescue Exception => e
|
|
233
|
+
handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{jobinst.class.name}, falling back to default"})
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
rv = rv.to_i if rv.respond_to?(:to_i)
|
|
238
|
+
delay = (count**4) + 15
|
|
239
|
+
if Integer === rv && rv > 0
|
|
240
|
+
delay = rv
|
|
241
|
+
elsif rv == :discard
|
|
242
|
+
return [:discard, nil] # do nothing, job goes poof
|
|
243
|
+
elsif rv == :kill
|
|
244
|
+
return [:kill, nil]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
[:default, delay]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def retries_exhausted(jobinst, msg, exception)
|
|
251
|
+
rv = begin
|
|
252
|
+
block = jobinst&.sidekiq_retries_exhausted_block
|
|
253
|
+
|
|
254
|
+
# the sidekiq_retries_exhausted_block can be defined in a wrapped class (ActiveJob for instance)
|
|
255
|
+
unless msg["wrapped"].nil?
|
|
256
|
+
wrapped = Object.const_get(msg["wrapped"])
|
|
257
|
+
block = wrapped.respond_to?(:sidekiq_retries_exhausted_block) ? wrapped.sidekiq_retries_exhausted_block : nil
|
|
258
|
+
end
|
|
188
259
|
block&.call(msg, exception)
|
|
189
260
|
rescue => e
|
|
190
261
|
handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
|
|
191
262
|
end
|
|
192
263
|
|
|
193
|
-
|
|
264
|
+
discarded = msg["dead"] == false || rv == :discard
|
|
265
|
+
|
|
266
|
+
if discarded
|
|
267
|
+
msg["discarded_at"] = now_ms
|
|
268
|
+
else
|
|
269
|
+
send_to_morgue(msg)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
run_death_handlers(msg, exception)
|
|
273
|
+
end
|
|
194
274
|
|
|
195
|
-
|
|
196
|
-
|
|
275
|
+
def run_death_handlers(job, exception)
|
|
276
|
+
@capsule.config.death_handlers.each do |handler|
|
|
277
|
+
handler.call(job, exception)
|
|
197
278
|
rescue => e
|
|
198
|
-
handle_exception(e, {context: "Error calling death handler", job:
|
|
279
|
+
handle_exception(e, {context: "Error calling death handler", job: job})
|
|
199
280
|
end
|
|
200
281
|
end
|
|
201
282
|
|
|
202
283
|
def send_to_morgue(msg)
|
|
203
284
|
logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
|
|
204
285
|
payload = Sidekiq.dump_json(msg)
|
|
205
|
-
|
|
286
|
+
now = Time.now.to_f
|
|
287
|
+
|
|
288
|
+
redis do |conn|
|
|
289
|
+
conn.multi do |xa|
|
|
290
|
+
xa.zadd("dead", now.to_s, payload)
|
|
291
|
+
xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
|
|
292
|
+
xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
|
|
293
|
+
end
|
|
294
|
+
end
|
|
206
295
|
end
|
|
207
296
|
|
|
208
297
|
def retry_attempts_from(msg_retry, default)
|
|
@@ -213,22 +302,6 @@ module Sidekiq
|
|
|
213
302
|
end
|
|
214
303
|
end
|
|
215
304
|
|
|
216
|
-
def delay_for(worker, count, exception)
|
|
217
|
-
jitter = rand(10) * (count + 1)
|
|
218
|
-
if worker&.sidekiq_retry_in_block
|
|
219
|
-
custom_retry_in = retry_in(worker, count, exception).to_i
|
|
220
|
-
return custom_retry_in + jitter if custom_retry_in > 0
|
|
221
|
-
end
|
|
222
|
-
(count**4) + 15 + jitter
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def retry_in(worker, count, exception)
|
|
226
|
-
worker.sidekiq_retry_in_block.call(count, exception)
|
|
227
|
-
rescue Exception => e
|
|
228
|
-
handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default"})
|
|
229
|
-
nil
|
|
230
|
-
end
|
|
231
|
-
|
|
232
305
|
def exception_caused_by_shutdown?(e, checked_causes = [])
|
|
233
306
|
return false unless e.cause
|
|
234
307
|
|
|
@@ -253,7 +326,7 @@ module Sidekiq
|
|
|
253
326
|
def compress_backtrace(backtrace)
|
|
254
327
|
serialized = Sidekiq.dump_json(backtrace)
|
|
255
328
|
compressed = Zlib::Deflate.deflate(serialized)
|
|
256
|
-
|
|
329
|
+
[compressed].pack("m0") # Base64.strict_encode64
|
|
257
330
|
end
|
|
258
331
|
end
|
|
259
332
|
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Sidekiq
|
|
7
|
+
module JobUtil
|
|
8
|
+
# These functions encapsulate various job utilities.
|
|
9
|
+
|
|
10
|
+
TRANSIENT_ATTRIBUTES = %w[]
|
|
11
|
+
|
|
12
|
+
def validate(item)
|
|
13
|
+
raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
|
|
14
|
+
raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array) || item["args"].is_a?(Enumerator::Lazy)
|
|
15
|
+
raise(ArgumentError, "Job class must be either a Class or String representation of the class name: `#{item}`") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
|
|
16
|
+
raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
|
|
17
|
+
raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
|
|
18
|
+
raise(ArgumentError, "retry_for must be a relative amount of time, e.g. 48.hours `#{item}`") if item["retry_for"] && item["retry_for"] > 1_000_000_000
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def verify_json(item)
|
|
22
|
+
job_class = item["wrapped"] || item["class"]
|
|
23
|
+
args = item["args"]
|
|
24
|
+
mode = Sidekiq::Config::DEFAULTS[:on_complex_arguments]
|
|
25
|
+
|
|
26
|
+
if mode == :raise || mode == :warn
|
|
27
|
+
if (unsafe_item = json_unsafe?(args))
|
|
28
|
+
msg = <<~EOM
|
|
29
|
+
Job arguments to #{job_class} must be native JSON types, but #{unsafe_item.inspect} is a #{unsafe_item.class}.
|
|
30
|
+
See https://github.com/sidekiq/sidekiq/wiki/Best-Practices
|
|
31
|
+
To disable this error, add `Sidekiq.strict_args!(false)` to your initializer.
|
|
32
|
+
EOM
|
|
33
|
+
|
|
34
|
+
if mode == :raise
|
|
35
|
+
raise(ArgumentError, msg)
|
|
36
|
+
else
|
|
37
|
+
warn(msg)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_item(item)
|
|
44
|
+
validate(item)
|
|
45
|
+
|
|
46
|
+
# merge in the default sidekiq_options for the item's class and/or wrapped element
|
|
47
|
+
# this allows ActiveJobs to control sidekiq_options too.
|
|
48
|
+
defaults = normalized_hash(item["class"])
|
|
49
|
+
defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?(:get_sidekiq_options)
|
|
50
|
+
item = defaults.merge(item)
|
|
51
|
+
|
|
52
|
+
raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
|
|
53
|
+
|
|
54
|
+
# remove job attributes which aren't necessary to persist into Redis
|
|
55
|
+
TRANSIENT_ATTRIBUTES.each { |key| item.delete(key) }
|
|
56
|
+
|
|
57
|
+
item["jid"] ||= SecureRandom.hex(12)
|
|
58
|
+
item["class"] = item["class"].to_s
|
|
59
|
+
item["queue"] = item["queue"].to_s
|
|
60
|
+
item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
|
|
61
|
+
item["created_at"] ||= now_in_millis
|
|
62
|
+
item
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def now_in_millis
|
|
66
|
+
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def normalized_hash(item_class)
|
|
70
|
+
if item_class.is_a?(Class)
|
|
71
|
+
raise(ArgumentError, "Message must include a Sidekiq::Job class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
|
|
72
|
+
item_class.get_sidekiq_options
|
|
73
|
+
else
|
|
74
|
+
Sidekiq.default_job_options
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
RECURSIVE_JSON_UNSAFE = {
|
|
81
|
+
Integer => ->(val) {},
|
|
82
|
+
Float => ->(val) {},
|
|
83
|
+
TrueClass => ->(val) {},
|
|
84
|
+
FalseClass => ->(val) {},
|
|
85
|
+
NilClass => ->(val) {},
|
|
86
|
+
String => ->(val) {},
|
|
87
|
+
Array => ->(val) {
|
|
88
|
+
val.each do |e|
|
|
89
|
+
unsafe_item = RECURSIVE_JSON_UNSAFE[e.class].call(e)
|
|
90
|
+
return unsafe_item unless unsafe_item.nil?
|
|
91
|
+
end
|
|
92
|
+
nil
|
|
93
|
+
},
|
|
94
|
+
Hash => ->(val) {
|
|
95
|
+
val.each do |k, v|
|
|
96
|
+
return k unless String === k
|
|
97
|
+
|
|
98
|
+
unsafe_item = RECURSIVE_JSON_UNSAFE[v.class].call(v)
|
|
99
|
+
return unsafe_item unless unsafe_item.nil?
|
|
100
|
+
end
|
|
101
|
+
nil
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
RECURSIVE_JSON_UNSAFE.default = ->(val) { val }
|
|
106
|
+
RECURSIVE_JSON_UNSAFE.compare_by_identity
|
|
107
|
+
private_constant :RECURSIVE_JSON_UNSAFE
|
|
108
|
+
|
|
109
|
+
def json_unsafe?(item)
|
|
110
|
+
RECURSIVE_JSON_UNSAFE[item.class].call(item)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|