sidekiq 5.2.5 → 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.

@@ -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
@@ -56,7 +56,8 @@ module Sidekiq
56
56
  # end
57
57
  #
58
58
  class JobRetry
59
- class Skip < ::RuntimeError; end
59
+ class Handled < ::RuntimeError; end
60
+ class Skip < Handled; end
60
61
 
61
62
  include Sidekiq::Util
62
63
 
@@ -71,7 +72,7 @@ module Sidekiq
71
72
  # require the worker to be instantiated.
72
73
  def global(msg, queue)
73
74
  yield
74
- rescue Skip => ex
75
+ rescue Handled => ex
75
76
  raise ex
76
77
  rescue Sidekiq::Shutdown => ey
77
78
  # ignore, will be pushed back onto queue during hard_shutdown
@@ -92,7 +93,7 @@ module Sidekiq
92
93
  end
93
94
  end
94
95
 
95
- raise e
96
+ raise Handled
96
97
  end
97
98
 
98
99
 
@@ -106,7 +107,7 @@ module Sidekiq
106
107
  # calling the handle_exception handlers.
107
108
  def local(worker, msg, queue)
108
109
  yield
109
- rescue Skip => ex
110
+ rescue Handled => ex
110
111
  raise ex
111
112
  rescue Sidekiq::Shutdown => ey
112
113
  # ignore, will be pushed back onto queue during hard_shutdown
@@ -140,9 +141,7 @@ module Sidekiq
140
141
  queue
141
142
  end
142
143
 
143
- # App code can stuff all sorts of crazy binary data into the error message
144
- # that won't convert to JSON.
145
- m = exception.message.to_s[0, 10_000]
144
+ m = exception_message(exception)
146
145
  if m.respond_to?(:scrub!)
147
146
  m.force_encoding("utf-8")
148
147
  m.scrub!
@@ -247,5 +246,17 @@ module Sidekiq
247
246
  exception_caused_by_shutdown?(e.cause, checked_causes)
248
247
  end
249
248
 
249
+ # Extract message from exception.
250
+ # Set a default if the message raises an error
251
+ def exception_message(exception)
252
+ begin
253
+ # App code can stuff all sorts of crazy binary data into the error message
254
+ # that won't convert to JSON.
255
+ exception.message.to_s[0, 10_000]
256
+ rescue
257
+ "!!! ERROR MESSAGE THREW AN ERROR !!!".dup
258
+ end
259
+ end
260
+
250
261
  end
251
262
  end
@@ -102,7 +102,7 @@ module Sidekiq
102
102
  _, exists, _, _, msg = Sidekiq.redis do |conn|
103
103
  conn.multi do
104
104
  conn.sadd('processes', key)
105
- conn.exists(key)
105
+ conn.exists?(key)
106
106
  conn.hmset(key, 'info', to_json, 'busy', curstate.size, 'beat', Time.now.to_f, 'quiet', @done)
107
107
  conn.expire(key, 60)
108
108
  conn.rpop("#{key}-signals")
@@ -7,7 +7,7 @@ module Sidekiq
7
7
  def initialize
8
8
  # With Rails 5+ we must use the Reloader **always**.
9
9
  # The reloader handles code loading and db connection management.
10
- if defined?(::Rails) && ::Rails::VERSION::MAJOR >= 5
10
+ if defined?(::Rails) && defined?(::Rails::VERSION) && ::Rails::VERSION::MAJOR >= 5
11
11
  raise ArgumentError, "Rails 5 no longer needs or uses the ActiveRecord middleware."
12
12
  end
13
13
  end
@@ -147,21 +147,19 @@ module Sidekiq
147
147
  jobstr = work.job
148
148
  queue = work.queue_name
149
149
 
150
- ack = false
150
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
151
+ job_hash = nil
151
152
  begin
152
- # Treat malformed JSON as a special case: job goes straight to the morgue.
153
- job_hash = nil
154
- begin
155
- job_hash = Sidekiq.load_json(jobstr)
156
- rescue => ex
157
- handle_exception(ex, { :context => "Invalid JSON for job", :jobstr => jobstr })
158
- # we can't notify because the job isn't a valid hash payload.
159
- DeadSet.new.kill(jobstr, notify_failure: false)
160
- ack = true
161
- raise
162
- end
153
+ job_hash = Sidekiq.load_json(jobstr)
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
163
160
 
164
- ack = true
161
+ ack = true
162
+ begin
165
163
  dispatch(job_hash, queue) do |worker|
166
164
  Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
167
165
  execute_job(worker, cloned(job_hash['args']))
@@ -172,10 +170,19 @@ module Sidekiq
172
170
  # within the timeout. Don't acknowledge the work since
173
171
  # we didn't properly finish it.
174
172
  ack = false
175
- rescue Exception => ex
176
- e = ex.is_a?(::Sidekiq::JobRetry::Skip) && ex.cause ? ex.cause : ex
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
177
  handle_exception(e, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
178
178
  raise e
179
+ rescue Exception => ex
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
179
186
  ensure
180
187
  work.acknowledge if ack
181
188
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Sidekiq
3
- VERSION = "5.2.5"
3
+ VERSION = "5.2.10"
4
4
  end
@@ -15,7 +15,7 @@ module Sidekiq
15
15
  end
16
16
 
17
17
  def halt(res)
18
- throw :halt, res
18
+ throw :halt, [res, {"Content-Type" => "text/plain"}, [res.to_s]]
19
19
  end
20
20
 
21
21
  def redirect(location)
@@ -45,7 +45,10 @@ module Sidekiq
45
45
 
46
46
  get "/" do
47
47
  @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
48
- stats_history = Sidekiq::Stats::History.new((params['days'] || 30).to_i)
48
+ days = (params["days"] || 30).to_i
49
+ return halt(401) if days < 1 || days > 180
50
+
51
+ stats_history = Sidekiq::Stats::History.new(days)
49
52
  @processed_history = stats_history.processed
50
53
  @failed_history = stats_history.failed
51
54
 
@@ -207,9 +207,16 @@ module Sidekiq
207
207
  end
208
208
 
209
209
  def display_args(args, truncate_after_chars = 2000)
210
- args.map do |arg|
211
- h(truncate(to_display(arg), truncate_after_chars))
212
- end.join(", ")
210
+ return "Invalid job payload, args is nil" if args == nil
211
+ return "Invalid job payload, args must be an Array, not #{args.class.name}" if !args.is_a?(Array)
212
+
213
+ begin
214
+ args.map do |arg|
215
+ h(truncate(to_display(arg), truncate_after_chars))
216
+ end.join(", ")
217
+ rescue
218
+ "Illegal job arguments: #{h args.inspect}"
219
+ end
213
220
  end
214
221
 
215
222
  def csrf_tag
@@ -46,6 +46,11 @@ module Sidekiq
46
46
  @opts = opts
47
47
  end
48
48
 
49
+ def set(options)
50
+ @opts.merge!(options)
51
+ self
52
+ end
53
+
49
54
  def perform_async(*args)
50
55
  @klass.client_push(@opts.merge('args' => args, 'class' => @klass))
51
56
  end
data/sidekiq.gemspec CHANGED
@@ -14,8 +14,8 @@ Gem::Specification.new do |gem|
14
14
  gem.version = Sidekiq::VERSION
15
15
  gem.required_ruby_version = ">= 2.2.2"
16
16
 
17
- gem.add_dependency 'redis', '>= 3.3.5', '< 5'
17
+ gem.add_dependency "redis", "~> 4.5", "< 4.6.0"
18
18
  gem.add_dependency 'connection_pool', '~> 2.2', '>= 2.2.2'
19
- gem.add_dependency 'rack', '>= 1.5.0'
19
+ gem.add_dependency 'rack', '~> 2.0'
20
20
  gem.add_dependency 'rack-protection', '>= 1.5.0'
21
21
  end