sidekiq 6.1.2 → 6.2.2

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +59 -1
  3. data/LICENSE +1 -1
  4. data/lib/sidekiq/api.rb +95 -57
  5. data/lib/sidekiq/cli.rb +14 -1
  6. data/lib/sidekiq/client.rb +1 -5
  7. data/lib/sidekiq/extensions/generic_proxy.rb +3 -1
  8. data/lib/sidekiq/fetch.rb +9 -1
  9. data/lib/sidekiq/job.rb +8 -0
  10. data/lib/sidekiq/job_logger.rb +1 -1
  11. data/lib/sidekiq/job_retry.rb +4 -7
  12. data/lib/sidekiq/launcher.rb +71 -18
  13. data/lib/sidekiq/logger.rb +3 -2
  14. data/lib/sidekiq/middleware/chain.rb +5 -3
  15. data/lib/sidekiq/scheduled.rb +7 -1
  16. data/lib/sidekiq/testing.rb +1 -3
  17. data/lib/sidekiq/util.rb +28 -0
  18. data/lib/sidekiq/version.rb +1 -1
  19. data/lib/sidekiq/web/action.rb +2 -2
  20. data/lib/sidekiq/web/application.rb +14 -6
  21. data/lib/sidekiq/web/csrf_protection.rb +28 -6
  22. data/lib/sidekiq/web/helpers.rb +31 -11
  23. data/lib/sidekiq/web/router.rb +4 -1
  24. data/lib/sidekiq/web.rb +34 -78
  25. data/sidekiq.gemspec +10 -2
  26. data/web/assets/images/apple-touch-icon.png +0 -0
  27. data/web/assets/stylesheets/application-dark.css +18 -14
  28. data/web/assets/stylesheets/application.css +29 -130
  29. data/web/locales/ar.yml +8 -2
  30. data/web/locales/en.yml +3 -0
  31. data/web/locales/es.yml +18 -2
  32. data/web/locales/fr.yml +8 -1
  33. data/web/locales/ja.yml +3 -0
  34. data/web/locales/lt.yml +1 -1
  35. data/web/views/_job_info.erb +1 -1
  36. data/web/views/busy.erb +48 -17
  37. data/web/views/dashboard.erb +14 -6
  38. data/web/views/dead.erb +1 -1
  39. data/web/views/layout.erb +1 -0
  40. data/web/views/morgue.erb +6 -6
  41. data/web/views/queue.erb +1 -1
  42. data/web/views/queues.erb +3 -3
  43. data/web/views/retries.erb +7 -7
  44. data/web/views/retry.erb +1 -1
  45. data/web/views/scheduled.erb +1 -1
  46. metadata +12 -26
  47. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
  48. data/.github/contributing.md +0 -32
  49. data/.github/workflows/ci.yml +0 -41
  50. data/.gitignore +0 -13
  51. data/.standard.yml +0 -20
  52. data/3.0-Upgrade.md +0 -70
  53. data/4.0-Upgrade.md +0 -53
  54. data/5.0-Upgrade.md +0 -56
  55. data/6.0-Upgrade.md +0 -72
  56. data/COMM-LICENSE +0 -97
  57. data/Ent-2.0-Upgrade.md +0 -37
  58. data/Ent-Changes.md +0 -281
  59. data/Gemfile +0 -24
  60. data/Gemfile.lock +0 -192
  61. data/Pro-2.0-Upgrade.md +0 -138
  62. data/Pro-3.0-Upgrade.md +0 -44
  63. data/Pro-4.0-Upgrade.md +0 -35
  64. data/Pro-5.0-Upgrade.md +0 -25
  65. data/Pro-Changes.md +0 -805
  66. data/Rakefile +0 -10
  67. data/code_of_conduct.md +0 -50
@@ -38,7 +38,7 @@ module Sidekiq
38
38
  # If we're using a wrapper class, like ActiveJob, use the "wrapped"
39
39
  # attribute to expose the underlying thing.
40
40
  h = {
41
- class: job_hash["wrapped"] || job_hash["class"],
41
+ class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
42
42
  jid: job_hash["jid"]
43
43
  }
44
44
  h[:bid] = job_hash["bid"] if job_hash["bid"]
@@ -61,6 +61,7 @@ module Sidekiq
61
61
  #
62
62
  class JobRetry
63
63
  class Handled < ::RuntimeError; end
64
+
64
65
  class Skip < Handled; end
65
66
 
66
67
  include Sidekiq::Util
@@ -213,16 +214,12 @@ module Sidekiq
213
214
  end
214
215
 
215
216
  def delay_for(worker, count, exception)
217
+ jitter = rand(10) * (count + 1)
216
218
  if worker&.sidekiq_retry_in_block
217
219
  custom_retry_in = retry_in(worker, count, exception).to_i
218
- return custom_retry_in if custom_retry_in > 0
220
+ return custom_retry_in + jitter if custom_retry_in > 0
219
221
  end
220
- seconds_to_delay(count)
221
- end
222
-
223
- # delayed_job uses the same basic formula
224
- def seconds_to_delay(count)
225
- (count**4) + 15 + (rand(30) * (count + 1))
222
+ (count**4) + 15 + jitter
226
223
  end
227
224
 
228
225
  def retry_in(worker, count, exception)
@@ -153,13 +153,21 @@ module Sidekiq
153
153
  end
154
154
  end
155
155
 
156
+ rtt = check_rtt
157
+
156
158
  fails = procd = 0
159
+ kb = memory_usage(::Process.pid)
157
160
 
158
161
  _, exists, _, _, msg = Sidekiq.redis { |conn|
159
162
  conn.multi {
160
163
  conn.sadd("processes", key)
161
164
  conn.exists?(key)
162
- conn.hmset(key, "info", to_json, "busy", curstate.size, "beat", Time.now.to_f, "quiet", @done)
165
+ conn.hmset(key, "info", to_json,
166
+ "busy", curstate.size,
167
+ "beat", Time.now.to_f,
168
+ "rtt_us", rtt,
169
+ "quiet", @done,
170
+ "rss", kb)
163
171
  conn.expire(key, 60)
164
172
  conn.rpop("#{key}-signals")
165
173
  }
@@ -180,27 +188,72 @@ module Sidekiq
180
188
  end
181
189
  end
182
190
 
183
- def to_data
184
- @data ||= begin
185
- {
186
- "hostname" => hostname,
187
- "started_at" => Time.now.to_f,
188
- "pid" => ::Process.pid,
189
- "tag" => @options[:tag] || "",
190
- "concurrency" => @options[:concurrency],
191
- "queues" => @options[:queues].uniq,
192
- "labels" => @options[:labels],
193
- "identity" => identity
194
- }
191
+ # We run the heartbeat every five seconds.
192
+ # Capture five samples of RTT, log a warning if each sample
193
+ # is above our warning threshold.
194
+ RTT_READINGS = RingBuffer.new(5)
195
+ RTT_WARNING_LEVEL = 50_000
196
+
197
+ def check_rtt
198
+ a = b = 0
199
+ Sidekiq.redis do |x|
200
+ a = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
201
+ x.ping
202
+ b = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
203
+ end
204
+ rtt = b - a
205
+ RTT_READINGS << rtt
206
+ # Ideal RTT for Redis is < 1000µs
207
+ # Workable is < 10,000µs
208
+ # Log a warning if it's a disaster.
209
+ if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
210
+ Sidekiq.logger.warn <<~EOM
211
+ Your Redis network connection is performing extremely poorly.
212
+ Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
213
+ Ensure Redis is running in the same AZ or datacenter as Sidekiq.
214
+ EOM
215
+ RTT_READINGS.reset
195
216
  end
217
+ rtt
218
+ end
219
+
220
+ MEMORY_GRABBER = case RUBY_PLATFORM
221
+ when /linux/
222
+ ->(pid) {
223
+ IO.readlines("/proc/#{$$}/status").each do |line|
224
+ next unless line.start_with?("VmRSS:")
225
+ break line.split[1].to_i
226
+ end
227
+ }
228
+ when /darwin|bsd/
229
+ ->(pid) {
230
+ `ps -o pid,rss -p #{pid}`.lines.last.split.last.to_i
231
+ }
232
+ else
233
+ ->(pid) { 0 }
234
+ end
235
+
236
+ def memory_usage(pid)
237
+ MEMORY_GRABBER.call(pid)
238
+ end
239
+
240
+ def to_data
241
+ @data ||= {
242
+ "hostname" => hostname,
243
+ "started_at" => Time.now.to_f,
244
+ "pid" => ::Process.pid,
245
+ "tag" => @options[:tag] || "",
246
+ "concurrency" => @options[:concurrency],
247
+ "queues" => @options[:queues].uniq,
248
+ "labels" => @options[:labels],
249
+ "identity" => identity
250
+ }
196
251
  end
197
252
 
198
253
  def to_json
199
- @json ||= begin
200
- # this data changes infrequently so dump it to a string
201
- # now so we don't need to dump it every heartbeat.
202
- Sidekiq.dump_json(to_data)
203
- end
254
+ # this data changes infrequently so dump it to a string
255
+ # now so we don't need to dump it every heartbeat.
256
+ @json ||= Sidekiq.dump_json(to_data)
204
257
  end
205
258
  end
206
259
  end
@@ -6,10 +6,11 @@ require "time"
6
6
  module Sidekiq
7
7
  module Context
8
8
  def self.with(hash)
9
+ orig_context = current.dup
9
10
  current.merge!(hash)
10
11
  yield
11
12
  ensure
12
- hash.each_key { |key| current.delete(key) }
13
+ Thread.current[:sidekiq_context] = orig_context
13
14
  end
14
15
 
15
16
  def self.current
@@ -89,7 +90,7 @@ module Sidekiq
89
90
  return true if @logdev.nil? || severity < level
90
91
 
91
92
  if message.nil?
92
- if block_given?
93
+ if block
93
94
  message = yield
94
95
  else
95
96
  message = progname
@@ -90,12 +90,12 @@ module Sidekiq
90
90
  end
91
91
 
92
92
  def add(klass, *args)
93
- remove(klass) if exists?(klass)
93
+ remove(klass)
94
94
  entries << Entry.new(klass, *args)
95
95
  end
96
96
 
97
97
  def prepend(klass, *args)
98
- remove(klass) if exists?(klass)
98
+ remove(klass)
99
99
  entries.insert(0, Entry.new(klass, *args))
100
100
  end
101
101
 
@@ -132,7 +132,7 @@ module Sidekiq
132
132
  def invoke(*args)
133
133
  return yield if empty?
134
134
 
135
- chain = retrieve.dup
135
+ chain = retrieve
136
136
  traverse_chain = proc do
137
137
  if chain.empty?
138
138
  yield
@@ -144,6 +144,8 @@ module Sidekiq
144
144
  end
145
145
  end
146
146
 
147
+ private
148
+
147
149
  class Entry
148
150
  attr_reader :klass
149
151
 
@@ -49,6 +49,7 @@ module Sidekiq
49
49
  @sleeper = ConnectionPool::TimedStack.new
50
50
  @done = false
51
51
  @thread = nil
52
+ @count_calls = 0
52
53
  end
53
54
 
54
55
  # Shut down this instance, will pause until the thread is dead.
@@ -152,8 +153,13 @@ module Sidekiq
152
153
  end
153
154
 
154
155
  def process_count
155
- pcount = Sidekiq::ProcessSet.new.size
156
+ # The work buried within Sidekiq::ProcessSet#cleanup can be
157
+ # expensive at scale. Cut it down by 90% with this counter.
158
+ # NB: This method is only called by the scheduler thread so we
159
+ # don't need to worry about the thread safety of +=.
160
+ pcount = Sidekiq::ProcessSet.new(@count_calls % 10 == 0).size
156
161
  pcount = 1 if pcount == 0
162
+ @count_calls += 1
157
163
  pcount
158
164
  end
159
165
 
@@ -338,7 +338,5 @@ module Sidekiq
338
338
  end
339
339
 
340
340
  if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
341
- puts("**************************************************")
342
- puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
343
- puts("**************************************************")
341
+ warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1)
344
342
  end
data/lib/sidekiq/util.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
3
4
  require "socket"
4
5
  require "securerandom"
5
6
  require "sidekiq/exception_handler"
@@ -8,6 +9,33 @@ module Sidekiq
8
9
  ##
9
10
  # This module is part of Sidekiq core and not intended for extensions.
10
11
  #
12
+
13
+ class RingBuffer
14
+ include Enumerable
15
+ extend Forwardable
16
+ def_delegators :@buf, :[], :each, :size
17
+
18
+ def initialize(size, default = 0)
19
+ @size = size
20
+ @buf = Array.new(size, default)
21
+ @index = 0
22
+ end
23
+
24
+ def <<(element)
25
+ @buf[@index % @size] = element
26
+ @index += 1
27
+ element
28
+ end
29
+
30
+ def buffer
31
+ @buf
32
+ end
33
+
34
+ def reset(default = 0)
35
+ @buf.fill(default)
36
+ end
37
+ end
38
+
11
39
  module Util
12
40
  include ExceptionHandler
13
41
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "6.1.2"
4
+ VERSION = "6.2.2"
5
5
  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)
@@ -68,7 +68,7 @@ module Sidekiq
68
68
  end
69
69
 
70
70
  def json(payload)
71
- [200, {"Content-Type" => "application/json", "Cache-Control" => "no-cache"}, [Sidekiq.dump_json(payload)]]
71
+ [200, {"Content-Type" => "application/json", "Cache-Control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
72
72
  end
73
73
 
74
74
  def initialize(env, block)
@@ -4,7 +4,6 @@ module Sidekiq
4
4
  class WebApplication
5
5
  extend WebRouter
6
6
 
7
- CONTENT_LENGTH = "Content-Length"
8
7
  REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
9
8
  CSP_HEADER = [
10
9
  "default-src 'self' https: http:",
@@ -42,6 +41,13 @@ module Sidekiq
42
41
  # nothing, backwards compatibility
43
42
  end
44
43
 
44
+ head "/" do
45
+ # HEAD / is the cheapest heartbeat possible,
46
+ # it hits Redis to ensure connectivity
47
+ Sidekiq.redis { |c| c.llen("queue:default") }
48
+ ""
49
+ end
50
+
45
51
  get "/" do
46
52
  @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
47
53
  stats_history = Sidekiq::Stats::History.new((params["days"] || 30).to_i)
@@ -76,15 +82,17 @@ module Sidekiq
76
82
  erb(:queues)
77
83
  end
78
84
 
85
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
86
+
79
87
  get "/queues/:name" do
80
88
  @name = route_params[:name]
81
89
 
82
- halt(404) unless @name
90
+ halt(404) if !@name || @name !~ QUEUE_NAME
83
91
 
84
92
  @count = (params["count"] || 25).to_i
85
93
  @queue = Sidekiq::Queue.new(@name)
86
94
  (@current_page, @total_size, @messages) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
87
- @messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
95
+ @messages = @messages.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
88
96
 
89
97
  erb(:queue)
90
98
  end
@@ -105,7 +113,7 @@ module Sidekiq
105
113
 
106
114
  post "/queues/:name/delete" do
107
115
  name = route_params[:name]
108
- Sidekiq::Job.new(params["key_val"], name).delete
116
+ Sidekiq::JobRecord.new(params["key_val"], name).delete
109
117
 
110
118
  redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
111
119
  end
@@ -306,7 +314,7 @@ module Sidekiq
306
314
  # rendered content goes here
307
315
  headers = {
308
316
  "Content-Type" => "text/html",
309
- "Cache-Control" => "no-cache",
317
+ "Cache-Control" => "private, no-store",
310
318
  "Content-Language" => action.locale,
311
319
  "Content-Security-Policy" => CSP_HEADER
312
320
  }
@@ -316,7 +324,7 @@ module Sidekiq
316
324
  end
317
325
 
318
326
  def self.helpers(mod = nil, &block)
319
- if block_given?
327
+ if block
320
328
  WebAction.class_eval(&block)
321
329
  else
322
330
  WebAction.send(:include, mod)
@@ -66,7 +66,31 @@ module Sidekiq
66
66
  end
67
67
 
68
68
  def session(env)
69
- env["rack.session"] || fail("you need to set up a session middleware *before* #{self.class}")
69
+ env["rack.session"] || fail(<<~EOM)
70
+ Sidekiq::Web needs a valid Rack session for CSRF protection. If this is a Rails app,
71
+ make sure you mount Sidekiq::Web *inside* your application routes:
72
+
73
+
74
+ Rails.application.routes.draw do
75
+ mount Sidekiq::Web => "/sidekiq"
76
+ ....
77
+ end
78
+
79
+
80
+ If this is a Rails app in API mode, you need to enable sessions.
81
+
82
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
83
+
84
+ If this is a bare Rack app, use a session middleware before Sidekiq::Web:
85
+
86
+ # first, use IRB to create a shared secret key for sessions and commit it
87
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
88
+
89
+ # now use the secret with a session cookie middleware
90
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
91
+ run Sidekiq::Web
92
+
93
+ EOM
70
94
  end
71
95
 
72
96
  def accept?(env)
@@ -90,13 +114,11 @@ module Sidekiq
90
114
  end
91
115
 
92
116
  sess = session(env)
93
-
94
- # Checks that Rack::Session::Cookie did not return empty session
95
- # object in case the digest verification failed
96
- return false if sess.empty?
97
-
98
117
  localtoken = sess[:csrf]
99
118
 
119
+ # Checks that Rack::Session::Cookie actualy contains the csrf toekn
120
+ return false if localtoken.nil?
121
+
100
122
  # Rotate the session token after every use
101
123
  sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
102
124
 
@@ -10,18 +10,25 @@ module Sidekiq
10
10
  module WebHelpers
11
11
  def strings(lang)
12
12
  @strings ||= {}
13
- @strings[lang] ||= begin
14
- # Allow sidekiq-web extensions to add locale paths
15
- # so extensions can be localized
16
- settings.locales.each_with_object({}) do |path, global|
17
- find_locale_files(lang).each do |file|
18
- strs = YAML.load(File.open(file))
19
- global.merge!(strs[lang])
20
- end
13
+
14
+ # Allow sidekiq-web extensions to add locale paths
15
+ # so extensions can be localized
16
+ @strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
17
+ find_locale_files(lang).each do |file|
18
+ strs = YAML.load(File.open(file))
19
+ global.merge!(strs[lang])
21
20
  end
22
21
  end
23
22
  end
24
23
 
24
+ def singularize(str, count)
25
+ if count == 1 && str.respond_to?(:singularize) # rails
26
+ str.singularize
27
+ else
28
+ str
29
+ end
30
+ end
31
+
25
32
  def clear_caches
26
33
  @strings = nil
27
34
  @locale_files = nil
@@ -118,7 +125,7 @@ module Sidekiq
118
125
  # within is used by Sidekiq Pro
119
126
  def display_tags(job, within = nil)
120
127
  job.tags.map { |tag|
121
- "<span class='jobtag label label-info'>#{::Rack::Utils.escape_html(tag)}</span>"
128
+ "<span class='label label-info jobtag'>#{::Rack::Utils.escape_html(tag)}</span>"
122
129
  }.join(" ")
123
130
  end
124
131
 
@@ -158,8 +165,7 @@ module Sidekiq
158
165
 
159
166
  def redis_connection
160
167
  Sidekiq.redis do |conn|
161
- c = conn.connection
162
- "redis://#{c[:location]}/#{c[:db]}"
168
+ conn.connection[:id]
163
169
  end
164
170
  end
165
171
 
@@ -258,7 +264,21 @@ module Sidekiq
258
264
  end
259
265
  end
260
266
 
267
+ def format_memory(rss_kb)
268
+ return "0" if rss_kb.nil? || rss_kb == 0
269
+
270
+ if rss_kb < 100_000
271
+ "#{number_with_delimiter(rss_kb)} KB"
272
+ elsif rss_kb < 10_000_000
273
+ "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
274
+ else
275
+ "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB"
276
+ end
277
+ end
278
+
261
279
  def number_with_delimiter(number)
280
+ return "" if number.nil?
281
+
262
282
  begin
263
283
  Float(number)
264
284
  rescue ArgumentError, TypeError