sidekiq 7.3.0 → 7.3.9

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +81 -0
  3. data/bin/sidekiqload +21 -12
  4. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  5. data/lib/generators/sidekiq/job_generator.rb +2 -0
  6. data/lib/sidekiq/api.rb +62 -33
  7. data/lib/sidekiq/capsule.rb +5 -3
  8. data/lib/sidekiq/cli.rb +1 -1
  9. data/lib/sidekiq/client.rb +21 -1
  10. data/lib/sidekiq/component.rb +22 -0
  11. data/lib/sidekiq/config.rb +22 -2
  12. data/lib/sidekiq/deploy.rb +2 -0
  13. data/lib/sidekiq/embedded.rb +2 -0
  14. data/lib/sidekiq/iterable_job.rb +2 -0
  15. data/lib/sidekiq/job/interrupt_handler.rb +2 -0
  16. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +3 -3
  17. data/lib/sidekiq/job/iterable.rb +69 -6
  18. data/lib/sidekiq/job_logger.rb +11 -23
  19. data/lib/sidekiq/job_util.rb +2 -0
  20. data/lib/sidekiq/launcher.rb +1 -1
  21. data/lib/sidekiq/metrics/query.rb +2 -0
  22. data/lib/sidekiq/metrics/shared.rb +15 -4
  23. data/lib/sidekiq/metrics/tracking.rb +13 -5
  24. data/lib/sidekiq/middleware/current_attributes.rb +19 -2
  25. data/lib/sidekiq/middleware/modules.rb +2 -0
  26. data/lib/sidekiq/monitor.rb +2 -1
  27. data/lib/sidekiq/paginator.rb +6 -0
  28. data/lib/sidekiq/processor.rb +10 -10
  29. data/lib/sidekiq/rails.rb +12 -0
  30. data/lib/sidekiq/redis_connection.rb +8 -1
  31. data/lib/sidekiq/ring_buffer.rb +2 -0
  32. data/lib/sidekiq/systemd.rb +2 -0
  33. data/lib/sidekiq/testing.rb +5 -5
  34. data/lib/sidekiq/version.rb +5 -1
  35. data/lib/sidekiq/web/action.rb +20 -4
  36. data/lib/sidekiq/web/application.rb +36 -79
  37. data/lib/sidekiq/web/helpers.rb +11 -10
  38. data/lib/sidekiq/web/router.rb +5 -2
  39. data/lib/sidekiq/web.rb +8 -1
  40. data/lib/sidekiq.rb +4 -3
  41. data/sidekiq.gemspec +1 -1
  42. data/web/assets/javascripts/dashboard-charts.js +2 -0
  43. data/web/assets/javascripts/dashboard.js +6 -0
  44. data/web/assets/stylesheets/application.css +9 -8
  45. data/web/locales/en.yml +3 -1
  46. data/web/locales/fr.yml +0 -1
  47. data/web/locales/gd.yml +0 -1
  48. data/web/locales/it.yml +32 -1
  49. data/web/locales/ja.yml +0 -1
  50. data/web/locales/pt-br.yml +1 -2
  51. data/web/locales/tr.yml +1 -2
  52. data/web/locales/uk.yml +24 -1
  53. data/web/locales/zh-cn.yml +0 -1
  54. data/web/locales/zh-tw.yml +0 -1
  55. data/web/views/_footer.erb +1 -2
  56. data/web/views/dashboard.erb +4 -1
  57. data/web/views/filtering.erb +1 -2
  58. data/web/views/metrics.erb +3 -4
  59. data/web/views/morgue.erb +2 -2
  60. data/web/views/queue.erb +1 -1
  61. metadata +10 -12
@@ -30,6 +30,40 @@ module Sidekiq
30
30
  @_cursor = nil
31
31
  @_start_time = nil
32
32
  @_runtime = 0
33
+ @_args = nil
34
+ @_cancelled = nil
35
+ end
36
+
37
+ def arguments
38
+ @_args
39
+ end
40
+
41
+ # Three days is the longest period you generally need to wait for a retry to
42
+ # execute when using the default retry scheme. We don't want to "forget" the job
43
+ # is cancelled before it has a chance to execute and cancel itself.
44
+ CANCELLATION_PERIOD = (3 * 86_400).to_s
45
+
46
+ # Set a flag in Redis to mark this job as cancelled.
47
+ # Cancellation is asynchronous and is checked at the start of iteration
48
+ # and every 5 seconds thereafter as part of the recurring state flush.
49
+ def cancel!
50
+ return @_cancelled if cancelled?
51
+
52
+ key = "it-#{jid}"
53
+ _, result, _ = Sidekiq.redis do |c|
54
+ c.pipelined do |p|
55
+ p.hsetnx(key, "cancelled", Time.now.to_i)
56
+ p.hget(key, "cancelled")
57
+ # TODO When Redis 7.2 is required
58
+ # p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
59
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
60
+ end
61
+ end
62
+ @_cancelled = result.to_i
63
+ end
64
+
65
+ def cancelled?
66
+ @_cancelled
33
67
  end
34
68
 
35
69
  # A hook to override that will be called when the job starts iterating.
@@ -91,13 +125,14 @@ module Sidekiq
91
125
  end
92
126
 
93
127
  # @api private
94
- def perform(*arguments)
128
+ def perform(*args)
129
+ @_args = args.dup.freeze
95
130
  fetch_previous_iteration_state
96
131
 
97
132
  @_executions += 1
98
133
  @_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
99
134
 
100
- enumerator = build_enumerator(*arguments, cursor: @_cursor)
135
+ enumerator = build_enumerator(*args, cursor: @_cursor)
101
136
  unless enumerator
102
137
  logger.info("'#build_enumerator' returned nil, skipping the job.")
103
138
  return
@@ -112,7 +147,7 @@ module Sidekiq
112
147
  end
113
148
 
114
149
  completed = catch(:abort) do
115
- iterate_with_enumerator(enumerator, arguments)
150
+ iterate_with_enumerator(enumerator, args)
116
151
  end
117
152
 
118
153
  on_stop
@@ -128,6 +163,10 @@ module Sidekiq
128
163
 
129
164
  private
130
165
 
166
+ def is_cancelled?
167
+ @_cancelled = Sidekiq.redis { |c| c.hget("it-#{jid}", "cancelled") }
168
+ end
169
+
131
170
  def fetch_previous_iteration_state
132
171
  state = Sidekiq.redis { |conn| conn.hgetall(iteration_key) }
133
172
 
@@ -144,6 +183,12 @@ module Sidekiq
144
183
  STATE_TTL = 30 * 24 * 60 * 60 # one month
145
184
 
146
185
  def iterate_with_enumerator(enumerator, arguments)
186
+ if is_cancelled?
187
+ logger.info { "Job cancelled" }
188
+ return true
189
+ end
190
+
191
+ time_limit = Sidekiq.default_configuration[:timeout]
147
192
  found_record = false
148
193
  state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
149
194
 
@@ -153,14 +198,21 @@ module Sidekiq
153
198
 
154
199
  is_interrupted = interrupted?
155
200
  if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - state_flushed_at >= STATE_FLUSH_INTERVAL || is_interrupted
156
- flush_state
201
+ _, _, cancelled = flush_state
157
202
  state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
203
+ if cancelled
204
+ @_cancelled = true
205
+ logger.info { "Job cancelled" }
206
+ return true
207
+ end
158
208
  end
159
209
 
160
210
  return false if is_interrupted
161
211
 
162
- around_iteration do
163
- each_iteration(object, *arguments)
212
+ verify_iteration_time(time_limit, object) do
213
+ around_iteration do
214
+ each_iteration(object, *arguments)
215
+ end
164
216
  end
165
217
  end
166
218
 
@@ -170,6 +222,16 @@ module Sidekiq
170
222
  @_runtime += (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @_start_time)
171
223
  end
172
224
 
225
+ def verify_iteration_time(time_limit, object)
226
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
227
+ yield
228
+ finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
229
+ total = finish - start
230
+ if total > time_limit
231
+ logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d) when processing `%s`. This can lead to job processing problems during deploys" % [total, time_limit, object] }
232
+ end
233
+ end
234
+
173
235
  def reenqueue_iteration_job
174
236
  flush_state
175
237
  logger.debug { "Interrupting job (cursor=#{@_cursor.inspect})" }
@@ -204,6 +266,7 @@ module Sidekiq
204
266
  conn.multi do |pipe|
205
267
  pipe.hset(key, state)
206
268
  pipe.expire(key, STATE_TTL)
269
+ pipe.hget(key, "cancelled")
207
270
  end
208
271
  end
209
272
  end
@@ -2,36 +2,24 @@
2
2
 
3
3
  module Sidekiq
4
4
  class JobLogger
5
- include Sidekiq::Component
6
-
7
5
  def initialize(config)
8
6
  @config = config
9
- @logger = logger
10
- end
11
-
12
- # If true we won't do any job logging out of the box.
13
- # The user is responsible for any logging.
14
- def skip_default_logging?
15
- config[:skip_default_job_logging]
7
+ @logger = @config.logger
8
+ @skip = !!@config[:skip_default_job_logging]
16
9
  end
17
10
 
18
11
  def call(item, queue)
19
- return yield if skip_default_logging?
12
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
13
+ @logger.info { "start" } unless @skip
20
14
 
21
- begin
22
- start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
23
- @logger.info("start")
15
+ yield
24
16
 
25
- yield
26
-
27
- Sidekiq::Context.add(:elapsed, elapsed(start))
28
- @logger.info("done")
29
- rescue Exception
30
- Sidekiq::Context.add(:elapsed, elapsed(start))
31
- @logger.info("fail")
32
-
33
- raise
34
- end
17
+ Sidekiq::Context.add(:elapsed, elapsed(start))
18
+ @logger.info { "done" } unless @skip
19
+ rescue Exception
20
+ Sidekiq::Context.add(:elapsed, elapsed(start))
21
+ @logger.info { "fail" } unless @skip
22
+ raise
35
23
  end
36
24
 
37
25
  def prepare(job_hash, &block)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "securerandom"
2
4
  require "time"
3
5
 
@@ -36,8 +36,8 @@ module Sidekiq
36
36
  # has a heartbeat thread, caller can use `async_beat: false`
37
37
  # and instead have thread call Launcher#heartbeat every N seconds.
38
38
  def run(async_beat: true)
39
- Sidekiq.freeze!
40
39
  logger.debug { @config.merge!({}) }
40
+ Sidekiq.freeze!
41
41
  @thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
42
42
  @poller.start
43
43
  @managers.each(&:start)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq"
2
4
  require "date"
3
5
  require "set"
@@ -1,10 +1,21 @@
1
- require "concurrent"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
4
  module Metrics
5
- # This is the only dependency on concurrent-ruby in Sidekiq but it's
6
- # mandatory for thread-safety until MRI supports atomic operations on values.
7
- Counter = ::Concurrent::AtomicFixnum
5
+ class Counter
6
+ def initialize
7
+ @value = 0
8
+ @lock = Mutex.new
9
+ end
10
+
11
+ def increment
12
+ @lock.synchronize { @value += 1 }
13
+ end
14
+
15
+ def value
16
+ @lock.synchronize { @value }
17
+ end
18
+ end
8
19
 
9
20
  # Implements space-efficient but statistically useful histogram storage.
10
21
  # A precise time histogram stores every time. Instead we break times into a set of
@@ -31,11 +31,11 @@ module Sidekiq
31
31
  # We don't track time for failed jobs as they can have very unpredictable
32
32
  # execution times. more important to know average time for successful jobs so we
33
33
  # can better recognize when a perf regression is introduced.
34
- @lock.synchronize {
35
- @grams[klass].record_time(time_ms)
36
- @jobs["#{klass}|ms"] += time_ms
37
- @totals["ms"] += time_ms
38
- }
34
+ track_time(klass, time_ms)
35
+ rescue JobRetry::Skip
36
+ # This is raised when iterable job is interrupted.
37
+ track_time(klass, time_ms)
38
+ raise
39
39
  rescue Exception
40
40
  @lock.synchronize {
41
41
  @jobs["#{klass}|f"] += 1
@@ -100,6 +100,14 @@ module Sidekiq
100
100
 
101
101
  private
102
102
 
103
+ def track_time(klass, time_ms)
104
+ @lock.synchronize {
105
+ @grams[klass].record_time(time_ms)
106
+ @jobs["#{klass}|ms"] += time_ms
107
+ @totals["ms"] += time_ms
108
+ }
109
+ end
110
+
103
111
  def reset
104
112
  @lock.synchronize {
105
113
  array = [@totals, @jobs, @grams]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/current_attributes"
2
4
 
3
5
  module Sidekiq
@@ -31,11 +33,26 @@ module Sidekiq
31
33
  attrs = strklass.constantize.attributes
32
34
  # Retries can push the job N times, we don't
33
35
  # want retries to reset cattr. #5692, #5090
34
- job[key] = attrs if attrs.any?
36
+ if attrs.any?
37
+ # Older rails has a bug that `CurrentAttributes#attributes` always returns
38
+ # the same hash instance. We need to dup it to avoid being accidentally mutated.
39
+ job[key] = if returns_same_object?
40
+ attrs.dup
41
+ else
42
+ attrs
43
+ end
44
+ end
35
45
  end
36
46
  end
37
47
  yield
38
48
  end
49
+
50
+ private
51
+
52
+ def returns_same_object?
53
+ ActiveSupport::VERSION::MAJOR < 8 ||
54
+ (ActiveSupport::VERSION::MAJOR == 8 && ActiveSupport::VERSION::MINOR == 0)
55
+ end
39
56
  end
40
57
 
41
58
  class Load
@@ -86,7 +103,7 @@ module Sidekiq
86
103
  cattrs = build_cattrs_hash(klass_or_array)
87
104
 
88
105
  config.client_middleware.add Save, cattrs
89
- config.server_middleware.add Load, cattrs
106
+ config.server_middleware.prepend Load, cattrs
90
107
  end
91
108
 
92
109
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sidekiq
2
4
  # Server-side middleware must import this Module in order
3
5
  # to get access to server resources during `call`.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "fileutils"
4
5
  require "sidekiq/api"
@@ -98,7 +99,7 @@ class Sidekiq::Monitor
98
99
  pad = opts[:pad] || 0
99
100
  max_length = opts[:max_length] || (80 - pad)
100
101
  out = []
101
- line = ""
102
+ line = +""
102
103
  values.each do |value|
103
104
  if (line.length + value.length) > max_length
104
105
  out << line
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Paginator
5
+ TYPE_CACHE = {
6
+ "dead" => "zset",
7
+ "retry" => "zset",
8
+ "schedule" => "zset"
9
+ }
10
+
5
11
  def page(key, pageidx = 1, page_size = 25, opts = nil)
6
12
  current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
7
13
  pageidx = current_page - 1
@@ -138,11 +138,11 @@ module Sidekiq
138
138
  # Effectively this block denotes a "unit of work" to Rails.
139
139
  @reloader.call do
140
140
  klass = Object.const_get(job_hash["class"])
141
- inst = klass.new
142
- inst.jid = job_hash["jid"]
143
- inst._context = self
144
- @retrier.local(inst, jobstr, queue) do
145
- yield inst
141
+ instance = klass.new
142
+ instance.jid = job_hash["jid"]
143
+ instance._context = self
144
+ @retrier.local(instance, jobstr, queue) do
145
+ yield instance
146
146
  end
147
147
  end
148
148
  end
@@ -180,9 +180,9 @@ module Sidekiq
180
180
  ack = false
181
181
  Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
182
182
  Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
183
- dispatch(job_hash, queue, jobstr) do |inst|
184
- config.server_middleware.invoke(inst, job_hash, queue) do
185
- execute_job(inst, job_hash["args"])
183
+ dispatch(job_hash, queue, jobstr) do |instance|
184
+ config.server_middleware.invoke(instance, job_hash, queue) do
185
+ execute_job(instance, job_hash["args"])
186
186
  end
187
187
  end
188
188
  ack = true
@@ -216,8 +216,8 @@ module Sidekiq
216
216
  end
217
217
  end
218
218
 
219
- def execute_job(inst, cloned_args)
220
- inst.perform(*cloned_args)
219
+ def execute_job(instance, cloned_args)
220
+ instance.perform(*cloned_args)
221
221
  end
222
222
 
223
223
  # Ruby doesn't provide atomic counters out of the box so we'll
data/lib/sidekiq/rails.rb CHANGED
@@ -4,6 +4,17 @@ require "sidekiq/job"
4
4
  require "rails"
5
5
 
6
6
  module Sidekiq
7
+ module ActiveJob
8
+ # @api private
9
+ class Wrapper
10
+ include Sidekiq::Job
11
+
12
+ def perform(job_data)
13
+ ::ActiveJob::Base.execute(job_data.merge("provider_job_id" => jid))
14
+ end
15
+ end
16
+ end
17
+
7
18
  class Rails < ::Rails::Engine
8
19
  class Reloader
9
20
  def initialize(app = ::Rails.application)
@@ -39,6 +50,7 @@ module Sidekiq
39
50
  # end
40
51
  initializer "sidekiq.active_job_integration" do
41
52
  ActiveSupport.on_load(:active_job) do
53
+ require_relative "../active_job/queue_adapters/sidekiq_adapter"
42
54
  include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
43
55
  end
44
56
  end
@@ -66,7 +66,14 @@ module Sidekiq
66
66
  scrubbed_options[:password] = redacted if scrubbed_options[:password]
67
67
  scrubbed_options[:sentinel_password] = redacted if scrubbed_options[:sentinel_password]
68
68
  scrubbed_options[:sentinels]&.each do |sentinel|
69
- sentinel[:password] = redacted if sentinel[:password]
69
+ if sentinel.is_a?(String)
70
+ if (uri = URI(sentinel)) && uri.password
71
+ uri.password = redacted
72
+ sentinel.replace(uri.to_s)
73
+ end
74
+ elsif sentinel[:password]
75
+ sentinel[:password] = redacted
76
+ end
70
77
  end
71
78
  scrubbed_options
72
79
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "forwardable"
2
4
 
3
5
  module Sidekiq
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Sidekiq's systemd integration allows Sidekiq to inform systemd:
3
5
  # 1. when it has successfully started
@@ -283,11 +283,11 @@ module Sidekiq
283
283
  end
284
284
 
285
285
  def process_job(job)
286
- inst = new
287
- inst.jid = job["jid"]
288
- inst.bid = job["bid"] if inst.respond_to?(:bid=)
289
- Sidekiq::Testing.server_middleware.invoke(inst, job, job["queue"]) do
290
- execute_job(inst, job["args"])
286
+ instance = new
287
+ instance.jid = job["jid"]
288
+ instance.bid = job["bid"] if instance.respond_to?(:bid=)
289
+ Sidekiq::Testing.server_middleware.invoke(instance, job, job["queue"]) do
290
+ execute_job(instance, job["args"])
291
291
  end
292
292
  end
293
293
 
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.3.0"
4
+ VERSION = "7.3.9"
5
5
  MAJOR = 7
6
+
7
+ def self.gem_version
8
+ Gem::Version.new(VERSION)
9
+ end
6
10
  end
@@ -27,6 +27,7 @@ module Sidekiq
27
27
  redirect current_location
28
28
  end
29
29
 
30
+ # deprecated, will warn in 8.0
30
31
  def params
31
32
  indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
32
33
 
@@ -36,8 +37,19 @@ module Sidekiq
36
37
  indifferent_hash
37
38
  end
38
39
 
39
- def route_params
40
- env[WebRouter::ROUTE_PARAMS]
40
+ # Use like `url_params("page")` within your action blocks
41
+ def url_params(key)
42
+ request.params[key]
43
+ end
44
+
45
+ # Use like `route_params(:name)` within your action blocks
46
+ # key is required in 8.0, nil is only used for backwards compatibility
47
+ def route_params(key = nil)
48
+ if key
49
+ env[WebRouter::ROUTE_PARAMS][key]
50
+ else
51
+ env[WebRouter::ROUTE_PARAMS]
52
+ end
41
53
  end
42
54
 
43
55
  def session
@@ -48,8 +60,12 @@ module Sidekiq
48
60
  if content.is_a? Symbol
49
61
  unless respond_to?(:"_erb_#{content}")
50
62
  views = options[:views] || Web.settings.views
51
- src = ERB.new(File.read("#{views}/#{content}.erb")).src
52
- WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
63
+ filename = "#{views}/#{content}.erb"
64
+ src = ERB.new(File.read(filename)).src
65
+
66
+ # Need to use lineno less by 1 because erb generates a
67
+ # comment before the source code.
68
+ WebAction.class_eval <<-RUBY, filename, -1 # standard:disable Style/EvalWithLocation
53
69
  def _erb_#{content}
54
70
  #{src}
55
71
  end