sidekiq 5.1.3 → 5.2.9

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.

Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +61 -0
  3. data/.gitignore +2 -0
  4. data/.travis.yml +2 -5
  5. data/COMM-LICENSE +11 -9
  6. data/Changes.md +73 -0
  7. data/Ent-Changes.md +22 -0
  8. data/Gemfile +20 -5
  9. data/Pro-Changes.md +30 -0
  10. data/README.md +1 -1
  11. data/Rakefile +2 -1
  12. data/bin/sidekiqctl +13 -92
  13. data/bin/sidekiqload +1 -1
  14. data/lib/sidekiq.rb +5 -3
  15. data/lib/sidekiq/api.rb +47 -14
  16. data/lib/sidekiq/cli.rb +64 -58
  17. data/lib/sidekiq/client.rb +4 -3
  18. data/lib/sidekiq/ctl.rb +221 -0
  19. data/lib/sidekiq/job_logger.rb +2 -2
  20. data/lib/sidekiq/job_retry.rb +33 -12
  21. data/lib/sidekiq/launcher.rb +14 -7
  22. data/lib/sidekiq/manager.rb +3 -3
  23. data/lib/sidekiq/middleware/server/active_record.rb +1 -1
  24. data/lib/sidekiq/processor.rb +76 -25
  25. data/lib/sidekiq/rails.rb +2 -1
  26. data/lib/sidekiq/redis_connection.rb +20 -1
  27. data/lib/sidekiq/scheduled.rb +32 -3
  28. data/lib/sidekiq/testing.rb +4 -4
  29. data/lib/sidekiq/version.rb +1 -1
  30. data/lib/sidekiq/web/application.rb +22 -0
  31. data/lib/sidekiq/web/helpers.rb +13 -6
  32. data/lib/sidekiq/worker.rb +24 -8
  33. data/sidekiq.gemspec +5 -12
  34. data/web/assets/javascripts/application.js +0 -0
  35. data/web/assets/javascripts/dashboard.js +15 -5
  36. data/web/assets/stylesheets/application.css +35 -2
  37. data/web/assets/stylesheets/bootstrap.css +2 -2
  38. data/web/locales/ar.yml +1 -0
  39. data/web/locales/en.yml +1 -0
  40. data/web/locales/es.yml +3 -3
  41. data/web/views/_nav.erb +3 -17
  42. data/web/views/layout.erb +1 -1
  43. data/web/views/queue.erb +1 -0
  44. data/web/views/queues.erb +1 -1
  45. data/web/views/retries.erb +4 -0
  46. metadata +12 -79
@@ -65,7 +65,7 @@ def handle_signal(launcher, sig)
65
65
  # http://jira.codehaus.org/browse/JRUBY-4637
66
66
  raise Interrupt
67
67
  when 'TERM'
68
- # Heroku sends TERM and then waits 10 seconds for process to exit.
68
+ # Heroku sends TERM and then waits 30 seconds for process to exit.
69
69
  raise Interrupt
70
70
  when 'TSTP'
71
71
  Sidekiq.logger.info "Received TSTP, no longer accepting new work"
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'sidekiq/version'
3
4
  fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.2.2." if RUBY_PLATFORM != 'java' && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.2')
4
5
 
@@ -17,7 +18,7 @@ module Sidekiq
17
18
  DEFAULTS = {
18
19
  queues: [],
19
20
  labels: [],
20
- concurrency: 25,
21
+ concurrency: 10,
21
22
  require: '.',
22
23
  environment: nil,
23
24
  timeout: 8,
@@ -56,6 +57,7 @@ module Sidekiq
56
57
  def self.options
57
58
  @options ||= DEFAULTS.dup
58
59
  end
60
+
59
61
  def self.options=(opts)
60
62
  @options = opts
61
63
  end
@@ -94,8 +96,8 @@ module Sidekiq
94
96
  begin
95
97
  yield conn
96
98
  rescue Redis::CommandError => ex
97
- #2550 Failover can cause the server to become a slave, need
98
- # to disconnect and reopen the socket to get back to the master.
99
+ #2550 Failover can cause the server to become a replica, need
100
+ # to disconnect and reopen the socket to get back to the primary.
99
101
  (conn.disconnect!; retryable = false; retry) if retryable && ex.message =~ /READONLY/
100
102
  raise
101
103
  end
@@ -2,7 +2,23 @@
2
2
  require 'sidekiq'
3
3
 
4
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
+
5
19
  class Stats
20
+ include RedisScanner
21
+
6
22
  def initialize
7
23
  fetch_stats!
8
24
  end
@@ -57,19 +73,25 @@ module Sidekiq
57
73
  conn.zcard('dead')
58
74
  conn.scard('processes')
59
75
  conn.lrange('queue:default', -1, -1)
60
- conn.smembers('processes')
61
- conn.smembers('queues')
62
76
  end
63
77
  end
64
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
+
65
87
  pipe2_res = Sidekiq.redis do |conn|
66
88
  conn.pipelined do
67
- pipe1_res[7].each {|key| conn.hget(key, 'busy') }
68
- pipe1_res[8].each {|queue| conn.llen("queue:#{queue}") }
89
+ processes.each {|key| conn.hget(key, 'busy') }
90
+ queues.each {|queue| conn.llen("queue:#{queue}") }
69
91
  end
70
92
  end
71
93
 
72
- s = pipe1_res[7].size
94
+ s = processes.size
73
95
  workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
74
96
  enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
75
97
 
@@ -116,9 +138,11 @@ module Sidekiq
116
138
  end
117
139
 
118
140
  class Queues
141
+ include RedisScanner
142
+
119
143
  def lengths
120
144
  Sidekiq.redis do |conn|
121
- queues = conn.smembers('queues')
145
+ queues = sscan(conn, 'queues')
122
146
 
123
147
  lengths = conn.pipelined do
124
148
  queues.each do |queue|
@@ -198,18 +222,19 @@ module Sidekiq
198
222
  #
199
223
  class Queue
200
224
  include Enumerable
225
+ extend RedisScanner
201
226
 
202
227
  ##
203
228
  # Return all known queues within Redis.
204
229
  #
205
230
  def self.all
206
- Sidekiq.redis { |c| c.smembers('queues') }.sort.map { |q| Sidekiq::Queue.new(q) }
231
+ Sidekiq.redis { |c| sscan(c, 'queues') }.sort.map { |q| Sidekiq::Queue.new(q) }
207
232
  end
208
233
 
209
234
  attr_reader :name
210
235
 
211
236
  def initialize(name="default")
212
- @name = name
237
+ @name = name.to_s
213
238
  @rname = "queue:#{name}"
214
239
  end
215
240
 
@@ -645,6 +670,12 @@ module Sidekiq
645
670
  each(&:retry)
646
671
  end
647
672
  end
673
+
674
+ def kill_all
675
+ while size > 0
676
+ each(&:kill)
677
+ end
678
+ end
648
679
  end
649
680
 
650
681
  ##
@@ -700,17 +731,18 @@ module Sidekiq
700
731
  #
701
732
  class ProcessSet
702
733
  include Enumerable
734
+ include RedisScanner
703
735
 
704
736
  def initialize(clean_plz=true)
705
- self.class.cleanup if clean_plz
737
+ cleanup if clean_plz
706
738
  end
707
739
 
708
740
  # Cleans up dead processes recorded in Redis.
709
741
  # Returns the number of processes cleaned.
710
- def self.cleanup
742
+ def cleanup
711
743
  count = 0
712
744
  Sidekiq.redis do |conn|
713
- procs = conn.smembers('processes').sort
745
+ procs = sscan(conn, 'processes').sort
714
746
  heartbeats = conn.pipelined do
715
747
  procs.each do |key|
716
748
  conn.hget(key, 'info')
@@ -730,7 +762,7 @@ module Sidekiq
730
762
  end
731
763
 
732
764
  def each
733
- procs = Sidekiq.redis { |conn| conn.smembers('processes') }.sort
765
+ procs = Sidekiq.redis { |conn| sscan(conn, 'processes') }.sort
734
766
 
735
767
  Sidekiq.redis do |conn|
736
768
  # We're making a tradeoff here between consuming more memory instead of
@@ -865,10 +897,11 @@ module Sidekiq
865
897
  #
866
898
  class Workers
867
899
  include Enumerable
900
+ include RedisScanner
868
901
 
869
902
  def each
870
903
  Sidekiq.redis do |conn|
871
- procs = conn.smembers('processes')
904
+ procs = sscan(conn, 'processes')
872
905
  procs.sort.each do |key|
873
906
  valid, workers = conn.pipelined do
874
907
  conn.exists(key)
@@ -890,7 +923,7 @@ module Sidekiq
890
923
  # which can easily get out of sync with crashy processes.
891
924
  def size
892
925
  Sidekiq.redis do |conn|
893
- procs = conn.smembers('processes')
926
+ procs = sscan(conn, 'processes')
894
927
  if procs.empty?
895
928
  0
896
929
  else
@@ -9,6 +9,7 @@ require 'fileutils'
9
9
 
10
10
  require 'sidekiq'
11
11
  require 'sidekiq/util'
12
+ require 'sidekiq/launcher'
12
13
 
13
14
  module Sidekiq
14
15
  class CLI
@@ -23,23 +24,13 @@ module Sidekiq
23
24
  proc { |me, data| "stopping" if me.stopping? },
24
25
  ]
25
26
 
26
- # Used for CLI testing
27
- attr_accessor :code
28
27
  attr_accessor :launcher
29
28
  attr_accessor :environment
30
29
 
31
- def initialize
32
- @code = nil
33
- end
34
-
35
- def parse(args=ARGV)
36
- @code = nil
37
-
30
+ def parse(args = ARGV)
38
31
  setup_options(args)
39
32
  initialize_logger
40
33
  validate!
41
- daemonize
42
- write_pid
43
34
  end
44
35
 
45
36
  def jruby?
@@ -50,8 +41,10 @@ module Sidekiq
50
41
  # global process state irreversibly. PRs which improve the
51
42
  # test coverage of Sidekiq::CLI are welcomed.
52
43
  def run
44
+ daemonize if options[:daemon]
45
+ write_pid
53
46
  boot_system
54
- print_banner
47
+ print_banner if environment == 'development' && $stdout.tty?
55
48
 
56
49
  self_read, self_write = IO.pipe
57
50
  sigs = %w(INT TERM TTIN TSTP)
@@ -79,6 +72,13 @@ module Sidekiq
79
72
  # fire startup and start multithreading.
80
73
  ver = Sidekiq.redis_info['redis_version']
81
74
  raise "You are using Redis v#{ver}, Sidekiq requires Redis v2.8.0 or greater" if ver < '2.8'
75
+ logger.warn "Sidekiq 6.0 will require Redis 4.0+, you are using Redis v#{ver}" if ver < '4'
76
+
77
+ # Since the user can pass us a connection pool explicitly in the initializer, we
78
+ # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
79
+ cursize = Sidekiq.redis_pool.size
80
+ needed = Sidekiq.options[:concurrency] + 2
81
+ raise "Your pool of #{cursize} Redis connections is too small, please increase the size to at least #{needed}" if cursize < needed
82
82
 
83
83
  # cache process identity
84
84
  Sidekiq.options[:identity] = identity
@@ -93,11 +93,14 @@ module Sidekiq
93
93
  logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(', ')}" }
94
94
  logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(', ')}" }
95
95
 
96
+ launch(self_read)
97
+ end
98
+
99
+ def launch(self_read)
96
100
  if !options[:daemon]
97
101
  logger.info 'Starting processing, hit Ctrl-C to stop'
98
102
  end
99
103
 
100
- require 'sidekiq/launcher'
101
104
  @launcher = Sidekiq::Launcher.new(options)
102
105
 
103
106
  begin
@@ -179,23 +182,15 @@ module Sidekiq
179
182
  private
180
183
 
181
184
  def print_banner
182
- # Print logo and banner for development
183
- if environment == 'development' && $stdout.tty?
184
- puts "\e[#{31}m"
185
- puts Sidekiq::CLI.banner
186
- puts "\e[0m"
187
- end
185
+ puts "\e[#{31}m"
186
+ puts Sidekiq::CLI.banner
187
+ puts "\e[0m"
188
188
  end
189
189
 
190
190
  def daemonize
191
- return unless options[:daemon]
192
-
193
191
  raise ArgumentError, "You really should set a logfile if you're going to daemonize" unless options[:logfile]
194
- files_to_reopen = []
195
- ObjectSpace.each_object(File) do |file|
196
- files_to_reopen << file unless file.closed?
197
- end
198
192
 
193
+ files_to_reopen = ObjectSpace.each_object(File).reject { |f| f.closed? }
199
194
  ::Process.daemon(true, true)
200
195
 
201
196
  files_to_reopen.each do |file|
@@ -233,15 +228,38 @@ module Sidekiq
233
228
  alias_method :☠, :exit
234
229
 
235
230
  def setup_options(args)
231
+ # parse CLI options
236
232
  opts = parse_options(args)
233
+
237
234
  set_environment opts[:environment]
238
235
 
239
- cfile = opts[:config_file]
240
- opts = parse_config(cfile).merge(opts) if cfile
236
+ # check config file presence
237
+ if opts[:config_file]
238
+ if opts[:config_file] && !File.exist?(opts[:config_file])
239
+ raise ArgumentError, "No such file #{opts[:config_file]}"
240
+ end
241
+ else
242
+ config_dir = if File.directory?(opts[:require].to_s)
243
+ File.join(opts[:require], 'config')
244
+ else
245
+ File.join(options[:require], 'config')
246
+ end
241
247
 
248
+ %w[sidekiq.yml sidekiq.yml.erb].each do |config_file|
249
+ path = File.join(config_dir, config_file)
250
+ opts[:config_file] ||= path if File.exist?(path)
251
+ end
252
+ end
253
+
254
+ # parse config file options
255
+ opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
256
+
257
+ # set defaults
258
+ opts[:queues] = Array(opts[:queues]) << 'default' if opts[:queues].nil? || opts[:queues].empty?
242
259
  opts[:strict] = true if opts[:strict].nil?
243
- opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if !opts[:concurrency] && ENV["RAILS_MAX_THREADS"]
260
+ opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if opts[:concurrency].nil? && ENV["RAILS_MAX_THREADS"]
244
261
 
262
+ # merge with defaults
245
263
  options.merge!(opts)
246
264
  end
247
265
 
@@ -252,8 +270,6 @@ module Sidekiq
252
270
  def boot_system
253
271
  ENV['RACK_ENV'] = ENV['RAILS_ENV'] = environment
254
272
 
255
- raise ArgumentError, "#{options[:require]} does not exist" unless File.exist?(options[:require])
256
-
257
273
  if File.directory?(options[:require])
258
274
  require 'rails'
259
275
  if ::Rails::VERSION::MAJOR < 4
@@ -273,10 +289,7 @@ module Sidekiq
273
289
  end
274
290
  options[:tag] ||= default_tag
275
291
  else
276
- not_required_message = "#{options[:require]} was not required, you should use an explicit path: " +
277
- "./#{options[:require]} or /path/to/#{options[:require]}"
278
-
279
- require(options[:require]) || raise(ArgumentError, not_required_message)
292
+ require options[:require]
280
293
  end
281
294
  end
282
295
 
@@ -292,8 +305,6 @@ module Sidekiq
292
305
  end
293
306
 
294
307
  def validate!
295
- options[:queues] << 'default' if options[:queues].empty?
296
-
297
308
  if !File.exist?(options[:require]) ||
298
309
  (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
299
310
  logger.info "=================================================================="
@@ -319,6 +330,7 @@ module Sidekiq
319
330
 
320
331
  o.on '-d', '--daemon', "Daemonize process" do |arg|
321
332
  opts[:daemon] = arg
333
+ puts "WARNING: Daemonization mode will be removed in Sidekiq 6.0, see #4045. Please use a proper process supervisor to start and manage your services"
322
334
  end
323
335
 
324
336
  o.on '-e', '--environment ENV', "Application environment" do |arg|
@@ -358,10 +370,12 @@ module Sidekiq
358
370
 
359
371
  o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
360
372
  opts[:logfile] = arg
373
+ puts "WARNING: Logfile redirection will be removed in Sidekiq 6.0, see #4045. Sidekiq will only log to STDOUT"
361
374
  end
362
375
 
363
376
  o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
364
377
  opts[:pidfile] = arg
378
+ puts "WARNING: PID file creation will be removed in Sidekiq 6.0, see #4045. Please use a proper process supervisor to start and manage your services"
365
379
  end
366
380
 
367
381
  o.on '-V', '--version', "Print version and exit" do |arg|
@@ -375,11 +389,8 @@ module Sidekiq
375
389
  logger.info @parser
376
390
  die 1
377
391
  end
378
- @parser.parse!(argv)
379
392
 
380
- %w[config/sidekiq.yml config/sidekiq.yml.erb].each do |filename|
381
- opts[:config_file] ||= filename if File.exist?(filename)
382
- end
393
+ @parser.parse!(argv)
383
394
 
384
395
  opts
385
396
  end
@@ -399,23 +410,18 @@ module Sidekiq
399
410
  end
400
411
  end
401
412
 
402
- def parse_config(cfile)
403
- opts = {}
404
- if File.exist?(cfile)
405
- opts = YAML.load(ERB.new(IO.read(cfile)).result) || opts
406
-
407
- if opts.respond_to? :deep_symbolize_keys!
408
- opts.deep_symbolize_keys!
409
- else
410
- symbolize_keys_deep!(opts)
411
- end
413
+ def parse_config(path)
414
+ opts = YAML.load(ERB.new(File.read(path)).result) || {}
412
415
 
413
- opts = opts.merge(opts.delete(environment.to_sym) || {})
414
- parse_queues(opts, opts.delete(:queues) || [])
416
+ if opts.respond_to? :deep_symbolize_keys!
417
+ opts.deep_symbolize_keys!
415
418
  else
416
- # allow a non-existent config file so Sidekiq
417
- # can be deployed by cap with just the defaults.
419
+ symbolize_keys_deep!(opts)
418
420
  end
421
+
422
+ opts = opts.merge(opts.delete(environment.to_sym) || {})
423
+ parse_queues(opts, opts.delete(:queues) || [])
424
+
419
425
  ns = opts.delete(:namespace)
420
426
  if ns
421
427
  # logger hasn't been initialized yet, puts is all we have.
@@ -429,10 +435,10 @@ module Sidekiq
429
435
  queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
430
436
  end
431
437
 
432
- def parse_queue(opts, q, weight=nil)
433
- [weight.to_i, 1].max.times do
434
- (opts[:queues] ||= []) << q
435
- end
438
+ def parse_queue(opts, queue, weight = nil)
439
+ opts[:queues] ||= []
440
+ raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
441
+ [weight.to_i, 1].max.times { opts[:queues] << queue }
436
442
  opts[:strict] = false if weight.to_i > 0
437
443
  end
438
444
  end
@@ -77,9 +77,10 @@ module Sidekiq
77
77
  end
78
78
 
79
79
  ##
80
- # Push a large number of jobs to Redis. In practice this method is only
81
- # useful if you are pushing thousands of jobs or more. This method
82
- # cuts out the redis network round trip latency.
80
+ # Push a large number of jobs to Redis. This method cuts out the redis
81
+ # network round trip latency. I wouldn't recommend pushing more than
82
+ # 1000 per call but YMMV based on network quality, size of job args, etc.
83
+ # A large number of jobs can cause a bit of Redis command processing latency.
83
84
  #
84
85
  # Takes the same arguments as #push except that args is expected to be
85
86
  # an Array of Arrays. All other keys are duplicated for each job. Each job