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/api.rb
CHANGED
@@ -1,9 +1,24 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
require 'sidekiq'
|
4
3
|
|
5
4
|
module Sidekiq
|
5
|
+
|
6
|
+
module RedisScanner
|
7
|
+
def sscan(conn, key)
|
8
|
+
cursor = '0'
|
9
|
+
result = []
|
10
|
+
loop do
|
11
|
+
cursor, values = conn.sscan(key, cursor)
|
12
|
+
result.push(*values)
|
13
|
+
break if cursor == '0'
|
14
|
+
end
|
15
|
+
result
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
6
19
|
class Stats
|
20
|
+
include RedisScanner
|
21
|
+
|
7
22
|
def initialize
|
8
23
|
fetch_stats!
|
9
24
|
end
|
@@ -51,33 +66,39 @@ module Sidekiq
|
|
51
66
|
def fetch_stats!
|
52
67
|
pipe1_res = Sidekiq.redis do |conn|
|
53
68
|
conn.pipelined do
|
54
|
-
conn.get('stat:processed'
|
55
|
-
conn.get('stat:failed'
|
56
|
-
conn.zcard('schedule'
|
57
|
-
conn.zcard('retry'
|
58
|
-
conn.zcard('dead'
|
59
|
-
conn.scard('processes'
|
60
|
-
conn.lrange('queue:default'
|
61
|
-
conn.smembers('processes'.freeze)
|
62
|
-
conn.smembers('queues'.freeze)
|
69
|
+
conn.get('stat:processed')
|
70
|
+
conn.get('stat:failed')
|
71
|
+
conn.zcard('schedule')
|
72
|
+
conn.zcard('retry')
|
73
|
+
conn.zcard('dead')
|
74
|
+
conn.scard('processes')
|
75
|
+
conn.lrange('queue:default', -1, -1)
|
63
76
|
end
|
64
77
|
end
|
65
78
|
|
79
|
+
processes = Sidekiq.redis do |conn|
|
80
|
+
sscan(conn, 'processes')
|
81
|
+
end
|
82
|
+
|
83
|
+
queues = Sidekiq.redis do |conn|
|
84
|
+
sscan(conn, 'queues')
|
85
|
+
end
|
86
|
+
|
66
87
|
pipe2_res = Sidekiq.redis do |conn|
|
67
88
|
conn.pipelined do
|
68
|
-
|
69
|
-
|
89
|
+
processes.each {|key| conn.hget(key, 'busy') }
|
90
|
+
queues.each {|queue| conn.llen("queue:#{queue}") }
|
70
91
|
end
|
71
92
|
end
|
72
93
|
|
73
|
-
s =
|
94
|
+
s = processes.size
|
74
95
|
workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
|
75
96
|
enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
|
76
97
|
|
77
98
|
default_queue_latency = if (entry = pipe1_res[6].first)
|
78
|
-
job = Sidekiq.load_json(entry)
|
99
|
+
job = Sidekiq.load_json(entry) rescue {}
|
79
100
|
now = Time.now.to_f
|
80
|
-
thence = job['enqueued_at'
|
101
|
+
thence = job['enqueued_at'] || now
|
81
102
|
now - thence
|
82
103
|
else
|
83
104
|
0
|
@@ -117,9 +138,11 @@ module Sidekiq
|
|
117
138
|
end
|
118
139
|
|
119
140
|
class Queues
|
141
|
+
include RedisScanner
|
142
|
+
|
120
143
|
def lengths
|
121
144
|
Sidekiq.redis do |conn|
|
122
|
-
queues = conn
|
145
|
+
queues = sscan(conn, 'queues')
|
123
146
|
|
124
147
|
lengths = conn.pipelined do
|
125
148
|
queues.each do |queue|
|
@@ -141,16 +164,18 @@ module Sidekiq
|
|
141
164
|
|
142
165
|
class History
|
143
166
|
def initialize(days_previous, start_date = nil)
|
167
|
+
#we only store five years of data in Redis
|
168
|
+
raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
|
144
169
|
@days_previous = days_previous
|
145
170
|
@start_date = start_date || Time.now.utc.to_date
|
146
171
|
end
|
147
172
|
|
148
173
|
def processed
|
149
|
-
date_stat_hash("processed")
|
174
|
+
@processed ||= date_stat_hash("processed")
|
150
175
|
end
|
151
176
|
|
152
177
|
def failed
|
153
|
-
date_stat_hash("failed")
|
178
|
+
@failed ||= date_stat_hash("failed")
|
154
179
|
end
|
155
180
|
|
156
181
|
private
|
@@ -163,16 +188,21 @@ module Sidekiq
|
|
163
188
|
|
164
189
|
while i < @days_previous
|
165
190
|
date = @start_date - i
|
166
|
-
datestr = date.strftime("%Y-%m-%d"
|
191
|
+
datestr = date.strftime("%Y-%m-%d")
|
167
192
|
keys << "stat:#{stat}:#{datestr}"
|
168
193
|
dates << datestr
|
169
194
|
i += 1
|
170
195
|
end
|
171
196
|
|
172
|
-
|
173
|
-
|
174
|
-
|
197
|
+
begin
|
198
|
+
Sidekiq.redis do |conn|
|
199
|
+
conn.mget(keys).each_with_index do |value, idx|
|
200
|
+
stat_hash[dates[idx]] = value ? value.to_i : 0
|
201
|
+
end
|
175
202
|
end
|
203
|
+
rescue Redis::CommandError
|
204
|
+
# mget will trigger a CROSSSLOT error when run against a Cluster
|
205
|
+
# TODO Someone want to add Cluster support?
|
176
206
|
end
|
177
207
|
|
178
208
|
stat_hash
|
@@ -194,18 +224,19 @@ module Sidekiq
|
|
194
224
|
#
|
195
225
|
class Queue
|
196
226
|
include Enumerable
|
227
|
+
extend RedisScanner
|
197
228
|
|
198
229
|
##
|
199
230
|
# Return all known queues within Redis.
|
200
231
|
#
|
201
232
|
def self.all
|
202
|
-
Sidekiq.redis { |c| c
|
233
|
+
Sidekiq.redis { |c| sscan(c, 'queues') }.sort.map { |q| Sidekiq::Queue.new(q) }
|
203
234
|
end
|
204
235
|
|
205
236
|
attr_reader :name
|
206
237
|
|
207
238
|
def initialize(name="default")
|
208
|
-
@name = name
|
239
|
+
@name = name.to_s
|
209
240
|
@rname = "queue:#{name}"
|
210
241
|
end
|
211
242
|
|
@@ -268,7 +299,7 @@ module Sidekiq
|
|
268
299
|
Sidekiq.redis do |conn|
|
269
300
|
conn.multi do
|
270
301
|
conn.del(@rname)
|
271
|
-
conn.srem("queues"
|
302
|
+
conn.srem("queues", name)
|
272
303
|
end
|
273
304
|
end
|
274
305
|
end
|
@@ -287,13 +318,25 @@ module Sidekiq
|
|
287
318
|
attr_reader :value
|
288
319
|
|
289
320
|
def initialize(item, queue_name=nil)
|
321
|
+
@args = nil
|
290
322
|
@value = item
|
291
|
-
@item = item.is_a?(Hash) ? item :
|
323
|
+
@item = item.is_a?(Hash) ? item : parse(item)
|
292
324
|
@queue = queue_name || @item['queue']
|
293
325
|
end
|
294
326
|
|
327
|
+
def parse(item)
|
328
|
+
Sidekiq.load_json(item)
|
329
|
+
rescue JSON::ParserError
|
330
|
+
# If the job payload in Redis is invalid JSON, we'll load
|
331
|
+
# the item as an empty hash and store the invalid JSON as
|
332
|
+
# the job 'args' for display in the Web UI.
|
333
|
+
@invalid = true
|
334
|
+
@args = [item]
|
335
|
+
{}
|
336
|
+
end
|
337
|
+
|
295
338
|
def klass
|
296
|
-
|
339
|
+
self['class']
|
297
340
|
end
|
298
341
|
|
299
342
|
def display_class
|
@@ -318,38 +361,42 @@ module Sidekiq
|
|
318
361
|
|
319
362
|
def display_args
|
320
363
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
321
|
-
@
|
364
|
+
@display_args ||= case klass
|
322
365
|
when /\ASidekiq::Extensions::Delayed/
|
323
366
|
safe_load(args[0], args) do |_, _, arg|
|
324
367
|
arg
|
325
368
|
end
|
326
369
|
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
327
|
-
job_args =
|
328
|
-
if 'ActionMailer::DeliveryJob' == (
|
329
|
-
|
330
|
-
|
370
|
+
job_args = self['wrapped'] ? args[0]["arguments"] : []
|
371
|
+
if 'ActionMailer::DeliveryJob' == (self['wrapped'] || args[0])
|
372
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
373
|
+
job_args.drop(3)
|
331
374
|
else
|
332
|
-
|
375
|
+
job_args
|
333
376
|
end
|
334
377
|
else
|
378
|
+
if self['encrypt']
|
379
|
+
# no point in showing 150+ bytes of random garbage
|
380
|
+
args[-1] = '[encrypted data]'
|
381
|
+
end
|
335
382
|
args
|
336
383
|
end
|
337
384
|
end
|
338
385
|
|
339
386
|
def args
|
340
|
-
@item['args']
|
387
|
+
@args || @item['args']
|
341
388
|
end
|
342
389
|
|
343
390
|
def jid
|
344
|
-
|
391
|
+
self['jid']
|
345
392
|
end
|
346
393
|
|
347
394
|
def enqueued_at
|
348
|
-
|
395
|
+
self['enqueued_at'] ? Time.at(self['enqueued_at']).utc : nil
|
349
396
|
end
|
350
397
|
|
351
398
|
def created_at
|
352
|
-
Time.at(
|
399
|
+
Time.at(self['created_at'] || self['enqueued_at'] || 0).utc
|
353
400
|
end
|
354
401
|
|
355
402
|
def queue
|
@@ -371,7 +418,10 @@ module Sidekiq
|
|
371
418
|
end
|
372
419
|
|
373
420
|
def [](name)
|
374
|
-
|
421
|
+
# nil will happen if the JSON fails to parse.
|
422
|
+
# We don't guarantee Sidekiq will work with bad job JSON but we should
|
423
|
+
# make a best effort to minimize the damage.
|
424
|
+
@item ? @item[name] : nil
|
375
425
|
end
|
376
426
|
|
377
427
|
private
|
@@ -434,14 +484,7 @@ module Sidekiq
|
|
434
484
|
# Place job in the dead set
|
435
485
|
def kill
|
436
486
|
remove_job do |message|
|
437
|
-
|
438
|
-
Sidekiq.redis do |conn|
|
439
|
-
conn.multi do
|
440
|
-
conn.zadd('dead', now, message)
|
441
|
-
conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
|
442
|
-
conn.zremrangebyrank('dead', 0, - DeadSet.max_jobs)
|
443
|
-
end
|
444
|
-
end
|
487
|
+
DeadSet.new.kill(message)
|
445
488
|
end
|
446
489
|
end
|
447
490
|
|
@@ -531,7 +574,7 @@ module Sidekiq
|
|
531
574
|
end
|
532
575
|
break if elements.empty?
|
533
576
|
page -= 1
|
534
|
-
elements.each do |element, score|
|
577
|
+
elements.reverse.each do |element, score|
|
535
578
|
yield SortedEntry.new(self, score, element)
|
536
579
|
end
|
537
580
|
offset_size = initial_size - @_size
|
@@ -629,6 +672,12 @@ module Sidekiq
|
|
629
672
|
each(&:retry)
|
630
673
|
end
|
631
674
|
end
|
675
|
+
|
676
|
+
def kill_all
|
677
|
+
while size > 0
|
678
|
+
each(&:kill)
|
679
|
+
end
|
680
|
+
end
|
632
681
|
end
|
633
682
|
|
634
683
|
##
|
@@ -639,6 +688,27 @@ module Sidekiq
|
|
639
688
|
super 'dead'
|
640
689
|
end
|
641
690
|
|
691
|
+
def kill(message, opts={})
|
692
|
+
now = Time.now.to_f
|
693
|
+
Sidekiq.redis do |conn|
|
694
|
+
conn.multi do
|
695
|
+
conn.zadd(name, now.to_s, message)
|
696
|
+
conn.zremrangebyscore(name, '-inf', now - self.class.timeout)
|
697
|
+
conn.zremrangebyrank(name, 0, - self.class.max_jobs)
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
if opts[:notify_failure] != false
|
702
|
+
job = Sidekiq.load_json(message)
|
703
|
+
r = RuntimeError.new("Job killed by API")
|
704
|
+
r.set_backtrace(caller)
|
705
|
+
Sidekiq.death_handlers.each do |handle|
|
706
|
+
handle.call(job, r)
|
707
|
+
end
|
708
|
+
end
|
709
|
+
true
|
710
|
+
end
|
711
|
+
|
642
712
|
def retry_all
|
643
713
|
while size > 0
|
644
714
|
each(&:retry)
|
@@ -663,17 +733,18 @@ module Sidekiq
|
|
663
733
|
#
|
664
734
|
class ProcessSet
|
665
735
|
include Enumerable
|
736
|
+
include RedisScanner
|
666
737
|
|
667
738
|
def initialize(clean_plz=true)
|
668
|
-
|
739
|
+
cleanup if clean_plz
|
669
740
|
end
|
670
741
|
|
671
742
|
# Cleans up dead processes recorded in Redis.
|
672
743
|
# Returns the number of processes cleaned.
|
673
|
-
def
|
744
|
+
def cleanup
|
674
745
|
count = 0
|
675
746
|
Sidekiq.redis do |conn|
|
676
|
-
procs = conn
|
747
|
+
procs = sscan(conn, 'processes').sort
|
677
748
|
heartbeats = conn.pipelined do
|
678
749
|
procs.each do |key|
|
679
750
|
conn.hget(key, 'info')
|
@@ -693,7 +764,7 @@ module Sidekiq
|
|
693
764
|
end
|
694
765
|
|
695
766
|
def each
|
696
|
-
procs = Sidekiq.redis { |conn| conn
|
767
|
+
procs = Sidekiq.redis { |conn| sscan(conn, 'processes') }.sort
|
697
768
|
|
698
769
|
Sidekiq.redis do |conn|
|
699
770
|
# We're making a tradeoff here between consuming more memory instead of
|
@@ -706,6 +777,11 @@ module Sidekiq
|
|
706
777
|
end
|
707
778
|
|
708
779
|
result.each do |info, busy, at_s, quiet|
|
780
|
+
# If a process is stopped between when we query Redis for `procs` and
|
781
|
+
# when we query for `result`, we will have an item in `result` that is
|
782
|
+
# composed of `nil` values.
|
783
|
+
next if info.nil?
|
784
|
+
|
709
785
|
hash = Sidekiq.load_json(info)
|
710
786
|
yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
|
711
787
|
end
|
@@ -721,6 +797,18 @@ module Sidekiq
|
|
721
797
|
def size
|
722
798
|
Sidekiq.redis { |conn| conn.scard('processes') }
|
723
799
|
end
|
800
|
+
|
801
|
+
# Returns the identity of the current cluster leader or "" if no leader.
|
802
|
+
# This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
|
803
|
+
# or Sidekiq Pro.
|
804
|
+
def leader
|
805
|
+
@leader ||= begin
|
806
|
+
x = Sidekiq.redis {|c| c.get("dear-leader") }
|
807
|
+
# need a non-falsy value so we can memoize
|
808
|
+
x = "" unless x
|
809
|
+
x
|
810
|
+
end
|
811
|
+
end
|
724
812
|
end
|
725
813
|
|
726
814
|
#
|
@@ -755,8 +843,12 @@ module Sidekiq
|
|
755
843
|
@attribs[key]
|
756
844
|
end
|
757
845
|
|
846
|
+
def identity
|
847
|
+
self['identity']
|
848
|
+
end
|
849
|
+
|
758
850
|
def quiet!
|
759
|
-
signal('
|
851
|
+
signal('TSTP')
|
760
852
|
end
|
761
853
|
|
762
854
|
def stop!
|
@@ -783,9 +875,6 @@ module Sidekiq
|
|
783
875
|
end
|
784
876
|
end
|
785
877
|
|
786
|
-
def identity
|
787
|
-
self['identity']
|
788
|
-
end
|
789
878
|
end
|
790
879
|
|
791
880
|
##
|
@@ -810,13 +899,14 @@ module Sidekiq
|
|
810
899
|
#
|
811
900
|
class Workers
|
812
901
|
include Enumerable
|
902
|
+
include RedisScanner
|
813
903
|
|
814
904
|
def each
|
815
905
|
Sidekiq.redis do |conn|
|
816
|
-
procs = conn
|
906
|
+
procs = sscan(conn, 'processes')
|
817
907
|
procs.sort.each do |key|
|
818
908
|
valid, workers = conn.pipelined do
|
819
|
-
conn.exists(key)
|
909
|
+
conn.exists?(key)
|
820
910
|
conn.hgetall("#{key}:workers")
|
821
911
|
end
|
822
912
|
next unless valid
|
@@ -835,7 +925,7 @@ module Sidekiq
|
|
835
925
|
# which can easily get out of sync with crashy processes.
|
836
926
|
def size
|
837
927
|
Sidekiq.redis do |conn|
|
838
|
-
procs = conn
|
928
|
+
procs = sscan(conn, 'processes')
|
839
929
|
if procs.empty?
|
840
930
|
0
|
841
931
|
else
|