sidekiq 7.3.10 → 8.0.10

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +135 -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 +152 -44
  9. data/lib/sidekiq/capsule.rb +10 -6
  10. data/lib/sidekiq/cli.rb +30 -23
  11. data/lib/sidekiq/client.rb +28 -17
  12. data/lib/sidekiq/component.rb +42 -3
  13. data/lib/sidekiq/config.rb +32 -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 +20 -10
  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/redis_connection.rb +2 -2
  36. data/lib/sidekiq/scheduled.rb +4 -4
  37. data/lib/sidekiq/testing.rb +3 -3
  38. data/lib/sidekiq/version.rb +2 -2
  39. data/lib/sidekiq/web/action.rb +146 -83
  40. data/lib/sidekiq/web/application.rb +361 -330
  41. data/lib/sidekiq/web/config.rb +120 -0
  42. data/lib/sidekiq/web/helpers.rb +58 -36
  43. data/lib/sidekiq/web/router.rb +60 -76
  44. data/lib/sidekiq/web.rb +51 -156
  45. data/lib/sidekiq.rb +6 -1
  46. data/sidekiq.gemspec +6 -6
  47. data/web/assets/images/logo.png +0 -0
  48. data/web/assets/images/status.png +0 -0
  49. data/web/assets/javascripts/application.js +42 -26
  50. data/web/assets/javascripts/base-charts.js +30 -16
  51. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  52. data/web/assets/javascripts/dashboard.js +1 -1
  53. data/web/assets/javascripts/metrics.js +16 -34
  54. data/web/assets/stylesheets/style.css +776 -0
  55. data/web/locales/ar.yml +2 -0
  56. data/web/locales/cs.yml +2 -0
  57. data/web/locales/da.yml +2 -0
  58. data/web/locales/de.yml +2 -0
  59. data/web/locales/el.yml +2 -0
  60. data/web/locales/en.yml +7 -0
  61. data/web/locales/es.yml +25 -2
  62. data/web/locales/fa.yml +2 -0
  63. data/web/locales/fr.yml +3 -1
  64. data/web/locales/gd.yml +2 -0
  65. data/web/locales/he.yml +2 -0
  66. data/web/locales/hi.yml +2 -0
  67. data/web/locales/it.yml +9 -0
  68. data/web/locales/ja.yml +2 -0
  69. data/web/locales/ko.yml +2 -0
  70. data/web/locales/lt.yml +2 -0
  71. data/web/locales/nb.yml +2 -0
  72. data/web/locales/nl.yml +2 -0
  73. data/web/locales/pl.yml +2 -0
  74. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -1
  75. data/web/locales/pt.yml +2 -0
  76. data/web/locales/ru.yml +2 -0
  77. data/web/locales/sv.yml +2 -0
  78. data/web/locales/ta.yml +2 -0
  79. data/web/locales/tr.yml +2 -0
  80. data/web/locales/uk.yml +7 -5
  81. data/web/locales/ur.yml +2 -0
  82. data/web/locales/vi.yml +2 -0
  83. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
  84. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -1
  85. data/web/views/_footer.erb +31 -33
  86. data/web/views/_job_info.erb +91 -89
  87. data/web/views/_metrics_period_select.erb +13 -10
  88. data/web/views/_nav.erb +14 -21
  89. data/web/views/_paging.erb +22 -21
  90. data/web/views/_poll_link.erb +2 -2
  91. data/web/views/_summary.erb +16 -16
  92. data/web/views/busy.erb +123 -125
  93. data/web/views/dashboard.erb +62 -66
  94. data/web/views/dead.erb +31 -27
  95. data/web/views/filtering.erb +4 -4
  96. data/web/views/layout.erb +12 -28
  97. data/web/views/metrics.erb +76 -82
  98. data/web/views/metrics_for_job.erb +45 -46
  99. data/web/views/morgue.erb +65 -70
  100. data/web/views/profiles.erb +43 -0
  101. data/web/views/queue.erb +54 -52
  102. data/web/views/queues.erb +43 -41
  103. data/web/views/retries.erb +70 -75
  104. data/web/views/retry.erb +32 -27
  105. data/web/views/scheduled.erb +63 -55
  106. data/web/views/scheduled_job_info.erb +3 -3
  107. metadata +24 -41
  108. data/web/assets/stylesheets/application-dark.css +0 -147
  109. data/web/assets/stylesheets/application-rtl.css +0 -163
  110. data/web/assets/stylesheets/application.css +0 -759
  111. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  112. data/web/assets/stylesheets/bootstrap.css +0 -5
  113. 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,35 @@ module Sidekiq
1037
1089
  self["identity"]
1038
1090
  end
1039
1091
 
1092
+ # deprecated, use capsules below
1040
1093
  def queues
1041
- self["queues"]
1094
+ # Backwards compatibility with <8.0.8
1095
+ if !self["capsules"]
1096
+ self["queues"]
1097
+ else
1098
+ capsules.values.flat_map { |x| x["weights"].keys }.uniq
1099
+ end
1042
1100
  end
1043
1101
 
1102
+ # deprecated, use capsules below
1044
1103
  def weights
1045
- self["weights"]
1104
+ # Backwards compatibility with <8.0.8
1105
+ if !self["capsules"]
1106
+ self["weights"]
1107
+ else
1108
+ hash = {}
1109
+ capsules.values.each do |cap|
1110
+ # Note: will lose data if two capsules are processing the same named queue
1111
+ cap["weights"].each_pair do |queue, weight|
1112
+ hash[queue] = weight
1113
+ end
1114
+ end
1115
+ hash
1116
+ end
1117
+ end
1118
+
1119
+ def capsules
1120
+ self["capsules"]
1046
1121
  end
1047
1122
 
1048
1123
  def version
@@ -1114,9 +1189,8 @@ module Sidekiq
1114
1189
  # works.each do |process_id, thread_id, work|
1115
1190
  # # process_id is a unique identifier per Sidekiq process
1116
1191
  # # 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.
1192
+ # # work is a `Sidekiq::Work` instance that has the following accessor methods.
1193
+ # # [work.queue, work.run_at, work.payload]
1120
1194
  # end
1121
1195
  #
1122
1196
  class WorkSet
@@ -1142,7 +1216,7 @@ module Sidekiq
1142
1216
  end
1143
1217
  end
1144
1218
 
1145
- results.sort_by { |(_, _, hsh)| hsh.raw("run_at") }.each(&block)
1219
+ results.sort_by { |(_, _, work)| work.run_at }.each(&block)
1146
1220
  end
1147
1221
 
1148
1222
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1172,13 +1246,14 @@ module Sidekiq
1172
1246
  #
1173
1247
  # @param jid [String] the job identifier
1174
1248
  # @return [Sidekiq::Work] the work or nil
1175
- def find_work_by_jid(jid)
1249
+ def find_work(jid)
1176
1250
  each do |_process_id, _thread_id, work|
1177
1251
  job = work.job
1178
1252
  return work if job.jid == jid
1179
1253
  end
1180
1254
  nil
1181
1255
  end
1256
+ alias_method :find_work_by_jid, :find_work
1182
1257
  end
1183
1258
 
1184
1259
  # Sidekiq::Work represents a job which is currently executing.
@@ -1208,33 +1283,66 @@ module Sidekiq
1208
1283
  def payload
1209
1284
  @hsh["payload"]
1210
1285
  end
1286
+ end
1211
1287
 
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)
1288
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1289
+ # Is "worker" a process, a type of job, a thread? Undefined!
1290
+ # WorkSet better describes the data.
1291
+ Workers = WorkSet
1217
1292
 
1218
- @hsh[key]
1219
- end
1293
+ class ProfileSet
1294
+ include Enumerable
1220
1295
 
1221
- # :nodoc:
1222
- # @api private
1223
- def raw(name)
1224
- @hsh[name]
1296
+ # This is a point in time/snapshot API, you'll need to instantiate a new instance
1297
+ # if you want to fetch newer records.
1298
+ def initialize
1299
+ @records = Sidekiq.redis do |c|
1300
+ # This throws away expired profiles
1301
+ c.zremrangebyscore("profiles", "-inf", Time.now.to_f.to_s)
1302
+ # retreive records, newest to oldest
1303
+ c.zrange("profiles", "+inf", 0, "byscore", "rev")
1304
+ end
1225
1305
  end
1226
1306
 
1227
- def method_missing(*all)
1228
- @hsh.send(*all)
1307
+ def size
1308
+ @records.size
1229
1309
  end
1230
1310
 
1231
- def respond_to_missing?(name, *args)
1232
- @hsh.respond_to?(name)
1311
+ def each(&block)
1312
+ fetch_keys = %w[started_at jid type token size elapsed].freeze
1313
+ arrays = Sidekiq.redis do |c|
1314
+ c.pipelined do |p|
1315
+ @records.each do |key|
1316
+ p.hmget(key, *fetch_keys)
1317
+ end
1318
+ end
1319
+ end
1320
+
1321
+ arrays.compact.map { |arr| ProfileRecord.new(arr) }.each(&block)
1233
1322
  end
1234
1323
  end
1235
1324
 
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
1325
+ class ProfileRecord
1326
+ attr_reader :started_at, :jid, :type, :token, :size, :elapsed
1327
+
1328
+ def initialize(arr)
1329
+ # Must be same order as fetch_keys above
1330
+ @started_at = Time.at(Integer(arr[0]))
1331
+ @jid = arr[1]
1332
+ @type = arr[2]
1333
+ @token = arr[3]
1334
+ @size = Integer(arr[4])
1335
+ @elapsed = Float(arr[5])
1336
+ end
1337
+
1338
+ def key
1339
+ "#{token}-#{jid}"
1340
+ end
1341
+
1342
+ def data
1343
+ Sidekiq.redis { |c| c.hget(key, "data") }
1344
+ end
1345
+ end
1240
1346
  end
1347
+
1348
+ 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
@@ -47,7 +49,7 @@ module Sidekiq # :nodoc:
47
49
  logger.info "Booted Rails #{::Rails.version} application in #{environment} environment" if rails_app?
48
50
 
49
51
  self_read, self_write = IO.pipe
50
- sigs = %w[INT TERM TTIN TSTP]
52
+ sigs = %w[INT TERM INFO TTIN TSTP]
51
53
  # USR1 and USR2 don't work on the JVM
52
54
  sigs << "USR2" if Sidekiq.pro? && !jruby?
53
55
  sigs.each do |sig|
@@ -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 != ""
@@ -199,6 +201,7 @@ module Sidekiq # :nodoc:
199
201
  cli.logger.info "Received TSTP, no longer accepting new work"
200
202
  cli.launcher.quiet
201
203
  },
204
+ # deprecated, use INFO
202
205
  "TTIN" => ->(cli) {
203
206
  Thread.list.each do |thread|
204
207
  cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
@@ -208,14 +211,24 @@ module Sidekiq # :nodoc:
208
211
  cli.logger.warn "<no backtrace available>"
209
212
  end
210
213
  end
214
+ },
215
+ "INFO" => ->(cli) {
216
+ Thread.list.each do |thread|
217
+ cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
218
+ if thread.backtrace
219
+ cli.logger.warn thread.backtrace.join("\n")
220
+ else
221
+ cli.logger.warn "<no backtrace available>"
222
+ end
223
+ end
211
224
  }
212
225
  }
213
- UNHANDLED_SIGNAL_HANDLER = ->(cli) { cli.logger.info "No signal handler registered, ignoring" }
214
- SIGNAL_HANDLERS.default = UNHANDLED_SIGNAL_HANDLER
215
226
 
216
227
  def handle_signal(sig)
217
228
  logger.debug "Got #{sig} signal"
218
- SIGNAL_HANDLERS[sig].call(self)
229
+ hndlr = SIGNAL_HANDLERS[sig]
230
+ hndlr ? hndlr.call(self) :
231
+ logger.warn("No #{sig} signal handler registered, ignoring")
219
232
  end
220
233
 
221
234
  private
@@ -298,29 +311,18 @@ module Sidekiq # :nodoc:
298
311
 
299
312
  if File.directory?(@config[:require])
300
313
  require "rails"
301
- if ::Rails::VERSION::MAJOR < 6
302
- warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 6+"
314
+ if ::Rails::VERSION::MAJOR < 7
315
+ warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 7+"
303
316
  end
304
317
  require "sidekiq/rails"
305
318
  require File.expand_path("#{@config[:require]}/config/environment.rb")
306
- @config[:tag] ||= default_tag
319
+ @config[:tag] ||= default_tag(::Rails.root)
307
320
  else
308
321
  require @config[:require]
322
+ @config[:tag] ||= default_tag
309
323
  end
310
324
  end
311
325
 
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
326
  def validate!
325
327
  if !File.exist?(@config[:require]) ||
326
328
  (File.directory?(@config[:require]) && !File.exist?("#{@config[:require]}/config/application.rb"))
@@ -395,7 +397,12 @@ module Sidekiq # :nodoc:
395
397
  end
396
398
 
397
399
  def initialize_logger
398
- @config.logger.level = ::Logger::DEBUG if @config[:verbose]
400
+ if @config[:verbose] || ENV["DEBUG_INVOCATION"] == "1"
401
+ # DEBUG_INVOCATION is a systemd-ism triggered by
402
+ # RestartMode=debug. We turn on debugging when the
403
+ # sidekiq process crashes and is restarted with this flag.
404
+ @config.logger.level = ::Logger::DEBUG
405
+ end
399
406
  end
400
407
 
401
408
  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