sidekiq 5.2.0 → 5.2.8

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.

@@ -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
 
@@ -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
@@ -670,6 +670,12 @@ module Sidekiq
670
670
  each(&:retry)
671
671
  end
672
672
  end
673
+
674
+ def kill_all
675
+ while size > 0
676
+ each(&:kill)
677
+ end
678
+ end
673
679
  end
674
680
 
675
681
  ##
@@ -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,7 @@ 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'
82
76
 
83
77
  # Since the user can pass us a connection pool explicitly in the initializer, we
84
78
  # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
@@ -99,11 +93,14 @@ module Sidekiq
99
93
  logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(', ')}" }
100
94
  logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(', ')}" }
101
95
 
96
+ launch(self_read)
97
+ end
98
+
99
+ def launch(self_read)
102
100
  if !options[:daemon]
103
101
  logger.info 'Starting processing, hit Ctrl-C to stop'
104
102
  end
105
103
 
106
- require 'sidekiq/launcher'
107
104
  @launcher = Sidekiq::Launcher.new(options)
108
105
 
109
106
  begin
@@ -185,23 +182,15 @@ module Sidekiq
185
182
  private
186
183
 
187
184
  def print_banner
188
- # Print logo and banner for development
189
- if environment == 'development' && $stdout.tty?
190
- puts "\e[#{31}m"
191
- puts Sidekiq::CLI.banner
192
- puts "\e[0m"
193
- end
185
+ puts "\e[#{31}m"
186
+ puts Sidekiq::CLI.banner
187
+ puts "\e[0m"
194
188
  end
195
189
 
196
190
  def daemonize
197
- return unless options[:daemon]
198
-
199
191
  raise ArgumentError, "You really should set a logfile if you're going to daemonize" unless options[:logfile]
200
- files_to_reopen = []
201
- ObjectSpace.each_object(File) do |file|
202
- files_to_reopen << file unless file.closed?
203
- end
204
192
 
193
+ files_to_reopen = ObjectSpace.each_object(File).reject { |f| f.closed? }
205
194
  ::Process.daemon(true, true)
206
195
 
207
196
  files_to_reopen.each do |file|
@@ -239,15 +228,38 @@ module Sidekiq
239
228
  alias_method :☠, :exit
240
229
 
241
230
  def setup_options(args)
231
+ # parse CLI options
242
232
  opts = parse_options(args)
233
+
243
234
  set_environment opts[:environment]
244
235
 
245
- cfile = opts[:config_file]
246
- 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
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
247
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?
248
259
  opts[:strict] = true if opts[:strict].nil?
249
- 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"]
250
261
 
262
+ # merge with defaults
251
263
  options.merge!(opts)
252
264
  end
253
265
 
@@ -258,8 +270,6 @@ module Sidekiq
258
270
  def boot_system
259
271
  ENV['RACK_ENV'] = ENV['RAILS_ENV'] = environment
260
272
 
261
- raise ArgumentError, "#{options[:require]} does not exist" unless File.exist?(options[:require])
262
-
263
273
  if File.directory?(options[:require])
264
274
  require 'rails'
265
275
  if ::Rails::VERSION::MAJOR < 4
@@ -279,10 +289,7 @@ module Sidekiq
279
289
  end
280
290
  options[:tag] ||= default_tag
281
291
  else
282
- not_required_message = "#{options[:require]} was not required, you should use an explicit path: " +
283
- "./#{options[:require]} or /path/to/#{options[:require]}"
284
-
285
- require(options[:require]) || raise(ArgumentError, not_required_message)
292
+ require options[:require]
286
293
  end
287
294
  end
288
295
 
@@ -298,8 +305,6 @@ module Sidekiq
298
305
  end
299
306
 
300
307
  def validate!
301
- options[:queues] << 'default' if options[:queues].empty?
302
-
303
308
  if !File.exist?(options[:require]) ||
304
309
  (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
305
310
  logger.info "=================================================================="
@@ -325,6 +330,7 @@ module Sidekiq
325
330
 
326
331
  o.on '-d', '--daemon', "Daemonize process" do |arg|
327
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"
328
334
  end
329
335
 
330
336
  o.on '-e', '--environment ENV', "Application environment" do |arg|
@@ -364,10 +370,12 @@ module Sidekiq
364
370
 
365
371
  o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
366
372
  opts[:logfile] = arg
373
+ puts "WARNING: Logfile redirection will be removed in Sidekiq 6.0, see #4045. Sidekiq will only log to STDOUT"
367
374
  end
368
375
 
369
376
  o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
370
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"
371
379
  end
372
380
 
373
381
  o.on '-V', '--version', "Print version and exit" do |arg|
@@ -381,11 +389,8 @@ module Sidekiq
381
389
  logger.info @parser
382
390
  die 1
383
391
  end
384
- @parser.parse!(argv)
385
392
 
386
- %w[config/sidekiq.yml config/sidekiq.yml.erb].each do |filename|
387
- opts[:config_file] ||= filename if File.exist?(filename)
388
- end
393
+ @parser.parse!(argv)
389
394
 
390
395
  opts
391
396
  end
@@ -405,23 +410,18 @@ module Sidekiq
405
410
  end
406
411
  end
407
412
 
408
- def parse_config(cfile)
409
- opts = {}
410
- if File.exist?(cfile)
411
- opts = YAML.load(ERB.new(IO.read(cfile)).result) || opts
412
-
413
- if opts.respond_to? :deep_symbolize_keys!
414
- opts.deep_symbolize_keys!
415
- else
416
- symbolize_keys_deep!(opts)
417
- end
413
+ def parse_config(path)
414
+ opts = YAML.load(ERB.new(File.read(path)).result) || {}
418
415
 
419
- opts = opts.merge(opts.delete(environment.to_sym) || {})
420
- parse_queues(opts, opts.delete(:queues) || [])
416
+ if opts.respond_to? :deep_symbolize_keys!
417
+ opts.deep_symbolize_keys!
421
418
  else
422
- # allow a non-existent config file so Sidekiq
423
- # can be deployed by cap with just the defaults.
419
+ symbolize_keys_deep!(opts)
424
420
  end
421
+
422
+ opts = opts.merge(opts.delete(environment.to_sym) || {})
423
+ parse_queues(opts, opts.delete(:queues) || [])
424
+
425
425
  ns = opts.delete(:namespace)
426
426
  if ns
427
427
  # logger hasn't been initialized yet, puts is all we have.
@@ -435,10 +435,10 @@ module Sidekiq
435
435
  queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
436
436
  end
437
437
 
438
- def parse_queue(opts, q, weight=nil)
439
- [weight.to_i, 1].max.times do
440
- (opts[:queues] ||= []) << q
441
- 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 }
442
442
  opts[:strict] = false if weight.to_i > 0
443
443
  end
444
444
  end
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ require 'sidekiq/api'
5
+
6
+ class Sidekiq::Ctl
7
+ DEFAULT_KILL_TIMEOUT = 10
8
+ CMD = File.basename($0)
9
+
10
+ attr_reader :stage, :pidfile, :kill_timeout
11
+
12
+ def self.print_usage
13
+ puts "#{CMD} - control Sidekiq from the command line."
14
+ puts
15
+ puts "Usage: #{CMD} quiet <pidfile> <kill_timeout>"
16
+ puts " #{CMD} stop <pidfile> <kill_timeout>"
17
+ puts " #{CMD} status <section>"
18
+ puts
19
+ puts " <pidfile> is path to a pidfile"
20
+ puts " <kill_timeout> is number of seconds to wait until Sidekiq exits"
21
+ puts " (default: #{Sidekiq::Ctl::DEFAULT_KILL_TIMEOUT}), after which Sidekiq will be KILL'd"
22
+ puts
23
+ puts " <section> (optional) view a specific section of the status output"
24
+ puts " Valid sections are: #{Sidekiq::Ctl::Status::VALID_SECTIONS.join(', ')}"
25
+ puts
26
+ puts "Be sure to set the kill_timeout LONGER than Sidekiq's -t timeout. If you want"
27
+ puts "to wait 60 seconds for jobs to finish, use `sidekiq -t 60` and `sidekiqctl stop"
28
+ puts " path_to_pidfile 61`"
29
+ puts
30
+ end
31
+
32
+ def initialize(stage, pidfile, timeout)
33
+ @stage = stage
34
+ @pidfile = pidfile
35
+ @kill_timeout = timeout
36
+
37
+ done('No pidfile given', :error) if !pidfile
38
+ done("Pidfile #{pidfile} does not exist", :warn) if !File.exist?(pidfile)
39
+ done('Invalid pidfile content', :error) if pid == 0
40
+
41
+ fetch_process
42
+
43
+ begin
44
+ send(stage)
45
+ rescue NoMethodError
46
+ done "Invalid command: #{stage}", :error
47
+ end
48
+ end
49
+
50
+ def fetch_process
51
+ Process.kill(0, pid)
52
+ rescue Errno::ESRCH
53
+ done "Process doesn't exist", :error
54
+ # We were not allowed to send a signal, but the process must have existed
55
+ # when Process.kill() was called.
56
+ rescue Errno::EPERM
57
+ return pid
58
+ end
59
+
60
+ def done(msg, error = nil)
61
+ puts msg
62
+ exit(exit_signal(error))
63
+ end
64
+
65
+ def exit_signal(error)
66
+ (error == :error) ? 1 : 0
67
+ end
68
+
69
+ def pid
70
+ @pid ||= File.read(pidfile).to_i
71
+ end
72
+
73
+ def quiet
74
+ `kill -TSTP #{pid}`
75
+ end
76
+
77
+ def stop
78
+ `kill -TERM #{pid}`
79
+ kill_timeout.times do
80
+ begin
81
+ Process.kill(0, pid)
82
+ rescue Errno::ESRCH
83
+ FileUtils.rm_f pidfile
84
+ done 'Sidekiq shut down gracefully.'
85
+ rescue Errno::EPERM
86
+ done 'Not permitted to shut down Sidekiq.'
87
+ end
88
+ sleep 1
89
+ end
90
+ `kill -9 #{pid}`
91
+ FileUtils.rm_f pidfile
92
+ done 'Sidekiq shut down forcefully.'
93
+ end
94
+ alias_method :shutdown, :stop
95
+
96
+ class Status
97
+ VALID_SECTIONS = %w[all version overview processes queues]
98
+ def display(section = nil)
99
+ section ||= 'all'
100
+ unless VALID_SECTIONS.include? section
101
+ puts "I don't know how to check the status of '#{section}'!"
102
+ puts "Try one of these: #{VALID_SECTIONS.join(', ')}"
103
+ return
104
+ end
105
+ send(section)
106
+ rescue StandardError => e
107
+ puts "Couldn't get status: #{e}"
108
+ end
109
+
110
+ def all
111
+ version
112
+ puts
113
+ overview
114
+ puts
115
+ processes
116
+ puts
117
+ queues
118
+ end
119
+
120
+ def version
121
+ puts "Sidekiq #{Sidekiq::VERSION}"
122
+ puts Time.now
123
+ end
124
+
125
+ def overview
126
+ puts '---- Overview ----'
127
+ puts " Processed: #{delimit stats.processed}"
128
+ puts " Failed: #{delimit stats.failed}"
129
+ puts " Busy: #{delimit stats.workers_size}"
130
+ puts " Enqueued: #{delimit stats.enqueued}"
131
+ puts " Retries: #{delimit stats.retry_size}"
132
+ puts " Scheduled: #{delimit stats.scheduled_size}"
133
+ puts " Dead: #{delimit stats.dead_size}"
134
+ end
135
+
136
+ def processes
137
+ puts "---- Processes (#{process_set.size}) ----"
138
+ process_set.each_with_index do |process, index|
139
+ puts "#{process['identity']} #{tags_for(process)}"
140
+ puts " Started: #{Time.at(process['started_at'])} (#{time_ago(process['started_at'])})"
141
+ puts " Threads: #{process['concurrency']} (#{process['busy']} busy)"
142
+ puts " Queues: #{split_multiline(process['queues'].sort, pad: 11)}"
143
+ puts '' unless (index+1) == process_set.size
144
+ end
145
+ end
146
+
147
+ COL_PAD = 2
148
+ def queues
149
+ puts "---- Queues (#{queue_data.size}) ----"
150
+ columns = {
151
+ name: [:ljust, (['name'] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
152
+ size: [:rjust, (['size'] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
153
+ latency: [:rjust, (['latency'] + queue_data.map(&:latency)).map(&:length).max + COL_PAD]
154
+ }
155
+ columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
156
+ puts
157
+ queue_data.each do |q|
158
+ columns.each do |col, (dir, width)|
159
+ print q.send(col).public_send(dir, width)
160
+ end
161
+ puts
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def delimit(number)
168
+ number.to_s.reverse.scan(/.{1,3}/).join(',').reverse
169
+ end
170
+
171
+ def split_multiline(values, opts = {})
172
+ return 'none' unless values
173
+ pad = opts[:pad] || 0
174
+ max_length = opts[:max_length] || (80 - pad)
175
+ out = []
176
+ line = ''
177
+ values.each do |value|
178
+ if (line.length + value.length) > max_length
179
+ out << line
180
+ line = ' ' * pad
181
+ end
182
+ line << value + ', '
183
+ end
184
+ out << line[0..-3]
185
+ out.join("\n")
186
+ end
187
+
188
+ def tags_for(process)
189
+ tags = [
190
+ process['tag'],
191
+ process['labels'],
192
+ (process['quiet'] == 'true' ? 'quiet' : nil)
193
+ ].flatten.compact
194
+ tags.any? ? "[#{tags.join('] [')}]" : nil
195
+ end
196
+
197
+ def time_ago(timestamp)
198
+ seconds = Time.now - Time.at(timestamp)
199
+ return 'just now' if seconds < 60
200
+ return 'a minute ago' if seconds < 120
201
+ return "#{seconds.floor / 60} minutes ago" if seconds < 3600
202
+ return 'an hour ago' if seconds < 7200
203
+ "#{seconds.floor / 60 / 60} hours ago"
204
+ end
205
+
206
+ QUEUE_STRUCT = Struct.new(:name, :size, :latency)
207
+ def queue_data
208
+ @queue_data ||= Sidekiq::Queue.all.map do |q|
209
+ QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf('%#.2f', q.latency))
210
+ end
211
+ end
212
+
213
+ def process_set
214
+ @process_set ||= Sidekiq::ProcessSet.new
215
+ end
216
+
217
+ def stats
218
+ @stats ||= Sidekiq::Stats.new
219
+ end
220
+ end
221
+ end