sidekiq 2.15.1 → 4.2.10
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 +7 -0
- data/.github/contributing.md +32 -0
- data/.github/issue_template.md +9 -0
- data/.gitignore +1 -0
- data/.travis.yml +16 -17
- data/3.0-Upgrade.md +70 -0
- data/4.0-Upgrade.md +53 -0
- data/COMM-LICENSE +56 -44
- data/Changes.md +644 -1
- data/Ent-Changes.md +173 -0
- data/Gemfile +27 -0
- data/LICENSE +1 -1
- data/Pro-2.0-Upgrade.md +138 -0
- data/Pro-3.0-Upgrade.md +44 -0
- data/Pro-Changes.md +457 -3
- data/README.md +46 -29
- data/Rakefile +6 -3
- data/bin/sidekiq +4 -0
- data/bin/sidekiqctl +41 -20
- data/bin/sidekiqload +154 -0
- data/code_of_conduct.md +50 -0
- data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
- data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
- data/lib/generators/sidekiq/worker_generator.rb +49 -0
- data/lib/sidekiq.rb +141 -29
- data/lib/sidekiq/api.rb +540 -106
- data/lib/sidekiq/cli.rb +131 -71
- data/lib/sidekiq/client.rb +168 -96
- data/lib/sidekiq/core_ext.rb +36 -8
- data/lib/sidekiq/exception_handler.rb +20 -28
- data/lib/sidekiq/extensions/action_mailer.rb +25 -5
- data/lib/sidekiq/extensions/active_record.rb +8 -4
- data/lib/sidekiq/extensions/class_methods.rb +9 -5
- data/lib/sidekiq/extensions/generic_proxy.rb +1 -0
- data/lib/sidekiq/fetch.rb +45 -101
- data/lib/sidekiq/launcher.rb +144 -30
- data/lib/sidekiq/logging.rb +69 -12
- data/lib/sidekiq/manager.rb +90 -140
- data/lib/sidekiq/middleware/chain.rb +18 -5
- data/lib/sidekiq/middleware/i18n.rb +9 -2
- data/lib/sidekiq/middleware/server/active_record.rb +1 -1
- data/lib/sidekiq/middleware/server/logging.rb +11 -11
- data/lib/sidekiq/middleware/server/retry_jobs.rb +98 -44
- data/lib/sidekiq/paginator.rb +20 -8
- data/lib/sidekiq/processor.rb +157 -96
- data/lib/sidekiq/rails.rb +109 -5
- data/lib/sidekiq/redis_connection.rb +70 -24
- data/lib/sidekiq/scheduled.rb +122 -50
- data/lib/sidekiq/testing.rb +171 -31
- data/lib/sidekiq/testing/inline.rb +1 -0
- data/lib/sidekiq/util.rb +31 -5
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web.rb +136 -263
- data/lib/sidekiq/web/action.rb +93 -0
- data/lib/sidekiq/web/application.rb +336 -0
- data/lib/sidekiq/web/helpers.rb +278 -0
- data/lib/sidekiq/web/router.rb +100 -0
- data/lib/sidekiq/worker.rb +40 -7
- data/sidekiq.gemspec +18 -14
- data/web/assets/images/favicon.ico +0 -0
- data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
- data/web/assets/javascripts/application.js +67 -19
- data/web/assets/javascripts/dashboard.js +138 -29
- data/web/assets/stylesheets/application.css +267 -406
- data/web/assets/stylesheets/bootstrap.css +4 -8
- data/web/locales/cs.yml +78 -0
- data/web/locales/da.yml +9 -1
- data/web/locales/de.yml +18 -9
- data/web/locales/el.yml +68 -0
- data/web/locales/en.yml +19 -4
- data/web/locales/es.yml +10 -1
- data/web/locales/fa.yml +79 -0
- data/web/locales/fr.yml +50 -32
- data/web/locales/hi.yml +75 -0
- data/web/locales/it.yml +27 -18
- data/web/locales/ja.yml +27 -12
- data/web/locales/ko.yml +8 -3
- data/web/locales/{no.yml → nb.yml} +19 -5
- data/web/locales/nl.yml +8 -3
- data/web/locales/pl.yml +0 -1
- data/web/locales/pt-br.yml +11 -4
- data/web/locales/pt.yml +8 -1
- data/web/locales/ru.yml +39 -21
- data/web/locales/sv.yml +68 -0
- data/web/locales/ta.yml +75 -0
- data/web/locales/uk.yml +76 -0
- data/web/locales/zh-cn.yml +68 -0
- data/web/locales/zh-tw.yml +68 -0
- data/web/views/_footer.erb +17 -0
- data/web/views/_job_info.erb +72 -60
- data/web/views/_nav.erb +58 -25
- data/web/views/_paging.erb +5 -5
- data/web/views/_poll_link.erb +7 -0
- data/web/views/_summary.erb +20 -14
- data/web/views/busy.erb +94 -0
- data/web/views/dashboard.erb +34 -21
- data/web/views/dead.erb +34 -0
- data/web/views/layout.erb +8 -30
- data/web/views/morgue.erb +75 -0
- data/web/views/queue.erb +37 -30
- data/web/views/queues.erb +26 -20
- data/web/views/retries.erb +60 -47
- data/web/views/retry.erb +23 -19
- data/web/views/scheduled.erb +39 -35
- data/web/views/scheduled_job_info.erb +2 -1
- metadata +152 -195
- data/Contributing.md +0 -29
- data/config.ru +0 -18
- data/lib/sidekiq/actor.rb +0 -7
- data/lib/sidekiq/capistrano.rb +0 -54
- data/lib/sidekiq/yaml_patch.rb +0 -21
- data/test/config.yml +0 -11
- data/test/env_based_config.yml +0 -11
- data/test/fake_env.rb +0 -0
- data/test/helper.rb +0 -42
- data/test/test_api.rb +0 -341
- data/test/test_cli.rb +0 -326
- data/test/test_client.rb +0 -211
- data/test/test_exception_handler.rb +0 -124
- data/test/test_extensions.rb +0 -105
- data/test/test_fetch.rb +0 -44
- data/test/test_manager.rb +0 -83
- data/test/test_middleware.rb +0 -135
- data/test/test_processor.rb +0 -160
- data/test/test_redis_connection.rb +0 -97
- data/test/test_retry.rb +0 -306
- data/test/test_scheduled.rb +0 -86
- data/test/test_scheduling.rb +0 -47
- data/test/test_sidekiq.rb +0 -37
- data/test/test_testing.rb +0 -82
- data/test/test_testing_fake.rb +0 -265
- data/test/test_testing_inline.rb +0 -92
- data/test/test_util.rb +0 -18
- data/test/test_web.rb +0 -372
- data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
- data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
- data/web/assets/images/status/active.png +0 -0
- data/web/assets/images/status/idle.png +0 -0
- data/web/assets/javascripts/locales/README.md +0 -27
- data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
- data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
- data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.cz.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
- data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
- data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
- data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
- data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
- data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
- data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
- data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
- data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
- data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
- data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
- data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
- data/web/assets/javascripts/locales/jquery.timeago.zh-CN.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.zh-TW.js +0 -20
- data/web/views/_poll.erb +0 -14
- data/web/views/_workers.erb +0 -29
- data/web/views/index.erb +0 -16
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Sidekiq
|
2
3
|
# Middleware is code configured to run before/after
|
3
4
|
# a message is processed. It is patterned after Rack
|
@@ -49,13 +50,16 @@ module Sidekiq
|
|
49
50
|
# end
|
50
51
|
# end
|
51
52
|
#
|
52
|
-
# This is an example of a minimal client middleware
|
53
|
+
# This is an example of a minimal client middleware, note
|
54
|
+
# the method must return the result or the job will not push
|
55
|
+
# to Redis:
|
53
56
|
#
|
54
57
|
# class MyClientHook
|
55
|
-
# def call(worker_class, msg, queue)
|
58
|
+
# def call(worker_class, msg, queue, redis_pool)
|
56
59
|
# puts "Before push"
|
57
|
-
# yield
|
60
|
+
# result = yield
|
58
61
|
# puts "After push"
|
62
|
+
# result
|
59
63
|
# end
|
60
64
|
# end
|
61
65
|
#
|
@@ -64,6 +68,10 @@ module Sidekiq
|
|
64
68
|
include Enumerable
|
65
69
|
attr_reader :entries
|
66
70
|
|
71
|
+
def initialize_copy(copy)
|
72
|
+
copy.instance_variable_set(:@entries, entries.dup)
|
73
|
+
end
|
74
|
+
|
67
75
|
def each(&block)
|
68
76
|
entries.each(&block)
|
69
77
|
end
|
@@ -82,6 +90,11 @@ module Sidekiq
|
|
82
90
|
entries << Entry.new(klass, *args)
|
83
91
|
end
|
84
92
|
|
93
|
+
def prepend(klass, *args)
|
94
|
+
remove(klass) if exists?(klass)
|
95
|
+
entries.insert(0, Entry.new(klass, *args))
|
96
|
+
end
|
97
|
+
|
85
98
|
def insert_before(oldklass, newklass, *args)
|
86
99
|
i = entries.index { |entry| entry.klass == newklass }
|
87
100
|
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
@@ -108,11 +121,11 @@ module Sidekiq
|
|
108
121
|
entries.clear
|
109
122
|
end
|
110
123
|
|
111
|
-
def invoke(*args
|
124
|
+
def invoke(*args)
|
112
125
|
chain = retrieve.dup
|
113
126
|
traverse_chain = lambda do
|
114
127
|
if chain.empty?
|
115
|
-
|
128
|
+
yield
|
116
129
|
else
|
117
130
|
chain.shift.call(*args, &traverse_chain)
|
118
131
|
end
|
@@ -1,8 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# Simple middleware to save the current locale and restore it when the job executes.
|
4
|
+
# Use it by requiring it in your initializer:
|
5
|
+
#
|
6
|
+
# require 'sidekiq/middleware/i18n'
|
7
|
+
#
|
1
8
|
module Sidekiq::Middleware::I18n
|
2
9
|
# Get the current locale and store it in the message
|
3
10
|
# to be sent to Sidekiq.
|
4
11
|
class Client
|
5
|
-
def call(worker_class, msg, queue)
|
12
|
+
def call(worker_class, msg, queue, redis_pool)
|
6
13
|
msg['locale'] ||= I18n.locale
|
7
14
|
yield
|
8
15
|
end
|
@@ -14,7 +21,7 @@ module Sidekiq::Middleware::I18n
|
|
14
21
|
I18n.locale = msg['locale'] || I18n.default_locale
|
15
22
|
yield
|
16
23
|
ensure
|
17
|
-
I18n.locale =
|
24
|
+
I18n.locale = I18n.default_locale
|
18
25
|
end
|
19
26
|
end
|
20
27
|
end
|
@@ -4,21 +4,21 @@ module Sidekiq
|
|
4
4
|
class Logging
|
5
5
|
|
6
6
|
def call(worker, item, queue)
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
raise
|
16
|
-
end
|
7
|
+
begin
|
8
|
+
start = Time.now
|
9
|
+
logger.info("start".freeze)
|
10
|
+
yield
|
11
|
+
logger.info("done: #{elapsed(start)} sec")
|
12
|
+
rescue Exception
|
13
|
+
logger.info("fail: #{elapsed(start)} sec")
|
14
|
+
raise
|
17
15
|
end
|
18
16
|
end
|
19
17
|
|
18
|
+
private
|
19
|
+
|
20
20
|
def elapsed(start)
|
21
|
-
(Time.now - start).
|
21
|
+
(Time.now - start).round(3)
|
22
22
|
end
|
23
23
|
|
24
24
|
def logger
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'sidekiq/scheduled'
|
2
|
+
require 'sidekiq/api'
|
2
3
|
|
3
4
|
module Sidekiq
|
4
5
|
module Middleware
|
@@ -6,21 +7,22 @@ module Sidekiq
|
|
6
7
|
##
|
7
8
|
# Automatically retry jobs that fail in Sidekiq.
|
8
9
|
# Sidekiq's retry support assumes a typical development lifecycle:
|
9
|
-
# 0. push some code changes with a bug in it
|
10
|
-
# 1. bug causes message processing to fail, sidekiq's middleware captures
|
11
|
-
# the message and pushes it onto a retry queue
|
12
|
-
# 2. sidekiq retries messages in the retry queue multiple times with
|
13
|
-
# an exponential delay, the message continues to fail
|
14
|
-
# 3. after a few days, a developer deploys a fix. the message is
|
15
|
-
# reprocessed successfully.
|
16
|
-
# 4. if 3 never happens, sidekiq will eventually give up and throw the
|
17
|
-
# message away. If the worker defines a method called 'retries_exhausted',
|
18
|
-
# this will be called before throwing the message away. If the
|
19
|
-
# 'retries_exhausted' method throws an exception, it's dropped and logged.
|
20
10
|
#
|
21
|
-
#
|
11
|
+
# 0. Push some code changes with a bug in it.
|
12
|
+
# 1. Bug causes job processing to fail, Sidekiq's middleware captures
|
13
|
+
# the job and pushes it onto a retry queue.
|
14
|
+
# 2. Sidekiq retries jobs in the retry queue multiple times with
|
15
|
+
# an exponential delay, the job continues to fail.
|
16
|
+
# 3. After a few days, a developer deploys a fix. The job is
|
17
|
+
# reprocessed successfully.
|
18
|
+
# 4. Once retries are exhausted, Sidekiq will give up and move the
|
19
|
+
# job to the Dead Job Queue (aka morgue) where it must be dealt with
|
20
|
+
# manually in the Web UI.
|
21
|
+
# 5. After 6 months on the DJQ, Sidekiq will discard the job.
|
22
22
|
#
|
23
|
-
#
|
23
|
+
# A job looks like:
|
24
|
+
#
|
25
|
+
# { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
|
24
26
|
#
|
25
27
|
# The 'retry' option also accepts a number (in place of 'true'):
|
26
28
|
#
|
@@ -29,7 +31,7 @@ module Sidekiq
|
|
29
31
|
# The job will be retried this number of times before giving up. (If simply
|
30
32
|
# 'true', Sidekiq retries 25 times)
|
31
33
|
#
|
32
|
-
# We'll add a bit more data to the
|
34
|
+
# We'll add a bit more data to the job to support retries:
|
33
35
|
#
|
34
36
|
# * 'queue' - the queue to use
|
35
37
|
# * 'retry_count' - number of times we've retried so far.
|
@@ -37,18 +39,28 @@ module Sidekiq
|
|
37
39
|
# * 'error_class' - the exception class
|
38
40
|
# * 'failed_at' - the first time it failed
|
39
41
|
# * 'retried_at' - the last time it was retried
|
42
|
+
# * 'backtrace' - the number of lines of error backtrace to store
|
40
43
|
#
|
41
|
-
# We don't store the backtrace as that can add a lot of overhead
|
42
|
-
# to the
|
44
|
+
# We don't store the backtrace by default as that can add a lot of overhead
|
45
|
+
# to the job and everyone is using an error service, right?
|
43
46
|
#
|
44
|
-
# The default number of retry attempts is 25
|
45
|
-
#
|
47
|
+
# The default number of retry attempts is 25 which works out to about 3 weeks
|
48
|
+
# of retries. You can pass a value for the max number of retry attempts when
|
49
|
+
# adding the middleware using the options hash:
|
46
50
|
#
|
47
51
|
# Sidekiq.configure_server do |config|
|
48
52
|
# config.server_middleware do |chain|
|
49
|
-
# chain.add Middleware::Server::RetryJobs,
|
53
|
+
# chain.add Sidekiq::Middleware::Server::RetryJobs, :max_retries => 7
|
50
54
|
# end
|
51
55
|
# end
|
56
|
+
#
|
57
|
+
# or limit the number of retries for a particular worker with:
|
58
|
+
#
|
59
|
+
# class MyWorker
|
60
|
+
# include Sidekiq::Worker
|
61
|
+
# sidekiq_options :retry => 10
|
62
|
+
# end
|
63
|
+
#
|
52
64
|
class RetryJobs
|
53
65
|
include Sidekiq::Util
|
54
66
|
|
@@ -64,7 +76,16 @@ module Sidekiq
|
|
64
76
|
# ignore, will be pushed back onto queue during hard_shutdown
|
65
77
|
raise
|
66
78
|
rescue Exception => e
|
79
|
+
# ignore, will be pushed back onto queue during hard_shutdown
|
80
|
+
raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
|
81
|
+
|
67
82
|
raise e unless msg['retry']
|
83
|
+
attempt_retry(worker, msg, queue, e)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def attempt_retry(worker, msg, queue, exception)
|
68
89
|
max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
|
69
90
|
|
70
91
|
msg['queue'] = if msg['retry_queue']
|
@@ -72,26 +93,35 @@ module Sidekiq
|
|
72
93
|
else
|
73
94
|
queue
|
74
95
|
end
|
75
|
-
|
76
|
-
|
96
|
+
|
97
|
+
# App code can stuff all sorts of crazy binary data into the error message
|
98
|
+
# that won't convert to JSON.
|
99
|
+
m = exception.message.to_s[0, 10_000]
|
100
|
+
if m.respond_to?(:scrub!)
|
101
|
+
m.force_encoding("utf-8")
|
102
|
+
m.scrub!
|
103
|
+
end
|
104
|
+
|
105
|
+
msg['error_message'] = m
|
106
|
+
msg['error_class'] = exception.class.name
|
77
107
|
count = if msg['retry_count']
|
78
|
-
msg['retried_at'] = Time.now.
|
108
|
+
msg['retried_at'] = Time.now.to_f
|
79
109
|
msg['retry_count'] += 1
|
80
110
|
else
|
81
|
-
msg['failed_at'] = Time.now.
|
111
|
+
msg['failed_at'] = Time.now.to_f
|
82
112
|
msg['retry_count'] = 0
|
83
113
|
end
|
84
114
|
|
85
115
|
if msg['backtrace'] == true
|
86
|
-
msg['error_backtrace'] =
|
87
|
-
elsif msg['backtrace']
|
116
|
+
msg['error_backtrace'] = exception.backtrace
|
117
|
+
elsif !msg['backtrace']
|
88
118
|
# do nothing
|
89
119
|
elsif msg['backtrace'].to_i != 0
|
90
|
-
msg['error_backtrace'] =
|
120
|
+
msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
|
91
121
|
end
|
92
122
|
|
93
123
|
if count < max_retry_attempts
|
94
|
-
delay = delay_for(worker, count)
|
124
|
+
delay = delay_for(worker, count, exception)
|
95
125
|
logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
|
96
126
|
retry_at = Time.now.to_f + delay
|
97
127
|
payload = Sidekiq.dump_json(msg)
|
@@ -100,35 +130,47 @@ module Sidekiq
|
|
100
130
|
end
|
101
131
|
else
|
102
132
|
# Goodbye dear message, you (re)tried your best I'm sure.
|
103
|
-
retries_exhausted(worker, msg)
|
133
|
+
retries_exhausted(worker, msg, exception)
|
104
134
|
end
|
105
135
|
|
106
|
-
raise
|
136
|
+
raise exception
|
107
137
|
end
|
108
138
|
|
109
|
-
def retries_exhausted(worker, msg)
|
110
|
-
logger.debug { "
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
worker.
|
139
|
+
def retries_exhausted(worker, msg, exception)
|
140
|
+
logger.debug { "Retries exhausted for job" }
|
141
|
+
begin
|
142
|
+
block = worker.sidekiq_retries_exhausted_block || Sidekiq.default_retries_exhausted
|
143
|
+
block.call(msg, exception) if block
|
144
|
+
rescue => e
|
145
|
+
handle_exception(e, { context: "Error calling retries_exhausted for #{worker.class}", job: msg })
|
116
146
|
end
|
117
147
|
|
118
|
-
|
119
|
-
|
148
|
+
send_to_morgue(msg) unless msg['dead'] == false
|
149
|
+
end
|
150
|
+
|
151
|
+
def send_to_morgue(msg)
|
152
|
+
Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
|
153
|
+
payload = Sidekiq.dump_json(msg)
|
154
|
+
now = Time.now.to_f
|
155
|
+
Sidekiq.redis do |conn|
|
156
|
+
conn.multi do
|
157
|
+
conn.zadd('dead', now, payload)
|
158
|
+
conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
|
159
|
+
conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
|
160
|
+
end
|
161
|
+
end
|
120
162
|
end
|
121
163
|
|
122
164
|
def retry_attempts_from(msg_retry, default)
|
123
|
-
if msg_retry.is_a?(
|
165
|
+
if msg_retry.is_a?(Integer)
|
124
166
|
msg_retry
|
125
167
|
else
|
126
168
|
default
|
127
169
|
end
|
128
170
|
end
|
129
171
|
|
130
|
-
def delay_for(worker, count)
|
131
|
-
worker.sidekiq_retry_in_block? && retry_in(worker, count) || seconds_to_delay(count)
|
172
|
+
def delay_for(worker, count, exception)
|
173
|
+
worker.sidekiq_retry_in_block? && retry_in(worker, count, exception) || seconds_to_delay(count)
|
132
174
|
end
|
133
175
|
|
134
176
|
# delayed_job uses the same basic formula
|
@@ -136,15 +178,27 @@ module Sidekiq
|
|
136
178
|
(count ** 4) + 15 + (rand(30)*(count+1))
|
137
179
|
end
|
138
180
|
|
139
|
-
def retry_in(worker, count)
|
181
|
+
def retry_in(worker, count, exception)
|
140
182
|
begin
|
141
|
-
worker.sidekiq_retry_in_block.call(count)
|
183
|
+
worker.sidekiq_retry_in_block.call(count, exception).to_i
|
142
184
|
rescue Exception => e
|
143
|
-
|
185
|
+
handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
|
144
186
|
nil
|
145
187
|
end
|
146
188
|
end
|
147
189
|
|
190
|
+
def exception_caused_by_shutdown?(e, checked_causes = [])
|
191
|
+
# In Ruby 2.1.0 only, check if exception is a result of shutdown.
|
192
|
+
return false unless defined?(e.cause)
|
193
|
+
|
194
|
+
# Handle circular causes
|
195
|
+
checked_causes << e.object_id
|
196
|
+
return false if checked_causes.include?(e.cause.object_id)
|
197
|
+
|
198
|
+
e.cause.instance_of?(Sidekiq::Shutdown) ||
|
199
|
+
exception_caused_by_shutdown?(e.cause, checked_causes)
|
200
|
+
end
|
201
|
+
|
148
202
|
end
|
149
203
|
end
|
150
204
|
end
|
data/lib/sidekiq/paginator.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Sidekiq
|
2
3
|
module Paginator
|
3
|
-
|
4
|
+
|
5
|
+
def page(key, pageidx=1, page_size=25, opts=nil)
|
4
6
|
current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
|
5
7
|
pageidx = current_page - 1
|
6
8
|
total_size = 0
|
@@ -13,19 +15,29 @@ module Sidekiq
|
|
13
15
|
|
14
16
|
case type
|
15
17
|
when 'zset'
|
16
|
-
|
17
|
-
items = conn.
|
18
|
+
rev = opts && opts[:reverse]
|
19
|
+
total_size, items = conn.multi do
|
20
|
+
conn.zcard(key)
|
21
|
+
if rev
|
22
|
+
conn.zrevrange(key, starting, ending, :with_scores => true)
|
23
|
+
else
|
24
|
+
conn.zrange(key, starting, ending, :with_scores => true)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
[current_page, total_size, items]
|
18
28
|
when 'list'
|
19
|
-
total_size = conn.
|
20
|
-
|
29
|
+
total_size, items = conn.multi do
|
30
|
+
conn.llen(key)
|
31
|
+
conn.lrange(key, starting, ending)
|
32
|
+
end
|
33
|
+
[current_page, total_size, items]
|
21
34
|
when 'none'
|
22
|
-
|
35
|
+
[1, 0, []]
|
23
36
|
else
|
24
37
|
raise "can't page a #{type}"
|
25
38
|
end
|
26
39
|
end
|
27
|
-
|
28
|
-
[current_page, total_size, items]
|
29
40
|
end
|
41
|
+
|
30
42
|
end
|
31
43
|
end
|
data/lib/sidekiq/processor.rb
CHANGED
@@ -1,140 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'sidekiq/util'
|
2
|
-
require 'sidekiq/
|
3
|
-
|
4
|
-
require '
|
5
|
-
require '
|
6
|
-
require 'sidekiq/middleware/server/logging'
|
3
|
+
require 'sidekiq/fetch'
|
4
|
+
require 'thread'
|
5
|
+
require 'concurrent/map'
|
6
|
+
require 'concurrent/atomic/atomic_fixnum'
|
7
7
|
|
8
8
|
module Sidekiq
|
9
9
|
##
|
10
|
-
# The Processor
|
11
|
-
#
|
12
|
-
#
|
10
|
+
# The Processor is a standalone thread which:
|
11
|
+
#
|
12
|
+
# 1. fetches a job from Redis
|
13
|
+
# 2. executes the job
|
14
|
+
# a. instantiate the Worker
|
15
|
+
# b. run the middleware chain
|
16
|
+
# c. call #perform
|
17
|
+
#
|
18
|
+
# A Processor can exit due to shutdown (processor_stopped)
|
19
|
+
# or due to an error during job execution (processor_died)
|
20
|
+
#
|
21
|
+
# If an error occurs in the job execution, the
|
22
|
+
# Processor calls the Manager to create a new one
|
23
|
+
# to replace itself and exits.
|
24
|
+
#
|
13
25
|
class Processor
|
14
|
-
STATS_TIMEOUT = 180 * 24 * 60 * 60
|
15
26
|
|
16
27
|
include Util
|
17
|
-
include Actor
|
18
28
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
29
|
+
attr_reader :thread
|
30
|
+
attr_reader :job
|
31
|
+
|
32
|
+
def initialize(mgr)
|
33
|
+
@mgr = mgr
|
34
|
+
@down = false
|
35
|
+
@done = false
|
36
|
+
@job = nil
|
37
|
+
@thread = nil
|
38
|
+
@strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
|
39
|
+
@reloader = Sidekiq.options[:reloader]
|
40
|
+
@executor = Sidekiq.options[:executor]
|
25
41
|
end
|
26
42
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
@
|
43
|
+
def terminate(wait=false)
|
44
|
+
@done = true
|
45
|
+
return if !@thread
|
46
|
+
@thread.value if wait
|
31
47
|
end
|
32
48
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
49
|
+
def kill(wait=false)
|
50
|
+
@done = true
|
51
|
+
return if !@thread
|
52
|
+
# unlike the other actors, terminate does not wait
|
53
|
+
# for the thread to finish because we don't know how
|
54
|
+
# long the job will take to finish. Instead we
|
55
|
+
# provide a `kill` method to call after the shutdown
|
56
|
+
# timeout passes.
|
57
|
+
@thread.raise ::Sidekiq::Shutdown
|
58
|
+
@thread.value if wait
|
59
|
+
end
|
36
60
|
|
37
|
-
|
38
|
-
|
61
|
+
def start
|
62
|
+
@thread ||= safe_thread("processor", &method(:run))
|
63
|
+
end
|
39
64
|
|
40
|
-
|
41
|
-
msg = Sidekiq.load_json(msgstr)
|
42
|
-
klass = msg['class'].constantize
|
43
|
-
worker = klass.new
|
44
|
-
worker.jid = msg['jid']
|
65
|
+
private unless $TESTING
|
45
66
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
51
|
-
rescue Sidekiq::Shutdown
|
52
|
-
# Had to force kill this job because it didn't finish
|
53
|
-
# within the timeout.
|
54
|
-
rescue Exception => ex
|
55
|
-
handle_exception(ex, msg || { :message => msgstr })
|
56
|
-
raise
|
57
|
-
ensure
|
58
|
-
work.acknowledge
|
67
|
+
def run
|
68
|
+
begin
|
69
|
+
while !@done
|
70
|
+
process_one
|
59
71
|
end
|
72
|
+
@mgr.processor_stopped(self)
|
73
|
+
rescue Sidekiq::Shutdown
|
74
|
+
@mgr.processor_stopped(self)
|
75
|
+
rescue Exception => ex
|
76
|
+
@mgr.processor_died(self, ex)
|
60
77
|
end
|
61
|
-
|
62
|
-
@boss.async.processor_done(current_actor)
|
63
78
|
end
|
64
79
|
|
65
|
-
def
|
66
|
-
|
80
|
+
def process_one
|
81
|
+
@job = fetch
|
82
|
+
process(@job) if @job
|
83
|
+
@job = nil
|
67
84
|
end
|
68
85
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
86
|
+
def get_one
|
87
|
+
begin
|
88
|
+
work = @strategy.retrieve_work
|
89
|
+
(logger.info { "Redis is online, #{Time.now - @down} sec downtime" }; @down = nil) if @down
|
90
|
+
work
|
91
|
+
rescue Sidekiq::Shutdown
|
92
|
+
rescue => ex
|
93
|
+
handle_fetch_exception(ex)
|
94
|
+
end
|
95
|
+
end
|
78
96
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
97
|
+
def fetch
|
98
|
+
j = get_one
|
99
|
+
if j && @done
|
100
|
+
j.requeue
|
101
|
+
nil
|
82
102
|
else
|
83
|
-
|
103
|
+
j
|
84
104
|
end
|
85
105
|
end
|
86
106
|
|
87
|
-
def
|
88
|
-
|
107
|
+
def handle_fetch_exception(ex)
|
108
|
+
if !@down
|
109
|
+
@down = Time.now
|
110
|
+
logger.error("Error fetching job: #{ex}")
|
111
|
+
ex.backtrace.each do |bt|
|
112
|
+
logger.error(bt)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
sleep(1)
|
116
|
+
nil
|
89
117
|
end
|
90
118
|
|
91
|
-
def
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
119
|
+
def process(work)
|
120
|
+
jobstr = work.job
|
121
|
+
queue = work.queue_name
|
122
|
+
|
123
|
+
ack = false
|
124
|
+
begin
|
125
|
+
job_hash = Sidekiq.load_json(jobstr)
|
126
|
+
@reloader.call do
|
127
|
+
klass = job_hash['class'.freeze].constantize
|
128
|
+
worker = klass.new
|
129
|
+
worker.jid = job_hash['jid'.freeze]
|
130
|
+
|
131
|
+
stats(worker, job_hash, queue) do
|
132
|
+
Sidekiq::Logging.with_context(log_context(job_hash)) do
|
133
|
+
ack = true
|
134
|
+
Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
|
135
|
+
@executor.call do
|
136
|
+
# Only ack if we either attempted to start this job or
|
137
|
+
# successfully completed it. This prevents us from
|
138
|
+
# losing jobs if a middleware raises an exception before yielding
|
139
|
+
execute_job(worker, cloned(job_hash['args'.freeze]))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
ack = true
|
98
145
|
end
|
146
|
+
rescue Sidekiq::Shutdown
|
147
|
+
# Had to force kill this job because it didn't finish
|
148
|
+
# within the timeout. Don't acknowledge the work since
|
149
|
+
# we didn't properly finish it.
|
150
|
+
ack = false
|
151
|
+
rescue Exception => ex
|
152
|
+
handle_exception(ex, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
|
153
|
+
raise
|
154
|
+
ensure
|
155
|
+
work.acknowledge if ack
|
99
156
|
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# If we're using a wrapper class, like ActiveJob, use the "wrapped"
|
160
|
+
# attribute to expose the underlying thing.
|
161
|
+
def log_context(item)
|
162
|
+
klass = item['wrapped'.freeze] || item['class'.freeze]
|
163
|
+
"#{klass} JID-#{item['jid'.freeze]}#{" BID-#{item['bid'.freeze]}" if item['bid'.freeze]}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def execute_job(worker, cloned_args)
|
167
|
+
worker.perform(*cloned_args)
|
168
|
+
end
|
169
|
+
|
170
|
+
def thread_identity
|
171
|
+
@str ||= Thread.current.object_id.to_s(36)
|
172
|
+
end
|
173
|
+
|
174
|
+
WORKER_STATE = Concurrent::Map.new
|
175
|
+
PROCESSED = Concurrent::AtomicFixnum.new
|
176
|
+
FAILURE = Concurrent::AtomicFixnum.new
|
177
|
+
|
178
|
+
def stats(worker, job_hash, queue)
|
179
|
+
tid = thread_identity
|
180
|
+
WORKER_STATE[tid] = {:queue => queue, :payload => cloned(job_hash), :run_at => Time.now.to_i }
|
100
181
|
|
101
182
|
begin
|
102
183
|
yield
|
103
184
|
rescue Exception
|
104
|
-
|
105
|
-
failed = "stat:failed:#{Time.now.utc.to_date}"
|
106
|
-
result = conn.multi do
|
107
|
-
conn.incrby("stat:failed", 1)
|
108
|
-
conn.incrby(failed, 1)
|
109
|
-
end
|
110
|
-
conn.expire(failed, STATS_TIMEOUT) if result.last == 1
|
111
|
-
end
|
185
|
+
FAILURE.increment
|
112
186
|
raise
|
113
187
|
ensure
|
114
|
-
|
115
|
-
|
116
|
-
result = conn.multi do
|
117
|
-
conn.srem("workers", identity)
|
118
|
-
conn.del("worker:#{identity}")
|
119
|
-
conn.del("worker:#{identity}:started")
|
120
|
-
conn.incrby("stat:processed", 1)
|
121
|
-
conn.incrby(processed, 1)
|
122
|
-
end
|
123
|
-
conn.expire(processed, STATS_TIMEOUT) if result.last == 1
|
124
|
-
end
|
188
|
+
WORKER_STATE.delete(tid)
|
189
|
+
PROCESSED.increment
|
125
190
|
end
|
126
191
|
end
|
127
192
|
|
128
|
-
#
|
129
|
-
|
130
|
-
|
131
|
-
# Clone the arguments passed to the worker so that if
|
132
|
-
# the message fails, what is pushed back onto Redis hasn't
|
193
|
+
# Deep clone the arguments passed to the worker so that if
|
194
|
+
# the job fails, what is pushed back onto Redis hasn't
|
133
195
|
# been mutated by the worker.
|
134
196
|
def cloned(ary)
|
135
|
-
ary
|
136
|
-
SINGLETON_CLASSES.include?(val.class) ? val : val.clone
|
137
|
-
end
|
197
|
+
Marshal.load(Marshal.dump(ary))
|
138
198
|
end
|
199
|
+
|
139
200
|
end
|
140
201
|
end
|