sidekiq 8.0.10 → 8.1.3

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +58 -0
  3. data/README.md +16 -1
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +11 -8
  7. data/lib/generators/sidekiq/job_generator.rb +15 -3
  8. data/lib/sidekiq/api.rb +130 -76
  9. data/lib/sidekiq/capsule.rb +0 -1
  10. data/lib/sidekiq/cli.rb +2 -1
  11. data/lib/sidekiq/client.rb +3 -1
  12. data/lib/sidekiq/component.rb +3 -0
  13. data/lib/sidekiq/config.rb +4 -5
  14. data/lib/sidekiq/job.rb +2 -0
  15. data/lib/sidekiq/job_retry.rb +7 -3
  16. data/lib/sidekiq/launcher.rb +5 -5
  17. data/lib/sidekiq/manager.rb +1 -1
  18. data/lib/sidekiq/paginator.rb +6 -1
  19. data/lib/sidekiq/profiler.rb +1 -1
  20. data/lib/sidekiq/scheduled.rb +6 -7
  21. data/lib/sidekiq/test_api.rb +331 -0
  22. data/lib/sidekiq/testing/inline.rb +2 -30
  23. data/lib/sidekiq/testing.rb +2 -334
  24. data/lib/sidekiq/tui/controls.rb +53 -0
  25. data/lib/sidekiq/tui/filtering.rb +53 -0
  26. data/lib/sidekiq/tui/tabs/base_tab.rb +187 -0
  27. data/lib/sidekiq/tui/tabs/busy.rb +118 -0
  28. data/lib/sidekiq/tui/tabs/dead.rb +19 -0
  29. data/lib/sidekiq/tui/tabs/home.rb +144 -0
  30. data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  31. data/lib/sidekiq/tui/tabs/queues.rb +95 -0
  32. data/lib/sidekiq/tui/tabs/retries.rb +19 -0
  33. data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  34. data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  35. data/lib/sidekiq/tui/tabs.rb +15 -0
  36. data/lib/sidekiq/tui.rb +380 -0
  37. data/lib/sidekiq/version.rb +1 -1
  38. data/lib/sidekiq/web/action.rb +1 -1
  39. data/lib/sidekiq/web/application.rb +2 -2
  40. data/lib/sidekiq/web/config.rb +3 -6
  41. data/lib/sidekiq/web/helpers.rb +43 -3
  42. data/lib/sidekiq/web.rb +23 -4
  43. data/lib/sidekiq.rb +7 -0
  44. data/sidekiq.gemspec +6 -6
  45. data/web/assets/javascripts/application.js +1 -1
  46. data/web/assets/stylesheets/style.css +2 -2
  47. data/web/locales/ar.yml +1 -1
  48. data/web/locales/fa.yml +1 -1
  49. data/web/locales/gd.yml +1 -1
  50. data/web/locales/he.yml +1 -1
  51. data/web/locales/pt-BR.yml +1 -1
  52. data/web/locales/ur.yml +1 -1
  53. data/web/locales/zh-TW.yml +1 -1
  54. data/web/views/{_paging.erb → _paging.html.erb} +1 -1
  55. data/web/views/{busy.erb → busy.html.erb} +1 -1
  56. data/web/views/{metrics.erb → metrics.html.erb} +3 -2
  57. metadata +51 -35
  58. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  59. /data/web/views/{_footer.erb → _footer.html.erb} +0 -0
  60. /data/web/views/{_job_info.erb → _job_info.html.erb} +0 -0
  61. /data/web/views/{_metrics_period_select.erb → _metrics_period_select.html.erb} +0 -0
  62. /data/web/views/{_nav.erb → _nav.html.erb} +0 -0
  63. /data/web/views/{_poll_link.erb → _poll_link.html.erb} +0 -0
  64. /data/web/views/{_summary.erb → _summary.html.erb} +0 -0
  65. /data/web/views/{dashboard.erb → dashboard.html.erb} +0 -0
  66. /data/web/views/{dead.erb → dead.html.erb} +0 -0
  67. /data/web/views/{filtering.erb → filtering.html.erb} +0 -0
  68. /data/web/views/{layout.erb → layout.html.erb} +0 -0
  69. /data/web/views/{metrics_for_job.erb → metrics_for_job.html.erb} +0 -0
  70. /data/web/views/{morgue.erb → morgue.html.erb} +0 -0
  71. /data/web/views/{profiles.erb → profiles.html.erb} +0 -0
  72. /data/web/views/{queue.erb → queue.html.erb} +0 -0
  73. /data/web/views/{queues.erb → queues.html.erb} +0 -0
  74. /data/web/views/{retries.erb → retries.html.erb} +0 -0
  75. /data/web/views/{retry.erb → retry.html.erb} +0 -0
  76. /data/web/views/{scheduled.erb → scheduled.html.erb} +0 -0
  77. /data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a831ed9ff5c1a11b447110fa77cbccf00fc44d3e3888fd5b1baa23cce5228ee9
4
- data.tar.gz: 0b11c1b102a8ff8313992201b519b87e1e9446b84c72f23369e3390f671680f6
3
+ metadata.gz: 205ebd1ee2e6fdbe27b860b06a03147e96d77a3abc32481eb3bbf51ce01a64a6
4
+ data.tar.gz: 355195d5aadd03117e7eee7bdf144d193a4c9a41d4534b5f05ea6fad847cb7df
5
5
  SHA512:
6
- metadata.gz: f6139796abb98a9bc67bf6498438f52e135b85ea6a360f86b45ddafd4f46d9d5567592940005d93bab5c3700430adfb5be340e08ca4a9e5cbde529d19a9c7dcc
7
- data.tar.gz: '08ac62d0c45ca1a8216833ecfb07ad8f37ca3d1a1137198f0994af7cf0b107c842741a4f8b62ea8c3f59b42aa209418f65d9bcf6b57127b6ae6e09daf9509907'
6
+ metadata.gz: a592aa88c757173e70ee511388f15642e2aad1347df15252e972cea12e42ab91a0e8df702c3ff9e0069595bb68dae32a6d53318df390563a9ae7cc0df31852dd
7
+ data.tar.gz: d10b27d602e3c53fe79a85d21a59d0492831c32c8a67e2a344869c42e22e402ad591f82e3835e14c879a81fe8020430450fecd6a324d008bf33c4592dee23ea9
data/Changes.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  [Sidekiq Changes](https://github.com/sidekiq/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/sidekiq/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/sidekiq/sidekiq/blob/main/Ent-Changes.md)
4
4
 
5
+ 8.1.3
6
+ ----------
7
+
8
+ - Fix edge case leading to duplicate, concurrent execution [#6379]
9
+ If 2 Capsules process jobs from the same queue, long-running
10
+ jobs could run in parallel during process shutdown.
11
+ - [SECURITY] Remove as much YAML usage as possible. [#6950]
12
+ Localization files in `web/locales` are now manually parsed.
13
+ Sidekiq::CLI will now only require YAML if you use a `-C` .yml file.
14
+
15
+ 8.1.2
16
+ ----------
17
+
18
+ - Initial release for `kiq`, Sidekiq's official terminal UI:
19
+ ```
20
+ bundle exec kiq
21
+ ```
22
+ Use REDIS_URL or REDIS_PROVIDER to point `kiq` to Redis.
23
+ - Mutation during iteration in `SortedSet#each` caused it to miss half of the jobs [#6936]
24
+ - Fix edge case resulting in nil crash on /busy page [#6954]
25
+
26
+ 8.1.1
27
+ ----------
28
+
29
+ - **DEPRECATION** `require 'sidekiq/testing'` and
30
+ `require 'sidekiq/testing/inline'`.
31
+ Add new `Sidekiq.testing!(mode)` API [#6931]
32
+ Requiring code should not enable process-wide changes.
33
+ ```ruby
34
+ # Old, implicit
35
+ require "sidekiq/testing"
36
+ require "sidekiq/testing/inline"
37
+ # New, more explicit
38
+ Sidekiq.testing!(:fake)
39
+ Sidekiq.testing!(:inline)
40
+ ```
41
+ - Fix race condition with Stop button in UI [#6935]
42
+ - Fix javascript error handler [#6893]
43
+
44
+ 8.1.0
45
+ ----------
46
+
47
+ - `retry_for` and `retry` are now mutually exclusive [#6878, Saidbek]
48
+ - `perform_inline` now enforces `strict_args!` [#6718, Saidbek]
49
+ - Integrate Herb linting for ERB templates [#6760, Saidbek]
50
+ - Remove CSRF code, use `Sec-Fetch-Site` header [#6874, deve1212]
51
+ - Allow custom Web UI `assets_path` for CDN purposes [#6865, stanhu]
52
+ - Upgrade to connection_pool 3.0
53
+ - Allow idle connection reaping after N seconds.
54
+ You can activate this **beta** feature like below.
55
+ Feedback requested: is this feature stable and useful for you in production?
56
+ This feature may or may not be enabled by default in Sidekiq 9.0.
57
+ ```ruby
58
+ Sidekiq.configure_server do |cfg|
59
+ cfg.reap_idle_redis_connections(60)
60
+ end
61
+ ```
62
+
5
63
  8.0.10
6
64
  ----------
7
65
 
data/README.md CHANGED
@@ -90,13 +90,28 @@ Useful resources:
90
90
  * The [Sidekiq tag](https://stackoverflow.com/questions/tagged/sidekiq) on Stack Overflow has lots of useful Q & A.
91
91
 
92
92
  Every Thursday morning is Sidekiq Office Hour: I video chat and answer questions.
93
- See the [Sidekiq support page](https://sidekiq.org/support.html) for details.
93
+ See the [Sidekiq support page](https://sidekiq.org/support/) for details.
94
94
 
95
95
  Contributing
96
96
  -----------------
97
97
 
98
98
  See [the contributing guidelines](https://github.com/sidekiq/sidekiq/blob/main/.github/contributing.md).
99
99
 
100
+ ### ERB Linting with HERB
101
+
102
+ This project uses [HERB](https://herb-tools.dev/) for ERB file linting and formatting. All ERB files have been renamed to use the `.html.erb` extension for better tooling support.
103
+
104
+ **Local Development:**
105
+ ```bash
106
+ # Run HERB linting
107
+ bundle exec rake lint:herb
108
+ # or
109
+ bin/lint-herb
110
+ ```
111
+
112
+ **CI Integration:**
113
+ HERB linting is automatically run in CI to ensure all ERB files are properly formatted and free of parse errors.
114
+
100
115
  License
101
116
  -----------------
102
117
 
data/bin/kiq ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This requires the default gemset so Sidekiq Pro
4
+ # and Sidekiq Enterprise can load any code extensions.
5
+ Bundler.require(:default, :tui)
6
+
7
+ require_relative "../lib/sidekiq/tui"
8
+
9
+ # Run any load hooks registered during Bundler.require
10
+ Sidekiq.loader.run_load_hooks(:tui)
11
+
12
+ tt = Sidekiq::TUI.new(Sidekiq.default_configuration)
13
+
14
+ RatatuiRuby.run do |tui|
15
+ tt.prepare(tui)
16
+ tt.run_loop
17
+ end
data/bin/lint-herb ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # HERB Linting Script
5
+ # Run this script to lint all ERB files in the project
6
+ # Usage: bin/lint-herb
7
+
8
+ require "bundler/setup"
9
+
10
+ puts "🔍 Running HERB linting on ERB files..."
11
+ puts
12
+
13
+ exec("bundle exec herb analyze web/views -n --no-log-file")
@@ -61,18 +61,21 @@ begin
61
61
 
62
62
  # @api private
63
63
  def enqueue(job)
64
- job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(
65
- wrapped: job.class,
66
- queue: job.queue_name
67
- ).perform_async(job.serialize)
64
+ # NB: Active Job only serializes keys it recognizes. We
65
+ # cannot set arbitrary key/values here.
66
+ options = {wrapped: job.class, queue: job.queue_name}
67
+ options[:profile] = job.profile if job.respond_to?(:profile) && !job.profile.nil?
68
+
69
+ wrapper = Sidekiq::ActiveJob::Wrapper.set(options)
70
+ job.provider_job_id = wrapper.perform_async(job.serialize)
68
71
  end
69
72
 
70
73
  # @api private
71
74
  def enqueue_at(job, timestamp)
72
- job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(
73
- wrapped: job.class,
74
- queue: job.queue_name
75
- ).perform_at(timestamp, job.serialize)
75
+ options = {wrapped: job.class, queue: job.queue_name}
76
+ options[:profile] = job.profile if job.respond_to?(:profile) && !job.profile.nil?
77
+
78
+ job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(options).perform_at(timestamp, job.serialize)
76
79
  end
77
80
 
78
81
  # @api private
@@ -14,7 +14,10 @@ module Sidekiq
14
14
  end
15
15
 
16
16
  def create_job_file
17
- template "job.rb.erb", File.join("app/sidekiq", class_path, "#{file_name}_job.rb")
17
+ template(
18
+ "job.rb.erb",
19
+ File.join(jobs_directory, class_path, "#{file_name}_job.rb")
20
+ )
18
21
  end
19
22
 
20
23
  def create_test_file
@@ -31,7 +34,8 @@ module Sidekiq
31
34
 
32
35
  def create_job_spec
33
36
  template_file = File.join(
34
- "spec/sidekiq",
37
+ "spec",
38
+ jobs_directory.gsub("app/", ""),
35
39
  class_path,
36
40
  "#{file_name}_job_spec.rb"
37
41
  )
@@ -40,7 +44,8 @@ module Sidekiq
40
44
 
41
45
  def create_job_test
42
46
  template_file = File.join(
43
- "test/sidekiq",
47
+ "test",
48
+ jobs_directory.gsub("app/", ""),
44
49
  class_path,
45
50
  "#{file_name}_job_test.rb"
46
51
  )
@@ -54,6 +59,13 @@ module Sidekiq
54
59
  def test_framework
55
60
  ::Rails.application.config.generators.options[:rails][:test_framework]
56
61
  end
62
+
63
+ # Can be set in an initializer or in application configuration
64
+ # with Rails.application.config.generators.options[:sidekiq][:jobs_directory] = "app/jobs"
65
+ # to change the directory that the job files are generated in to.
66
+ def jobs_directory
67
+ ::Rails.application.config.generators.options[:sidekiq].fetch(:jobs_directory, "app/sidekiq")
68
+ end
57
69
  end
58
70
  end
59
71
  end
data/lib/sidekiq/api.rb CHANGED
@@ -17,12 +17,37 @@ require "sidekiq/metrics/query"
17
17
  #
18
18
 
19
19
  module Sidekiq
20
+ module ApiUtils
21
+ # @api private
22
+ # Calculate the latency in seconds for a job based on its enqueued timestamp
23
+ # @param job [Hash] the job hash
24
+ # @return [Float] latency in seconds
25
+ def calculate_latency(job)
26
+ timestamp = job["enqueued_at"] || job["created_at"]
27
+ return 0.0 unless timestamp
28
+
29
+ if timestamp.is_a?(Float)
30
+ # old format
31
+ Time.now.to_f - timestamp
32
+ else
33
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
34
+ (now - timestamp) / 1000.0
35
+ end
36
+ end
37
+ end
38
+
20
39
  # Retrieve runtime statistics from Redis regarding
21
40
  # this Sidekiq cluster.
22
41
  #
23
42
  # stat = Sidekiq::Stats.new
24
43
  # stat.processed
25
44
  class Stats
45
+ QueueSummary = Data.define(:name, :size, :latency, :paused) do
46
+ alias_method :paused?, :paused
47
+ end
48
+
49
+ include ApiUtils
50
+
26
51
  def initialize
27
52
  fetch_stats_fast!
28
53
  end
@@ -63,6 +88,7 @@ module Sidekiq
63
88
  stat :default_queue_latency
64
89
  end
65
90
 
91
+ # @return [Hash{String => Integer}] a hash of queue names to their lengths
66
92
  def queues
67
93
  Sidekiq.redis do |conn|
68
94
  queues = conn.sscan("queues").to_a
@@ -78,6 +104,41 @@ module Sidekiq
78
104
  end
79
105
  end
80
106
 
107
+ # More detailed information about each queue: name, size, latency, paused status
108
+ # @return [Array<QueueSummary>]
109
+ def queue_summaries
110
+ Sidekiq.redis do |conn|
111
+ queues = conn.sscan("queues").to_a
112
+ return [] if queues.empty?
113
+
114
+ results = conn.pipelined { |pipeline|
115
+ queues.each do |queue|
116
+ pipeline.llen("queue:#{queue}")
117
+ pipeline.lindex("queue:#{queue}", -1)
118
+ pipeline.sismember("paused", queue)
119
+ end
120
+ }
121
+
122
+ queue_summaries = []
123
+ queues.each_with_index do |name, idx|
124
+ size = results[idx * 3]
125
+ last_item = results[idx * 3 + 1]
126
+ paused = results[idx * 3 + 2] > 0
127
+
128
+ latency = if last_item
129
+ job = Sidekiq.load_json(last_item)
130
+ calculate_latency(job)
131
+ else
132
+ 0.0
133
+ end
134
+
135
+ queue_summaries << QueueSummary.new(name:, size:, latency:, paused:)
136
+ end
137
+
138
+ queue_summaries.sort_by { |qd| -qd.size }
139
+ end
140
+ end
141
+
81
142
  # O(1) redis calls
82
143
  # @api private
83
144
  def fetch_stats_fast!
@@ -100,19 +161,7 @@ module Sidekiq
100
161
  {}
101
162
  end
102
163
 
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
164
+ calculate_latency(job)
116
165
  else
117
166
  0.0
118
167
  end
@@ -235,6 +284,7 @@ module Sidekiq
235
284
  # end
236
285
  class Queue
237
286
  include Enumerable
287
+ include ApiUtils
238
288
 
239
289
  ##
240
290
  # Fetch all known queues within Redis.
@@ -245,6 +295,7 @@ module Sidekiq
245
295
  end
246
296
 
247
297
  attr_reader :name
298
+ alias_method :id, :name
248
299
 
249
300
  # @param name [String] the name of the queue
250
301
  def initialize(name = "default")
@@ -277,19 +328,7 @@ module Sidekiq
277
328
  return 0.0 unless entry
278
329
 
279
330
  job = Sidekiq.load_json(entry)
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
331
+ calculate_latency(job)
293
332
  end
294
333
 
295
334
  def each
@@ -352,6 +391,8 @@ module Sidekiq
352
391
  # The job should be considered immutable but may be
353
392
  # removed from the queue via JobRecord#delete.
354
393
  class JobRecord
394
+ include ApiUtils
395
+
355
396
  # the parsed Hash of job data
356
397
  # @!attribute [r] Item
357
398
  attr_reader :item
@@ -478,17 +519,7 @@ module Sidekiq
478
519
  end
479
520
 
480
521
  def latency
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
522
+ calculate_latency(@item)
492
523
  end
493
524
 
494
525
  # Remove this job from the queue
@@ -553,17 +584,24 @@ module Sidekiq
553
584
  # could be the scheduled time for it to run (e.g. scheduled set),
554
585
  # or the expiration date after which the entry should be deleted (e.g. dead set).
555
586
  class SortedEntry < JobRecord
556
- attr_reader :score
557
587
  attr_reader :parent
558
588
 
559
589
  # :nodoc:
560
590
  # @api private
561
591
  def initialize(parent, score, item)
562
592
  super(item)
563
- @score = Float(score)
593
+ @score = score
564
594
  @parent = parent
565
595
  end
566
596
 
597
+ def score
598
+ Float(@score)
599
+ end
600
+
601
+ def id
602
+ "#{@score}|#{item["jid"]}"
603
+ end
604
+
567
605
  # The timestamp associated with this entry
568
606
  def at
569
607
  Time.at(score).utc
@@ -574,7 +612,7 @@ module Sidekiq
574
612
  if @value
575
613
  @parent.delete_by_value(@parent.name, @value)
576
614
  else
577
- @parent.delete_by_jid(score, jid)
615
+ @parent.delete_by_jid(@score, jid)
578
616
  end
579
617
  end
580
618
 
@@ -583,7 +621,7 @@ module Sidekiq
583
621
  # @param at [Time] the new timestamp for this job
584
622
  def reschedule(at)
585
623
  Sidekiq.redis do |conn|
586
- conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
624
+ conn.zincrby(@parent.name, at.to_f - score, Sidekiq.dump_json(@item))
587
625
  end
588
626
  end
589
627
 
@@ -619,38 +657,8 @@ module Sidekiq
619
657
 
620
658
  private
621
659
 
622
- def remove_job
623
- Sidekiq.redis do |conn|
624
- results = conn.multi { |transaction|
625
- transaction.zrange(parent.name, score, score, "BYSCORE")
626
- transaction.zremrangebyscore(parent.name, score, score)
627
- }.first
628
-
629
- if results.size == 1
630
- yield results.first
631
- else
632
- # multiple jobs with the same score
633
- # find the one with the right JID and push it
634
- matched, nonmatched = results.partition { |message|
635
- if message.index(jid)
636
- msg = Sidekiq.load_json(message)
637
- msg["jid"] == jid
638
- else
639
- false
640
- end
641
- }
642
-
643
- msg = matched.first
644
- yield msg if msg
645
-
646
- # push the rest back onto the sorted set
647
- conn.multi do |transaction|
648
- nonmatched.each do |message|
649
- transaction.zadd(parent.name, score.to_f.to_s, message)
650
- end
651
- end
652
- end
653
- end
660
+ def remove_job(&)
661
+ parent.remove_job(self, &)
654
662
  end
655
663
  end
656
664
 
@@ -819,6 +827,46 @@ module Sidekiq
819
827
  nil
820
828
  end
821
829
 
830
+ def remove_job(entry)
831
+ score = entry.score
832
+ jid = entry.jid
833
+ Sidekiq.redis do |conn|
834
+ results = conn.multi { |transaction|
835
+ transaction.zrange(name, score, score, "BYSCORE")
836
+ transaction.zremrangebyscore(name, score, score)
837
+ }.first
838
+
839
+ if results.size == 1
840
+ yield results.first
841
+ @_size -= 1
842
+ else
843
+ # multiple jobs with the same score
844
+ # find the one with the right JID and push it
845
+ matched, nonmatched = results.partition { |message|
846
+ if message.index(jid)
847
+ msg = Sidekiq.load_json(message)
848
+ msg["jid"] == jid
849
+ else
850
+ false
851
+ end
852
+ }
853
+
854
+ msg = matched.first
855
+ if msg
856
+ yield msg
857
+ @_size -= 1
858
+ end
859
+
860
+ # push the rest back onto the sorted set
861
+ conn.multi do |transaction|
862
+ nonmatched.each do |message|
863
+ transaction.zadd(name, score.to_f.to_s, message)
864
+ end
865
+ end
866
+ end
867
+ end
868
+ end
869
+
822
870
  # :nodoc:
823
871
  # @api private
824
872
  def delete_by_value(name, value)
@@ -992,19 +1040,20 @@ module Sidekiq
992
1040
  # you'll be happier this way
993
1041
  conn.pipelined do |pipeline|
994
1042
  procs.each do |key|
995
- pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
1043
+ pipeline.hmget(key, "info", "concurrency", "busy", "beat", "quiet", "rss", "rtt_us")
996
1044
  end
997
1045
  end
998
1046
  }
999
1047
 
1000
- result.each do |info, busy, beat, quiet, rss, rtt_us|
1048
+ result.each do |info, concurrency, busy, beat, quiet, rss, rtt_us|
1001
1049
  # If a process is stopped between when we query Redis for `procs` and
1002
1050
  # when we query for `result`, we will have an item in `result` that is
1003
1051
  # composed of `nil` values.
1004
1052
  next if info.nil?
1005
1053
 
1006
1054
  hash = Sidekiq.load_json(info)
1007
- yield Process.new(hash.merge("busy" => busy.to_i,
1055
+ yield Process.new(hash.merge("concurrency" => concurrency.to_i,
1056
+ "busy" => busy.to_i,
1008
1057
  "beat" => beat.to_f,
1009
1058
  "quiet" => quiet,
1010
1059
  "rss" => rss.to_i,
@@ -1088,6 +1137,7 @@ module Sidekiq
1088
1137
  def identity
1089
1138
  self["identity"]
1090
1139
  end
1140
+ alias_method :id, :identity
1091
1141
 
1092
1142
  # deprecated, use capsules below
1093
1143
  def queues
@@ -1161,6 +1211,10 @@ module Sidekiq
1161
1211
  self["quiet"] == "true"
1162
1212
  end
1163
1213
 
1214
+ def leader?
1215
+ Sidekiq.redis { |c| c.get("dear-leader") == identity }
1216
+ end
1217
+
1164
1218
  private
1165
1219
 
1166
1220
  def signal(sig)
@@ -51,7 +51,6 @@ module Sidekiq
51
51
  end
52
52
 
53
53
  def stop
54
- fetcher&.bulk_requeue([])
55
54
  end
56
55
 
57
56
  # Sidekiq checks queues in three modes:
data/lib/sidekiq/cli.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  $stdout.sync = true
4
4
 
5
- require "yaml"
6
5
  require "optparse"
7
6
  require "erb"
8
7
  require "fileutils"
@@ -203,6 +202,7 @@ module Sidekiq # :nodoc:
203
202
  },
204
203
  # deprecated, use INFO
205
204
  "TTIN" => ->(cli) {
205
+ cli.logger.error { "DEPRECATED: Please use the INFO signal for backtraces, support for TTIN will be removed in Sidekiq 9.0." }
206
206
  Thread.list.each do |thread|
207
207
  cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
208
208
  if thread.backtrace
@@ -408,6 +408,7 @@ module Sidekiq # :nodoc:
408
408
  def parse_config(path)
409
409
  erb = ERB.new(File.read(path), trim_mode: "-")
410
410
  erb.filename = File.expand_path(path)
411
+ require "yaml"
411
412
  opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
412
413
 
413
414
  if opts.respond_to? :deep_symbolize_keys!
@@ -132,12 +132,14 @@ module Sidekiq
132
132
  # push_bulk('class' => MyJob, 'args' => (1..100_000).to_a, batch_size: 1_000)
133
133
  #
134
134
  def push_bulk(items)
135
- batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
136
135
  args = items["args"]
137
136
  at = items.delete("at") || items.delete(:at)
138
137
  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) })
139
138
  raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
140
139
 
140
+ # Use a smaller batch size by default for scheduled jobs since adding to sorted sets is more costly.
141
+ batch_size = items.delete(:batch_size) || items.delete("batch_size") || (at ? 100 : 1_000)
142
+
141
143
  jid = items.delete("jid")
142
144
  raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
143
145
 
@@ -57,6 +57,9 @@ module Sidekiq
57
57
  end
58
58
 
59
59
  def tid
60
+ # We XOR with PID to ensure Thread IDs changes after fork.
61
+ # I'm unclear why we don't multiply the two values to better guarantee
62
+ # a unique value but it's been this way for quite a while now. #3685
60
63
  Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
61
64
  end
62
65
 
@@ -37,7 +37,7 @@ module Sidekiq
37
37
  reloader: proc { |&block| block.call },
38
38
  backtrace_cleaner: ->(backtrace) { backtrace },
39
39
  logged_job_attributes: ["bid", "tags"],
40
- reap_connections: nil # TODO Enable by default
40
+ redis_idle_timeout: nil
41
41
  }
42
42
 
43
43
  ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
@@ -146,10 +146,9 @@ module Sidekiq
146
146
  @redis_config = @redis_config.merge(hash)
147
147
  end
148
148
 
149
- def reap_idle_redis_connections(timeout = nil)
150
- self[:reap_connections] = timeout
149
+ def reap_idle_redis_connections(timeout = 60)
150
+ self[:redis_idle_timeout] = timeout
151
151
  end
152
- alias_method :reap, :reap_idle_redis_connections
153
152
 
154
153
  def redis_pool
155
154
  Thread.current[:sidekiq_redis_pool] || Thread.current[:sidekiq_capsule]&.redis_pool || local_redis_pool
@@ -256,7 +255,7 @@ module Sidekiq
256
255
  # Register a proc to handle any error which occurs within the Sidekiq process.
257
256
  #
258
257
  # Sidekiq.configure_server do |config|
259
- # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
258
+ # config.error_handlers << proc {|ex,ctx_hash,config| MyErrorService.notify(ex, ctx_hash) }
260
259
  # end
261
260
  #
262
261
  # The default error handler logs errors to @logger.
data/lib/sidekiq/job.rb CHANGED
@@ -226,6 +226,8 @@ module Sidekiq
226
226
  end
227
227
  return nil unless result
228
228
 
229
+ verify_json(item)
230
+
229
231
  # round-trip the payload via JSON
230
232
  msg = Sidekiq.load_json(Sidekiq.dump_json(item))
231
233
 
@@ -178,10 +178,14 @@ module Sidekiq
178
178
  msg["error_backtrace"] = compress_backtrace(lines)
179
179
  end
180
180
 
181
- return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
182
-
181
+ # retry_for and retry are mutually exclusive - if retry_for is set,
182
+ # we exclusively use duration-based retry logic and ignore count-based logic
183
183
  rf = msg["retry_for"]
184
- return retries_exhausted(jobinst, msg, exception) if rf && (time_for(msg["failed_at"]) + rf) < Time.now
184
+ if rf
185
+ return retries_exhausted(jobinst, msg, exception) if (time_for(msg["failed_at"]) + rf) < Time.now
186
+ elsif count >= max_retry_attempts
187
+ return retries_exhausted(jobinst, msg, exception)
188
+ end
185
189
 
186
190
  strategy, delay = delay_for(jobinst, count, exception, msg)
187
191
  case strategy