sidekiq 7.3.9 → 8.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +116 -0
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +10 -10
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +104 -58
  7. data/lib/sidekiq/api.rb +124 -39
  8. data/lib/sidekiq/capsule.rb +6 -6
  9. data/lib/sidekiq/cli.rb +15 -19
  10. data/lib/sidekiq/client.rb +28 -17
  11. data/lib/sidekiq/component.rb +42 -3
  12. data/lib/sidekiq/config.rb +23 -20
  13. data/lib/sidekiq/embedded.rb +2 -1
  14. data/lib/sidekiq/iterable_job.rb +1 -0
  15. data/lib/sidekiq/job/iterable.rb +44 -16
  16. data/lib/sidekiq/job.rb +2 -2
  17. data/lib/sidekiq/job_logger.rb +4 -4
  18. data/lib/sidekiq/job_retry.rb +33 -10
  19. data/lib/sidekiq/job_util.rb +5 -1
  20. data/lib/sidekiq/launcher.rb +2 -1
  21. data/lib/sidekiq/loader.rb +57 -0
  22. data/lib/sidekiq/logger.rb +25 -69
  23. data/lib/sidekiq/manager.rb +0 -1
  24. data/lib/sidekiq/metrics/query.rb +71 -45
  25. data/lib/sidekiq/metrics/shared.rb +8 -5
  26. data/lib/sidekiq/metrics/tracking.rb +12 -7
  27. data/lib/sidekiq/middleware/current_attributes.rb +11 -19
  28. data/lib/sidekiq/paginator.rb +8 -1
  29. data/lib/sidekiq/processor.rb +21 -14
  30. data/lib/sidekiq/profiler.rb +72 -0
  31. data/lib/sidekiq/rails.rb +46 -67
  32. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  33. data/lib/sidekiq/redis_connection.rb +14 -3
  34. data/lib/sidekiq/testing.rb +3 -3
  35. data/lib/sidekiq/transaction_aware_client.rb +13 -5
  36. data/lib/sidekiq/version.rb +2 -2
  37. data/lib/sidekiq/web/action.rb +146 -83
  38. data/lib/sidekiq/web/application.rb +353 -332
  39. data/lib/sidekiq/web/config.rb +120 -0
  40. data/lib/sidekiq/web/helpers.rb +57 -27
  41. data/lib/sidekiq/web/router.rb +60 -76
  42. data/lib/sidekiq/web.rb +51 -156
  43. data/lib/sidekiq.rb +6 -1
  44. data/sidekiq.gemspec +6 -6
  45. data/web/assets/images/logo.png +0 -0
  46. data/web/assets/images/status.png +0 -0
  47. data/web/assets/javascripts/application.js +26 -26
  48. data/web/assets/javascripts/base-charts.js +30 -16
  49. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  50. data/web/assets/javascripts/dashboard.js +1 -1
  51. data/web/assets/javascripts/metrics.js +16 -34
  52. data/web/assets/stylesheets/style.css +759 -0
  53. data/web/locales/ar.yml +1 -0
  54. data/web/locales/cs.yml +1 -0
  55. data/web/locales/da.yml +1 -0
  56. data/web/locales/de.yml +1 -0
  57. data/web/locales/el.yml +1 -0
  58. data/web/locales/en.yml +6 -0
  59. data/web/locales/es.yml +24 -2
  60. data/web/locales/fa.yml +1 -0
  61. data/web/locales/fr.yml +1 -0
  62. data/web/locales/gd.yml +1 -0
  63. data/web/locales/he.yml +1 -0
  64. data/web/locales/hi.yml +1 -0
  65. data/web/locales/it.yml +8 -0
  66. data/web/locales/ja.yml +1 -0
  67. data/web/locales/ko.yml +1 -0
  68. data/web/locales/lt.yml +1 -0
  69. data/web/locales/nb.yml +1 -0
  70. data/web/locales/nl.yml +1 -0
  71. data/web/locales/pl.yml +1 -0
  72. data/web/locales/{pt-br.yml → pt-BR.yml} +2 -1
  73. data/web/locales/pt.yml +1 -0
  74. data/web/locales/ru.yml +1 -0
  75. data/web/locales/sv.yml +1 -0
  76. data/web/locales/ta.yml +1 -0
  77. data/web/locales/tr.yml +1 -0
  78. data/web/locales/uk.yml +6 -5
  79. data/web/locales/ur.yml +1 -0
  80. data/web/locales/vi.yml +1 -0
  81. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -73
  82. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -1
  83. data/web/views/_footer.erb +31 -33
  84. data/web/views/_job_info.erb +91 -89
  85. data/web/views/_metrics_period_select.erb +13 -10
  86. data/web/views/_nav.erb +14 -21
  87. data/web/views/_paging.erb +23 -21
  88. data/web/views/_poll_link.erb +2 -2
  89. data/web/views/_summary.erb +16 -16
  90. data/web/views/busy.erb +124 -122
  91. data/web/views/dashboard.erb +62 -66
  92. data/web/views/dead.erb +31 -27
  93. data/web/views/filtering.erb +3 -3
  94. data/web/views/layout.erb +13 -29
  95. data/web/views/metrics.erb +75 -81
  96. data/web/views/metrics_for_job.erb +45 -46
  97. data/web/views/morgue.erb +61 -70
  98. data/web/views/profiles.erb +43 -0
  99. data/web/views/queue.erb +54 -52
  100. data/web/views/queues.erb +43 -41
  101. data/web/views/retries.erb +66 -75
  102. data/web/views/retry.erb +32 -27
  103. data/web/views/scheduled.erb +59 -55
  104. data/web/views/scheduled_job_info.erb +1 -1
  105. metadata +26 -25
  106. data/web/assets/stylesheets/application-dark.css +0 -147
  107. data/web/assets/stylesheets/application-rtl.css +0 -163
  108. data/web/assets/stylesheets/application.css +0 -759
  109. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  110. data/web/assets/stylesheets/bootstrap.css +0 -5
  111. data/web/views/_status.erb +0 -4
data/lib/sidekiq/api.rb CHANGED
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq"
4
-
5
3
  require "zlib"
6
- require "set"
7
4
 
5
+ require "sidekiq"
8
6
  require "sidekiq/metrics/query"
9
7
 
10
8
  #
@@ -101,11 +99,22 @@ module Sidekiq
101
99
  rescue
102
100
  {}
103
101
  end
104
- now = Time.now.to_f
105
- thence = job["enqueued_at"] || now
106
- now - thence
102
+
103
+ enqueued_at = job["enqueued_at"]
104
+ if enqueued_at
105
+ if enqueued_at.is_a?(Float)
106
+ # old format
107
+ now = Time.now.to_f
108
+ now - enqueued_at
109
+ else
110
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
111
+ (now - enqueued_at) / 1000.0
112
+ end
113
+ else
114
+ 0.0
115
+ end
107
116
  else
108
- 0
117
+ 0.0
109
118
  end
110
119
 
111
120
  @stats = {
@@ -265,11 +274,22 @@ module Sidekiq
265
274
  entry = Sidekiq.redis { |conn|
266
275
  conn.lindex(@rname, -1)
267
276
  }
268
- return 0 unless entry
277
+ return 0.0 unless entry
278
+
269
279
  job = Sidekiq.load_json(entry)
270
- now = Time.now.to_f
271
- thence = job["enqueued_at"] || now
272
- now - thence
280
+ enqueued_at = job["enqueued_at"]
281
+ if enqueued_at
282
+ if enqueued_at.is_a?(Float)
283
+ # old format
284
+ now = Time.now.to_f
285
+ now - enqueued_at
286
+ else
287
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
288
+ (now - enqueued_at) / 1000.0
289
+ end
290
+ else
291
+ 0.0
292
+ end
273
293
  end
274
294
 
275
295
  def each
@@ -421,12 +441,26 @@ module Sidekiq
421
441
  self["bid"]
422
442
  end
423
443
 
444
+ def failed_at
445
+ if self["failed_at"]
446
+ time_from_timestamp(self["failed_at"])
447
+ end
448
+ end
449
+
450
+ def retried_at
451
+ if self["retried_at"]
452
+ time_from_timestamp(self["retried_at"])
453
+ end
454
+ end
455
+
424
456
  def enqueued_at
425
- self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
457
+ if self["enqueued_at"]
458
+ time_from_timestamp(self["enqueued_at"])
459
+ end
426
460
  end
427
461
 
428
462
  def created_at
429
- Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
463
+ time_from_timestamp(self["created_at"] || self["enqueued_at"] || 0)
430
464
  end
431
465
 
432
466
  def tags
@@ -444,8 +478,17 @@ module Sidekiq
444
478
  end
445
479
 
446
480
  def latency
447
- now = Time.now.to_f
448
- now - (@item["enqueued_at"] || @item["created_at"] || now)
481
+ timestamp = @item["enqueued_at"] || @item["created_at"]
482
+ if timestamp
483
+ if timestamp.is_a?(Float)
484
+ # old format
485
+ Time.now.to_f - timestamp
486
+ else
487
+ (::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) - timestamp) / 1000.0
488
+ end
489
+ else
490
+ 0.0
491
+ end
449
492
  end
450
493
 
451
494
  # Remove this job from the queue
@@ -494,6 +537,15 @@ module Sidekiq
494
537
  uncompressed = Zlib::Inflate.inflate(strict_base64_decoded)
495
538
  Sidekiq.load_json(uncompressed)
496
539
  end
540
+
541
+ def time_from_timestamp(timestamp)
542
+ if timestamp.is_a?(Float)
543
+ # old format, timestamps were stored as fractional seconds since the epoch
544
+ Time.at(timestamp).utc
545
+ else
546
+ Time.at(timestamp / 1000, timestamp % 1000, :millisecond)
547
+ end
548
+ end
497
549
  end
498
550
 
499
551
  # Represents a job within a Redis sorted set where the score
@@ -1114,9 +1166,8 @@ module Sidekiq
1114
1166
  # works.each do |process_id, thread_id, work|
1115
1167
  # # process_id is a unique identifier per Sidekiq process
1116
1168
  # # thread_id is a unique identifier per thread
1117
- # # work is a Hash which looks like:
1118
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
1119
- # # run_at is an epoch Integer.
1169
+ # # work is a `Sidekiq::Work` instance that has the following accessor methods.
1170
+ # # [work.queue, work.run_at, work.payload]
1120
1171
  # end
1121
1172
  #
1122
1173
  class WorkSet
@@ -1142,7 +1193,7 @@ module Sidekiq
1142
1193
  end
1143
1194
  end
1144
1195
 
1145
- results.sort_by { |(_, _, hsh)| hsh.raw("run_at") }.each(&block)
1196
+ results.sort_by { |(_, _, work)| work.run_at }.each(&block)
1146
1197
  end
1147
1198
 
1148
1199
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1172,13 +1223,14 @@ module Sidekiq
1172
1223
  #
1173
1224
  # @param jid [String] the job identifier
1174
1225
  # @return [Sidekiq::Work] the work or nil
1175
- def find_work_by_jid(jid)
1226
+ def find_work(jid)
1176
1227
  each do |_process_id, _thread_id, work|
1177
1228
  job = work.job
1178
1229
  return work if job.jid == jid
1179
1230
  end
1180
1231
  nil
1181
1232
  end
1233
+ alias_method :find_work_by_jid, :find_work
1182
1234
  end
1183
1235
 
1184
1236
  # Sidekiq::Work represents a job which is currently executing.
@@ -1208,33 +1260,66 @@ module Sidekiq
1208
1260
  def payload
1209
1261
  @hsh["payload"]
1210
1262
  end
1263
+ end
1211
1264
 
1212
- # deprecated
1213
- def [](key)
1214
- kwargs = {uplevel: 1}
1215
- kwargs[:category] = :deprecated if RUBY_VERSION > "3.0" # TODO
1216
- warn("Direct access to `Sidekiq::Work` attributes is deprecated, please use `#payload`, `#queue`, `#run_at` or `#job` instead", **kwargs)
1265
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1266
+ # Is "worker" a process, a type of job, a thread? Undefined!
1267
+ # WorkSet better describes the data.
1268
+ Workers = WorkSet
1217
1269
 
1218
- @hsh[key]
1219
- end
1270
+ class ProfileSet
1271
+ include Enumerable
1220
1272
 
1221
- # :nodoc:
1222
- # @api private
1223
- def raw(name)
1224
- @hsh[name]
1273
+ # This is a point in time/snapshot API, you'll need to instantiate a new instance
1274
+ # if you want to fetch newer records.
1275
+ def initialize
1276
+ @records = Sidekiq.redis do |c|
1277
+ # This throws away expired profiles
1278
+ c.zremrangebyscore("profiles", "-inf", Time.now.to_f.to_s)
1279
+ # retreive records, newest to oldest
1280
+ c.zrange("profiles", "+inf", 0, "byscore", "rev")
1281
+ end
1225
1282
  end
1226
1283
 
1227
- def method_missing(*all)
1228
- @hsh.send(*all)
1284
+ def size
1285
+ @records.size
1229
1286
  end
1230
1287
 
1231
- def respond_to_missing?(name, *args)
1232
- @hsh.respond_to?(name)
1288
+ def each(&block)
1289
+ fetch_keys = %w[started_at jid type token size elapsed].freeze
1290
+ arrays = Sidekiq.redis do |c|
1291
+ c.pipelined do |p|
1292
+ @records.each do |key|
1293
+ p.hmget(key, *fetch_keys)
1294
+ end
1295
+ end
1296
+ end
1297
+
1298
+ arrays.compact.map { |arr| ProfileRecord.new(arr) }.each(&block)
1233
1299
  end
1234
1300
  end
1235
1301
 
1236
- # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1237
- # Is "worker" a process, a type of job, a thread? Undefined!
1238
- # WorkSet better describes the data.
1239
- Workers = WorkSet
1302
+ class ProfileRecord
1303
+ attr_reader :started_at, :jid, :type, :token, :size, :elapsed
1304
+
1305
+ def initialize(arr)
1306
+ # Must be same order as fetch_keys above
1307
+ @started_at = Time.at(Integer(arr[0]))
1308
+ @jid = arr[1]
1309
+ @type = arr[2]
1310
+ @token = arr[3]
1311
+ @size = Integer(arr[4])
1312
+ @elapsed = Float(arr[5])
1313
+ end
1314
+
1315
+ def key
1316
+ "#{token}-#{jid}"
1317
+ end
1318
+
1319
+ def data
1320
+ Sidekiq.redis { |c| c.hget(key, "data") }
1321
+ end
1322
+ end
1240
1323
  end
1324
+
1325
+ Sidekiq.loader.run_load_hooks(:api)
@@ -11,12 +11,12 @@ module Sidekiq
11
11
  # This capsule will pull jobs from the "single" queue and process
12
12
  # the jobs with one thread, meaning the jobs will be processed serially.
13
13
  #
14
- # Sidekiq.configure_server do |config|
15
- # config.capsule("single-threaded") do |cap|
16
- # cap.concurrency = 1
17
- # cap.queues = %w(single)
14
+ # Sidekiq.configure_server do |config|
15
+ # config.capsule("single-threaded") do |cap|
16
+ # cap.concurrency = 1
17
+ # cap.queues = %w(single)
18
+ # end
18
19
  # end
19
- # end
20
20
  class Capsule
21
21
  include Sidekiq::Component
22
22
  extend Forwardable
@@ -27,7 +27,7 @@ module Sidekiq
27
27
  attr_reader :mode
28
28
  attr_reader :weights
29
29
 
30
- def_delegators :@config, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
30
+ def_delegators :@config, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig, :thread_priority
31
31
 
32
32
  def initialize(name, config)
33
33
  @name = name
data/lib/sidekiq/cli.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  $stdout.sync = true
4
4
 
5
5
  require "yaml"
6
- require "singleton"
7
6
  require "optparse"
8
7
  require "erb"
9
8
  require "fileutils"
@@ -17,7 +16,6 @@ require "sidekiq/launcher"
17
16
  module Sidekiq # :nodoc:
18
17
  class CLI
19
18
  include Sidekiq::Component
20
- include Singleton unless $TESTING
21
19
 
22
20
  attr_accessor :launcher
23
21
  attr_accessor :environment
@@ -31,6 +29,10 @@ module Sidekiq # :nodoc:
31
29
  validate!
32
30
  end
33
31
 
32
+ def self.instance
33
+ @instance ||= new
34
+ end
35
+
34
36
  def jruby?
35
37
  defined?(::JRUBY_VERSION)
36
38
  end
@@ -74,7 +76,7 @@ module Sidekiq # :nodoc:
74
76
  # fire startup and start multithreading.
75
77
  info = @config.redis_info
76
78
  ver = Gem::Version.new(info["redis_version"])
77
- raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
79
+ raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
78
80
 
79
81
  maxmemory_policy = info["maxmemory_policy"]
80
82
  if maxmemory_policy != "noeviction" && maxmemory_policy != ""
@@ -298,29 +300,18 @@ module Sidekiq # :nodoc:
298
300
 
299
301
  if File.directory?(@config[:require])
300
302
  require "rails"
301
- if ::Rails::VERSION::MAJOR < 6
302
- warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 6+"
303
+ if ::Rails::VERSION::MAJOR < 7
304
+ warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 7+"
303
305
  end
304
306
  require "sidekiq/rails"
305
307
  require File.expand_path("#{@config[:require]}/config/environment.rb")
306
- @config[:tag] ||= default_tag
308
+ @config[:tag] ||= default_tag(::Rails.root)
307
309
  else
308
310
  require @config[:require]
311
+ @config[:tag] ||= default_tag
309
312
  end
310
313
  end
311
314
 
312
- def default_tag
313
- dir = ::Rails.root
314
- name = File.basename(dir)
315
- prevdir = File.dirname(dir) # Capistrano release directory?
316
- if name.to_i != 0 && prevdir
317
- if File.basename(prevdir) == "releases"
318
- return File.basename(File.dirname(prevdir))
319
- end
320
- end
321
- name
322
- end
323
-
324
315
  def validate!
325
316
  if !File.exist?(@config[:require]) ||
326
317
  (File.directory?(@config[:require]) && !File.exist?("#{@config[:require]}/config/application.rb"))
@@ -395,7 +386,12 @@ module Sidekiq # :nodoc:
395
386
  end
396
387
 
397
388
  def initialize_logger
398
- @config.logger.level = ::Logger::DEBUG if @config[:verbose]
389
+ if @config[:verbose] || ENV["DEBUG_INVOCATION"] == "1"
390
+ # DEBUG_INVOCATION is a systemd-ism triggered by
391
+ # RestartMode=debug. We turn on debugging when the
392
+ # sidekiq process crashes and is restarted with this flag.
393
+ @config.logger.level = ::Logger::DEBUG
394
+ end
399
395
  end
400
396
 
401
397
  def parse_config(path)
@@ -67,9 +67,7 @@ module Sidekiq
67
67
  c.pipelined do |p|
68
68
  p.hsetnx(key, "cancelled", Time.now.to_i)
69
69
  p.hget(key, "cancelled")
70
- p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
71
- # TODO When Redis 7.2 is required
72
- # p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
70
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
73
71
  end
74
72
  end
75
73
  result.to_i
@@ -119,6 +117,9 @@ module Sidekiq
119
117
  # larger than 1000 but YMMV based on network quality, size of job args, etc.
120
118
  # A large number of jobs can cause a bit of Redis command processing latency.
121
119
  #
120
+ # Accepts an additional `:spread_interval` option (in seconds) to randomly spread
121
+ # the jobs schedule times over the specified interval.
122
+ #
122
123
  # Takes the same arguments as #push except that args is expected to be
123
124
  # an Array of Arrays. All other keys are duplicated for each job. Each job
124
125
  # is run through the client middleware pipeline and each job gets its own Job ID
@@ -133,13 +134,24 @@ module Sidekiq
133
134
  def push_bulk(items)
134
135
  batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
135
136
  args = items["args"]
136
- at = items.delete("at")
137
+ at = items.delete("at") || items.delete(:at)
137
138
  raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all? { |entry| entry.is_a?(Numeric) })
138
139
  raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
139
140
 
140
141
  jid = items.delete("jid")
141
142
  raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
142
143
 
144
+ spread_interval = items.delete(:spread_interval) || items.delete("spread_interval")
145
+ raise ArgumentError, "Jobs 'spread_interval' must be a positive Numeric" if spread_interval && (!spread_interval.is_a?(Numeric) || spread_interval <= 0)
146
+ raise ArgumentError, "Only one of 'at' or 'spread_interval' can be provided" if at && spread_interval
147
+
148
+ if !at && spread_interval
149
+ # Do not use spread interval smaller than pooling interval.
150
+ spread_interval = [spread_interval, 5].max
151
+ now = Time.now.to_f
152
+ at = args.map { now + rand * spread_interval }
153
+ end
154
+
143
155
  normed = normalize_item(items)
144
156
  slice_index = 0
145
157
  result = args.each_slice(batch_size).flat_map do |slice|
@@ -266,22 +278,21 @@ module Sidekiq
266
278
  if payloads.first.key?("at")
267
279
  conn.zadd("schedule", payloads.flat_map { |hash|
268
280
  at = hash["at"].to_s
269
- # ActiveJob sets this but the job has not been enqueued yet
270
- hash.delete("enqueued_at")
271
- # TODO: Use hash.except("at") when support for Ruby 2.7 is dropped
272
- hash = hash.dup
273
- hash.delete("at")
281
+ # ActiveJob sets enqueued_at but the job has not been enqueued yet
282
+ hash = hash.except("enqueued_at", "at")
274
283
  [at, Sidekiq.dump_json(hash)]
275
284
  })
276
285
  else
277
- queue = payloads.first["queue"]
278
- now = Time.now.to_f
279
- to_push = payloads.map { |entry|
280
- entry["enqueued_at"] = now
281
- Sidekiq.dump_json(entry)
282
- }
283
- conn.sadd("queues", [queue])
284
- conn.lpush("queue:#{queue}", to_push)
286
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) # milliseconds since the epoch
287
+ grouped_queues = payloads.group_by { |job| job["queue"] }
288
+ conn.sadd("queues", grouped_queues.keys)
289
+ grouped_queues.each do |queue, grouped_payloads|
290
+ to_push = grouped_payloads.map { |entry|
291
+ entry["enqueued_at"] = now
292
+ Sidekiq.dump_json(entry)
293
+ }
294
+ conn.lpush("queue:#{queue}", to_push)
295
+ end
285
296
  end
286
297
  end
287
298
  end
@@ -1,11 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
+ # Ruby's default thread priority is 0, which uses 100ms time slices.
5
+ # This can lead to some surprising thread starvation; if using a lot of
6
+ # CPU-heavy concurrency, it may take several seconds before a Thread gets
7
+ # on the CPU.
8
+ #
9
+ # Negative priorities lower the timeslice by half, so -1 = 50ms, -2 = 25ms, etc.
10
+ # With more frequent timeslices, we reduce the risk of unintentional timeouts
11
+ # and starvation.
12
+ #
13
+ # Customize like so:
14
+ #
15
+ # Sidekiq.configure_server do |cfg|
16
+ # cfg.thread_priority = 0
17
+ # end
18
+ #
19
+ DEFAULT_THREAD_PRIORITY = -1
20
+
4
21
  ##
5
- # Sidekiq::Component assumes a config instance is available at @config
22
+ # Sidekiq::Component provides a set of utility methods depending only
23
+ # on Sidekiq::Config. It assumes a config instance is available at @config.
6
24
  module Component # :nodoc:
7
25
  attr_reader :config
8
26
 
27
+ # This is epoch milliseconds, appropriate for persistence
28
+ def real_ms
29
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
30
+ end
31
+
32
+ # used for time difference and relative comparisons, not persistence.
33
+ def mono_ms
34
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
35
+ end
36
+
9
37
  def watchdog(last_words)
10
38
  yield
11
39
  rescue Exception => ex
@@ -13,11 +41,11 @@ module Sidekiq
13
41
  raise ex
14
42
  end
15
43
 
16
- def safe_thread(name, &block)
44
+ def safe_thread(name, priority: nil, &block)
17
45
  Thread.new do
18
46
  Thread.current.name = "sidekiq.#{name}"
19
47
  watchdog(name, &block)
20
- end
48
+ end.tap { |t| t.priority = (priority || config.thread_priority || DEFAULT_THREAD_PRIORITY) }
21
49
  end
22
50
 
23
51
  def logger
@@ -86,5 +114,16 @@ module Sidekiq
86
114
  end.join(", ")
87
115
  }>"
88
116
  end
117
+
118
+ def default_tag(dir = Dir.pwd)
119
+ name = File.basename(dir)
120
+ prevdir = File.dirname(dir) # Capistrano release directory?
121
+ if name.to_i != 0 && prevdir
122
+ if File.basename(prevdir) == "releases"
123
+ return File.basename(File.dirname(prevdir))
124
+ end
125
+ end
126
+ name
127
+ end
89
128
  end
90
129
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
-
5
- require "set"
6
4
  require "sidekiq/redis_connection"
7
5
 
8
6
  module Sidekiq
@@ -19,16 +17,16 @@ module Sidekiq
19
17
  poll_interval_average: nil,
20
18
  average_scheduled_poll_interval: 5,
21
19
  on_complex_arguments: :raise,
22
- iteration: {
23
- max_job_runtime: nil,
24
- retry_backoff: 0
25
- },
20
+ # if the Iterable job runs longer than this value (in seconds), then the job
21
+ # will be interrupted after the current iteration and re-enqueued at the back of the queue
22
+ max_iteration_runtime: nil,
26
23
  error_handlers: [],
27
24
  death_handlers: [],
28
25
  lifecycle_events: {
29
26
  startup: [],
30
27
  quiet: [],
31
28
  shutdown: [],
29
+ exit: [],
32
30
  # triggers when we fire the first heartbeat on startup OR repairing a network partition
33
31
  heartbeat: [],
34
32
  # triggers on EVERY heartbeat call, every 10 seconds
@@ -41,12 +39,22 @@ module Sidekiq
41
39
  }
42
40
 
43
41
  ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
44
- l = cfg.logger
45
- l.warn(Sidekiq.dump_json(ctx)) unless ctx.empty?
46
- l.warn("#{ex.class.name}: #{ex.message}")
47
- unless ex.backtrace.nil?
48
- backtrace = cfg[:backtrace_cleaner].call(ex.backtrace)
49
- l.warn(backtrace.join("\n"))
42
+ Sidekiq::Context.with(ctx) do
43
+ dev = cfg[:environment] == "development"
44
+ fancy = dev && $stdout.tty? # 🎩
45
+ # Weird logic here but we want to show the backtrace in local
46
+ # development or if verbose logging is enabled.
47
+ #
48
+ # `full_message` contains the error class, message and backtrace
49
+ # `detailed_message` contains the error class and message
50
+ #
51
+ # Absolutely terrible API names. Not useful at all to have two
52
+ # methods with similar but obscure names.
53
+ if dev || cfg.logger.debug?
54
+ cfg.logger.info { ex.full_message(highlight: fancy) }
55
+ else
56
+ cfg.logger.info { ex.detailed_message(highlight: fancy) }
57
+ end
50
58
  end
51
59
  }
52
60
 
@@ -60,6 +68,7 @@ module Sidekiq
60
68
 
61
69
  def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
62
70
  attr_reader :capsules
71
+ attr_accessor :thread_priority
63
72
 
64
73
  def inspect
65
74
  "#<#{self.class.name} @options=#{
@@ -249,7 +258,7 @@ module Sidekiq
249
258
  end
250
259
 
251
260
  # Register a block to run at a point in the Sidekiq lifecycle.
252
- # :startup, :quiet or :shutdown are valid events.
261
+ # :startup, :quiet, :shutdown, or :exit are valid events.
253
262
  #
254
263
  # Sidekiq.configure_server do |config|
255
264
  # config.on(:shutdown) do
@@ -293,13 +302,7 @@ module Sidekiq
293
302
  p ["!!!!!", ex]
294
303
  end
295
304
  @options[:error_handlers].each do |handler|
296
- if parameter_size(handler) == 2
297
- # TODO Remove in 8.0
298
- logger.info { "DEPRECATION: Sidekiq exception handlers now take three arguments, see #{handler}" }
299
- handler.call(ex, {_config: self}.merge(ctx))
300
- else
301
- handler.call(ex, ctx, self)
302
- end
305
+ handler.call(ex, ctx, self)
303
306
  rescue Exception => e
304
307
  l = logger
305
308
  l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
@@ -34,6 +34,7 @@ module Sidekiq
34
34
  private
35
35
 
36
36
  def housekeeping
37
+ @config[:tag] ||= default_tag
37
38
  logger.info "Running in #{RUBY_DESCRIPTION}"
38
39
  logger.info Sidekiq::LICENSE
39
40
  logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
@@ -42,7 +43,7 @@ module Sidekiq
42
43
  # fire startup and start multithreading.
43
44
  info = config.redis_info
44
45
  ver = Gem::Version.new(info["redis_version"])
45
- raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
46
+ raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
46
47
 
47
48
  maxmemory_policy = info["maxmemory_policy"]
48
49
  if maxmemory_policy != "noeviction"
@@ -46,6 +46,7 @@ module Sidekiq
46
46
  # def on_start
47
47
  # def on_resume
48
48
  # def on_stop
49
+ # def on_cancel
49
50
  # def on_complete
50
51
  # def around_iteration
51
52
  #