sidekiq 7.2.0 → 7.2.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30f824346db9b0ebf8ee13c6ac0101494e5fd6d05b4ed2ef3ad97c5b17ccbc10
4
- data.tar.gz: bed22f02925116256550bbc34ef9decd70bdbca356d5d61abedd4d14a7dcac45
3
+ metadata.gz: 6c43e6b585c25dcfc8ef8364bb36cf74f9167b981ad03faa3a8d76e0d45ebe55
4
+ data.tar.gz: d8c65dc03008f7280b36af94db753d4c7f68267c2eb0d78cd018322887aabbb0
5
5
  SHA512:
6
- metadata.gz: 347e82cf6f215a1e4bd09c3f12d382be79d96bb83ccd1d09f928db19b936c2dd72c0f2e3a8988160086da7928a7911d84eddd005428c7b6a337763c4b60a3492
7
- data.tar.gz: ef0a03f45d4d35e832f36b7a09ae0694838cd0cf3582dc20d53956bb5a61ba07811d410600b43a9bdf777e68dd2549910a125d23885afca8388b8513bb7ac8d1
6
+ metadata.gz: d2687692b873ab82bda2ad32e9be795150cd0a8d3d330bc19f5b509ba729bef33189e06ebac86b1906c2682187391d6cf0d532e47d03fcbea83058109c5816ef
7
+ data.tar.gz: 431a482baeb03fc4de50fbdfba8717fc332a9d6564fde98a77699a7bd174fa3194431385951cf689c64e04853039c95fcf287084f283e8d381b3b37d5bc665e0
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
+ 7.2.4
6
+ ----------
7
+
8
+ - Fix XSS in metrics filtering introduced in 7.2.0, CVE-2024-32887
9
+ Thanks to @UmerAdeemCheema for the security report.
10
+
11
+ 7.2.3
12
+ ----------
13
+
14
+ - [Support Dragonfly.io](https://www.mikeperham.com/2024/02/01/supporting-dragonfly/) as an alternative Redis implementation
15
+ - Fix error unpacking some compressed error backtraces [#6241]
16
+ - Fix potential heartbeat data leak [#6227]
17
+ - Add ability to find a currently running work by jid [#6212, fatkodima]
18
+
19
+ 7.2.2
20
+ ----------
21
+
22
+ - Add `Process.warmup` call in Ruby 3.3+
23
+ - Batch jobs now skip transactional push [#6160]
24
+
25
+ 7.2.1
26
+ ----------
27
+
28
+ - Add `Sidekiq::Work` type which replaces the raw Hash as the third parameter in
29
+ `Sidekiq::WorkSet#each { |pid, tid, hash| ... }` [#6145]
30
+ - **DEPRECATED**: direct access to the attributes within the `hash` block parameter above.
31
+ The `Sidekiq::Work` instance contains accessor methods to get at the same data, e.g.
32
+ ```ruby
33
+ work["queue"] # Old
34
+ work.queue # New
35
+ ```
36
+ - Fix Ruby 3.3 warnings around `base64` gem [#6151, earlopain]
37
+
5
38
  7.2.0
6
39
  ----------
7
40
 
data/README.md CHANGED
@@ -14,11 +14,11 @@ Rails to make background processing dead simple.
14
14
  Requirements
15
15
  -----------------
16
16
 
17
- - Redis: 6.2+
17
+ - Redis: Redis 6.2+ or Dragonfly 1.13+
18
18
  - Ruby: MRI 2.7+ or JRuby 9.3+.
19
19
 
20
20
  Sidekiq 7.0 supports Rails 6.0+ but does not require it.
21
-
21
+ As of 7.2, Sidekiq supports Dragonfly as an alternative to Redis for data storage.
22
22
 
23
23
  Installation
24
24
  -----------------
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # bin/bench is a helpful script to load test and
5
+ # performance tune Sidekiq's core. It's a configurable script,
6
+ # which accepts the following parameters as ENV variables.
7
+ #
8
+ # QUEUES
9
+ # Number of queues to consume from. Default is 8
10
+ #
11
+ # PROCESSES
12
+ # The number of processes this benchmark will create. Each process, consumes
13
+ # from one of the available queues. When processes are more than the number of
14
+ # queues, they are distributed to processes in round robin. Default is 8
15
+ #
16
+ # ELEMENTS
17
+ # Number of jobs to push to each queue. Default is 1000
18
+ #
19
+ # ITERATIONS
20
+ # Each queue pushes ITERATIONS times ELEMENTS jobs. Default is 1000
21
+ #
22
+ # PORT
23
+ # The port of the Dragonfly instance. Default is 6379
24
+ #
25
+ # IP
26
+ # The ip of the Dragonfly instance. Default is 127.0.0.1
27
+ #
28
+ # Example Usage:
29
+ #
30
+ # > RUBY_YJIT_ENABLE=1 THREADS=10 PROCESSES=8 QUEUES=8 bin/multi_queue_bench
31
+ #
32
+ # None of this script is considered a public API and may change over time.
33
+ #
34
+
35
+ # Quiet some warnings we see when running in warning mode:
36
+ # RUBYOPT=-w bundle exec sidekiq
37
+ $TESTING = false
38
+ puts RUBY_DESCRIPTION
39
+
40
+ require "bundler/setup"
41
+ Bundler.require(:default, :load_test)
42
+
43
+ class LoadWorker
44
+ include Sidekiq::Job
45
+ sidekiq_options retry: 1
46
+ sidekiq_retry_in do |x|
47
+ 1
48
+ end
49
+
50
+ def perform(idx, ts = nil)
51
+ puts(Time.now.to_f - ts) if !ts.nil?
52
+ # raise idx.to_s if idx % 100 == 1
53
+ end
54
+ end
55
+
56
+ def Process.rss
57
+ `ps -o rss= -p #{Process.pid}`.chomp.to_i
58
+ end
59
+
60
+ $iterations = ENV["ITERATIONS"] ? Integer(ENV["ITERATIONS"]) : 1_000
61
+ $elements = ENV["ELEMENTS"] ? Integer(ENV["ELEMENTS"]) : 1_000
62
+ $port = ENV["PORT"] ? Integer(ENV["PORT"]) : 6379
63
+ $ip = ENV["IP"] ? String(ENV["IP"]) : "127.0.0.1"
64
+
65
+ class Loader
66
+ def initialize
67
+ @iter = $iterations
68
+ @count = $elements
69
+ end
70
+
71
+ def configure(queue)
72
+ @x = Sidekiq.configure_embed do |config|
73
+ config.redis = {db: 0, host: $ip, port: $port}
74
+ config.concurrency = Integer(ENV.fetch("THREADS", "30"))
75
+ config.queues = queue
76
+ config.logger.level = Logger::WARN
77
+ config.average_scheduled_poll_interval = 2
78
+ config.reliable! if defined?(Sidekiq::Pro)
79
+ end
80
+
81
+ @self_read, @self_write = IO.pipe
82
+ %w[INT TERM TSTP TTIN].each do |sig|
83
+ trap sig do
84
+ @self_write.puts(sig)
85
+ end
86
+ rescue ArgumentError
87
+ puts "Signal #{sig} not supported"
88
+ end
89
+ end
90
+
91
+ def handle_signal(sig)
92
+ launcher = @x
93
+ Sidekiq.logger.debug "Got #{sig} signal"
94
+ case sig
95
+ when "INT"
96
+ # Handle Ctrl-C in JRuby like MRI
97
+ # http://jira.codehaus.org/browse/JRUBY-4637
98
+ raise Interrupt
99
+ when "TERM"
100
+ # Heroku sends TERM and then waits 30 seconds for process to exit.
101
+ raise Interrupt
102
+ when "TSTP"
103
+ Sidekiq.logger.info "Received TSTP, no longer accepting new work"
104
+ launcher.quiet
105
+ when "TTIN"
106
+ Thread.list.each do |thread|
107
+ Sidekiq.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread["label"]}"
108
+ if thread.backtrace
109
+ Sidekiq.logger.warn thread.backtrace.join("\n")
110
+ else
111
+ Sidekiq.logger.warn "<no backtrace available>"
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def setup(queue)
118
+ Sidekiq.logger.error("Setup RSS: #{Process.rss}")
119
+ Sidekiq.logger.error("Pushing work to queue: #{queue}")
120
+ start = Time.now
121
+ @iter.times do
122
+ arr = Array.new(@count) { |idx| [idx] }
123
+ # Sidekiq always prepends "queue:" to the queue name,
124
+ # that's why we pass 'q1', 'q2', etc instead of 'queue:q1'
125
+ Sidekiq::Client.push_bulk("class" => LoadWorker, "args" => arr, "queue" => queue)
126
+ $stdout.write "."
127
+ end
128
+ puts "Done"
129
+ end
130
+
131
+ def monitor_single(queue)
132
+ q = "queue:#{queue}"
133
+ @monitor_single = Thread.new do
134
+ GC.start
135
+ loop do
136
+ sleep 0.2
137
+ total = Sidekiq.redis do |conn|
138
+ conn.llen q
139
+ end
140
+
141
+ if total == 0
142
+ sleep 0.1
143
+ @x.stop
144
+ Process.kill("INT", $$)
145
+ break
146
+ end
147
+
148
+ end
149
+ end
150
+ end
151
+
152
+ def monitor_all(queues)
153
+ @monitor_all = Thread.new do
154
+ GC.start
155
+ loop do
156
+ sleep 0.2
157
+ qsize = 0
158
+ queues.each do |q|
159
+ tmp = Sidekiq.redis do |conn|
160
+ conn.llen q
161
+ end
162
+ qsize = qsize + tmp
163
+ end
164
+ total = qsize
165
+
166
+ if total == 0
167
+ ending = Time.now - @start
168
+ size = @iter * @count * queues.length()
169
+ Sidekiq.logger.error("Done, #{size} jobs in #{ending} sec, #{(size / ending).to_i} jobs/sec")
170
+ Sidekiq.logger.error("Ending RSS: #{Process.rss}")
171
+
172
+ sleep 0.1
173
+ @x.stop
174
+ Process.kill("INT", $$)
175
+ break
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ def run(queues, queue, monitor_all_queues)
182
+ Sidekiq.logger.warn("Consuming from #{queue}")
183
+ if monitor_all_queues
184
+ monitor_all(queues)
185
+ else
186
+ monitor_single(queue)
187
+ end
188
+
189
+ @start = Time.now
190
+ @x.run
191
+
192
+ while (readable_io = IO.select([@self_read]))
193
+ signal = readable_io.first[0].gets.strip
194
+ handle_signal(signal)
195
+ end
196
+ # normal
197
+ rescue Interrupt
198
+ rescue => e
199
+ raise e if $DEBUG
200
+ warn e.message
201
+ warn e.backtrace.join("\n")
202
+ exit 1
203
+ ensure
204
+ @x.stop
205
+ end
206
+ end
207
+
208
+ def setup(queue)
209
+ ll = Loader.new
210
+ ll.configure(queue)
211
+ ll.setup(queue)
212
+ end
213
+
214
+ def consume(queues, queue, monitor_all_queues)
215
+ ll = Loader.new
216
+ ll.configure(queue)
217
+ ll.run(queues, queue, monitor_all_queues)
218
+ end
219
+
220
+ # We assign one queue to each sidekiq process
221
+ def run(number_of_processes, total_queues)
222
+ read_stream, write_stream = IO.pipe
223
+
224
+ queues = []
225
+ (0..total_queues-1).each do |idx|
226
+ queues.push("queue:q#{idx}")
227
+ end
228
+
229
+ Sidekiq.logger.info("Queues are: #{queues}")
230
+
231
+ # Produce
232
+ start = Time.now
233
+ (0..total_queues-1).each do |idx|
234
+ Process.fork do
235
+ queue_num = "q#{idx}"
236
+ setup(queue_num)
237
+ end
238
+ end
239
+
240
+ queue_sz = $iterations * $elements * total_queues
241
+ Process.waitall
242
+
243
+ ending = Time.now - start
244
+ #Sidekiq.logger.info("Pushed #{queue_sz} in #{ending} secs")
245
+
246
+ # Consume
247
+ (0..number_of_processes-1).each do |idx|
248
+ Process.fork do
249
+ # First process only consumes from it's own queue but monitors all queues.
250
+ # It works as a synchronization point. Once all processes finish
251
+ # (that is, when all queues are emptied) it prints the the stats.
252
+ if idx == 0
253
+ queue = "q#{idx}"
254
+ consume(queues, queue, true)
255
+ else
256
+ queue = "q#{idx % total_queues}"
257
+ consume(queues, queue, false)
258
+ end
259
+ end
260
+ end
261
+
262
+ Process.waitall
263
+ write_stream.close
264
+ results = read_stream.read
265
+ read_stream.close
266
+ end
267
+
268
+ $total_processes = ENV["PROCESSES"] ? Integer(ENV["PROCESSES"]) : 8;
269
+ $total_queues = ENV["QUEUES"] ? Integer(ENV["QUEUES"]) : 8;
270
+
271
+ run($total_processes, $total_queues)
data/lib/sidekiq/api.rb CHANGED
@@ -4,7 +4,6 @@ require "sidekiq"
4
4
 
5
5
  require "zlib"
6
6
  require "set"
7
- require "base64"
8
7
 
9
8
  require "sidekiq/metrics/query"
10
9
 
@@ -491,8 +490,8 @@ module Sidekiq
491
490
  end
492
491
 
493
492
  def uncompress_backtrace(backtrace)
494
- decoded = Base64.decode64(backtrace)
495
- uncompressed = Zlib::Inflate.inflate(decoded)
493
+ strict_base64_decoded = backtrace.unpack1("m")
494
+ uncompressed = Zlib::Inflate.inflate(strict_base64_decoded)
496
495
  Sidekiq.load_json(uncompressed)
497
496
  end
498
497
  end
@@ -774,7 +773,7 @@ module Sidekiq
774
773
  #
775
774
  class ScheduledSet < JobSet
776
775
  def initialize
777
- super "schedule"
776
+ super("schedule")
778
777
  end
779
778
  end
780
779
 
@@ -788,7 +787,7 @@ module Sidekiq
788
787
  #
789
788
  class RetrySet < JobSet
790
789
  def initialize
791
- super "retry"
790
+ super("retry")
792
791
  end
793
792
 
794
793
  # Enqueues all jobs pending within the retry set.
@@ -809,7 +808,7 @@ module Sidekiq
809
808
  #
810
809
  class DeadSet < JobSet
811
810
  def initialize
812
- super "dead"
811
+ super("dead")
813
812
  end
814
813
 
815
814
  # Add the given job to the Dead set.
@@ -1110,11 +1109,11 @@ module Sidekiq
1110
1109
 
1111
1110
  procs.zip(all_works).each do |key, workers|
1112
1111
  workers.each_pair do |tid, json|
1113
- results << [key, tid, Sidekiq.load_json(json)] unless json.empty?
1112
+ results << [key, tid, Sidekiq::Work.new(key, tid, Sidekiq.load_json(json))] unless json.empty?
1114
1113
  end
1115
1114
  end
1116
1115
 
1117
- results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
1116
+ results.sort_by { |(_, _, hsh)| hsh.raw("run_at") }.each(&block)
1118
1117
  end
1119
1118
 
1120
1119
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1137,7 +1136,74 @@ module Sidekiq
1137
1136
  end
1138
1137
  end
1139
1138
  end
1139
+
1140
+ ##
1141
+ # Find the work which represents a job with the given JID.
1142
+ # *This is a slow O(n) operation*. Do not use for app logic.
1143
+ #
1144
+ # @param jid [String] the job identifier
1145
+ # @return [Sidekiq::Work] the work or nil
1146
+ def find_work_by_jid(jid)
1147
+ each do |_process_id, _thread_id, work|
1148
+ job = work.job
1149
+ return work if job.jid == jid
1150
+ end
1151
+ nil
1152
+ end
1140
1153
  end
1154
+
1155
+ # Sidekiq::Work represents a job which is currently executing.
1156
+ class Work
1157
+ attr_reader :process_id
1158
+ attr_reader :thread_id
1159
+
1160
+ def initialize(pid, tid, hsh)
1161
+ @process_id = pid
1162
+ @thread_id = tid
1163
+ @hsh = hsh
1164
+ @job = nil
1165
+ end
1166
+
1167
+ def queue
1168
+ @hsh["queue"]
1169
+ end
1170
+
1171
+ def run_at
1172
+ Time.at(@hsh["run_at"])
1173
+ end
1174
+
1175
+ def job
1176
+ @job ||= Sidekiq::JobRecord.new(@hsh["payload"])
1177
+ end
1178
+
1179
+ def payload
1180
+ @hsh["payload"]
1181
+ end
1182
+
1183
+ # deprecated
1184
+ def [](key)
1185
+ kwargs = {uplevel: 1}
1186
+ kwargs[:category] = :deprecated if RUBY_VERSION > "3.0" # TODO
1187
+ warn("Direct access to `Sidekiq::Work` attributes is deprecated, please use `#payload`, `#queue`, `#run_at` or `#job` instead", **kwargs)
1188
+
1189
+ @hsh[key]
1190
+ end
1191
+
1192
+ # :nodoc:
1193
+ # @api private
1194
+ def raw(name)
1195
+ @hsh[name]
1196
+ end
1197
+
1198
+ def method_missing(*all)
1199
+ @hsh.send(*all)
1200
+ end
1201
+
1202
+ def respond_to_missing?(name)
1203
+ @hsh.respond_to?(name)
1204
+ end
1205
+ end
1206
+
1141
1207
  # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1142
1208
  # Is "worker" a process, a type of job, a thread? Undefined!
1143
1209
  # WorkSet better describes the data.
data/lib/sidekiq/cli.rb CHANGED
@@ -38,7 +38,7 @@ module Sidekiq # :nodoc:
38
38
  # Code within this method is not tested because it alters
39
39
  # global process state irreversibly. PRs which improve the
40
40
  # test coverage of Sidekiq::CLI are welcomed.
41
- def run(boot_app: true)
41
+ def run(boot_app: true, warmup: true)
42
42
  boot_application if boot_app
43
43
 
44
44
  if environment == "development" && $stdout.tty? && @config.logger.formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
@@ -101,6 +101,8 @@ module Sidekiq # :nodoc:
101
101
  # Touch middleware so it isn't lazy loaded by multiple threads, #3043
102
102
  @config.server_middleware
103
103
 
104
+ ::Process.warmup if warmup && ::Process.respond_to?(:warmup)
105
+
104
106
  # Before this point, the process is initializing with just the main thread.
105
107
  # Starting here the process will now have multiple threads running.
106
108
  fire_event(:startup, reverse: false, reraise: true)
@@ -34,7 +34,7 @@ module Sidekiq
34
34
  # handle an very common error in marking deploys:
35
35
  # having every process mark its deploy, leading
36
36
  # to N marks for each deploy. Instead we round the time
37
- # to the minute so that multple marks within that minute
37
+ # to the minute so that multiple marks within that minute
38
38
  # will all naturally rollup into one mark per minute.
39
39
  whence = at.utc
40
40
  floor = Time.utc(whence.year, whence.month, whence.mday, whence.hour, whence.min, 0)
data/lib/sidekiq/job.rb CHANGED
@@ -109,7 +109,7 @@ module Sidekiq
109
109
  m = "#{name}="
110
110
  undef_method(m) if method_defined?(m) || private_method_defined?(m)
111
111
  end
112
- define_singleton_method("#{name}=") do |val|
112
+ define_singleton_method(:"#{name}=") do |val|
113
113
  singleton_class.class_eval do
114
114
  ACCESSOR_MUTEX.synchronize do
115
115
  undef_method(synchronized_getter) if method_defined?(synchronized_getter) || private_method_defined?(synchronized_getter)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zlib"
4
- require "base64"
5
4
  require "sidekiq/component"
6
5
 
7
6
  module Sidekiq
@@ -295,7 +294,7 @@ module Sidekiq
295
294
  def compress_backtrace(backtrace)
296
295
  serialized = Sidekiq.dump_json(backtrace)
297
296
  compressed = Zlib::Deflate.deflate(serialized)
298
- Base64.encode64(compressed)
297
+ [compressed].pack("m0") # Base64.strict_encode64
299
298
  end
300
299
  end
301
300
  end
@@ -145,15 +145,17 @@ module Sidekiq
145
145
  flush_stats
146
146
 
147
147
  curstate = Processor::WORK_STATE.dup
148
+ curstate.transform_values! { |val| Sidekiq.dump_json(val) }
149
+
148
150
  redis do |conn|
149
151
  # work is the current set of executing jobs
150
152
  work_key = "#{key}:work"
151
- conn.pipelined do |transaction|
153
+ conn.multi do |transaction|
152
154
  transaction.unlink(work_key)
153
- curstate.each_pair do |tid, hash|
154
- transaction.hset(work_key, tid, Sidekiq.dump_json(hash))
155
+ if curstate.size > 0
156
+ transaction.hset(work_key, curstate)
157
+ transaction.expire(work_key, 60)
155
158
  end
156
- transaction.expire(work_key, 60)
157
159
  end
158
160
  end
159
161
 
@@ -36,7 +36,7 @@ module Sidekiq
36
36
  end
37
37
 
38
38
  LEVELS.each do |level, numeric_level|
39
- define_method("#{level}?") do
39
+ define_method(:"#{level}?") do
40
40
  local_level.nil? ? super() : local_level <= numeric_level
41
41
  end
42
42
  end
@@ -119,6 +119,7 @@ module Sidekiq
119
119
 
120
120
  def total_avg(metric = "ms")
121
121
  completed = totals["p"] - totals["f"]
122
+ return 0 if completed.zero?
122
123
  totals[metric].to_f / completed
123
124
  end
124
125
 
@@ -103,12 +103,16 @@ module Sidekiq
103
103
  def reset
104
104
  @lock.synchronize {
105
105
  array = [@totals, @jobs, @grams]
106
- @totals = Hash.new(0)
107
- @jobs = Hash.new(0)
108
- @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
106
+ reset_instance_variables
109
107
  array
110
108
  }
111
109
  end
110
+
111
+ def reset_instance_variables
112
+ @totals = Hash.new(0)
113
+ @jobs = Hash.new(0)
114
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
115
+ end
112
116
  end
113
117
 
114
118
  class Middleware
@@ -54,7 +54,7 @@ module Sidekiq
54
54
  cattrs_to_reset << constklass
55
55
 
56
56
  job[key].each do |(attribute, value)|
57
- constklass.public_send("#{attribute}=", value)
57
+ constklass.public_send(:"#{attribute}=", value)
58
58
  end
59
59
  end
60
60
  end
@@ -187,7 +187,7 @@ module Sidekiq
187
187
  # we didn't properly finish it.
188
188
  rescue Sidekiq::JobRetry::Handled => h
189
189
  # this is the common case: job raised error and Sidekiq::JobRetry::Handled
190
- # signals that we created a retry successfully. We can acknowlege the job.
190
+ # signals that we created a retry successfully. We can acknowledge the job.
191
191
  ack = true
192
192
  e = h.cause || h
193
193
  handle_exception(e, {context: "Job raised exception", job: job_hash})
data/lib/sidekiq/rails.rb CHANGED
@@ -20,6 +20,10 @@ module Sidekiq
20
20
  def inspect
21
21
  "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
22
22
  end
23
+
24
+ def to_hash
25
+ {app: @app.class.name}
26
+ end
23
27
  end
24
28
 
25
29
  # By including the Options module, we allow AJs to directly control sidekiq features
@@ -56,10 +60,10 @@ module Sidekiq
56
60
  # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
57
61
  # it will appear in the Sidekiq console with all of the job context.
58
62
  unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
59
- if ::Rails::VERSION::STRING < "7.1"
60
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
61
- else
63
+ if ::Rails.logger.respond_to?(:broadcast_to)
62
64
  ::Rails.logger.broadcast_to(config.logger)
65
+ else
66
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
63
67
  end
64
68
  end
65
69
  end
@@ -32,8 +32,8 @@ module Sidekiq
32
32
  zremrangebyrank zremrangebyscore]
33
33
 
34
34
  USED_COMMANDS.each do |name|
35
- define_method(name) do |*args|
36
- @client.call(name, *args)
35
+ define_method(name) do |*args, **kwargs|
36
+ @client.call(name, *args, **kwargs)
37
37
  end
38
38
  end
39
39
 
@@ -39,9 +39,8 @@ module Sidekiq
39
39
  uri.password = redacted
40
40
  scrubbed_options[:url] = uri.to_s
41
41
  end
42
- if scrubbed_options[:password]
43
- scrubbed_options[:password] = redacted
44
- end
42
+ scrubbed_options[:password] = redacted if scrubbed_options[:password]
43
+ scrubbed_options[:sentinel_password] = redacted if scrubbed_options[:sentinel_password]
45
44
  scrubbed_options[:sentinels]&.each do |sentinel|
46
45
  sentinel[:password] = redacted if sentinel[:password]
47
46
  end
@@ -67,9 +66,7 @@ module Sidekiq
67
66
  EOM
68
67
  end
69
68
 
70
- ENV[
71
- p || "REDIS_URL"
72
- ]
69
+ ENV[p.to_s] || ENV["REDIS_URL"]
73
70
  end
74
71
  end
75
72
  end
@@ -144,7 +144,7 @@ module Sidekiq
144
144
  # In the example above, each process should schedule every 10 seconds on average. We special
145
145
  # case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
146
146
  # As we run more processes, the scheduling interval average will approach an even spread
147
- # between 0 and poll interval so we don't need this artifical boost.
147
+ # between 0 and poll interval so we don't need this artificial boost.
148
148
  #
149
149
  count = process_count
150
150
  interval = poll_interval_average(count)
@@ -112,7 +112,7 @@ module Sidekiq
112
112
  # The Queues class is only for testing the fake queue implementation.
113
113
  # There are 2 data structures involved in tandem. This is due to the
114
114
  # Rspec syntax of change(HardJob.jobs, :size). It keeps a reference
115
- # to the array. Because the array was dervied from a filter of the total
115
+ # to the array. Because the array was derived from a filter of the total
116
116
  # jobs enqueued, it appeared as though the array didn't change.
117
117
  #
118
118
  # To solve this, we'll keep 2 hashes containing the jobs. One with keys based
@@ -278,7 +278,7 @@ module Sidekiq
278
278
  def perform_one
279
279
  raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
280
280
  next_job = jobs.first
281
- Queues.delete_for(next_job["jid"], queue, to_s)
281
+ Queues.delete_for(next_job["jid"], next_job["queue"], to_s)
282
282
  process_job(next_job)
283
283
  end
284
284
 
@@ -9,7 +9,14 @@ module Sidekiq
9
9
  @redis_client = Client.new(pool: pool, config: config)
10
10
  end
11
11
 
12
+ def batching?
13
+ Thread.current[:sidekiq_batch]
14
+ end
15
+
12
16
  def push(item)
17
+ # 6160 we can't support both Sidekiq::Batch and transactions.
18
+ return @redis_client.push(item) if batching?
19
+
13
20
  # pre-allocate the JID so we can return it immediately and
14
21
  # save it to the database as part of the transaction.
15
22
  item["jid"] ||= SecureRandom.hex(12)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.2.0"
4
+ VERSION = "7.2.4"
5
5
  MAJOR = 7
6
6
  end
@@ -22,6 +22,11 @@ module Sidekiq
22
22
  throw :halt, [302, {Web::LOCATION => "#{request.base_url}#{location}"}, []]
23
23
  end
24
24
 
25
+ def reload_page
26
+ current_location = request.referer.gsub(request.base_url, "")
27
+ redirect current_location
28
+ end
29
+
25
30
  def params
26
31
  indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
27
32
 
@@ -49,9 +49,9 @@ module Sidekiq
49
49
 
50
50
  head "/" do
51
51
  # HEAD / is the cheapest heartbeat possible,
52
- # it hits Redis to ensure connectivity
53
- Sidekiq.redis { |c| c.llen("queue:default") }
54
- ""
52
+ # it hits Redis to ensure connectivity and returns
53
+ # the size of the default queue
54
+ Sidekiq.redis { |c| c.llen("queue:default") }.to_s
55
55
  end
56
56
 
57
57
  get "/" do
@@ -394,6 +394,18 @@ module Sidekiq
394
394
  erb :morgue
395
395
  end
396
396
 
397
+ post "/change_locale" do
398
+ locale = params["locale"]
399
+
400
+ match = available_locales.find { |available|
401
+ locale == available
402
+ }
403
+
404
+ session[:locale] = match if match
405
+
406
+ reload_page
407
+ end
408
+
397
409
  def call(env)
398
410
  action = self.class.match(env)
399
411
  return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
@@ -27,7 +27,6 @@
27
27
  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
28
 
29
29
  require "securerandom"
30
- require "base64"
31
30
  require "rack/request"
32
31
 
33
32
  module Sidekiq
@@ -57,7 +56,7 @@ module Sidekiq
57
56
  end
58
57
 
59
58
  def logger(env)
60
- @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
59
+ @logger ||= env["rack.logger"] || ::Logger.new(env["rack.errors"])
61
60
  end
62
61
 
63
62
  def deny(env)
@@ -116,7 +115,7 @@ module Sidekiq
116
115
  sess = session(env)
117
116
  localtoken = sess[:csrf]
118
117
 
119
- # Checks that Rack::Session::Cookie actualy contains the csrf toekn
118
+ # Checks that Rack::Session::Cookie actually contains the csrf token
120
119
  return false if localtoken.nil?
121
120
 
122
121
  # Rotate the session token after every use
@@ -143,7 +142,7 @@ module Sidekiq
143
142
  one_time_pad = SecureRandom.random_bytes(token.length)
144
143
  encrypted_token = xor_byte_strings(one_time_pad, token)
145
144
  masked_token = one_time_pad + encrypted_token
146
- Base64.urlsafe_encode64(masked_token)
145
+ encode_token(masked_token)
147
146
  end
148
147
 
149
148
  # Essentially the inverse of +mask_token+.
@@ -168,8 +167,12 @@ module Sidekiq
168
167
  ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
169
168
  end
170
169
 
170
+ def encode_token(token)
171
+ [token].pack("m0").tr("+/", "-_")
172
+ end
173
+
171
174
  def decode_token(token)
172
- Base64.urlsafe_decode64(token)
175
+ token.tr("-_", "+/").unpack1("m0")
173
176
  end
174
177
 
175
178
  def xor_byte_strings(s1, s2)
@@ -121,6 +121,10 @@ module Sidekiq
121
121
  #
122
122
  # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
123
123
  def locale
124
+ # session[:locale] is set via the locale selector from the footer
125
+ # defined?(session) && session are used to avoid exceptions when running tests
126
+ return session[:locale] if defined?(session) && session&.[](:locale)
127
+
124
128
  @locale ||= begin
125
129
  matched_locale = user_preferred_languages.map { |preferred|
126
130
  preferred_language = preferred.split("-", 2).first
@@ -340,7 +344,8 @@ module Sidekiq
340
344
  end
341
345
 
342
346
  def pollable?
343
- !(current_path == "" || current_path.start_with?("metrics"))
347
+ # there's no point to refreshing the metrics pages every N seconds
348
+ !(current_path == "" || current_path.index("metrics"))
344
349
  end
345
350
 
346
351
  def retry_or_delete_or_kill(job, params)
data/sidekiq.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |gem|
23
23
  "rubygems_mfa_required" => "true"
24
24
  }
25
25
 
26
- gem.add_dependency "redis-client", ">= 0.14.0"
26
+ gem.add_dependency "redis-client", ">= 0.19.0"
27
27
  gem.add_dependency "connection_pool", ">= 2.3.0"
28
28
  gem.add_dependency "rack", ">= 2.2.4"
29
29
  gem.add_dependency "concurrent-ruby", "< 2"
@@ -47,6 +47,8 @@ function addListeners() {
47
47
  scheduleLivePoll();
48
48
  }
49
49
  }
50
+
51
+ document.getElementById("locale-select").addEventListener("change", updateLocale);
50
52
  }
51
53
 
52
54
  function addPollingListeners(_event) {
@@ -175,3 +177,7 @@ function replacePage(text) {
175
177
  function showError(error) {
176
178
  console.error(error)
177
179
  }
180
+
181
+ function updateLocale(event) {
182
+ event.target.form.submit();
183
+ };
@@ -151,3 +151,13 @@ div.interval-slider {
151
151
  padding-left: 5px;
152
152
  }
153
153
  }
154
+
155
+ #locale-select {
156
+ float: right;
157
+ }
158
+
159
+ @media (max-width: 767px) {
160
+ #locale-select {
161
+ float: none;
162
+ }
163
+ }
@@ -731,3 +731,16 @@ div.interval-slider input {
731
731
  canvas {
732
732
  margin: 20px 0 30px;
733
733
  }
734
+
735
+ #locale-select {
736
+ float: left;
737
+ margin: 8px 15px;
738
+ }
739
+
740
+ @media (max-width: 767px) {
741
+ #locale-select {
742
+ float: none;
743
+ width: auto;
744
+ margin: 15px auto;
745
+ }
746
+ }
@@ -15,7 +15,19 @@
15
15
  <p class="navbar-text"><a rel=help href="https://github.com/sidekiq/sidekiq/wiki">docs</a></p>
16
16
  </li>
17
17
  <li>
18
- <p class="navbar-text"><a rel=external href="https://github.com/sidekiq/sidekiq/tree/main/web/locales"><%= locale %></a></p>
18
+ <form id="locale-form" class="form-inline" action="<%= root_path %>change_locale" method="post">
19
+ <%= csrf_tag %>
20
+ <label class="sr-only" for="locale">Language</label>
21
+ <select id="locale-select" class="form-control" name="locale">
22
+ <% available_locales.each do |locale_option| %>
23
+ <% if locale_option == locale %>
24
+ <option selected value="<%= locale_option %>"><%= locale_option %></option>
25
+ <% else %>
26
+ <option value="<%= locale_option %>"><%= locale_option %></option>
27
+ <% end %>
28
+ <% end %>
29
+ </select>
30
+ </form>
19
31
  </li>
20
32
  </ul>
21
33
  </div>
data/web/views/busy.erb CHANGED
@@ -125,14 +125,14 @@
125
125
  <th><%= t('Arguments') %></th>
126
126
  <th><%= t('Started') %></th>
127
127
  </thead>
128
- <% @workset.each do |process, thread, msg| %>
129
- <% job = Sidekiq::JobRecord.new(msg['payload']) %>
128
+ <% @workset.each do |process, thread, work| %>
129
+ <% job = work.job %>
130
130
  <tr>
131
131
  <td><%= process %></td>
132
132
  <td><%= thread %></td>
133
133
  <td><%= job.jid %></td>
134
134
  <td>
135
- <a href="<%= root_path %>queues/<%= msg['queue'] %>"><%= msg['queue'] %></a>
135
+ <a href="<%= root_path %>queues/<%= work.queue %>"><%= work.queue %></a>
136
136
  </td>
137
137
  <td>
138
138
  <%= job.display_class %>
@@ -141,7 +141,7 @@
141
141
  <td>
142
142
  <div class="args"><%= display_args(job.display_args) %></div>
143
143
  </td>
144
- <td><%= relative_time(Time.at(msg['run_at'])) %></td>
144
+ <td><%= relative_time(work.run_at) %></td>
145
145
  </tr>
146
146
  <% end %>
147
147
  </table>
@@ -12,7 +12,7 @@
12
12
  <form id="metrics-form" class="form-inline" action="<%= root_path %>filter/metrics" method="post">
13
13
  <%= csrf_tag %>
14
14
  <label for="substr"><%= t('Filter') %></label>
15
- <input id="class-filter" class="form-control" type="text" name="substr" placeholder="<%= t('Name') %>" value="<%= params[:substr] %>">
15
+ <input id="class-filter" class="form-control" type="text" name="substr" placeholder="<%= t('Name') %>" value="<%= h params[:substr] %>">
16
16
  <select id="period-selector" class="form-control" name="period">
17
17
  <% @periods.each_key do |code| %>
18
18
  <% if code == @period %>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.0
4
+ version: 7.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-31 00:00:00.000000000 Z
11
+ date: 2024-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.14.0
19
+ version: 0.19.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.14.0
26
+ version: 0.19.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: connection_pool
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -78,6 +78,7 @@ files:
78
78
  - Changes.md
79
79
  - LICENSE.txt
80
80
  - README.md
81
+ - bin/multi_queue_bench
81
82
  - bin/sidekiq
82
83
  - bin/sidekiqload
83
84
  - bin/sidekiqmon