sidekiq 4.2.10 → 6.1.2
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/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- data/.github/workflows/ci.yml +41 -0
- data/.gitignore +2 -1
- data/.standard.yml +20 -0
- data/5.0-Upgrade.md +56 -0
- data/6.0-Upgrade.md +72 -0
- data/COMM-LICENSE +12 -10
- data/Changes.md +354 -1
- data/Ent-2.0-Upgrade.md +37 -0
- data/Ent-Changes.md +111 -3
- data/Gemfile +16 -21
- data/Gemfile.lock +192 -0
- data/LICENSE +1 -1
- data/Pro-4.0-Upgrade.md +35 -0
- data/Pro-5.0-Upgrade.md +25 -0
- data/Pro-Changes.md +181 -4
- data/README.md +19 -33
- data/Rakefile +6 -8
- data/bin/sidekiq +26 -2
- data/bin/sidekiqload +37 -34
- data/bin/sidekiqmon +8 -0
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
- data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
- data/lib/generators/sidekiq/worker_generator.rb +21 -13
- data/lib/sidekiq.rb +86 -61
- data/lib/sidekiq/api.rb +320 -209
- data/lib/sidekiq/cli.rb +207 -217
- data/lib/sidekiq/client.rb +78 -51
- data/lib/sidekiq/delay.rb +41 -0
- data/lib/sidekiq/exception_handler.rb +12 -16
- data/lib/sidekiq/extensions/action_mailer.rb +13 -22
- data/lib/sidekiq/extensions/active_record.rb +13 -10
- data/lib/sidekiq/extensions/class_methods.rb +14 -11
- data/lib/sidekiq/extensions/generic_proxy.rb +10 -4
- data/lib/sidekiq/fetch.rb +29 -30
- data/lib/sidekiq/job_logger.rb +63 -0
- data/lib/sidekiq/job_retry.rb +262 -0
- data/lib/sidekiq/launcher.rb +102 -69
- data/lib/sidekiq/logger.rb +165 -0
- data/lib/sidekiq/manager.rb +16 -19
- data/lib/sidekiq/middleware/chain.rb +15 -5
- data/lib/sidekiq/middleware/i18n.rb +5 -7
- data/lib/sidekiq/monitor.rb +133 -0
- data/lib/sidekiq/paginator.rb +18 -14
- data/lib/sidekiq/processor.rb +161 -82
- data/lib/sidekiq/rails.rb +27 -100
- data/lib/sidekiq/redis_connection.rb +60 -20
- data/lib/sidekiq/scheduled.rb +61 -35
- data/lib/sidekiq/sd_notify.rb +149 -0
- data/lib/sidekiq/systemd.rb +24 -0
- data/lib/sidekiq/testing.rb +48 -28
- data/lib/sidekiq/testing/inline.rb +2 -1
- data/lib/sidekiq/util.rb +20 -16
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web.rb +57 -57
- data/lib/sidekiq/web/action.rb +14 -14
- data/lib/sidekiq/web/application.rb +103 -84
- data/lib/sidekiq/web/csrf_protection.rb +158 -0
- data/lib/sidekiq/web/helpers.rb +126 -71
- data/lib/sidekiq/web/router.rb +18 -17
- data/lib/sidekiq/worker.rb +164 -41
- data/sidekiq.gemspec +15 -27
- data/web/assets/javascripts/application.js +25 -27
- data/web/assets/javascripts/dashboard.js +33 -37
- data/web/assets/stylesheets/application-dark.css +143 -0
- data/web/assets/stylesheets/application-rtl.css +246 -0
- data/web/assets/stylesheets/application.css +385 -10
- data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
- data/web/assets/stylesheets/bootstrap.css +2 -2
- data/web/locales/ar.yml +81 -0
- data/web/locales/de.yml +14 -2
- data/web/locales/en.yml +4 -0
- data/web/locales/es.yml +4 -3
- data/web/locales/fa.yml +1 -0
- data/web/locales/fr.yml +2 -2
- data/web/locales/he.yml +79 -0
- data/web/locales/ja.yml +9 -4
- data/web/locales/lt.yml +83 -0
- data/web/locales/pl.yml +4 -4
- data/web/locales/ru.yml +4 -0
- data/web/locales/ur.yml +80 -0
- data/web/locales/vi.yml +83 -0
- data/web/views/_footer.erb +5 -2
- data/web/views/_job_info.erb +2 -1
- data/web/views/_nav.erb +4 -18
- data/web/views/_paging.erb +1 -1
- data/web/views/busy.erb +15 -8
- data/web/views/dashboard.erb +1 -1
- data/web/views/dead.erb +2 -2
- data/web/views/layout.erb +12 -2
- data/web/views/morgue.erb +9 -6
- data/web/views/queue.erb +18 -8
- data/web/views/queues.erb +11 -1
- data/web/views/retries.erb +14 -7
- data/web/views/retry.erb +2 -2
- data/web/views/scheduled.erb +7 -4
- metadata +41 -188
- data/.github/issue_template.md +0 -9
- data/.travis.yml +0 -18
- data/bin/sidekiqctl +0 -99
- data/lib/sidekiq/core_ext.rb +0 -119
- data/lib/sidekiq/logging.rb +0 -106
- data/lib/sidekiq/middleware/server/active_record.rb +0 -13
- data/lib/sidekiq/middleware/server/logging.rb +0 -31
- data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Sidekiq
|
3
4
|
# Middleware is code configured to run before/after
|
4
5
|
# a message is processed. It is patterned after Rack
|
@@ -66,7 +67,6 @@ module Sidekiq
|
|
66
67
|
module Middleware
|
67
68
|
class Chain
|
68
69
|
include Enumerable
|
69
|
-
attr_reader :entries
|
70
70
|
|
71
71
|
def initialize_copy(copy)
|
72
72
|
copy.instance_variable_set(:@entries, entries.dup)
|
@@ -77,10 +77,14 @@ module Sidekiq
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def initialize
|
80
|
-
@entries =
|
80
|
+
@entries = nil
|
81
81
|
yield self if block_given?
|
82
82
|
end
|
83
83
|
|
84
|
+
def entries
|
85
|
+
@entries ||= []
|
86
|
+
end
|
87
|
+
|
84
88
|
def remove(klass)
|
85
89
|
entries.delete_if { |entry| entry.klass == klass }
|
86
90
|
end
|
@@ -106,13 +110,17 @@ module Sidekiq
|
|
106
110
|
i = entries.index { |entry| entry.klass == newklass }
|
107
111
|
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
108
112
|
i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
|
109
|
-
entries.insert(i+1, new_entry)
|
113
|
+
entries.insert(i + 1, new_entry)
|
110
114
|
end
|
111
115
|
|
112
116
|
def exists?(klass)
|
113
117
|
any? { |entry| entry.klass == klass }
|
114
118
|
end
|
115
119
|
|
120
|
+
def empty?
|
121
|
+
@entries.nil? || @entries.empty?
|
122
|
+
end
|
123
|
+
|
116
124
|
def retrieve
|
117
125
|
map(&:make_new)
|
118
126
|
end
|
@@ -122,8 +130,10 @@ module Sidekiq
|
|
122
130
|
end
|
123
131
|
|
124
132
|
def invoke(*args)
|
133
|
+
return yield if empty?
|
134
|
+
|
125
135
|
chain = retrieve.dup
|
126
|
-
traverse_chain =
|
136
|
+
traverse_chain = proc do
|
127
137
|
if chain.empty?
|
128
138
|
yield
|
129
139
|
else
|
@@ -139,7 +149,7 @@ module Sidekiq
|
|
139
149
|
|
140
150
|
def initialize(klass, *args)
|
141
151
|
@klass = klass
|
142
|
-
@args
|
152
|
+
@args = args
|
143
153
|
end
|
144
154
|
|
145
155
|
def make_new
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
#
|
3
4
|
# Simple middleware to save the current locale and restore it when the job executes.
|
4
5
|
# Use it by requiring it in your initializer:
|
@@ -9,19 +10,16 @@ module Sidekiq::Middleware::I18n
|
|
9
10
|
# Get the current locale and store it in the message
|
10
11
|
# to be sent to Sidekiq.
|
11
12
|
class Client
|
12
|
-
def call(
|
13
|
-
msg[
|
13
|
+
def call(_worker, msg, _queue, _redis)
|
14
|
+
msg["locale"] ||= I18n.locale
|
14
15
|
yield
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
18
19
|
# Pull the msg locale out and set the current thread to use it.
|
19
20
|
class Server
|
20
|
-
def call(
|
21
|
-
I18n.
|
22
|
-
yield
|
23
|
-
ensure
|
24
|
-
I18n.locale = I18n.default_locale
|
21
|
+
def call(_worker, msg, _queue, &block)
|
22
|
+
I18n.with_locale(msg.fetch("locale", I18n.default_locale), &block)
|
25
23
|
end
|
26
24
|
end
|
27
25
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "sidekiq/api"
|
5
|
+
|
6
|
+
class Sidekiq::Monitor
|
7
|
+
class Status
|
8
|
+
VALID_SECTIONS = %w[all version overview processes queues]
|
9
|
+
COL_PAD = 2
|
10
|
+
|
11
|
+
def display(section = nil)
|
12
|
+
section ||= "all"
|
13
|
+
unless VALID_SECTIONS.include? section
|
14
|
+
puts "I don't know how to check the status of '#{section}'!"
|
15
|
+
puts "Try one of these: #{VALID_SECTIONS.join(", ")}"
|
16
|
+
return
|
17
|
+
end
|
18
|
+
send(section)
|
19
|
+
rescue => e
|
20
|
+
puts "Couldn't get status: #{e}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def all
|
24
|
+
version
|
25
|
+
puts
|
26
|
+
overview
|
27
|
+
puts
|
28
|
+
processes
|
29
|
+
puts
|
30
|
+
queues
|
31
|
+
end
|
32
|
+
|
33
|
+
def version
|
34
|
+
puts "Sidekiq #{Sidekiq::VERSION}"
|
35
|
+
puts Time.now.utc
|
36
|
+
end
|
37
|
+
|
38
|
+
def overview
|
39
|
+
puts "---- Overview ----"
|
40
|
+
puts " Processed: #{delimit stats.processed}"
|
41
|
+
puts " Failed: #{delimit stats.failed}"
|
42
|
+
puts " Busy: #{delimit stats.workers_size}"
|
43
|
+
puts " Enqueued: #{delimit stats.enqueued}"
|
44
|
+
puts " Retries: #{delimit stats.retry_size}"
|
45
|
+
puts " Scheduled: #{delimit stats.scheduled_size}"
|
46
|
+
puts " Dead: #{delimit stats.dead_size}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def processes
|
50
|
+
puts "---- Processes (#{process_set.size}) ----"
|
51
|
+
process_set.each_with_index do |process, index|
|
52
|
+
puts "#{process["identity"]} #{tags_for(process)}"
|
53
|
+
puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})"
|
54
|
+
puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)"
|
55
|
+
puts " Queues: #{split_multiline(process["queues"].sort, pad: 11)}"
|
56
|
+
puts "" unless (index + 1) == process_set.size
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def queues
|
61
|
+
puts "---- Queues (#{queue_data.size}) ----"
|
62
|
+
columns = {
|
63
|
+
name: [:ljust, (["name"] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
|
64
|
+
size: [:rjust, (["size"] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
|
65
|
+
latency: [:rjust, (["latency"] + queue_data.map(&:latency)).map(&:length).max + COL_PAD]
|
66
|
+
}
|
67
|
+
columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
|
68
|
+
puts
|
69
|
+
queue_data.each do |q|
|
70
|
+
columns.each do |col, (dir, width)|
|
71
|
+
print q.send(col).public_send(dir, width)
|
72
|
+
end
|
73
|
+
puts
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def delimit(number)
|
80
|
+
number.to_s.reverse.scan(/.{1,3}/).join(",").reverse
|
81
|
+
end
|
82
|
+
|
83
|
+
def split_multiline(values, opts = {})
|
84
|
+
return "none" unless values
|
85
|
+
pad = opts[:pad] || 0
|
86
|
+
max_length = opts[:max_length] || (80 - pad)
|
87
|
+
out = []
|
88
|
+
line = ""
|
89
|
+
values.each do |value|
|
90
|
+
if (line.length + value.length) > max_length
|
91
|
+
out << line
|
92
|
+
line = " " * pad
|
93
|
+
end
|
94
|
+
line << value + ", "
|
95
|
+
end
|
96
|
+
out << line[0..-3]
|
97
|
+
out.join("\n")
|
98
|
+
end
|
99
|
+
|
100
|
+
def tags_for(process)
|
101
|
+
tags = [
|
102
|
+
process["tag"],
|
103
|
+
process["labels"],
|
104
|
+
(process["quiet"] == "true" ? "quiet" : nil)
|
105
|
+
].flatten.compact
|
106
|
+
tags.any? ? "[#{tags.join("] [")}]" : nil
|
107
|
+
end
|
108
|
+
|
109
|
+
def time_ago(timestamp)
|
110
|
+
seconds = Time.now - Time.at(timestamp)
|
111
|
+
return "just now" if seconds < 60
|
112
|
+
return "a minute ago" if seconds < 120
|
113
|
+
return "#{seconds.floor / 60} minutes ago" if seconds < 3600
|
114
|
+
return "an hour ago" if seconds < 7200
|
115
|
+
"#{seconds.floor / 60 / 60} hours ago"
|
116
|
+
end
|
117
|
+
|
118
|
+
QUEUE_STRUCT = Struct.new(:name, :size, :latency)
|
119
|
+
def queue_data
|
120
|
+
@queue_data ||= Sidekiq::Queue.all.map { |q|
|
121
|
+
QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf("%#.2f", q.latency))
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def process_set
|
126
|
+
@process_set ||= Sidekiq::ProcessSet.new
|
127
|
+
end
|
128
|
+
|
129
|
+
def stats
|
130
|
+
@stats ||= Sidekiq::Stats.new
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/sidekiq/paginator.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Sidekiq
|
3
4
|
module Paginator
|
4
|
-
|
5
|
-
def page(key, pageidx=1, page_size=25, opts=nil)
|
5
|
+
def page(key, pageidx = 1, page_size = 25, opts = nil)
|
6
6
|
current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
|
7
7
|
pageidx = current_page - 1
|
8
8
|
total_size = 0
|
@@ -12,32 +12,36 @@ module Sidekiq
|
|
12
12
|
|
13
13
|
Sidekiq.redis do |conn|
|
14
14
|
type = conn.type(key)
|
15
|
+
rev = opts && opts[:reverse]
|
15
16
|
|
16
17
|
case type
|
17
|
-
when
|
18
|
-
|
19
|
-
total_size, items = conn.multi do
|
18
|
+
when "zset"
|
19
|
+
total_size, items = conn.multi {
|
20
20
|
conn.zcard(key)
|
21
21
|
if rev
|
22
|
-
conn.zrevrange(key, starting, ending, :
|
22
|
+
conn.zrevrange(key, starting, ending, with_scores: true)
|
23
23
|
else
|
24
|
-
conn.zrange(key, starting, ending, :
|
24
|
+
conn.zrange(key, starting, ending, with_scores: true)
|
25
25
|
end
|
26
|
-
|
26
|
+
}
|
27
27
|
[current_page, total_size, items]
|
28
|
-
when
|
29
|
-
total_size, items = conn.multi
|
28
|
+
when "list"
|
29
|
+
total_size, items = conn.multi {
|
30
30
|
conn.llen(key)
|
31
|
-
|
32
|
-
|
31
|
+
if rev
|
32
|
+
conn.lrange(key, -ending - 1, -starting - 1)
|
33
|
+
else
|
34
|
+
conn.lrange(key, starting, ending)
|
35
|
+
end
|
36
|
+
}
|
37
|
+
items.reverse! if rev
|
33
38
|
[current_page, total_size, items]
|
34
|
-
when
|
39
|
+
when "none"
|
35
40
|
[1, 0, []]
|
36
41
|
else
|
37
42
|
raise "can't page a #{type}"
|
38
43
|
end
|
39
44
|
end
|
40
45
|
end
|
41
|
-
|
42
46
|
end
|
43
47
|
end
|
data/lib/sidekiq/processor.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
2
|
+
|
3
|
+
require "sidekiq/util"
|
4
|
+
require "sidekiq/fetch"
|
5
|
+
require "sidekiq/job_logger"
|
6
|
+
require "sidekiq/job_retry"
|
7
7
|
|
8
8
|
module Sidekiq
|
9
9
|
##
|
@@ -23,32 +23,32 @@ module Sidekiq
|
|
23
23
|
# to replace itself and exits.
|
24
24
|
#
|
25
25
|
class Processor
|
26
|
-
|
27
26
|
include Util
|
28
27
|
|
29
28
|
attr_reader :thread
|
30
29
|
attr_reader :job
|
31
30
|
|
32
|
-
def initialize(mgr)
|
31
|
+
def initialize(mgr, options)
|
33
32
|
@mgr = mgr
|
34
33
|
@down = false
|
35
34
|
@done = false
|
36
35
|
@job = nil
|
37
36
|
@thread = nil
|
38
|
-
@strategy =
|
39
|
-
@reloader =
|
40
|
-
@
|
37
|
+
@strategy = options[:fetch]
|
38
|
+
@reloader = options[:reloader] || proc { |&block| block.call }
|
39
|
+
@job_logger = (options[:job_logger] || Sidekiq::JobLogger).new
|
40
|
+
@retrier = Sidekiq::JobRetry.new
|
41
41
|
end
|
42
42
|
|
43
|
-
def terminate(wait=false)
|
43
|
+
def terminate(wait = false)
|
44
44
|
@done = true
|
45
|
-
return
|
45
|
+
return unless @thread
|
46
46
|
@thread.value if wait
|
47
47
|
end
|
48
48
|
|
49
|
-
def kill(wait=false)
|
49
|
+
def kill(wait = false)
|
50
50
|
@done = true
|
51
|
-
return
|
51
|
+
return unless @thread
|
52
52
|
# unlike the other actors, terminate does not wait
|
53
53
|
# for the thread to finish because we don't know how
|
54
54
|
# long the job will take to finish. Instead we
|
@@ -65,16 +65,12 @@ module Sidekiq
|
|
65
65
|
private unless $TESTING
|
66
66
|
|
67
67
|
def run
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
@mgr.processor_stopped(self)
|
75
|
-
rescue Exception => ex
|
76
|
-
@mgr.processor_died(self, ex)
|
77
|
-
end
|
68
|
+
process_one until @done
|
69
|
+
@mgr.processor_stopped(self)
|
70
|
+
rescue Sidekiq::Shutdown
|
71
|
+
@mgr.processor_stopped(self)
|
72
|
+
rescue Exception => ex
|
73
|
+
@mgr.processor_died(self, ex)
|
78
74
|
end
|
79
75
|
|
80
76
|
def process_one
|
@@ -84,14 +80,15 @@ module Sidekiq
|
|
84
80
|
end
|
85
81
|
|
86
82
|
def get_one
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
rescue Sidekiq::Shutdown
|
92
|
-
rescue => ex
|
93
|
-
handle_fetch_exception(ex)
|
83
|
+
work = @strategy.retrieve_work
|
84
|
+
if @down
|
85
|
+
logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" }
|
86
|
+
@down = nil
|
94
87
|
end
|
88
|
+
work
|
89
|
+
rescue Sidekiq::Shutdown
|
90
|
+
rescue => ex
|
91
|
+
handle_fetch_exception(ex)
|
95
92
|
end
|
96
93
|
|
97
94
|
def fetch
|
@@ -105,97 +102,179 @@ module Sidekiq
|
|
105
102
|
end
|
106
103
|
|
107
104
|
def handle_fetch_exception(ex)
|
108
|
-
|
109
|
-
@down =
|
105
|
+
unless @down
|
106
|
+
@down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
110
107
|
logger.error("Error fetching job: #{ex}")
|
111
|
-
ex
|
112
|
-
logger.error(bt)
|
113
|
-
end
|
108
|
+
handle_exception(ex)
|
114
109
|
end
|
115
110
|
sleep(1)
|
116
111
|
nil
|
117
112
|
end
|
118
113
|
|
114
|
+
def dispatch(job_hash, queue, jobstr)
|
115
|
+
# since middleware can mutate the job hash
|
116
|
+
# we need to clone it to report the original
|
117
|
+
# job structure to the Web UI
|
118
|
+
# or to push back to redis when retrying.
|
119
|
+
# To avoid costly and, most of the time, useless cloning here,
|
120
|
+
# we pass original String of JSON to respected methods
|
121
|
+
# to re-parse it there if we need access to the original, untouched job
|
122
|
+
|
123
|
+
@job_logger.prepare(job_hash) do
|
124
|
+
@retrier.global(jobstr, queue) do
|
125
|
+
@job_logger.call(job_hash, queue) do
|
126
|
+
stats(jobstr, queue) do
|
127
|
+
# Rails 5 requires a Reloader to wrap code execution. In order to
|
128
|
+
# constantize the worker and instantiate an instance, we have to call
|
129
|
+
# the Reloader. It handles code loading, db connection management, etc.
|
130
|
+
# Effectively this block denotes a "unit of work" to Rails.
|
131
|
+
@reloader.call do
|
132
|
+
klass = constantize(job_hash["class"])
|
133
|
+
worker = klass.new
|
134
|
+
worker.jid = job_hash["jid"]
|
135
|
+
@retrier.local(worker, jobstr, queue) do
|
136
|
+
yield worker
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
119
145
|
def process(work)
|
120
146
|
jobstr = work.job
|
121
147
|
queue = work.queue_name
|
122
148
|
|
123
|
-
|
149
|
+
# Treat malformed JSON as a special case: job goes straight to the morgue.
|
150
|
+
job_hash = nil
|
124
151
|
begin
|
125
152
|
job_hash = Sidekiq.load_json(jobstr)
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
153
|
+
rescue => ex
|
154
|
+
handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
|
155
|
+
# we can't notify because the job isn't a valid hash payload.
|
156
|
+
DeadSet.new.kill(jobstr, notify_failure: false)
|
157
|
+
return work.acknowledge
|
158
|
+
end
|
159
|
+
|
160
|
+
ack = false
|
161
|
+
begin
|
162
|
+
dispatch(job_hash, queue, jobstr) do |worker|
|
163
|
+
Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
|
164
|
+
execute_job(worker, job_hash["args"])
|
143
165
|
end
|
144
|
-
ack = true
|
145
166
|
end
|
167
|
+
ack = true
|
146
168
|
rescue Sidekiq::Shutdown
|
147
169
|
# Had to force kill this job because it didn't finish
|
148
170
|
# within the timeout. Don't acknowledge the work since
|
149
171
|
# we didn't properly finish it.
|
150
|
-
|
172
|
+
rescue Sidekiq::JobRetry::Handled => h
|
173
|
+
# this is the common case: job raised error and Sidekiq::JobRetry::Handled
|
174
|
+
# signals that we created a retry successfully. We can acknowlege the job.
|
175
|
+
ack = true
|
176
|
+
e = h.cause || h
|
177
|
+
handle_exception(e, {context: "Job raised exception", job: job_hash, jobstr: jobstr})
|
178
|
+
raise e
|
151
179
|
rescue Exception => ex
|
152
|
-
|
153
|
-
|
180
|
+
# Unexpected error! This is very bad and indicates an exception that got past
|
181
|
+
# the retry subsystem (e.g. network partition). We won't acknowledge the job
|
182
|
+
# so it can be rescued when using Sidekiq Pro.
|
183
|
+
handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
|
184
|
+
raise ex
|
154
185
|
ensure
|
155
|
-
|
186
|
+
if ack
|
187
|
+
# We don't want a shutdown signal to interrupt job acknowledgment.
|
188
|
+
Thread.handle_interrupt(Sidekiq::Shutdown => :never) do
|
189
|
+
work.acknowledge
|
190
|
+
end
|
191
|
+
end
|
156
192
|
end
|
157
193
|
end
|
158
194
|
|
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
195
|
def execute_job(worker, cloned_args)
|
167
196
|
worker.perform(*cloned_args)
|
168
197
|
end
|
169
198
|
|
170
|
-
|
171
|
-
|
199
|
+
# Ruby doesn't provide atomic counters out of the box so we'll
|
200
|
+
# implement something simple ourselves.
|
201
|
+
# https://bugs.ruby-lang.org/issues/14706
|
202
|
+
class Counter
|
203
|
+
def initialize
|
204
|
+
@value = 0
|
205
|
+
@lock = Mutex.new
|
206
|
+
end
|
207
|
+
|
208
|
+
def incr(amount = 1)
|
209
|
+
@lock.synchronize { @value += amount }
|
210
|
+
end
|
211
|
+
|
212
|
+
def reset
|
213
|
+
@lock.synchronize {
|
214
|
+
val = @value
|
215
|
+
@value = 0
|
216
|
+
val
|
217
|
+
}
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here
|
222
|
+
class SharedWorkerState
|
223
|
+
def initialize
|
224
|
+
@worker_state = {}
|
225
|
+
@lock = Mutex.new
|
226
|
+
end
|
227
|
+
|
228
|
+
def set(tid, hash)
|
229
|
+
@lock.synchronize { @worker_state[tid] = hash }
|
230
|
+
end
|
231
|
+
|
232
|
+
def delete(tid)
|
233
|
+
@lock.synchronize { @worker_state.delete(tid) }
|
234
|
+
end
|
235
|
+
|
236
|
+
def dup
|
237
|
+
@lock.synchronize { @worker_state.dup }
|
238
|
+
end
|
239
|
+
|
240
|
+
def size
|
241
|
+
@lock.synchronize { @worker_state.size }
|
242
|
+
end
|
243
|
+
|
244
|
+
def clear
|
245
|
+
@lock.synchronize { @worker_state.clear }
|
246
|
+
end
|
172
247
|
end
|
173
248
|
|
174
|
-
|
175
|
-
|
176
|
-
|
249
|
+
PROCESSED = Counter.new
|
250
|
+
FAILURE = Counter.new
|
251
|
+
WORKER_STATE = SharedWorkerState.new
|
177
252
|
|
178
|
-
def stats(
|
179
|
-
tid
|
180
|
-
WORKER_STATE[tid] = {:queue => queue, :payload => cloned(job_hash), :run_at => Time.now.to_i }
|
253
|
+
def stats(jobstr, queue)
|
254
|
+
WORKER_STATE.set(tid, {queue: queue, payload: jobstr, run_at: Time.now.to_i})
|
181
255
|
|
182
256
|
begin
|
183
257
|
yield
|
184
258
|
rescue Exception
|
185
|
-
FAILURE.
|
259
|
+
FAILURE.incr
|
186
260
|
raise
|
187
261
|
ensure
|
188
262
|
WORKER_STATE.delete(tid)
|
189
|
-
PROCESSED.
|
263
|
+
PROCESSED.incr
|
190
264
|
end
|
191
265
|
end
|
192
266
|
|
193
|
-
|
194
|
-
|
195
|
-
# been mutated by the worker.
|
196
|
-
def cloned(ary)
|
197
|
-
Marshal.load(Marshal.dump(ary))
|
198
|
-
end
|
267
|
+
def constantize(str)
|
268
|
+
return Object.const_get(str) unless str.include?("::")
|
199
269
|
|
270
|
+
names = str.split("::")
|
271
|
+
names.shift if names.empty? || names.first.empty?
|
272
|
+
|
273
|
+
names.inject(Object) do |constant, name|
|
274
|
+
# the false flag limits search for name to under the constant namespace
|
275
|
+
# which mimics Rails' behaviour
|
276
|
+
constant.const_get(name, false)
|
277
|
+
end
|
278
|
+
end
|
200
279
|
end
|
201
280
|
end
|