sidekiq 7.2.0 → 7.2.4

Sign up to get free protection for your applications and to get access to all the features.
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