sidekiq 7.2.4 → 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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +116 -0
  3. data/README.md +1 -1
  4. data/bin/sidekiqload +21 -12
  5. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  6. data/lib/generators/sidekiq/job_generator.rb +2 -0
  7. data/lib/sidekiq/api.rb +63 -34
  8. data/lib/sidekiq/capsule.rb +8 -3
  9. data/lib/sidekiq/cli.rb +2 -1
  10. data/lib/sidekiq/client.rb +21 -1
  11. data/lib/sidekiq/component.rb +22 -0
  12. data/lib/sidekiq/config.rb +27 -3
  13. data/lib/sidekiq/deploy.rb +2 -0
  14. data/lib/sidekiq/embedded.rb +2 -0
  15. data/lib/sidekiq/fetch.rb +1 -1
  16. data/lib/sidekiq/iterable_job.rb +55 -0
  17. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  18. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  19. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  20. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  21. data/lib/sidekiq/job/iterable.rb +294 -0
  22. data/lib/sidekiq/job.rb +13 -2
  23. data/lib/sidekiq/job_logger.rb +7 -6
  24. data/lib/sidekiq/job_retry.rb +6 -1
  25. data/lib/sidekiq/job_util.rb +2 -0
  26. data/lib/sidekiq/launcher.rb +1 -1
  27. data/lib/sidekiq/metrics/query.rb +2 -0
  28. data/lib/sidekiq/metrics/shared.rb +15 -4
  29. data/lib/sidekiq/metrics/tracking.rb +13 -5
  30. data/lib/sidekiq/middleware/current_attributes.rb +46 -13
  31. data/lib/sidekiq/middleware/modules.rb +2 -0
  32. data/lib/sidekiq/monitor.rb +2 -1
  33. data/lib/sidekiq/paginator.rb +6 -0
  34. data/lib/sidekiq/processor.rb +20 -10
  35. data/lib/sidekiq/rails.rb +12 -0
  36. data/lib/sidekiq/redis_client_adapter.rb +8 -5
  37. data/lib/sidekiq/redis_connection.rb +33 -2
  38. data/lib/sidekiq/ring_buffer.rb +2 -0
  39. data/lib/sidekiq/systemd.rb +2 -0
  40. data/lib/sidekiq/testing.rb +5 -5
  41. data/lib/sidekiq/version.rb +5 -1
  42. data/lib/sidekiq/web/action.rb +21 -4
  43. data/lib/sidekiq/web/application.rb +43 -82
  44. data/lib/sidekiq/web/helpers.rb +62 -15
  45. data/lib/sidekiq/web/router.rb +5 -2
  46. data/lib/sidekiq/web.rb +54 -2
  47. data/lib/sidekiq.rb +5 -3
  48. data/sidekiq.gemspec +3 -2
  49. data/web/assets/javascripts/application.js +6 -1
  50. data/web/assets/javascripts/dashboard-charts.js +24 -12
  51. data/web/assets/javascripts/dashboard.js +7 -1
  52. data/web/assets/stylesheets/application.css +16 -3
  53. data/web/locales/en.yml +3 -1
  54. data/web/locales/fr.yml +0 -1
  55. data/web/locales/gd.yml +0 -1
  56. data/web/locales/it.yml +32 -1
  57. data/web/locales/ja.yml +0 -1
  58. data/web/locales/pt-br.yml +1 -2
  59. data/web/locales/tr.yml +100 -0
  60. data/web/locales/uk.yml +24 -1
  61. data/web/locales/zh-cn.yml +0 -1
  62. data/web/locales/zh-tw.yml +0 -1
  63. data/web/views/_footer.erb +1 -2
  64. data/web/views/dashboard.erb +10 -7
  65. data/web/views/filtering.erb +1 -2
  66. data/web/views/layout.erb +6 -6
  67. data/web/views/metrics.erb +7 -8
  68. data/web/views/metrics_for_job.erb +4 -4
  69. data/web/views/morgue.erb +2 -2
  70. data/web/views/queue.erb +1 -1
  71. metadata +32 -13
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/job/iterable"
4
+
5
+ # Iterable jobs are ones which provide a sequence to process using
6
+ # `build_enumerator(*args, cursor: cursor)` and then process each
7
+ # element of that sequence in `each_iteration(item, *args)`.
8
+ #
9
+ # The job is kicked off as normal:
10
+ #
11
+ # ProcessUserSet.perform_async(123)
12
+ #
13
+ # but instead of calling `perform`, Sidekiq will call:
14
+ #
15
+ # enum = ProcessUserSet#build_enumerator(123, cursor:nil)
16
+ #
17
+ # Your Enumerator must yield `(object, updated_cursor)` and
18
+ # Sidekiq will call your `each_iteration` method:
19
+ #
20
+ # ProcessUserSet#each_iteration(object, 123)
21
+ #
22
+ # After every iteration, Sidekiq will check for shutdown. If we are
23
+ # stopping, the cursor will be saved to Redis and the job re-queued
24
+ # to pick up the rest of the work upon restart. Your job will get
25
+ # the updated_cursor so it can pick up right where it stopped.
26
+ #
27
+ # enum = ProcessUserSet#build_enumerator(123, cursor: updated_cursor)
28
+ #
29
+ # The cursor object must be serializable to JSON.
30
+ #
31
+ # Note there are several APIs to help you build enumerators for
32
+ # ActiveRecord Relations, CSV files, etc. See sidekiq/job/iterable/*.rb.
33
+ module Sidekiq
34
+ module IterableJob
35
+ def self.included(base)
36
+ base.include Sidekiq::Job
37
+ base.include Sidekiq::Job::Iterable
38
+ end
39
+
40
+ # def build_enumerator(*args, cursor:)
41
+ # def each_iteration(item, *args)
42
+
43
+ # Your job can also define several callbacks during points
44
+ # in each job's lifecycle.
45
+ #
46
+ # def on_start
47
+ # def on_resume
48
+ # def on_stop
49
+ # def on_complete
50
+ # def around_iteration
51
+ #
52
+ # To keep things simple and compatible, this is the same
53
+ # API as the `sidekiq-iteration` gem.
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Job
5
+ class InterruptHandler
6
+ include Sidekiq::ServerMiddleware
7
+
8
+ def call(instance, hash, queue)
9
+ yield
10
+ rescue Interrupted
11
+ logger.debug "Interrupted, re-queueing..."
12
+ c = Sidekiq::Client.new
13
+ c.push(hash)
14
+ raise Sidekiq::JobRetry::Skip
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ Sidekiq.configure_server do |config|
21
+ config.server_middleware do |chain|
22
+ chain.add Sidekiq::Job::InterruptHandler
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Job
5
+ module Iterable
6
+ # @api private
7
+ class ActiveRecordEnumerator
8
+ def initialize(relation, cursor: nil, **options)
9
+ @relation = relation
10
+ @cursor = cursor
11
+ @options = options
12
+ end
13
+
14
+ def records
15
+ Enumerator.new(-> { @relation.count }) do |yielder|
16
+ @relation.find_each(**@options, start: @cursor) do |record|
17
+ yielder.yield(record, record.id)
18
+ end
19
+ end
20
+ end
21
+
22
+ def batches
23
+ Enumerator.new(-> { @relation.count }) do |yielder|
24
+ @relation.find_in_batches(**@options, start: @cursor) do |batch|
25
+ yielder.yield(batch, batch.first.id)
26
+ end
27
+ end
28
+ end
29
+
30
+ def relations
31
+ Enumerator.new(-> { relations_size }) do |yielder|
32
+ # Convenience to use :batch_size for all the
33
+ # ActiveRecord batching methods.
34
+ options = @options.dup
35
+ options[:of] ||= options.delete(:batch_size)
36
+
37
+ @relation.in_batches(**options, start: @cursor) do |relation|
38
+ first_record = relation.first
39
+ yielder.yield(relation, first_record.id)
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def relations_size
47
+ batch_size = @options[:batch_size] || 1000
48
+ (@relation.count + batch_size - 1) / batch_size # ceiling division
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Job
5
+ module Iterable
6
+ # @api private
7
+ class CsvEnumerator
8
+ def initialize(csv)
9
+ unless defined?(CSV) && csv.instance_of?(CSV)
10
+ raise ArgumentError, "CsvEnumerator.new takes CSV object"
11
+ end
12
+
13
+ @csv = csv
14
+ end
15
+
16
+ def rows(cursor:)
17
+ @csv.lazy
18
+ .each_with_index
19
+ .drop(cursor || 0)
20
+ .to_enum { count_of_rows_in_file }
21
+ end
22
+
23
+ def batches(cursor:, batch_size: 100)
24
+ @csv.lazy
25
+ .each_slice(batch_size)
26
+ .with_index
27
+ .drop(cursor || 0)
28
+ .to_enum { (count_of_rows_in_file.to_f / batch_size).ceil }
29
+ end
30
+
31
+ private
32
+
33
+ def count_of_rows_in_file
34
+ filepath = @csv.path
35
+ return unless filepath
36
+
37
+ count = IO.popen(["wc", "-l", filepath]) do |out|
38
+ out.read.strip.to_i
39
+ end
40
+
41
+ count -= 1 if @csv.headers
42
+ count
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_record_enumerator"
4
+ require_relative "csv_enumerator"
5
+
6
+ module Sidekiq
7
+ module Job
8
+ module Iterable
9
+ module Enumerators
10
+ # Builds Enumerator object from a given array, using +cursor+ as an offset.
11
+ #
12
+ # @param array [Array]
13
+ # @param cursor [Integer] offset to start iteration from
14
+ #
15
+ # @return [Enumerator]
16
+ #
17
+ # @example
18
+ # array_enumerator(['build', 'enumerator', 'from', 'any', 'array'], cursor: cursor)
19
+ #
20
+ def array_enumerator(array, cursor:)
21
+ raise ArgumentError, "array must be an Array" unless array.is_a?(Array)
22
+
23
+ x = array.each_with_index.drop(cursor || 0)
24
+ x.to_enum { x.size }
25
+ end
26
+
27
+ # Builds Enumerator from `ActiveRecord::Relation`.
28
+ # Each Enumerator tick moves the cursor one row forward.
29
+ #
30
+ # @param relation [ActiveRecord::Relation] relation to iterate
31
+ # @param cursor [Object] offset id to start iteration from
32
+ # @param options [Hash] additional options that will be passed to relevant
33
+ # ActiveRecord batching methods
34
+ #
35
+ # @return [ActiveRecordEnumerator]
36
+ #
37
+ # @example
38
+ # def build_enumerator(cursor:)
39
+ # active_record_records_enumerator(User.all, cursor: cursor)
40
+ # end
41
+ #
42
+ # def each_iteration(user)
43
+ # user.notify_about_something
44
+ # end
45
+ #
46
+ def active_record_records_enumerator(relation, cursor:, **options)
47
+ ActiveRecordEnumerator.new(relation, cursor: cursor, **options).records
48
+ end
49
+
50
+ # Builds Enumerator from `ActiveRecord::Relation` and enumerates on batches of records.
51
+ # Each Enumerator tick moves the cursor `:batch_size` rows forward.
52
+ # @see #active_record_records_enumerator
53
+ #
54
+ # @example
55
+ # def build_enumerator(product_id, cursor:)
56
+ # active_record_batches_enumerator(
57
+ # Comment.where(product_id: product_id).select(:id),
58
+ # cursor: cursor,
59
+ # batch_size: 100
60
+ # )
61
+ # end
62
+ #
63
+ # def each_iteration(batch_of_comments, product_id)
64
+ # comment_ids = batch_of_comments.map(&:id)
65
+ # CommentService.call(comment_ids: comment_ids)
66
+ # end
67
+ #
68
+ def active_record_batches_enumerator(relation, cursor:, **options)
69
+ ActiveRecordEnumerator.new(relation, cursor: cursor, **options).batches
70
+ end
71
+
72
+ # Builds Enumerator from `ActiveRecord::Relation` and enumerates on batches,
73
+ # yielding `ActiveRecord::Relation`s.
74
+ # @see #active_record_records_enumerator
75
+ #
76
+ # @example
77
+ # def build_enumerator(product_id, cursor:)
78
+ # active_record_relations_enumerator(
79
+ # Product.find(product_id).comments,
80
+ # cursor: cursor,
81
+ # batch_size: 100,
82
+ # )
83
+ # end
84
+ #
85
+ # def each_iteration(batch_of_comments, product_id)
86
+ # # batch_of_comments will be a Comment::ActiveRecord_Relation
87
+ # batch_of_comments.update_all(deleted: true)
88
+ # end
89
+ #
90
+ def active_record_relations_enumerator(relation, cursor:, **options)
91
+ ActiveRecordEnumerator.new(relation, cursor: cursor, **options).relations
92
+ end
93
+
94
+ # Builds Enumerator from a CSV file.
95
+ #
96
+ # @param csv [CSV] an instance of CSV object
97
+ # @param cursor [Integer] offset to start iteration from
98
+ #
99
+ # @example
100
+ # def build_enumerator(import_id, cursor:)
101
+ # import = Import.find(import_id)
102
+ # csv_enumerator(import.csv, cursor: cursor)
103
+ # end
104
+ #
105
+ # def each_iteration(csv_row)
106
+ # # insert csv_row into database
107
+ # end
108
+ #
109
+ def csv_enumerator(csv, cursor:)
110
+ CsvEnumerator.new(csv).rows(cursor: cursor)
111
+ end
112
+
113
+ # Builds Enumerator from a CSV file and enumerates on batches of records.
114
+ #
115
+ # @param csv [CSV] an instance of CSV object
116
+ # @param cursor [Integer] offset to start iteration from
117
+ # @option options :batch_size [Integer] (100) size of the batch
118
+ #
119
+ # @example
120
+ # def build_enumerator(import_id, cursor:)
121
+ # import = Import.find(import_id)
122
+ # csv_batches_enumerator(import.csv, cursor: cursor)
123
+ # end
124
+ #
125
+ # def each_iteration(batch_of_csv_rows)
126
+ # # ...
127
+ # end
128
+ #
129
+ def csv_batches_enumerator(csv, cursor:, **options)
130
+ CsvEnumerator.new(csv).batches(cursor: cursor, **options)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "iterable/enumerators"
4
+
5
+ module Sidekiq
6
+ module Job
7
+ class Interrupted < ::RuntimeError; end
8
+
9
+ module Iterable
10
+ include Enumerators
11
+
12
+ # @api private
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ # @api private
18
+ module ClassMethods
19
+ def method_added(method_name)
20
+ raise "#{self} is an iterable job and must not define #perform" if method_name == :perform
21
+ super
22
+ end
23
+ end
24
+
25
+ # @api private
26
+ def initialize
27
+ super
28
+
29
+ @_executions = 0
30
+ @_cursor = nil
31
+ @_start_time = nil
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
67
+ end
68
+
69
+ # A hook to override that will be called when the job starts iterating.
70
+ #
71
+ # It is called only once, for the first time.
72
+ #
73
+ def on_start
74
+ end
75
+
76
+ # A hook to override that will be called around each iteration.
77
+ #
78
+ # Can be useful for some metrics collection, performance tracking etc.
79
+ #
80
+ def around_iteration
81
+ yield
82
+ end
83
+
84
+ # A hook to override that will be called when the job resumes iterating.
85
+ #
86
+ def on_resume
87
+ end
88
+
89
+ # A hook to override that will be called each time the job is interrupted.
90
+ #
91
+ # This can be due to interruption or sidekiq stopping.
92
+ #
93
+ def on_stop
94
+ end
95
+
96
+ # A hook to override that will be called when the job finished iterating.
97
+ #
98
+ def on_complete
99
+ end
100
+
101
+ # The enumerator to be iterated over.
102
+ #
103
+ # @return [Enumerator]
104
+ #
105
+ # @raise [NotImplementedError] with a message advising subclasses to
106
+ # implement an override for this method.
107
+ #
108
+ def build_enumerator(*)
109
+ raise NotImplementedError, "#{self.class.name} must implement a '#build_enumerator' method"
110
+ end
111
+
112
+ # The action to be performed on each item from the enumerator.
113
+ #
114
+ # @return [void]
115
+ #
116
+ # @raise [NotImplementedError] with a message advising subclasses to
117
+ # implement an override for this method.
118
+ #
119
+ def each_iteration(*)
120
+ raise NotImplementedError, "#{self.class.name} must implement an '#each_iteration' method"
121
+ end
122
+
123
+ def iteration_key
124
+ "it-#{jid}"
125
+ end
126
+
127
+ # @api private
128
+ def perform(*args)
129
+ @_args = args.dup.freeze
130
+ fetch_previous_iteration_state
131
+
132
+ @_executions += 1
133
+ @_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
134
+
135
+ enumerator = build_enumerator(*args, cursor: @_cursor)
136
+ unless enumerator
137
+ logger.info("'#build_enumerator' returned nil, skipping the job.")
138
+ return
139
+ end
140
+
141
+ assert_enumerator!(enumerator)
142
+
143
+ if @_executions == 1
144
+ on_start
145
+ else
146
+ on_resume
147
+ end
148
+
149
+ completed = catch(:abort) do
150
+ iterate_with_enumerator(enumerator, args)
151
+ end
152
+
153
+ on_stop
154
+ completed = handle_completed(completed)
155
+
156
+ if completed
157
+ on_complete
158
+ cleanup
159
+ else
160
+ reenqueue_iteration_job
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def is_cancelled?
167
+ @_cancelled = Sidekiq.redis { |c| c.hget("it-#{jid}", "cancelled") }
168
+ end
169
+
170
+ def fetch_previous_iteration_state
171
+ state = Sidekiq.redis { |conn| conn.hgetall(iteration_key) }
172
+
173
+ unless state.empty?
174
+ @_executions = state["ex"].to_i
175
+ @_cursor = Sidekiq.load_json(state["c"])
176
+ @_runtime = state["rt"].to_f
177
+ end
178
+ end
179
+
180
+ STATE_FLUSH_INTERVAL = 5 # seconds
181
+ # we need to keep the state around as long as the job
182
+ # might be retrying
183
+ STATE_TTL = 30 * 24 * 60 * 60 # one month
184
+
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]
192
+ found_record = false
193
+ state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
194
+
195
+ enumerator.each do |object, cursor|
196
+ found_record = true
197
+ @_cursor = cursor
198
+
199
+ is_interrupted = interrupted?
200
+ if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - state_flushed_at >= STATE_FLUSH_INTERVAL || is_interrupted
201
+ _, _, cancelled = flush_state
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
208
+ end
209
+
210
+ return false if is_interrupted
211
+
212
+ verify_iteration_time(time_limit, object) do
213
+ around_iteration do
214
+ each_iteration(object, *arguments)
215
+ end
216
+ end
217
+ end
218
+
219
+ logger.debug("Enumerator found nothing to iterate!") unless found_record
220
+ true
221
+ ensure
222
+ @_runtime += (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @_start_time)
223
+ end
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
+
235
+ def reenqueue_iteration_job
236
+ flush_state
237
+ logger.debug { "Interrupting job (cursor=#{@_cursor.inspect})" }
238
+
239
+ raise Interrupted
240
+ end
241
+
242
+ def assert_enumerator!(enum)
243
+ unless enum.is_a?(Enumerator)
244
+ raise ArgumentError, <<~MSG
245
+ #build_enumerator must return an Enumerator, but returned #{enum.class}.
246
+ Example:
247
+ def build_enumerator(params, cursor:)
248
+ active_record_records_enumerator(
249
+ Shop.find(params["shop_id"]).products,
250
+ cursor: cursor
251
+ )
252
+ end
253
+ MSG
254
+ end
255
+ end
256
+
257
+ def flush_state
258
+ key = iteration_key
259
+ state = {
260
+ "ex" => @_executions,
261
+ "c" => Sidekiq.dump_json(@_cursor),
262
+ "rt" => @_runtime
263
+ }
264
+
265
+ Sidekiq.redis do |conn|
266
+ conn.multi do |pipe|
267
+ pipe.hset(key, state)
268
+ pipe.expire(key, STATE_TTL)
269
+ pipe.hget(key, "cancelled")
270
+ end
271
+ end
272
+ end
273
+
274
+ def cleanup
275
+ logger.debug {
276
+ format("Completed iteration. executions=%d runtime=%.3f", @_executions, @_runtime)
277
+ }
278
+ Sidekiq.redis { |conn| conn.unlink(iteration_key) }
279
+ end
280
+
281
+ def handle_completed(completed)
282
+ case completed
283
+ when nil, # someone aborted the job but wants to call the on_complete callback
284
+ true
285
+ true
286
+ when false
287
+ false
288
+ else
289
+ raise "Unexpected thrown value: #{completed.inspect}"
290
+ end
291
+ end
292
+ end
293
+ end
294
+ end
data/lib/sidekiq/job.rb CHANGED
@@ -69,7 +69,11 @@ module Sidekiq
69
69
  # In practice, any option is allowed. This is the main mechanism to configure the
70
70
  # options for a specific job.
71
71
  def sidekiq_options(opts = {})
72
- opts = opts.transform_keys(&:to_s) # stringify
72
+ # stringify 2 levels of keys
73
+ opts = opts.to_h do |k, v|
74
+ [k.to_s, (Hash === v) ? v.transform_keys(&:to_s) : v]
75
+ end
76
+
73
77
  self.sidekiq_options_hash = get_sidekiq_options.merge(opts)
74
78
  end
75
79
 
@@ -155,6 +159,9 @@ module Sidekiq
155
159
 
156
160
  attr_accessor :jid
157
161
 
162
+ # This attribute is implementation-specific and not a public API
163
+ attr_accessor :_context
164
+
158
165
  def self.included(base)
159
166
  raise ArgumentError, "Sidekiq::Job cannot be included in an ActiveJob: #{base.name}" if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
160
167
 
@@ -166,6 +173,10 @@ module Sidekiq
166
173
  Sidekiq.logger
167
174
  end
168
175
 
176
+ def interrupted?
177
+ @_context&.stopping?
178
+ end
179
+
169
180
  # This helper class encapsulates the set options for `set`, e.g.
170
181
  #
171
182
  # SomeJob.set(queue: 'foo').perform_async(....)
@@ -366,7 +377,7 @@ module Sidekiq
366
377
 
367
378
  def build_client # :nodoc:
368
379
  pool = Thread.current[:sidekiq_redis_pool] || get_sidekiq_options["pool"] || Sidekiq.default_configuration.redis_pool
369
- client_class = get_sidekiq_options["client_class"] || Sidekiq::Client
380
+ client_class = Thread.current[:sidekiq_client_class] || get_sidekiq_options["client_class"] || Sidekiq::Client
370
381
  client_class.new(pool: pool)
371
382
  end
372
383
  end
@@ -2,22 +2,23 @@
2
2
 
3
3
  module Sidekiq
4
4
  class JobLogger
5
- def initialize(logger)
6
- @logger = logger
5
+ def initialize(config)
6
+ @config = config
7
+ @logger = @config.logger
8
+ @skip = !!@config[:skip_default_job_logging]
7
9
  end
8
10
 
9
11
  def call(item, queue)
10
12
  start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
11
- @logger.info("start")
13
+ @logger.info { "start" } unless @skip
12
14
 
13
15
  yield
14
16
 
15
17
  Sidekiq::Context.add(:elapsed, elapsed(start))
16
- @logger.info("done")
18
+ @logger.info { "done" } unless @skip
17
19
  rescue Exception
18
20
  Sidekiq::Context.add(:elapsed, elapsed(start))
19
- @logger.info("fail")
20
-
21
+ @logger.info { "fail" } unless @skip
21
22
  raise
22
23
  end
23
24
 
@@ -59,8 +59,13 @@ module Sidekiq
59
59
  # end
60
60
  #
61
61
  class JobRetry
62
+ # Handled means the job failed but has been dealt with
63
+ # (by creating a retry, rescheduling it, etc). It still
64
+ # needs to be logged and dispatched to error_handlers.
62
65
  class Handled < ::RuntimeError; end
63
66
 
67
+ # Skip means the job failed but Sidekiq does not need to
68
+ # create a retry, log it or send to error_handlers.
64
69
  class Skip < Handled; end
65
70
 
66
71
  include Sidekiq::Component
@@ -129,7 +134,7 @@ module Sidekiq
129
134
  process_retry(jobinst, msg, queue, e)
130
135
  # We've handled this error associated with this job, don't
131
136
  # need to handle it at the global level
132
- raise Skip
137
+ raise Handled
133
138
  end
134
139
 
135
140
  private