sidekiq 7.3.10 → 8.0.9

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 +125 -4
  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/generators/sidekiq/templates/job.rb.erb +1 -1
  8. data/lib/sidekiq/api.rb +142 -44
  9. data/lib/sidekiq/capsule.rb +10 -6
  10. data/lib/sidekiq/cli.rb +15 -19
  11. data/lib/sidekiq/client.rb +28 -17
  12. data/lib/sidekiq/component.rb +42 -3
  13. data/lib/sidekiq/config.rb +31 -22
  14. data/lib/sidekiq/embedded.rb +2 -1
  15. data/lib/sidekiq/iterable_job.rb +1 -0
  16. data/lib/sidekiq/job/iterable.rb +46 -18
  17. data/lib/sidekiq/job.rb +2 -2
  18. data/lib/sidekiq/job_logger.rb +8 -6
  19. data/lib/sidekiq/job_retry.rb +33 -10
  20. data/lib/sidekiq/job_util.rb +5 -1
  21. data/lib/sidekiq/launcher.rb +16 -6
  22. data/lib/sidekiq/loader.rb +57 -0
  23. data/lib/sidekiq/logger.rb +25 -69
  24. data/lib/sidekiq/manager.rb +0 -1
  25. data/lib/sidekiq/metrics/query.rb +71 -45
  26. data/lib/sidekiq/metrics/shared.rb +8 -5
  27. data/lib/sidekiq/metrics/tracking.rb +12 -7
  28. data/lib/sidekiq/middleware/current_attributes.rb +11 -19
  29. data/lib/sidekiq/monitor.rb +4 -8
  30. data/lib/sidekiq/paginator.rb +8 -1
  31. data/lib/sidekiq/processor.rb +21 -14
  32. data/lib/sidekiq/profiler.rb +73 -0
  33. data/lib/sidekiq/rails.rb +46 -67
  34. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  35. data/lib/sidekiq/testing.rb +3 -3
  36. data/lib/sidekiq/version.rb +2 -2
  37. data/lib/sidekiq/web/action.rb +146 -83
  38. data/lib/sidekiq/web/application.rb +361 -330
  39. data/lib/sidekiq/web/config.rb +120 -0
  40. data/lib/sidekiq/web/helpers.rb +56 -34
  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 +42 -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 +776 -0
  53. data/web/locales/ar.yml +2 -0
  54. data/web/locales/cs.yml +2 -0
  55. data/web/locales/da.yml +2 -0
  56. data/web/locales/de.yml +2 -0
  57. data/web/locales/el.yml +2 -0
  58. data/web/locales/en.yml +7 -0
  59. data/web/locales/es.yml +25 -2
  60. data/web/locales/fa.yml +2 -0
  61. data/web/locales/fr.yml +2 -0
  62. data/web/locales/gd.yml +2 -0
  63. data/web/locales/he.yml +2 -0
  64. data/web/locales/hi.yml +2 -0
  65. data/web/locales/it.yml +9 -0
  66. data/web/locales/ja.yml +2 -0
  67. data/web/locales/ko.yml +2 -0
  68. data/web/locales/lt.yml +2 -0
  69. data/web/locales/nb.yml +2 -0
  70. data/web/locales/nl.yml +2 -0
  71. data/web/locales/pl.yml +2 -0
  72. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -1
  73. data/web/locales/pt.yml +2 -0
  74. data/web/locales/ru.yml +2 -0
  75. data/web/locales/sv.yml +2 -0
  76. data/web/locales/ta.yml +2 -0
  77. data/web/locales/tr.yml +2 -0
  78. data/web/locales/uk.yml +7 -5
  79. data/web/locales/ur.yml +2 -0
  80. data/web/locales/vi.yml +2 -0
  81. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
  82. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -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 +22 -21
  88. data/web/views/_poll_link.erb +2 -2
  89. data/web/views/_summary.erb +16 -16
  90. data/web/views/busy.erb +123 -125
  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 +12 -28
  95. data/web/views/metrics.erb +76 -82
  96. data/web/views/metrics_for_job.erb +45 -46
  97. data/web/views/morgue.erb +65 -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 +70 -75
  102. data/web/views/retry.erb +32 -27
  103. data/web/views/scheduled.erb +63 -55
  104. data/web/views/scheduled_job_info.erb +3 -3
  105. metadata +24 -41
  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
@@ -1007,9 +1059,9 @@ module Sidekiq
1007
1059
  # 'started_at' => <process start time>,
1008
1060
  # 'pid' => 12345,
1009
1061
  # 'tag' => 'myapp'
1010
- # 'concurrency' => 25,
1011
- # 'queues' => ['default', 'low'],
1012
- # 'busy' => 10,
1062
+ # 'concurrency' => 5,
1063
+ # 'capsules' => {"default" => {"mode" => "weighted", "concurrency" => 5, "weights" => {"default" => 2, "low" => 1}}},
1064
+ # 'busy' => 3,
1013
1065
  # 'beat' => <last heartbeat>,
1014
1066
  # 'identity' => <unique string identifying the process>,
1015
1067
  # 'embedded' => true,
@@ -1037,12 +1089,25 @@ module Sidekiq
1037
1089
  self["identity"]
1038
1090
  end
1039
1091
 
1092
+ # deprecated, use capsules below
1040
1093
  def queues
1041
- self["queues"]
1094
+ capsules.values.flat_map { |x| x["weights"].keys }.uniq
1042
1095
  end
1043
1096
 
1097
+ # deprecated, use capsules below
1044
1098
  def weights
1045
- self["weights"]
1099
+ hash = {}
1100
+ capsules.values.each do |cap|
1101
+ # Note: will lose data if two capsules are processing the same named queue
1102
+ cap["weights"].each_pair do |queue, weight|
1103
+ hash[queue] = weight
1104
+ end
1105
+ end
1106
+ hash
1107
+ end
1108
+
1109
+ def capsules
1110
+ self["capsules"]
1046
1111
  end
1047
1112
 
1048
1113
  def version
@@ -1114,9 +1179,8 @@ module Sidekiq
1114
1179
  # works.each do |process_id, thread_id, work|
1115
1180
  # # process_id is a unique identifier per Sidekiq process
1116
1181
  # # 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.
1182
+ # # work is a `Sidekiq::Work` instance that has the following accessor methods.
1183
+ # # [work.queue, work.run_at, work.payload]
1120
1184
  # end
1121
1185
  #
1122
1186
  class WorkSet
@@ -1142,7 +1206,7 @@ module Sidekiq
1142
1206
  end
1143
1207
  end
1144
1208
 
1145
- results.sort_by { |(_, _, hsh)| hsh.raw("run_at") }.each(&block)
1209
+ results.sort_by { |(_, _, work)| work.run_at }.each(&block)
1146
1210
  end
1147
1211
 
1148
1212
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1172,13 +1236,14 @@ module Sidekiq
1172
1236
  #
1173
1237
  # @param jid [String] the job identifier
1174
1238
  # @return [Sidekiq::Work] the work or nil
1175
- def find_work_by_jid(jid)
1239
+ def find_work(jid)
1176
1240
  each do |_process_id, _thread_id, work|
1177
1241
  job = work.job
1178
1242
  return work if job.jid == jid
1179
1243
  end
1180
1244
  nil
1181
1245
  end
1246
+ alias_method :find_work_by_jid, :find_work
1182
1247
  end
1183
1248
 
1184
1249
  # Sidekiq::Work represents a job which is currently executing.
@@ -1208,33 +1273,66 @@ module Sidekiq
1208
1273
  def payload
1209
1274
  @hsh["payload"]
1210
1275
  end
1276
+ end
1211
1277
 
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)
1278
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1279
+ # Is "worker" a process, a type of job, a thread? Undefined!
1280
+ # WorkSet better describes the data.
1281
+ Workers = WorkSet
1217
1282
 
1218
- @hsh[key]
1219
- end
1283
+ class ProfileSet
1284
+ include Enumerable
1220
1285
 
1221
- # :nodoc:
1222
- # @api private
1223
- def raw(name)
1224
- @hsh[name]
1286
+ # This is a point in time/snapshot API, you'll need to instantiate a new instance
1287
+ # if you want to fetch newer records.
1288
+ def initialize
1289
+ @records = Sidekiq.redis do |c|
1290
+ # This throws away expired profiles
1291
+ c.zremrangebyscore("profiles", "-inf", Time.now.to_f.to_s)
1292
+ # retreive records, newest to oldest
1293
+ c.zrange("profiles", "+inf", 0, "byscore", "rev")
1294
+ end
1225
1295
  end
1226
1296
 
1227
- def method_missing(*all)
1228
- @hsh.send(*all)
1297
+ def size
1298
+ @records.size
1229
1299
  end
1230
1300
 
1231
- def respond_to_missing?(name, *args)
1232
- @hsh.respond_to?(name)
1301
+ def each(&block)
1302
+ fetch_keys = %w[started_at jid type token size elapsed].freeze
1303
+ arrays = Sidekiq.redis do |c|
1304
+ c.pipelined do |p|
1305
+ @records.each do |key|
1306
+ p.hmget(key, *fetch_keys)
1307
+ end
1308
+ end
1309
+ end
1310
+
1311
+ arrays.compact.map { |arr| ProfileRecord.new(arr) }.each(&block)
1233
1312
  end
1234
1313
  end
1235
1314
 
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
1315
+ class ProfileRecord
1316
+ attr_reader :started_at, :jid, :type, :token, :size, :elapsed
1317
+
1318
+ def initialize(arr)
1319
+ # Must be same order as fetch_keys above
1320
+ @started_at = Time.at(Integer(arr[0]))
1321
+ @jid = arr[1]
1322
+ @type = arr[2]
1323
+ @token = arr[3]
1324
+ @size = Integer(arr[4])
1325
+ @elapsed = Float(arr[5])
1326
+ end
1327
+
1328
+ def key
1329
+ "#{token}-#{jid}"
1330
+ end
1331
+
1332
+ def data
1333
+ Sidekiq.redis { |c| c.hget(key, "data") }
1334
+ end
1335
+ end
1240
1336
  end
1337
+
1338
+ 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
@@ -38,6 +38,10 @@ module Sidekiq
38
38
  @mode = :strict
39
39
  end
40
40
 
41
+ def to_h
42
+ {concurrency: concurrency, mode: mode, weights: weights}
43
+ end
44
+
41
45
  def fetcher
42
46
  @fetcher ||= begin
43
47
  instance = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
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
@@ -37,16 +35,27 @@ module Sidekiq
37
35
  dead_max_jobs: 10_000,
38
36
  dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
39
37
  reloader: proc { |&block| block.call },
40
- backtrace_cleaner: ->(backtrace) { backtrace }
38
+ backtrace_cleaner: ->(backtrace) { backtrace },
39
+ logged_job_attributes: ["bid", "tags"]
41
40
  }
42
41
 
43
42
  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"))
43
+ Sidekiq::Context.with(ctx) do
44
+ dev = cfg[:environment] == "development"
45
+ fancy = dev && $stdout.tty? # 🎩
46
+ # Weird logic here but we want to show the backtrace in local
47
+ # development or if verbose logging is enabled.
48
+ #
49
+ # `full_message` contains the error class, message and backtrace
50
+ # `detailed_message` contains the error class and message
51
+ #
52
+ # Absolutely terrible API names. Not useful at all to have two
53
+ # methods with similar but obscure names.
54
+ if dev || cfg.logger.debug?
55
+ cfg.logger.info { ex.full_message(highlight: fancy) }
56
+ else
57
+ cfg.logger.info { ex.detailed_message(highlight: fancy) }
58
+ end
50
59
  end
51
60
  }
52
61
 
@@ -60,6 +69,7 @@ module Sidekiq
60
69
 
61
70
  def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
62
71
  attr_reader :capsules
72
+ attr_accessor :thread_priority
63
73
 
64
74
  def inspect
65
75
  "#<#{self.class.name} @options=#{
@@ -135,11 +145,16 @@ module Sidekiq
135
145
  @redis_config = @redis_config.merge(hash)
136
146
  end
137
147
 
148
+ def reap_idle_redis_connections(timeout = nil)
149
+ self[:reap_connections] = timeout
150
+ end
151
+ alias_method :reap, :reap_idle_redis_connections
152
+
138
153
  def redis_pool
139
154
  Thread.current[:sidekiq_redis_pool] || Thread.current[:sidekiq_capsule]&.redis_pool || local_redis_pool
140
155
  end
141
156
 
142
- private def local_redis_pool
157
+ def local_redis_pool
143
158
  # this is our internal client/housekeeping pool. each capsule has its
144
159
  # own pool for executing threads.
145
160
  @redis ||= new_redis_pool(10, "internal")
@@ -249,7 +264,7 @@ module Sidekiq
249
264
  end
250
265
 
251
266
  # Register a block to run at a point in the Sidekiq lifecycle.
252
- # :startup, :quiet or :shutdown are valid events.
267
+ # :startup, :quiet, :shutdown, or :exit are valid events.
253
268
  #
254
269
  # Sidekiq.configure_server do |config|
255
270
  # config.on(:shutdown) do
@@ -293,13 +308,7 @@ module Sidekiq
293
308
  p ["!!!!!", ex]
294
309
  end
295
310
  @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
311
+ handler.call(ex, ctx, self)
303
312
  rescue Exception => e
304
313
  l = logger
305
314
  l.error "!!! ERROR HANDLER THREW AN ERROR !!!"