sidekiq 4.2.10 → 5.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 +5 -5
- data/.circleci/config.yml +61 -0
- data/.github/issue_template.md +3 -1
- data/.gitignore +3 -0
- data/.travis.yml +6 -13
- data/5.0-Upgrade.md +56 -0
- data/COMM-LICENSE +12 -10
- data/Changes.md +177 -1
- data/Ent-Changes.md +67 -2
- data/Gemfile +12 -22
- data/LICENSE +1 -1
- data/Pro-4.0-Upgrade.md +35 -0
- data/Pro-Changes.md +133 -2
- data/README.md +8 -6
- data/Rakefile +2 -5
- data/bin/sidekiqctl +13 -92
- data/bin/sidekiqload +5 -10
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
- data/lib/sidekiq/api.rb +148 -58
- data/lib/sidekiq/cli.rb +120 -81
- data/lib/sidekiq/client.rb +25 -18
- data/lib/sidekiq/core_ext.rb +1 -119
- data/lib/sidekiq/ctl.rb +221 -0
- data/lib/sidekiq/delay.rb +42 -0
- data/lib/sidekiq/exception_handler.rb +2 -4
- data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
- data/lib/sidekiq/fetch.rb +1 -1
- data/lib/sidekiq/job_logger.rb +25 -0
- data/lib/sidekiq/job_retry.rb +262 -0
- data/lib/sidekiq/launcher.rb +20 -20
- data/lib/sidekiq/logging.rb +18 -2
- data/lib/sidekiq/manager.rb +5 -6
- data/lib/sidekiq/middleware/server/active_record.rb +10 -0
- data/lib/sidekiq/processor.rb +126 -48
- data/lib/sidekiq/rails.rb +8 -73
- data/lib/sidekiq/redis_connection.rb +43 -5
- data/lib/sidekiq/scheduled.rb +35 -8
- data/lib/sidekiq/testing.rb +16 -7
- data/lib/sidekiq/util.rb +5 -2
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +3 -7
- data/lib/sidekiq/web/application.rb +37 -17
- data/lib/sidekiq/web/helpers.rb +69 -22
- data/lib/sidekiq/web/router.rb +10 -10
- data/lib/sidekiq/web.rb +4 -4
- data/lib/sidekiq/worker.rb +118 -19
- data/lib/sidekiq.rb +27 -27
- data/sidekiq.gemspec +6 -17
- data/web/assets/javascripts/application.js +0 -0
- data/web/assets/javascripts/dashboard.js +32 -17
- data/web/assets/stylesheets/application-rtl.css +246 -0
- data/web/assets/stylesheets/application.css +371 -6
- 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/en.yml +2 -0
- data/web/locales/es.yml +4 -3
- data/web/locales/fa.yml +1 -0
- data/web/locales/he.yml +79 -0
- data/web/locales/ja.yml +5 -3
- data/web/locales/ur.yml +80 -0
- data/web/views/_footer.erb +5 -2
- data/web/views/_nav.erb +4 -18
- data/web/views/_paging.erb +1 -1
- data/web/views/busy.erb +9 -5
- data/web/views/dashboard.erb +1 -1
- data/web/views/layout.erb +11 -2
- data/web/views/morgue.erb +4 -4
- data/web/views/queue.erb +8 -7
- data/web/views/queues.erb +2 -0
- data/web/views/retries.erb +9 -5
- data/web/views/scheduled.erb +2 -2
- metadata +30 -159
- data/lib/sidekiq/middleware/server/logging.rb +0 -31
- data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
data/lib/sidekiq/logging.rb
CHANGED
@@ -11,7 +11,7 @@ module Sidekiq
|
|
11
11
|
|
12
12
|
# Provide a call() method that returns the formatted message.
|
13
13
|
def call(severity, time, program_name, message)
|
14
|
-
"#{time.utc.iso8601(3)} #{::Process.pid} TID-#{
|
14
|
+
"#{time.utc.iso8601(3)} #{::Process.pid} TID-#{Sidekiq::Logging.tid}#{context} #{severity}: #{message}\n"
|
15
15
|
end
|
16
16
|
|
17
17
|
def context
|
@@ -22,10 +22,26 @@ module Sidekiq
|
|
22
22
|
|
23
23
|
class WithoutTimestamp < Pretty
|
24
24
|
def call(severity, time, program_name, message)
|
25
|
-
"#{::Process.pid} TID-#{
|
25
|
+
"#{::Process.pid} TID-#{Sidekiq::Logging.tid}#{context} #{severity}: #{message}\n"
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
def self.tid
|
30
|
+
Thread.current['sidekiq_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.job_hash_context(job_hash)
|
34
|
+
# If we're using a wrapper class, like ActiveJob, use the "wrapped"
|
35
|
+
# attribute to expose the underlying thing.
|
36
|
+
klass = job_hash['wrapped'] || job_hash["class"]
|
37
|
+
bid = job_hash['bid']
|
38
|
+
"#{klass} JID-#{job_hash['jid']}#{" BID-#{bid}" if bid}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.with_job_hash_context(job_hash, &block)
|
42
|
+
with_context(job_hash_context(job_hash), &block)
|
43
|
+
end
|
44
|
+
|
29
45
|
def self.with_context(msg)
|
30
46
|
Thread.current[:sidekiq_context] ||= []
|
31
47
|
Thread.current[:sidekiq_context] << msg
|
data/lib/sidekiq/manager.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
require 'sidekiq/util'
|
4
3
|
require 'sidekiq/processor'
|
@@ -31,7 +30,7 @@ module Sidekiq
|
|
31
30
|
def initialize(options={})
|
32
31
|
logger.debug { options.inspect }
|
33
32
|
@options = options
|
34
|
-
@count = options[:concurrency] ||
|
33
|
+
@count = options[:concurrency] || 10
|
35
34
|
raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
|
36
35
|
|
37
36
|
@done = false
|
@@ -54,7 +53,7 @@ module Sidekiq
|
|
54
53
|
|
55
54
|
logger.info { "Terminating quiet workers" }
|
56
55
|
@workers.each { |x| x.terminate }
|
57
|
-
fire_event(:quiet, true)
|
56
|
+
fire_event(:quiet, reverse: true)
|
58
57
|
end
|
59
58
|
|
60
59
|
# hack for quicker development / testing environment #2774
|
@@ -62,7 +61,7 @@ module Sidekiq
|
|
62
61
|
|
63
62
|
def stop(deadline)
|
64
63
|
quiet
|
65
|
-
fire_event(:shutdown, true)
|
64
|
+
fire_event(:shutdown, reverse: true)
|
66
65
|
|
67
66
|
# some of the shutdown events can be async,
|
68
67
|
# we don't have any way to know when they're done but
|
@@ -71,11 +70,11 @@ module Sidekiq
|
|
71
70
|
return if @workers.empty?
|
72
71
|
|
73
72
|
logger.info { "Pausing to allow workers to finish..." }
|
74
|
-
remaining = deadline -
|
73
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
75
74
|
while remaining > PAUSE_TIME
|
76
75
|
return if @workers.empty?
|
77
76
|
sleep PAUSE_TIME
|
78
|
-
remaining = deadline -
|
77
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
79
78
|
end
|
80
79
|
return if @workers.empty?
|
81
80
|
|
@@ -1,7 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Sidekiq
|
2
3
|
module Middleware
|
3
4
|
module Server
|
4
5
|
class ActiveRecord
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
# With Rails 5+ we must use the Reloader **always**.
|
9
|
+
# The reloader handles code loading and db connection management.
|
10
|
+
if defined?(::Rails) && defined?(::Rails::VERSION) && ::Rails::VERSION::MAJOR >= 5
|
11
|
+
raise ArgumentError, "Rails 5 no longer needs or uses the ActiveRecord middleware."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
5
15
|
def call(*args)
|
6
16
|
yield
|
7
17
|
ensure
|
data/lib/sidekiq/processor.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'sidekiq/util'
|
3
3
|
require 'sidekiq/fetch'
|
4
|
+
require 'sidekiq/job_logger'
|
5
|
+
require 'sidekiq/job_retry'
|
4
6
|
require 'thread'
|
5
|
-
require 'concurrent/map'
|
6
|
-
require 'concurrent/atomic/atomic_fixnum'
|
7
7
|
|
8
8
|
module Sidekiq
|
9
9
|
##
|
@@ -37,7 +37,8 @@ module Sidekiq
|
|
37
37
|
@thread = nil
|
38
38
|
@strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
|
39
39
|
@reloader = Sidekiq.options[:reloader]
|
40
|
-
@
|
40
|
+
@logging = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
|
41
|
+
@retrier = Sidekiq::JobRetry.new
|
41
42
|
end
|
42
43
|
|
43
44
|
def terminate(wait=false)
|
@@ -86,7 +87,7 @@ module Sidekiq
|
|
86
87
|
def get_one
|
87
88
|
begin
|
88
89
|
work = @strategy.retrieve_work
|
89
|
-
(logger.info { "Redis is online, #{
|
90
|
+
(logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" }; @down = nil) if @down
|
90
91
|
work
|
91
92
|
rescue Sidekiq::Shutdown
|
92
93
|
rescue => ex
|
@@ -106,95 +107,172 @@ module Sidekiq
|
|
106
107
|
|
107
108
|
def handle_fetch_exception(ex)
|
108
109
|
if !@down
|
109
|
-
@down =
|
110
|
+
@down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
110
111
|
logger.error("Error fetching job: #{ex}")
|
111
|
-
ex
|
112
|
-
logger.error(bt)
|
113
|
-
end
|
112
|
+
handle_exception(ex)
|
114
113
|
end
|
115
114
|
sleep(1)
|
116
115
|
nil
|
117
116
|
end
|
118
117
|
|
118
|
+
def dispatch(job_hash, queue)
|
119
|
+
# since middleware can mutate the job hash
|
120
|
+
# we clone here so we report the original
|
121
|
+
# job structure to the Web UI
|
122
|
+
pristine = cloned(job_hash)
|
123
|
+
|
124
|
+
Sidekiq::Logging.with_job_hash_context(job_hash) do
|
125
|
+
@retrier.global(pristine, queue) do
|
126
|
+
@logging.call(job_hash, queue) do
|
127
|
+
stats(pristine, queue) do
|
128
|
+
# Rails 5 requires a Reloader to wrap code execution. In order to
|
129
|
+
# constantize the worker and instantiate an instance, we have to call
|
130
|
+
# the Reloader. It handles code loading, db connection management, etc.
|
131
|
+
# Effectively this block denotes a "unit of work" to Rails.
|
132
|
+
@reloader.call do
|
133
|
+
klass = constantize(job_hash['class'])
|
134
|
+
worker = klass.new
|
135
|
+
worker.jid = job_hash['jid']
|
136
|
+
@retrier.local(worker, pristine, queue) do
|
137
|
+
yield worker
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
119
146
|
def process(work)
|
120
147
|
jobstr = work.job
|
121
148
|
queue = work.queue_name
|
122
149
|
|
123
|
-
|
150
|
+
# Treat malformed JSON as a special case: job goes straight to the morgue.
|
151
|
+
job_hash = nil
|
124
152
|
begin
|
125
153
|
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
|
154
|
+
rescue => ex
|
155
|
+
handle_exception(ex, { :context => "Invalid JSON for job", :jobstr => jobstr })
|
156
|
+
# we can't notify because the job isn't a valid hash payload.
|
157
|
+
DeadSet.new.kill(jobstr, notify_failure: false)
|
158
|
+
return work.acknowledge
|
159
|
+
end
|
160
|
+
|
161
|
+
ack = true
|
162
|
+
begin
|
163
|
+
dispatch(job_hash, queue) do |worker|
|
164
|
+
Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
|
165
|
+
execute_job(worker, cloned(job_hash['args']))
|
143
166
|
end
|
144
|
-
ack = true
|
145
167
|
end
|
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
|
ack = false
|
173
|
+
rescue Sidekiq::JobRetry::Handled => h
|
174
|
+
# this is the common case: job raised error and Sidekiq::JobRetry::Handled
|
175
|
+
# signals that we created a retry successfully. We can acknowlege the job.
|
176
|
+
e = h.cause ? 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
|
+
ack = false
|
184
|
+
handle_exception(ex, { :context => "Internal exception!", :job => job_hash, :jobstr => jobstr })
|
185
|
+
raise e
|
154
186
|
ensure
|
155
187
|
work.acknowledge if ack
|
156
188
|
end
|
157
189
|
end
|
158
190
|
|
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
191
|
def execute_job(worker, cloned_args)
|
167
192
|
worker.perform(*cloned_args)
|
168
193
|
end
|
169
194
|
|
170
|
-
|
171
|
-
|
195
|
+
# Ruby doesn't provide atomic counters out of the box so we'll
|
196
|
+
# implement something simple ourselves.
|
197
|
+
# https://bugs.ruby-lang.org/issues/14706
|
198
|
+
class Counter
|
199
|
+
def initialize
|
200
|
+
@value = 0
|
201
|
+
@lock = Mutex.new
|
202
|
+
end
|
203
|
+
|
204
|
+
def incr(amount=1)
|
205
|
+
@lock.synchronize { @value = @value + amount }
|
206
|
+
end
|
207
|
+
|
208
|
+
def reset
|
209
|
+
@lock.synchronize { val = @value; @value = 0; val }
|
210
|
+
end
|
172
211
|
end
|
173
212
|
|
174
|
-
|
175
|
-
|
176
|
-
|
213
|
+
# jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here
|
214
|
+
class SharedWorkerState
|
215
|
+
def initialize
|
216
|
+
@worker_state = {}
|
217
|
+
@lock = Mutex.new
|
218
|
+
end
|
177
219
|
|
178
|
-
|
179
|
-
|
180
|
-
|
220
|
+
def set(tid, hash)
|
221
|
+
@lock.synchronize { @worker_state[tid] = hash }
|
222
|
+
end
|
223
|
+
|
224
|
+
def delete(tid)
|
225
|
+
@lock.synchronize { @worker_state.delete(tid) }
|
226
|
+
end
|
227
|
+
|
228
|
+
def dup
|
229
|
+
@lock.synchronize { @worker_state.dup }
|
230
|
+
end
|
231
|
+
|
232
|
+
def size
|
233
|
+
@lock.synchronize { @worker_state.size }
|
234
|
+
end
|
235
|
+
|
236
|
+
def clear
|
237
|
+
@lock.synchronize { @worker_state.clear }
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
PROCESSED = Counter.new
|
242
|
+
FAILURE = Counter.new
|
243
|
+
WORKER_STATE = SharedWorkerState.new
|
244
|
+
|
245
|
+
def stats(job_hash, queue)
|
246
|
+
tid = Sidekiq::Logging.tid
|
247
|
+
WORKER_STATE.set(tid, {:queue => queue, :payload => job_hash, :run_at => Time.now.to_i })
|
181
248
|
|
182
249
|
begin
|
183
250
|
yield
|
184
251
|
rescue Exception
|
185
|
-
FAILURE.
|
252
|
+
FAILURE.incr
|
186
253
|
raise
|
187
254
|
ensure
|
188
255
|
WORKER_STATE.delete(tid)
|
189
|
-
PROCESSED.
|
256
|
+
PROCESSED.incr
|
190
257
|
end
|
191
258
|
end
|
192
259
|
|
193
260
|
# Deep clone the arguments passed to the worker so that if
|
194
261
|
# the job fails, what is pushed back onto Redis hasn't
|
195
262
|
# been mutated by the worker.
|
196
|
-
def cloned(
|
197
|
-
Marshal.load(Marshal.dump(
|
263
|
+
def cloned(thing)
|
264
|
+
Marshal.load(Marshal.dump(thing))
|
265
|
+
end
|
266
|
+
|
267
|
+
def constantize(str)
|
268
|
+
names = str.split('::')
|
269
|
+
names.shift if names.empty? || names.first.empty?
|
270
|
+
|
271
|
+
names.inject(Object) do |constant, name|
|
272
|
+
# the false flag limits search for name to under the constant namespace
|
273
|
+
# which mimics Rails' behaviour
|
274
|
+
constant.const_defined?(name, false) ? constant.const_get(name, false) : constant.const_missing(name)
|
275
|
+
end
|
198
276
|
end
|
199
277
|
|
200
278
|
end
|
data/lib/sidekiq/rails.rb
CHANGED
@@ -1,36 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
module Sidekiq
|
3
|
-
def self.hook_rails!
|
4
|
-
return if defined?(@delay_removed)
|
5
|
-
|
6
|
-
ActiveSupport.on_load(:active_record) do
|
7
|
-
include Sidekiq::Extensions::ActiveRecord
|
8
|
-
end
|
9
|
-
|
10
|
-
ActiveSupport.on_load(:action_mailer) do
|
11
|
-
extend Sidekiq::Extensions::ActionMailer
|
12
|
-
end
|
13
|
-
|
14
|
-
Module.__send__(:include, Sidekiq::Extensions::Klass)
|
15
|
-
end
|
16
|
-
|
17
|
-
# Removes the generic aliases which MAY clash with names of already
|
18
|
-
# created methods by other applications. The methods `sidekiq_delay`,
|
19
|
-
# `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
|
20
|
-
def self.remove_delay!
|
21
|
-
@delay_removed = true
|
22
|
-
|
23
|
-
[Extensions::ActiveRecord,
|
24
|
-
Extensions::ActionMailer,
|
25
|
-
Extensions::Klass].each do |mod|
|
26
|
-
mod.module_eval do
|
27
|
-
remove_method :delay if respond_to?(:delay)
|
28
|
-
remove_method :delay_for if respond_to?(:delay_for)
|
29
|
-
remove_method :delay_until if respond_to?(:delay_until)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
2
|
|
3
|
+
module Sidekiq
|
34
4
|
class Rails < ::Rails::Engine
|
35
5
|
# We need to setup this up before any application configuration which might
|
36
6
|
# change Sidekiq middleware.
|
@@ -48,10 +18,6 @@ module Sidekiq
|
|
48
18
|
end
|
49
19
|
end
|
50
20
|
|
51
|
-
initializer 'sidekiq' do
|
52
|
-
Sidekiq.hook_rails!
|
53
|
-
end
|
54
|
-
|
55
21
|
config.after_initialize do
|
56
22
|
# This hook happens after all initializers are run, just before returning
|
57
23
|
# from config/environment.rb back to sidekiq/cli.rb.
|
@@ -62,40 +28,11 @@ module Sidekiq
|
|
62
28
|
#
|
63
29
|
Sidekiq.configure_server do |_|
|
64
30
|
if ::Rails::VERSION::MAJOR >= 5
|
65
|
-
|
66
|
-
# the ActiveRecord middleware so make sure it's not in the chain already.
|
67
|
-
if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
|
68
|
-
raise ArgumentError, "You are using the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration."
|
69
|
-
elsif ::Rails.application.config.cache_classes
|
70
|
-
# The reloader API has proven to be troublesome under load in production.
|
71
|
-
# We won't use it at all when classes are cached, see #3154
|
72
|
-
Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
|
73
|
-
Sidekiq.options[:executor] = Sidekiq::Rails::Executor.new
|
74
|
-
else
|
75
|
-
Sidekiq.logger.debug { "Enabling Rails 5+ live code reloading, so hot!" }
|
76
|
-
Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
|
77
|
-
Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
|
78
|
-
end
|
31
|
+
Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
|
79
32
|
end
|
80
33
|
end
|
81
34
|
end
|
82
35
|
|
83
|
-
class Executor
|
84
|
-
def initialize(app = ::Rails.application)
|
85
|
-
@app = app
|
86
|
-
end
|
87
|
-
|
88
|
-
def call
|
89
|
-
@app.executor.wrap do
|
90
|
-
yield
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def inspect
|
95
|
-
"#<Sidekiq::Rails::Executor @app=#{@app.class.name}>"
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
36
|
class Reloader
|
100
37
|
def initialize(app = ::Rails.application)
|
101
38
|
@app = app
|
@@ -111,13 +48,11 @@ module Sidekiq
|
|
111
48
|
"#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
|
112
49
|
end
|
113
50
|
end
|
114
|
-
|
115
|
-
module PsychAutoload
|
116
|
-
def resolve_class(klass_name)
|
117
|
-
klass_name && klass_name.constantize
|
118
|
-
rescue NameError
|
119
|
-
super
|
120
|
-
end
|
121
|
-
end
|
122
51
|
end if defined?(::Rails)
|
123
52
|
end
|
53
|
+
|
54
|
+
if defined?(::Rails) && ::Rails::VERSION::MAJOR < 4
|
55
|
+
$stderr.puts("**************************************************")
|
56
|
+
$stderr.puts("⛔️ WARNING: Sidekiq server is no longer supported by Rails 3.2 - please ensure your server/workers are updated")
|
57
|
+
$stderr.puts("**************************************************")
|
58
|
+
end
|
@@ -8,11 +8,22 @@ module Sidekiq
|
|
8
8
|
class << self
|
9
9
|
|
10
10
|
def create(options={})
|
11
|
-
options
|
11
|
+
options.keys.each do |key|
|
12
|
+
options[key.to_sym] = options.delete(key)
|
13
|
+
end
|
12
14
|
|
15
|
+
options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{$$}" if !options.has_key?(:id)
|
13
16
|
options[:url] ||= determine_redis_provider
|
14
17
|
|
15
|
-
size = options[:size]
|
18
|
+
size = if options[:size]
|
19
|
+
options[:size]
|
20
|
+
elsif Sidekiq.server?
|
21
|
+
Sidekiq.options[:concurrency] + 5
|
22
|
+
elsif ENV['RAILS_MAX_THREADS']
|
23
|
+
Integer(ENV['RAILS_MAX_THREADS'])
|
24
|
+
else
|
25
|
+
5
|
26
|
+
end
|
16
27
|
|
17
28
|
verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
|
18
29
|
|
@@ -35,7 +46,7 @@ module Sidekiq
|
|
35
46
|
# - enterprise's leader election
|
36
47
|
# - enterprise's cron support
|
37
48
|
def verify_sizing(size, concurrency)
|
38
|
-
raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but
|
49
|
+
raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but must have at least #{concurrency + 2}" if size <= concurrency
|
39
50
|
end
|
40
51
|
|
41
52
|
def build_client(options)
|
@@ -67,7 +78,7 @@ module Sidekiq
|
|
67
78
|
opts.delete(:network_timeout)
|
68
79
|
end
|
69
80
|
|
70
|
-
opts[:driver] ||= 'ruby'
|
81
|
+
opts[:driver] ||= Redis::Connection.drivers.last || 'ruby'
|
71
82
|
|
72
83
|
# Issue #3303, redis-rb will silently retry an operation.
|
73
84
|
# This can lead to duplicate jobs if Sidekiq::Client's LPUSH
|
@@ -98,7 +109,34 @@ module Sidekiq
|
|
98
109
|
end
|
99
110
|
|
100
111
|
def determine_redis_provider
|
101
|
-
|
112
|
+
# If you have this in your environment:
|
113
|
+
# MY_REDIS_URL=redis://hostname.example.com:1238/4
|
114
|
+
# then set:
|
115
|
+
# REDIS_PROVIDER=MY_REDIS_URL
|
116
|
+
# and Sidekiq will find your custom URL variable with no custom
|
117
|
+
# initialization code at all.
|
118
|
+
p = ENV['REDIS_PROVIDER']
|
119
|
+
if p && p =~ /\:/
|
120
|
+
Sidekiq.logger.error <<-EOM
|
121
|
+
|
122
|
+
#################################################################################
|
123
|
+
|
124
|
+
REDIS_PROVIDER should be set to the **name** of the variable which contains the Redis URL, not a URL itself.
|
125
|
+
Platforms like Heroku sell addons that publish a *_URL variable. You tell Sidekiq with REDIS_PROVIDER, e.g.:
|
126
|
+
|
127
|
+
REDIS_PROVIDER=REDISTOGO_URL
|
128
|
+
REDISTOGO_URL=redis://somehost.example.com:6379/4
|
129
|
+
|
130
|
+
Use REDIS_URL if you wish to point Sidekiq to a URL directly.
|
131
|
+
|
132
|
+
This configuration error will crash starting in Sidekiq 5.3.
|
133
|
+
|
134
|
+
#################################################################################
|
135
|
+
EOM
|
136
|
+
end
|
137
|
+
ENV[
|
138
|
+
ENV['REDIS_PROVIDER'] || 'REDIS_URL'
|
139
|
+
]
|
102
140
|
end
|
103
141
|
|
104
142
|
end
|
data/lib/sidekiq/scheduled.rb
CHANGED
@@ -17,7 +17,7 @@ module Sidekiq
|
|
17
17
|
# We need to go through the list one at a time to reduce the risk of something
|
18
18
|
# going wrong between the time jobs are popped from the scheduled queue and when
|
19
19
|
# they are pushed onto a work queue and losing the jobs.
|
20
|
-
while job = conn.zrangebyscore(sorted_set, '-inf'
|
20
|
+
while job = conn.zrangebyscore(sorted_set, '-inf', now, :limit => [0, 1]).first do
|
21
21
|
|
22
22
|
# Pop item off the queue and add it to the work queue. If the job can't be popped from
|
23
23
|
# the queue, it's because another process already popped it so we can move on to the
|
@@ -79,9 +79,7 @@ module Sidekiq
|
|
79
79
|
# Most likely a problem with redis networking.
|
80
80
|
# Punt and try again at the next interval
|
81
81
|
logger.error ex.message
|
82
|
-
ex
|
83
|
-
logger.error(bt)
|
84
|
-
end
|
82
|
+
handle_exception(ex)
|
85
83
|
end
|
86
84
|
end
|
87
85
|
|
@@ -95,13 +93,38 @@ module Sidekiq
|
|
95
93
|
# if poll_interval_average hasn't been calculated yet, we can
|
96
94
|
# raise an error trying to reach Redis.
|
97
95
|
logger.error ex.message
|
98
|
-
|
96
|
+
handle_exception(ex)
|
99
97
|
sleep 5
|
100
98
|
end
|
101
99
|
|
102
|
-
# Calculates a random interval that is ±50% the desired average.
|
103
100
|
def random_poll_interval
|
104
|
-
|
101
|
+
# We want one Sidekiq process to schedule jobs every N seconds. We have M processes
|
102
|
+
# and **don't** want to coordinate.
|
103
|
+
#
|
104
|
+
# So in N*M second timespan, we want each process to schedule once. The basic loop is:
|
105
|
+
#
|
106
|
+
# * sleep a random amount within that N*M timespan
|
107
|
+
# * wake up and schedule
|
108
|
+
#
|
109
|
+
# We want to avoid one edge case: imagine a set of 2 processes, scheduling every 5 seconds,
|
110
|
+
# so N*M = 10. Each process decides to randomly sleep 8 seconds, now we've failed to meet
|
111
|
+
# that 5 second average. Thankfully each schedule cycle will sleep randomly so the next
|
112
|
+
# iteration could see each process sleep for 1 second, undercutting our average.
|
113
|
+
#
|
114
|
+
# So below 10 processes, we special case and ensure the processes sleep closer to the average.
|
115
|
+
# In the example above, each process should schedule every 10 seconds on average. We special
|
116
|
+
# case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
|
117
|
+
# As we run more processes, the scheduling interval average will approach an even spread
|
118
|
+
# between 0 and poll interval so we don't need this artifical boost.
|
119
|
+
#
|
120
|
+
if process_count < 10
|
121
|
+
# For small clusters, calculate a random interval that is ±50% the desired average.
|
122
|
+
poll_interval_average * rand + poll_interval_average.to_f / 2
|
123
|
+
else
|
124
|
+
# With 10+ processes, we should have enough randomness to get decent polling
|
125
|
+
# across the entire timespan
|
126
|
+
poll_interval_average * rand
|
127
|
+
end
|
105
128
|
end
|
106
129
|
|
107
130
|
# We do our best to tune the poll interval to the size of the active Sidekiq
|
@@ -125,9 +148,13 @@ module Sidekiq
|
|
125
148
|
# This minimizes a single point of failure by dispersing check-ins but without taxing
|
126
149
|
# Redis if you run many Sidekiq processes.
|
127
150
|
def scaled_poll_interval
|
151
|
+
process_count * Sidekiq.options[:average_scheduled_poll_interval]
|
152
|
+
end
|
153
|
+
|
154
|
+
def process_count
|
128
155
|
pcount = Sidekiq::ProcessSet.new.size
|
129
156
|
pcount = 1 if pcount == 0
|
130
|
-
pcount
|
157
|
+
pcount
|
131
158
|
end
|
132
159
|
|
133
160
|
def initial_wait
|
data/lib/sidekiq/testing.rb
CHANGED
@@ -55,6 +55,15 @@ module Sidekiq
|
|
55
55
|
yield @server_chain if block_given?
|
56
56
|
@server_chain
|
57
57
|
end
|
58
|
+
|
59
|
+
def constantize(str)
|
60
|
+
names = str.split('::')
|
61
|
+
names.shift if names.empty? || names.first.empty?
|
62
|
+
|
63
|
+
names.inject(Object) do |constant, name|
|
64
|
+
constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
65
|
+
end
|
66
|
+
end
|
58
67
|
end
|
59
68
|
end
|
60
69
|
|
@@ -63,9 +72,7 @@ module Sidekiq
|
|
63
72
|
|
64
73
|
class EmptyQueueError < RuntimeError; end
|
65
74
|
|
66
|
-
|
67
|
-
alias_method :raw_push_real, :raw_push
|
68
|
-
|
75
|
+
module TestingClient
|
69
76
|
def raw_push(payloads)
|
70
77
|
if Sidekiq::Testing.fake?
|
71
78
|
payloads.each do |job|
|
@@ -76,18 +83,20 @@ module Sidekiq
|
|
76
83
|
true
|
77
84
|
elsif Sidekiq::Testing.inline?
|
78
85
|
payloads.each do |job|
|
79
|
-
klass = job['class']
|
86
|
+
klass = Sidekiq::Testing.constantize(job['class'])
|
80
87
|
job['id'] ||= SecureRandom.hex(12)
|
81
88
|
job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
|
82
89
|
klass.process_job(job_hash)
|
83
90
|
end
|
84
91
|
true
|
85
92
|
else
|
86
|
-
|
93
|
+
super
|
87
94
|
end
|
88
95
|
end
|
89
96
|
end
|
90
97
|
|
98
|
+
Sidekiq::Client.prepend TestingClient
|
99
|
+
|
91
100
|
module Queues
|
92
101
|
##
|
93
102
|
# The Queues class is only for testing the fake queue implementation.
|
@@ -309,7 +318,7 @@ module Sidekiq
|
|
309
318
|
worker_classes = jobs.map { |job| job["class"] }.uniq
|
310
319
|
|
311
320
|
worker_classes.each do |worker_class|
|
312
|
-
|
321
|
+
Sidekiq::Testing.constantize(worker_class).drain
|
313
322
|
end
|
314
323
|
end
|
315
324
|
end
|
@@ -317,7 +326,7 @@ module Sidekiq
|
|
317
326
|
end
|
318
327
|
end
|
319
328
|
|
320
|
-
if defined?(::Rails) && !Rails.env.test?
|
329
|
+
if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test?
|
321
330
|
puts("**************************************************")
|
322
331
|
puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
|
323
332
|
puts("**************************************************")
|