sidekiq 8.0.10 → 8.1.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +33 -0
  3. data/README.md +15 -0
  4. data/bin/lint-herb +13 -0
  5. data/bin/tui +5 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +5 -2
  7. data/lib/generators/sidekiq/job_generator.rb +15 -3
  8. data/lib/sidekiq/api.rb +84 -41
  9. data/lib/sidekiq/cli.rb +1 -0
  10. data/lib/sidekiq/config.rb +3 -4
  11. data/lib/sidekiq/job.rb +2 -0
  12. data/lib/sidekiq/job_retry.rb +7 -3
  13. data/lib/sidekiq/launcher.rb +4 -4
  14. data/lib/sidekiq/scheduled.rb +4 -2
  15. data/lib/sidekiq/test_api.rb +331 -0
  16. data/lib/sidekiq/testing/inline.rb +2 -30
  17. data/lib/sidekiq/testing.rb +2 -334
  18. data/lib/sidekiq/tui.rb +1019 -0
  19. data/lib/sidekiq/version.rb +1 -1
  20. data/lib/sidekiq/web/action.rb +1 -1
  21. data/lib/sidekiq/web/application.rb +2 -2
  22. data/lib/sidekiq/web/config.rb +3 -6
  23. data/lib/sidekiq/web/helpers.rb +12 -1
  24. data/lib/sidekiq/web.rb +23 -4
  25. data/lib/sidekiq.rb +7 -0
  26. data/sidekiq.gemspec +5 -5
  27. data/web/assets/javascripts/application.js +1 -1
  28. data/web/assets/stylesheets/style.css +0 -2
  29. data/web/views/{busy.erb → busy.html.erb} +1 -1
  30. data/web/views/{metrics.erb → metrics.html.erb} +3 -2
  31. metadata +38 -35
  32. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  33. /data/web/views/{_footer.erb → _footer.html.erb} +0 -0
  34. /data/web/views/{_job_info.erb → _job_info.html.erb} +0 -0
  35. /data/web/views/{_metrics_period_select.erb → _metrics_period_select.html.erb} +0 -0
  36. /data/web/views/{_nav.erb → _nav.html.erb} +0 -0
  37. /data/web/views/{_paging.erb → _paging.html.erb} +0 -0
  38. /data/web/views/{_poll_link.erb → _poll_link.html.erb} +0 -0
  39. /data/web/views/{_summary.erb → _summary.html.erb} +0 -0
  40. /data/web/views/{dashboard.erb → dashboard.html.erb} +0 -0
  41. /data/web/views/{dead.erb → dead.html.erb} +0 -0
  42. /data/web/views/{filtering.erb → filtering.html.erb} +0 -0
  43. /data/web/views/{layout.erb → layout.html.erb} +0 -0
  44. /data/web/views/{metrics_for_job.erb → metrics_for_job.html.erb} +0 -0
  45. /data/web/views/{morgue.erb → morgue.html.erb} +0 -0
  46. /data/web/views/{profiles.erb → profiles.html.erb} +0 -0
  47. /data/web/views/{queue.erb → queue.html.erb} +0 -0
  48. /data/web/views/{queues.erb → queues.html.erb} +0 -0
  49. /data/web/views/{retries.erb → retries.html.erb} +0 -0
  50. /data/web/views/{retry.erb → retry.html.erb} +0 -0
  51. /data/web/views/{scheduled.erb → scheduled.html.erb} +0 -0
  52. /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: 4fb090d79e2cf2b320fdd47ee55a574a68a46b77a23db27b849ddb6f54acb00a
4
+ data.tar.gz: 4863ef28ecdd8ad2cd868caf2f47cc9302b8eb88f4ff9da4f2b5641beca50d88
5
5
  SHA512:
6
- metadata.gz: f6139796abb98a9bc67bf6498438f52e135b85ea6a360f86b45ddafd4f46d9d5567592940005d93bab5c3700430adfb5be340e08ca4a9e5cbde529d19a9c7dcc
7
- data.tar.gz: '08ac62d0c45ca1a8216833ecfb07ad8f37ca3d1a1137198f0994af7cf0b107c842741a4f8b62ea8c3f59b42aa209418f65d9bcf6b57127b6ae6e09daf9509907'
6
+ metadata.gz: 4ba71a2c43cec613337119cd29830339e213a8f876db4db84c62ac67890ee020ab841a4f7fe478f7f0fcd135571b885e54c9af12998ea6947ac51b543d52b42c
7
+ data.tar.gz: 9cabfc243aacec3c57c697b83374a5b934cc985c1e4ee86185a6faba7fe18f6540e3fa0b203c5dbfc0e22c03513f66e973be4152d8c8e12db29e97e21a94360f
data/Changes.md CHANGED
@@ -2,6 +2,39 @@
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.1
6
+ ----------
7
+
8
+ - Add new `Sidekiq.testing!(mode)` API [#6931]
9
+ Requiring code should not enable process-wide changes.
10
+ ```ruby
11
+ # Old, implicit
12
+ require "sidekiq/testing"
13
+ # New, more explicit
14
+ Sidekiq.testing!(:fake)
15
+ ```
16
+ - Fix race condition with Stop button in UI [#6935]
17
+ - Fix javascript error handler [#6893]
18
+
19
+ 8.1.0
20
+ ----------
21
+
22
+ - `retry_for` and `retry` are now mutually exclusive [#6878, Saidbek]
23
+ - `perform_inline` now enforces `strict_args!` [#6718, Saidbek]
24
+ - Integrate Herb linting for ERB templates [#6760, Saidbek]
25
+ - Remove CSRF code, use `Sec-Fetch-Site` header [#6874, deve1212]
26
+ - Allow custom Web UI `assets_path` for CDN purposes [#6865, stanhu]
27
+ - Upgrade to connection_pool 3.0
28
+ - Allow idle connection reaping after N seconds.
29
+ You can activate this **beta** feature like below.
30
+ Feedback requested: is this feature stable and useful for you in production?
31
+ This feature may or may not be enabled by default in Sidekiq 9.0.
32
+ ```ruby
33
+ Sidekiq.configure_server do |cfg|
34
+ cfg.reap_idle_redis_connections(60)
35
+ end
36
+ ```
37
+
5
38
  8.0.10
6
39
  ----------
7
40
 
data/README.md CHANGED
@@ -97,6 +97,21 @@ Contributing
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/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")
data/bin/tui ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/sidekiq/tui"
4
+
5
+ Sidekiq::TUI.new.run
@@ -61,10 +61,13 @@ begin
61
61
 
62
62
  # @api private
63
63
  def enqueue(job)
64
- job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(
64
+ # NB: Active Job only serializes keys it recognizes. We
65
+ # cannot set arbitrary key/values here.
66
+ wrapper = Sidekiq::ActiveJob::Wrapper.set(
65
67
  wrapped: job.class,
66
68
  queue: job.queue_name
67
- ).perform_async(job.serialize)
69
+ )
70
+ job.provider_job_id = wrapper.perform_async(job.serialize)
68
71
  end
69
72
 
70
73
  # @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
 
@@ -1088,6 +1126,7 @@ module Sidekiq
1088
1126
  def identity
1089
1127
  self["identity"]
1090
1128
  end
1129
+ alias_method :id, :identity
1091
1130
 
1092
1131
  # deprecated, use capsules below
1093
1132
  def queues
@@ -1161,6 +1200,10 @@ module Sidekiq
1161
1200
  self["quiet"] == "true"
1162
1201
  end
1163
1202
 
1203
+ def leader?
1204
+ Sidekiq.redis { |c| c.get("dear-leader") == identity }
1205
+ end
1206
+
1164
1207
  private
1165
1208
 
1166
1209
  def signal(sig)
data/lib/sidekiq/cli.rb CHANGED
@@ -203,6 +203,7 @@ module Sidekiq # :nodoc:
203
203
  },
204
204
  # deprecated, use INFO
205
205
  "TTIN" => ->(cli) {
206
+ cli.logger.error { "DEPRECATED: Please use the INFO signal for backtraces, support for TTIN will be removed in Sidekiq 9.0." }
206
207
  Thread.list.each do |thread|
207
208
  cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
208
209
  if thread.backtrace
@@ -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
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
@@ -142,10 +142,10 @@ module Sidekiq
142
142
  key = identity
143
143
  fails = procd = 0
144
144
 
145
- idle = config[:reap_connections]
146
- if idle
147
- config.capsules.each_value { |cap| cap.local_redis_pool.reap(idle, &:close) }
148
- config.local_redis_pool.reap(idle, &:close)
145
+ idle_timeout = config[:redis_idle_timeout]
146
+ if idle_timeout
147
+ config.capsules.each_value { |cap| cap.local_redis_pool.reap(idle_seconds: idle_timeout, &:close) }
148
+ config.local_redis_pool.reap(idle_seconds: idle_timeout, &:close)
149
149
  end
150
150
 
151
151
  begin
@@ -72,6 +72,7 @@ module Sidekiq
72
72
  include Sidekiq::Component
73
73
 
74
74
  INITIAL_WAIT = 10
75
+ attr_accessor :rnd
75
76
 
76
77
  def initialize(config)
77
78
  @config = config
@@ -80,6 +81,7 @@ module Sidekiq
80
81
  @done = false
81
82
  @thread = nil
82
83
  @count_calls = 0
84
+ @rnd = Random.new
83
85
  end
84
86
 
85
87
  # Shut down this instance, will pause until the thread is dead.
@@ -151,11 +153,11 @@ module Sidekiq
151
153
 
152
154
  if count < 10
153
155
  # For small clusters, calculate a random interval that is ±50% the desired average.
154
- interval * rand + interval.to_f / 2
156
+ interval * @rnd.rand + interval.to_f / 2
155
157
  else
156
158
  # With 10+ processes, we should have enough randomness to get decent polling
157
159
  # across the entire timespan
158
- interval * rand * 2
160
+ interval * @rnd.rand * 2
159
161
  end
160
162
  end
161
163