sidekiq 4.0.0 → 5.0.0
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 +4 -4
- data/{Contributing.md → .github/contributing.md} +0 -0
- data/.github/issue_template.md +9 -0
- data/.gitignore +1 -0
- data/.travis.yml +12 -10
- data/4.0-Upgrade.md +4 -1
- data/5.0-Upgrade.md +56 -0
- data/COMM-LICENSE +1 -1
- data/Changes.md +236 -7
- data/Ent-Changes.md +111 -3
- data/Gemfile +7 -6
- data/Pro-3.0-Upgrade.md +5 -7
- data/Pro-Changes.md +162 -5
- data/README.md +31 -21
- data/Rakefile +5 -2
- data/bin/sidekiq +0 -1
- data/bin/sidekiqctl +1 -1
- data/bin/sidekiqload +15 -33
- data/code_of_conduct.md +50 -0
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +2 -2
- data/lib/generators/sidekiq/templates/worker_test.rb.erb +6 -6
- data/lib/sidekiq.rb +46 -21
- data/lib/sidekiq/api.rb +94 -30
- data/lib/sidekiq/cli.rb +38 -16
- data/lib/sidekiq/client.rb +32 -26
- data/lib/sidekiq/core_ext.rb +14 -0
- data/lib/sidekiq/delay.rb +21 -0
- data/lib/sidekiq/exception_handler.rb +2 -1
- data/lib/sidekiq/extensions/action_mailer.rb +1 -0
- data/lib/sidekiq/extensions/active_record.rb +1 -0
- data/lib/sidekiq/extensions/class_methods.rb +1 -0
- data/lib/sidekiq/extensions/generic_proxy.rb +8 -1
- data/lib/sidekiq/fetch.rb +2 -1
- data/lib/sidekiq/job_logger.rb +27 -0
- data/lib/sidekiq/job_retry.rb +235 -0
- data/lib/sidekiq/launcher.rb +42 -33
- data/lib/sidekiq/logging.rb +3 -1
- data/lib/sidekiq/manager.rb +9 -4
- data/lib/sidekiq/middleware/chain.rb +1 -0
- data/lib/sidekiq/middleware/i18n.rb +1 -0
- data/lib/sidekiq/middleware/server/active_record.rb +9 -0
- data/lib/sidekiq/paginator.rb +1 -0
- data/lib/sidekiq/processor.rb +73 -19
- data/lib/sidekiq/rails.rb +47 -25
- data/lib/sidekiq/redis_connection.rb +14 -3
- data/lib/sidekiq/scheduled.rb +12 -1
- data/lib/sidekiq/testing.rb +65 -13
- data/lib/sidekiq/testing/inline.rb +1 -0
- data/lib/sidekiq/util.rb +3 -15
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web.rb +120 -184
- data/lib/sidekiq/web/action.rb +89 -0
- data/lib/sidekiq/web/application.rb +331 -0
- data/lib/sidekiq/{web_helpers.rb → web/helpers.rb} +57 -27
- data/lib/sidekiq/web/router.rb +100 -0
- data/lib/sidekiq/worker.rb +52 -11
- data/sidekiq.gemspec +12 -7
- data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
- data/web/assets/javascripts/application.js +24 -20
- data/web/assets/javascripts/dashboard.js +11 -13
- data/web/assets/stylesheets/application-rtl.css +246 -0
- data/web/assets/stylesheets/application.css +362 -5
- data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
- data/web/assets/stylesheets/bootstrap.css +4 -8
- data/web/locales/ar.yml +80 -0
- data/web/locales/cs.yml +11 -1
- data/web/locales/de.yml +1 -1
- data/web/locales/en.yml +2 -0
- data/web/locales/fa.yml +80 -0
- data/web/locales/fr.yml +21 -12
- data/web/locales/he.yml +79 -0
- data/web/locales/ja.yml +19 -10
- data/web/locales/ru.yml +3 -0
- data/web/locales/ur.yml +80 -0
- data/web/views/_footer.erb +2 -2
- data/web/views/_job_info.erb +5 -1
- data/web/views/_nav.erb +2 -2
- data/web/views/_paging.erb +1 -1
- data/web/views/busy.erb +14 -9
- data/web/views/dashboard.erb +5 -5
- data/web/views/dead.erb +1 -1
- data/web/views/layout.erb +13 -5
- data/web/views/morgue.erb +16 -12
- data/web/views/queue.erb +11 -11
- data/web/views/queues.erb +3 -3
- data/web/views/retries.erb +15 -13
- data/web/views/retry.erb +2 -2
- data/web/views/scheduled.erb +4 -4
- data/web/views/scheduled_job_info.erb +1 -1
- metadata +97 -148
- data/lib/sidekiq/middleware/server/logging.rb +0 -40
- data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -206
- data/test/config.yml +0 -9
- data/test/env_based_config.yml +0 -11
- data/test/fake_env.rb +0 -0
- data/test/fixtures/en.yml +0 -2
- data/test/helper.rb +0 -74
- data/test/test_actors.rb +0 -137
- data/test/test_api.rb +0 -494
- data/test/test_cli.rb +0 -335
- data/test/test_client.rb +0 -194
- data/test/test_exception_handler.rb +0 -55
- data/test/test_extensions.rb +0 -126
- data/test/test_fetch.rb +0 -49
- data/test/test_launcher.rb +0 -80
- data/test/test_logging.rb +0 -34
- data/test/test_manager.rb +0 -49
- data/test/test_middleware.rb +0 -157
- data/test/test_processor.rb +0 -200
- data/test/test_rails.rb +0 -21
- data/test/test_redis_connection.rb +0 -126
- data/test/test_retry.rb +0 -325
- data/test/test_scheduled.rb +0 -114
- data/test/test_scheduling.rb +0 -49
- data/test/test_sidekiq.rb +0 -99
- data/test/test_testing.rb +0 -142
- data/test/test_testing_fake.rb +0 -331
- data/test/test_testing_inline.rb +0 -93
- data/test/test_util.rb +0 -16
- data/test/test_web.rb +0 -608
- data/test/test_web_helpers.rb +0 -53
- 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.cs.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
- 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_js.erb +0 -5
data/lib/sidekiq/cli.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
$stdout.sync = true
|
3
4
|
|
4
5
|
require 'yaml'
|
@@ -42,6 +43,10 @@ module Sidekiq
|
|
42
43
|
write_pid
|
43
44
|
end
|
44
45
|
|
46
|
+
def jruby?
|
47
|
+
defined?(::JRUBY_VERSION)
|
48
|
+
end
|
49
|
+
|
45
50
|
# Code within this method is not tested because it alters
|
46
51
|
# global process state irreversibly. PRs which improve the
|
47
52
|
# test coverage of Sidekiq::CLI are welcomed.
|
@@ -50,8 +55,14 @@ module Sidekiq
|
|
50
55
|
print_banner
|
51
56
|
|
52
57
|
self_read, self_write = IO.pipe
|
58
|
+
sigs = %w(INT TERM TTIN TSTP)
|
59
|
+
# USR1 and USR2 don't work on the JVM
|
60
|
+
if !jruby?
|
61
|
+
sigs << 'USR1'
|
62
|
+
sigs << 'USR2'
|
63
|
+
end
|
53
64
|
|
54
|
-
|
65
|
+
sigs.each do |sig|
|
55
66
|
begin
|
56
67
|
trap sig do
|
57
68
|
self_write.puts(sig)
|
@@ -65,20 +76,20 @@ module Sidekiq
|
|
65
76
|
logger.info Sidekiq::LICENSE
|
66
77
|
logger.info "Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org" unless defined?(::Sidekiq::Pro)
|
67
78
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
79
|
+
# touch the connection pool so it is created before we
|
80
|
+
# fire startup and start multithreading.
|
81
|
+
ver = Sidekiq.redis_info['redis_version']
|
82
|
+
raise "You are using Redis v#{ver}, Sidekiq requires Redis v2.8.0 or greater" if ver < '2.8'
|
83
|
+
|
84
|
+
# Touch middleware so it isn't lazy loaded by multiple threads, #3043
|
85
|
+
Sidekiq.server_middleware
|
74
86
|
|
75
87
|
# Before this point, the process is initializing with just the main thread.
|
76
88
|
# Starting here the process will now have multiple threads running.
|
77
89
|
fire_event(:startup)
|
78
90
|
|
79
|
-
logger.debug {
|
80
|
-
|
81
|
-
}
|
91
|
+
logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(', ')}" }
|
92
|
+
logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(', ')}" }
|
82
93
|
|
83
94
|
if !options[:daemon]
|
84
95
|
logger.info 'Starting processing, hit Ctrl-C to stop'
|
@@ -134,6 +145,10 @@ module Sidekiq
|
|
134
145
|
when 'USR1'
|
135
146
|
Sidekiq.logger.info "Received USR1, no longer accepting new work"
|
136
147
|
launcher.quiet
|
148
|
+
when 'TSTP'
|
149
|
+
# USR1 is not available on JVM, allow TSTP as an alternate signal
|
150
|
+
Sidekiq.logger.info "Received TSTP, no longer accepting new work"
|
151
|
+
launcher.quiet
|
137
152
|
when 'USR2'
|
138
153
|
if Sidekiq.options[:logfile]
|
139
154
|
Sidekiq.logger.info "Received USR2, reopening log file"
|
@@ -141,7 +156,7 @@ module Sidekiq
|
|
141
156
|
end
|
142
157
|
when 'TTIN'
|
143
158
|
Thread.list.each do |thread|
|
144
|
-
Sidekiq.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['
|
159
|
+
Sidekiq.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['sidekiq_label']}"
|
145
160
|
if thread.backtrace
|
146
161
|
Sidekiq.logger.warn thread.backtrace.join("\n")
|
147
162
|
else
|
@@ -207,6 +222,8 @@ module Sidekiq
|
|
207
222
|
opts = parse_config(cfile).merge(opts) if cfile
|
208
223
|
|
209
224
|
opts[:strict] = true if opts[:strict].nil?
|
225
|
+
opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if !opts[:concurrency] && ENV["RAILS_MAX_THREADS"]
|
226
|
+
opts[:identity] = identity
|
210
227
|
|
211
228
|
options.merge!(opts)
|
212
229
|
end
|
@@ -223,21 +240,26 @@ module Sidekiq
|
|
223
240
|
if File.directory?(options[:require])
|
224
241
|
require 'rails'
|
225
242
|
if ::Rails::VERSION::MAJOR < 4
|
226
|
-
|
227
|
-
|
228
|
-
::Rails.application.eager_load!
|
229
|
-
else
|
243
|
+
raise "Sidekiq no longer supports this version of Rails"
|
244
|
+
elsif ::Rails::VERSION::MAJOR == 4
|
230
245
|
# Painful contortions, see 1791 for discussion
|
246
|
+
# No autoloading, we want to force eager load for everything.
|
231
247
|
require File.expand_path("#{options[:require]}/config/application.rb")
|
232
248
|
::Rails::Application.initializer "sidekiq.eager_load" do
|
233
249
|
::Rails.application.config.eager_load = true
|
234
250
|
end
|
235
251
|
require 'sidekiq/rails'
|
236
252
|
require File.expand_path("#{options[:require]}/config/environment.rb")
|
253
|
+
else
|
254
|
+
require 'sidekiq/rails'
|
255
|
+
require File.expand_path("#{options[:require]}/config/environment.rb")
|
237
256
|
end
|
238
257
|
options[:tag] ||= default_tag
|
239
258
|
else
|
240
|
-
|
259
|
+
not_required_message = "#{options[:require]} was not required, you should use an explicit path: " +
|
260
|
+
"./#{options[:require]} or /path/to/#{options[:require]}"
|
261
|
+
|
262
|
+
require(options[:require]) || raise(ArgumentError, not_required_message)
|
241
263
|
end
|
242
264
|
end
|
243
265
|
|
data/lib/sidekiq/client.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'securerandom'
|
2
3
|
require 'sidekiq/middleware/chain'
|
3
4
|
|
@@ -35,10 +36,8 @@ module Sidekiq
|
|
35
36
|
# Sidekiq::Client.new(ConnectionPool.new { Redis.new })
|
36
37
|
#
|
37
38
|
# Generally this is only needed for very large Sidekiq installs processing
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# Some features, like the API, do not support sharding: they are designed to work
|
41
|
-
# against a single Redis instance only.
|
39
|
+
# thousands of jobs per second. I don't recommend sharding unless you
|
40
|
+
# cannot scale any other way (e.g. splitting your app into smaller apps).
|
42
41
|
def initialize(redis_pool=nil)
|
43
42
|
@redis_pool = redis_pool || Thread.current[:sidekiq_via_pool] || Sidekiq.redis_pool
|
44
43
|
end
|
@@ -49,11 +48,13 @@ module Sidekiq
|
|
49
48
|
# queue - the named queue to use, default 'default'
|
50
49
|
# class - the worker class to call, required
|
51
50
|
# args - an array of simple arguments to the perform method, must be JSON-serializable
|
52
|
-
#
|
51
|
+
# at - timestamp to schedule the job (optional), must be Numeric (e.g. Time.now.to_f)
|
52
|
+
# retry - whether to retry this job if it fails, default true or an integer number of retries
|
53
53
|
# backtrace - whether to save any error backtrace, default false
|
54
54
|
#
|
55
55
|
# All options must be strings, not symbols. NB: because we are serializing to JSON, all
|
56
|
-
# symbols in 'args' will be converted to strings.
|
56
|
+
# symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
|
57
|
+
# space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful.
|
57
58
|
#
|
58
59
|
# Returns a unique Job ID. If middleware stops the job, nil will be returned instead.
|
59
60
|
#
|
@@ -62,19 +63,18 @@ module Sidekiq
|
|
62
63
|
#
|
63
64
|
def push(item)
|
64
65
|
normed = normalize_item(item)
|
65
|
-
payload = process_single(item['class'], normed)
|
66
|
+
payload = process_single(item['class'.freeze], normed)
|
66
67
|
|
67
68
|
if payload
|
68
69
|
raw_push([payload])
|
69
|
-
payload['jid']
|
70
|
+
payload['jid'.freeze]
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
74
|
##
|
74
75
|
# Push a large number of jobs to Redis. In practice this method is only
|
75
|
-
# useful if you are pushing
|
76
|
-
#
|
77
|
-
# basically cuts down on the redis round trip latency.
|
76
|
+
# useful if you are pushing thousands of jobs or more. This method
|
77
|
+
# cuts out the redis network round trip latency.
|
78
78
|
#
|
79
79
|
# Takes the same arguments as #push except that args is expected to be
|
80
80
|
# an Array of Arrays. All other keys are duplicated for each job. Each job
|
@@ -84,14 +84,19 @@ module Sidekiq
|
|
84
84
|
# Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
|
85
85
|
# than the number given if the middleware stopped processing for one or more jobs.
|
86
86
|
def push_bulk(items)
|
87
|
+
arg = items['args'.freeze].first
|
88
|
+
return [] unless arg # no jobs to push
|
89
|
+
raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !arg.is_a?(Array)
|
90
|
+
|
87
91
|
normed = normalize_item(items)
|
88
|
-
payloads = items['args'].map do |args|
|
89
|
-
|
90
|
-
process_single(items['class'],
|
92
|
+
payloads = items['args'.freeze].map do |args|
|
93
|
+
copy = normed.merge('args'.freeze => args, 'jid'.freeze => SecureRandom.hex(12), 'enqueued_at'.freeze => Time.now.to_f)
|
94
|
+
result = process_single(items['class'.freeze], copy)
|
95
|
+
result ? result : nil
|
91
96
|
end.compact
|
92
97
|
|
93
98
|
raw_push(payloads) if !payloads.empty?
|
94
|
-
payloads.collect { |payload| payload['jid'] }
|
99
|
+
payloads.collect { |payload| payload['jid'.freeze] }
|
95
100
|
end
|
96
101
|
|
97
102
|
# Allows sharding of jobs across any number of Redis instances. All jobs
|
@@ -104,13 +109,12 @@ module Sidekiq
|
|
104
109
|
# end
|
105
110
|
#
|
106
111
|
# Generally this is only needed for very large Sidekiq installs processing
|
107
|
-
#
|
108
|
-
# you
|
109
|
-
# Some features, like the API, do not support sharding: they are designed to work
|
110
|
-
# against a single Redis instance.
|
112
|
+
# thousands of jobs per second. I do not recommend sharding unless
|
113
|
+
# you cannot scale any other way (e.g. splitting your app into smaller apps).
|
111
114
|
def self.via(pool)
|
112
115
|
raise ArgumentError, "No pool given" if pool.nil?
|
113
|
-
|
116
|
+
current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
|
117
|
+
raise RuntimeError, "Sidekiq::Client.via is not re-entrant" if current_sidekiq_pool && current_sidekiq_pool != pool
|
114
118
|
Thread.current[:sidekiq_via_pool] = pool
|
115
119
|
yield
|
116
120
|
ensure
|
@@ -136,14 +140,14 @@ module Sidekiq
|
|
136
140
|
# Messages are enqueued to the 'default' queue.
|
137
141
|
#
|
138
142
|
def enqueue(klass, *args)
|
139
|
-
klass.client_push('class' => klass, 'args' => args)
|
143
|
+
klass.client_push('class'.freeze => klass, 'args'.freeze => args)
|
140
144
|
end
|
141
145
|
|
142
146
|
# Example usage:
|
143
147
|
# Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
|
144
148
|
#
|
145
149
|
def enqueue_to(queue, klass, *args)
|
146
|
-
klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
|
150
|
+
klass.client_push('queue'.freeze => queue, 'class'.freeze => klass, 'args'.freeze => args)
|
147
151
|
end
|
148
152
|
|
149
153
|
# Example usage:
|
@@ -154,7 +158,7 @@ module Sidekiq
|
|
154
158
|
now = Time.now.to_f
|
155
159
|
ts = (int < 1_000_000_000 ? now + int : int)
|
156
160
|
|
157
|
-
item = { 'class' => klass, 'args' => args, 'at' => ts, 'queue' => queue }
|
161
|
+
item = { 'class'.freeze => klass, 'args'.freeze => args, 'at'.freeze => ts, 'queue'.freeze => queue }
|
158
162
|
item.delete('at'.freeze) if ts <= now
|
159
163
|
|
160
164
|
klass.client_push(item)
|
@@ -180,13 +184,13 @@ module Sidekiq
|
|
180
184
|
end
|
181
185
|
|
182
186
|
def atomic_push(conn, payloads)
|
183
|
-
if payloads.first['at']
|
187
|
+
if payloads.first['at'.freeze]
|
184
188
|
conn.zadd('schedule'.freeze, payloads.map do |hash|
|
185
189
|
at = hash.delete('at'.freeze).to_s
|
186
190
|
[at, Sidekiq.dump_json(hash)]
|
187
191
|
end)
|
188
192
|
else
|
189
|
-
q = payloads.first['queue']
|
193
|
+
q = payloads.first['queue'.freeze]
|
190
194
|
now = Time.now.to_f
|
191
195
|
to_push = payloads.map do |entry|
|
192
196
|
entry['enqueued_at'.freeze] = now
|
@@ -198,7 +202,7 @@ module Sidekiq
|
|
198
202
|
end
|
199
203
|
|
200
204
|
def process_single(worker_class, item)
|
201
|
-
queue = item['queue']
|
205
|
+
queue = item['queue'.freeze]
|
202
206
|
|
203
207
|
middleware.invoke(worker_class, item, queue, @redis_pool) do
|
204
208
|
item
|
@@ -209,6 +213,8 @@ module Sidekiq
|
|
209
213
|
raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash) && item.has_key?('class'.freeze) && item.has_key?('args'.freeze)
|
210
214
|
raise(ArgumentError, "Job args must be an Array") unless item['args'].is_a?(Array)
|
211
215
|
raise(ArgumentError, "Job class must be either a Class or String representation of the class name") unless item['class'.freeze].is_a?(Class) || item['class'.freeze].is_a?(String)
|
216
|
+
raise(ArgumentError, "Job 'at' must be a Numeric timestamp") if item.has_key?('at'.freeze) && !item['at'].is_a?(Numeric)
|
217
|
+
#raise(ArgumentError, "Arguments must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices") unless JSON.load(JSON.dump(item['args'])) == item['args']
|
212
218
|
|
213
219
|
normalized_hash(item['class'.freeze])
|
214
220
|
.each{ |key, value| item[key] = value if item[key].nil? }
|
data/lib/sidekiq/core_ext.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
begin
|
2
3
|
require 'active_support/core_ext/class/attribute'
|
3
4
|
rescue LoadError
|
@@ -103,3 +104,16 @@ rescue LoadError
|
|
103
104
|
end
|
104
105
|
|
105
106
|
|
107
|
+
begin
|
108
|
+
require 'active_support/core_ext/kernel/reporting'
|
109
|
+
rescue LoadError
|
110
|
+
module Kernel
|
111
|
+
module_function
|
112
|
+
def silence_warnings
|
113
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
114
|
+
yield
|
115
|
+
ensure
|
116
|
+
$VERBOSE = old_verbose
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Extensions
|
3
|
+
|
4
|
+
def self.enable_delay!
|
5
|
+
if defined?(::ActiveSupport)
|
6
|
+
ActiveSupport.on_load(:active_record) do
|
7
|
+
require 'sidekiq/extensions/active_record'
|
8
|
+
include Sidekiq::Extensions::ActiveRecord
|
9
|
+
end
|
10
|
+
ActiveSupport.on_load(:action_mailer) do
|
11
|
+
require 'sidekiq/extensions/action_mailer'
|
12
|
+
extend Sidekiq::Extensions::ActionMailer
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'sidekiq/extensions/class_methods'
|
17
|
+
Module.__send__(:include, Sidekiq::Extensions::Klass)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'sidekiq'
|
2
3
|
|
3
4
|
module Sidekiq
|
@@ -5,7 +6,7 @@ module Sidekiq
|
|
5
6
|
|
6
7
|
class Logger
|
7
8
|
def call(ex, ctxHash)
|
8
|
-
Sidekiq.logger.warn(ctxHash) if !ctxHash.empty?
|
9
|
+
Sidekiq.logger.warn(Sidekiq.dump_json(ctxHash)) if !ctxHash.empty?
|
9
10
|
Sidekiq.logger.warn "#{ex.class.name}: #{ex.message}"
|
10
11
|
Sidekiq.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
|
11
12
|
end
|
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'yaml'
|
2
3
|
|
3
4
|
module Sidekiq
|
4
5
|
module Extensions
|
6
|
+
SIZE_LIMIT = 8_192
|
7
|
+
|
5
8
|
class Proxy < BasicObject
|
6
9
|
def initialize(performable, target, options={})
|
7
10
|
@performable = performable
|
@@ -16,7 +19,11 @@ module Sidekiq
|
|
16
19
|
# to JSON and then deserialized on the other side back into a
|
17
20
|
# Ruby object.
|
18
21
|
obj = [@target, name, args]
|
19
|
-
|
22
|
+
marshalled = ::YAML.dump(obj)
|
23
|
+
if marshalled.size > SIZE_LIMIT
|
24
|
+
::Sidekiq.logger.warn { "#{@target}.#{name} job argument is #{marshalled.bytesize} bytes, you should refactor it to reduce the size" }
|
25
|
+
end
|
26
|
+
@performable.client_push({ 'class' => @performable, 'args' => [marshalled] }.merge(@opts))
|
20
27
|
end
|
21
28
|
end
|
22
29
|
|
data/lib/sidekiq/fetch.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
class JobLogger
|
3
|
+
|
4
|
+
def call(item, queue)
|
5
|
+
begin
|
6
|
+
start = Time.now
|
7
|
+
logger.info("start".freeze)
|
8
|
+
yield
|
9
|
+
logger.info("done: #{elapsed(start)} sec")
|
10
|
+
rescue Exception
|
11
|
+
logger.info("fail: #{elapsed(start)} sec")
|
12
|
+
raise
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def elapsed(start)
|
19
|
+
(Time.now - start).round(3)
|
20
|
+
end
|
21
|
+
|
22
|
+
def logger
|
23
|
+
Sidekiq.logger
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,235 @@
|
|
1
|
+
require 'sidekiq/scheduled'
|
2
|
+
require 'sidekiq/api'
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
##
|
6
|
+
# Automatically retry jobs that fail in Sidekiq.
|
7
|
+
# Sidekiq's retry support assumes a typical development lifecycle:
|
8
|
+
#
|
9
|
+
# 0. Push some code changes with a bug in it.
|
10
|
+
# 1. Bug causes job processing to fail, Sidekiq's middleware captures
|
11
|
+
# the job and pushes it onto a retry queue.
|
12
|
+
# 2. Sidekiq retries jobs in the retry queue multiple times with
|
13
|
+
# an exponential delay, the job continues to fail.
|
14
|
+
# 3. After a few days, a developer deploys a fix. The job is
|
15
|
+
# reprocessed successfully.
|
16
|
+
# 4. Once retries are exhausted, Sidekiq will give up and move the
|
17
|
+
# job to the Dead Job Queue (aka morgue) where it must be dealt with
|
18
|
+
# manually in the Web UI.
|
19
|
+
# 5. After 6 months on the DJQ, Sidekiq will discard the job.
|
20
|
+
#
|
21
|
+
# A job looks like:
|
22
|
+
#
|
23
|
+
# { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
|
24
|
+
#
|
25
|
+
# The 'retry' option also accepts a number (in place of 'true'):
|
26
|
+
#
|
27
|
+
# { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
|
28
|
+
#
|
29
|
+
# The job will be retried this number of times before giving up. (If simply
|
30
|
+
# 'true', Sidekiq retries 25 times)
|
31
|
+
#
|
32
|
+
# We'll add a bit more data to the job to support retries:
|
33
|
+
#
|
34
|
+
# * 'queue' - the queue to use
|
35
|
+
# * 'retry_count' - number of times we've retried so far.
|
36
|
+
# * 'error_message' - the message from the exception
|
37
|
+
# * 'error_class' - the exception class
|
38
|
+
# * 'failed_at' - the first time it failed
|
39
|
+
# * 'retried_at' - the last time it was retried
|
40
|
+
# * 'backtrace' - the number of lines of error backtrace to store
|
41
|
+
#
|
42
|
+
# We don't store the backtrace by default as that can add a lot of overhead
|
43
|
+
# to the job and everyone is using an error service, right?
|
44
|
+
#
|
45
|
+
# The default number of retries is 25 which works out to about 3 weeks
|
46
|
+
# You can change the default maximum number of retries in your initializer:
|
47
|
+
#
|
48
|
+
# Sidekiq.options[:max_retries] = 7
|
49
|
+
#
|
50
|
+
# or limit the number of retries for a particular worker with:
|
51
|
+
#
|
52
|
+
# class MyWorker
|
53
|
+
# include Sidekiq::Worker
|
54
|
+
# sidekiq_options :retry => 10
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
class JobRetry
|
58
|
+
class Skip < ::RuntimeError; end
|
59
|
+
|
60
|
+
include Sidekiq::Util
|
61
|
+
|
62
|
+
DEFAULT_MAX_RETRY_ATTEMPTS = 25
|
63
|
+
|
64
|
+
def initialize(options = {})
|
65
|
+
@max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
|
66
|
+
end
|
67
|
+
|
68
|
+
# The global retry handler requires only the barest of data.
|
69
|
+
# We want to be able to retry as much as possible so we don't
|
70
|
+
# require the worker to be instantiated.
|
71
|
+
def global(msg, queue)
|
72
|
+
yield
|
73
|
+
rescue Skip => ex
|
74
|
+
raise ex
|
75
|
+
rescue Sidekiq::Shutdown => ey
|
76
|
+
# ignore, will be pushed back onto queue during hard_shutdown
|
77
|
+
raise ey
|
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
|
+
|
82
|
+
raise e unless msg['retry']
|
83
|
+
attempt_retry(nil, msg, queue, e)
|
84
|
+
raise e
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
# The local retry support means that any errors that occur within
|
89
|
+
# this block can be associated with the given worker instance.
|
90
|
+
# This is required to support the `sidekiq_retries_exhausted` block.
|
91
|
+
#
|
92
|
+
# Note that any exception from the block is wrapped in the Skip
|
93
|
+
# exception so the global block does not reprocess the error. The
|
94
|
+
# Skip exception is unwrapped within Sidekiq::Processor#process before
|
95
|
+
# calling the handle_exception handlers.
|
96
|
+
def local(worker, msg, queue)
|
97
|
+
yield
|
98
|
+
rescue Skip => ex
|
99
|
+
raise ex
|
100
|
+
rescue Sidekiq::Shutdown => ey
|
101
|
+
# ignore, will be pushed back onto queue during hard_shutdown
|
102
|
+
raise ey
|
103
|
+
rescue Exception => e
|
104
|
+
# ignore, will be pushed back onto queue during hard_shutdown
|
105
|
+
raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
|
106
|
+
|
107
|
+
if msg['retry'] == nil
|
108
|
+
msg['retry'] = worker.class.get_sidekiq_options['retry']
|
109
|
+
end
|
110
|
+
|
111
|
+
raise e unless msg['retry']
|
112
|
+
attempt_retry(worker, msg, queue, e)
|
113
|
+
# We've handled this error associated with this job, don't
|
114
|
+
# need to handle it at the global level
|
115
|
+
raise Skip
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
# Note that +worker+ can be nil here if an error is raised before we can
|
121
|
+
# instantiate the worker instance. All access must be guarded and
|
122
|
+
# best effort.
|
123
|
+
def attempt_retry(worker, msg, queue, exception)
|
124
|
+
max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
|
125
|
+
|
126
|
+
msg['queue'] = if msg['retry_queue']
|
127
|
+
msg['retry_queue']
|
128
|
+
else
|
129
|
+
queue
|
130
|
+
end
|
131
|
+
|
132
|
+
# App code can stuff all sorts of crazy binary data into the error message
|
133
|
+
# that won't convert to JSON.
|
134
|
+
m = exception.message.to_s[0, 10_000]
|
135
|
+
if m.respond_to?(:scrub!)
|
136
|
+
m.force_encoding("utf-8")
|
137
|
+
m.scrub!
|
138
|
+
end
|
139
|
+
|
140
|
+
msg['error_message'] = m
|
141
|
+
msg['error_class'] = exception.class.name
|
142
|
+
count = if msg['retry_count']
|
143
|
+
msg['retried_at'] = Time.now.to_f
|
144
|
+
msg['retry_count'] += 1
|
145
|
+
else
|
146
|
+
msg['failed_at'] = Time.now.to_f
|
147
|
+
msg['retry_count'] = 0
|
148
|
+
end
|
149
|
+
|
150
|
+
if msg['backtrace'] == true
|
151
|
+
msg['error_backtrace'] = exception.backtrace
|
152
|
+
elsif !msg['backtrace']
|
153
|
+
# do nothing
|
154
|
+
elsif msg['backtrace'].to_i != 0
|
155
|
+
msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
|
156
|
+
end
|
157
|
+
|
158
|
+
if count < max_retry_attempts
|
159
|
+
delay = delay_for(worker, count, exception)
|
160
|
+
logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
|
161
|
+
retry_at = Time.now.to_f + delay
|
162
|
+
payload = Sidekiq.dump_json(msg)
|
163
|
+
Sidekiq.redis do |conn|
|
164
|
+
conn.zadd('retry', retry_at.to_s, payload)
|
165
|
+
end
|
166
|
+
else
|
167
|
+
# Goodbye dear message, you (re)tried your best I'm sure.
|
168
|
+
retries_exhausted(worker, msg, exception)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def retries_exhausted(worker, msg, exception)
|
173
|
+
logger.debug { "Retries exhausted for job" }
|
174
|
+
begin
|
175
|
+
block = worker && worker.sidekiq_retries_exhausted_block || Sidekiq.default_retries_exhausted
|
176
|
+
block.call(msg, exception) if block
|
177
|
+
rescue => e
|
178
|
+
handle_exception(e, { context: "Error calling retries_exhausted for #{msg['class']}", job: msg })
|
179
|
+
end
|
180
|
+
|
181
|
+
send_to_morgue(msg) unless msg['dead'] == false
|
182
|
+
end
|
183
|
+
|
184
|
+
def send_to_morgue(msg)
|
185
|
+
Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
|
186
|
+
payload = Sidekiq.dump_json(msg)
|
187
|
+
now = Time.now.to_f
|
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
|
195
|
+
end
|
196
|
+
|
197
|
+
def retry_attempts_from(msg_retry, default)
|
198
|
+
if msg_retry.is_a?(Integer)
|
199
|
+
msg_retry
|
200
|
+
else
|
201
|
+
default
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def delay_for(worker, count, exception)
|
206
|
+
worker && worker.sidekiq_retry_in_block? && retry_in(worker, count, exception) || seconds_to_delay(count)
|
207
|
+
end
|
208
|
+
|
209
|
+
# delayed_job uses the same basic formula
|
210
|
+
def seconds_to_delay(count)
|
211
|
+
(count ** 4) + 15 + (rand(30)*(count+1))
|
212
|
+
end
|
213
|
+
|
214
|
+
def retry_in(worker, count, exception)
|
215
|
+
begin
|
216
|
+
worker.sidekiq_retry_in_block.call(count, exception).to_i
|
217
|
+
rescue Exception => e
|
218
|
+
handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
|
219
|
+
nil
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def exception_caused_by_shutdown?(e, checked_causes = [])
|
224
|
+
return false unless e.cause
|
225
|
+
|
226
|
+
# Handle circular causes
|
227
|
+
checked_causes << e.object_id
|
228
|
+
return false if checked_causes.include?(e.cause.object_id)
|
229
|
+
|
230
|
+
e.cause.instance_of?(Sidekiq::Shutdown) ||
|
231
|
+
exception_caused_by_shutdown?(e.cause, checked_causes)
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
end
|