sidekiq 7.1.4 → 8.0.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +333 -0
  3. data/README.md +16 -13
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +31 -22
  6. data/bin/webload +69 -0
  7. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +121 -0
  8. data/lib/generators/sidekiq/job_generator.rb +2 -0
  9. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  10. data/lib/sidekiq/api.rb +260 -67
  11. data/lib/sidekiq/capsule.rb +17 -8
  12. data/lib/sidekiq/cli.rb +19 -20
  13. data/lib/sidekiq/client.rb +48 -15
  14. data/lib/sidekiq/component.rb +64 -3
  15. data/lib/sidekiq/config.rb +60 -18
  16. data/lib/sidekiq/deploy.rb +4 -2
  17. data/lib/sidekiq/embedded.rb +4 -1
  18. data/lib/sidekiq/fetch.rb +2 -1
  19. data/lib/sidekiq/iterable_job.rb +56 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +322 -0
  25. data/lib/sidekiq/job.rb +16 -5
  26. data/lib/sidekiq/job_logger.rb +15 -12
  27. data/lib/sidekiq/job_retry.rb +41 -13
  28. data/lib/sidekiq/job_util.rb +7 -1
  29. data/lib/sidekiq/launcher.rb +23 -11
  30. data/lib/sidekiq/loader.rb +57 -0
  31. data/lib/sidekiq/logger.rb +25 -69
  32. data/lib/sidekiq/manager.rb +0 -1
  33. data/lib/sidekiq/metrics/query.rb +76 -45
  34. data/lib/sidekiq/metrics/shared.rb +23 -9
  35. data/lib/sidekiq/metrics/tracking.rb +32 -15
  36. data/lib/sidekiq/middleware/current_attributes.rb +39 -14
  37. data/lib/sidekiq/middleware/i18n.rb +2 -0
  38. data/lib/sidekiq/middleware/modules.rb +2 -0
  39. data/lib/sidekiq/monitor.rb +6 -9
  40. data/lib/sidekiq/paginator.rb +16 -3
  41. data/lib/sidekiq/processor.rb +37 -20
  42. data/lib/sidekiq/profiler.rb +73 -0
  43. data/lib/sidekiq/rails.rb +47 -57
  44. data/lib/sidekiq/redis_client_adapter.rb +25 -8
  45. data/lib/sidekiq/redis_connection.rb +49 -9
  46. data/lib/sidekiq/ring_buffer.rb +3 -0
  47. data/lib/sidekiq/scheduled.rb +2 -2
  48. data/lib/sidekiq/systemd.rb +2 -0
  49. data/lib/sidekiq/testing.rb +34 -15
  50. data/lib/sidekiq/transaction_aware_client.rb +20 -5
  51. data/lib/sidekiq/version.rb +6 -2
  52. data/lib/sidekiq/web/action.rb +149 -64
  53. data/lib/sidekiq/web/application.rb +367 -297
  54. data/lib/sidekiq/web/config.rb +120 -0
  55. data/lib/sidekiq/web/csrf_protection.rb +8 -5
  56. data/lib/sidekiq/web/helpers.rb +146 -64
  57. data/lib/sidekiq/web/router.rb +61 -74
  58. data/lib/sidekiq/web.rb +53 -106
  59. data/lib/sidekiq.rb +11 -4
  60. data/sidekiq.gemspec +6 -5
  61. data/web/assets/images/logo.png +0 -0
  62. data/web/assets/images/status.png +0 -0
  63. data/web/assets/javascripts/application.js +66 -24
  64. data/web/assets/javascripts/base-charts.js +30 -16
  65. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  66. data/web/assets/javascripts/dashboard-charts.js +37 -11
  67. data/web/assets/javascripts/dashboard.js +15 -11
  68. data/web/assets/javascripts/metrics.js +50 -34
  69. data/web/assets/stylesheets/style.css +776 -0
  70. data/web/locales/ar.yml +2 -0
  71. data/web/locales/cs.yml +2 -0
  72. data/web/locales/da.yml +2 -0
  73. data/web/locales/de.yml +2 -0
  74. data/web/locales/el.yml +2 -0
  75. data/web/locales/en.yml +12 -1
  76. data/web/locales/es.yml +25 -2
  77. data/web/locales/fa.yml +2 -0
  78. data/web/locales/fr.yml +2 -1
  79. data/web/locales/gd.yml +2 -1
  80. data/web/locales/he.yml +2 -0
  81. data/web/locales/hi.yml +2 -0
  82. data/web/locales/it.yml +41 -1
  83. data/web/locales/ja.yml +2 -1
  84. data/web/locales/ko.yml +2 -0
  85. data/web/locales/lt.yml +2 -0
  86. data/web/locales/nb.yml +2 -0
  87. data/web/locales/nl.yml +2 -0
  88. data/web/locales/pl.yml +2 -0
  89. data/web/locales/{pt-br.yml → pt-BR.yml} +4 -3
  90. data/web/locales/pt.yml +2 -0
  91. data/web/locales/ru.yml +2 -0
  92. data/web/locales/sv.yml +2 -0
  93. data/web/locales/ta.yml +2 -0
  94. data/web/locales/tr.yml +102 -0
  95. data/web/locales/uk.yml +29 -4
  96. data/web/locales/ur.yml +2 -0
  97. data/web/locales/vi.yml +2 -0
  98. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -74
  99. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -2
  100. data/web/views/_footer.erb +31 -22
  101. data/web/views/_job_info.erb +91 -89
  102. data/web/views/_metrics_period_select.erb +13 -10
  103. data/web/views/_nav.erb +14 -21
  104. data/web/views/_paging.erb +22 -21
  105. data/web/views/_poll_link.erb +2 -2
  106. data/web/views/_summary.erb +23 -23
  107. data/web/views/busy.erb +123 -125
  108. data/web/views/dashboard.erb +71 -82
  109. data/web/views/dead.erb +31 -27
  110. data/web/views/filtering.erb +6 -0
  111. data/web/views/layout.erb +13 -29
  112. data/web/views/metrics.erb +70 -68
  113. data/web/views/metrics_for_job.erb +30 -40
  114. data/web/views/morgue.erb +65 -70
  115. data/web/views/profiles.erb +43 -0
  116. data/web/views/queue.erb +54 -52
  117. data/web/views/queues.erb +43 -37
  118. data/web/views/retries.erb +70 -75
  119. data/web/views/retry.erb +32 -27
  120. data/web/views/scheduled.erb +63 -55
  121. data/web/views/scheduled_job_info.erb +3 -3
  122. metadata +49 -27
  123. data/web/assets/stylesheets/application-dark.css +0 -147
  124. data/web/assets/stylesheets/application-rtl.css +0 -153
  125. data/web/assets/stylesheets/application.css +0 -724
  126. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  127. data/web/assets/stylesheets/bootstrap.css +0 -5
  128. data/web/views/_status.erb +0 -4
@@ -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,322 @@
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
+ @current_object = nil
36
+ end
37
+
38
+ # Access to the current object while iterating.
39
+ # This value is not reset so the latest element is
40
+ # explicitly available to cleanup/complete callbacks.
41
+ attr_reader :current_object
42
+
43
+ def arguments
44
+ @_args
45
+ end
46
+
47
+ # Three days is the longest period you generally need to wait for a retry to
48
+ # execute when using the default retry scheme. We don't want to "forget" the job
49
+ # is cancelled before it has a chance to execute and cancel itself.
50
+ CANCELLATION_PERIOD = (3 * 86_400).to_s
51
+
52
+ # Set a flag in Redis to mark this job as cancelled.
53
+ # Cancellation is asynchronous and is checked at the start of iteration
54
+ # and every 5 seconds thereafter as part of the recurring state flush.
55
+ def cancel!
56
+ return @_cancelled if cancelled?
57
+
58
+ key = iteration_key
59
+ _, result, _ = Sidekiq.redis do |c|
60
+ c.pipelined do |p|
61
+ p.hsetnx(key, "cancelled", Time.now.to_i)
62
+ p.hget(key, "cancelled")
63
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
64
+ end
65
+ end
66
+ @_cancelled = result.to_i
67
+ end
68
+
69
+ def cancelled?
70
+ @_cancelled
71
+ end
72
+
73
+ def cursor
74
+ @_cursor.freeze
75
+ end
76
+
77
+ # A hook to override that will be called when the job starts iterating.
78
+ #
79
+ # It is called only once, for the first time.
80
+ #
81
+ def on_start
82
+ end
83
+
84
+ # A hook to override that will be called around each iteration.
85
+ #
86
+ # Can be useful for some metrics collection, performance tracking etc.
87
+ #
88
+ def around_iteration
89
+ yield
90
+ end
91
+
92
+ # A hook to override that will be called when the job resumes iterating.
93
+ #
94
+ def on_resume
95
+ end
96
+
97
+ # A hook to override that will be called each time the job is interrupted.
98
+ #
99
+ # This can be due to interruption or sidekiq stopping.
100
+ #
101
+ def on_stop
102
+ end
103
+
104
+ # A hook to override that will be called when the job is cancelled.
105
+ #
106
+ def on_cancel
107
+ end
108
+
109
+ # A hook to override that will be called when the job finished iterating.
110
+ #
111
+ def on_complete
112
+ end
113
+
114
+ # The enumerator to be iterated over.
115
+ #
116
+ # @return [Enumerator]
117
+ #
118
+ # @raise [NotImplementedError] with a message advising subclasses to
119
+ # implement an override for this method.
120
+ #
121
+ def build_enumerator(*)
122
+ raise NotImplementedError, "#{self.class.name} must implement a '#build_enumerator' method"
123
+ end
124
+
125
+ # The action to be performed on each item from the enumerator.
126
+ #
127
+ # @return [void]
128
+ #
129
+ # @raise [NotImplementedError] with a message advising subclasses to
130
+ # implement an override for this method.
131
+ #
132
+ def each_iteration(*)
133
+ raise NotImplementedError, "#{self.class.name} must implement an '#each_iteration' method"
134
+ end
135
+
136
+ def iteration_key
137
+ "it-#{jid}"
138
+ end
139
+
140
+ # @api private
141
+ def perform(*args)
142
+ @_args = args.dup.freeze
143
+ fetch_previous_iteration_state
144
+
145
+ @_executions += 1
146
+ @_start_time = mono_now
147
+
148
+ enumerator = build_enumerator(*args, cursor: @_cursor)
149
+ unless enumerator
150
+ logger.info("'#build_enumerator' returned nil, skipping the job.")
151
+ return
152
+ end
153
+
154
+ assert_enumerator!(enumerator)
155
+
156
+ if @_executions == 1
157
+ on_start
158
+ else
159
+ on_resume
160
+ end
161
+
162
+ completed = catch(:abort) do
163
+ iterate_with_enumerator(enumerator, args)
164
+ end
165
+
166
+ on_stop
167
+ completed = handle_completed(completed)
168
+
169
+ if completed
170
+ on_complete
171
+ cleanup
172
+ else
173
+ reenqueue_iteration_job
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ def is_cancelled?
180
+ @_cancelled = Sidekiq.redis { |c| c.hget(iteration_key, "cancelled") }
181
+ end
182
+
183
+ def fetch_previous_iteration_state
184
+ state = Sidekiq.redis { |conn| conn.hgetall(iteration_key) }
185
+
186
+ unless state.empty?
187
+ @_executions = state["ex"].to_i
188
+ @_cursor = Sidekiq.load_json(state["c"])
189
+ @_runtime = state["rt"].to_f
190
+ end
191
+ end
192
+
193
+ STATE_FLUSH_INTERVAL = 5 # seconds
194
+ # we need to keep the state around as long as the job
195
+ # might be retrying
196
+ STATE_TTL = 30 * 24 * 60 * 60 # one month
197
+
198
+ def iterate_with_enumerator(enumerator, arguments)
199
+ if is_cancelled?
200
+ on_cancel
201
+ logger.info { "Job cancelled" }
202
+ return true
203
+ end
204
+
205
+ time_limit = Sidekiq.default_configuration[:timeout]
206
+ found_record = false
207
+ state_flushed_at = mono_now
208
+
209
+ enumerator.each do |object, cursor|
210
+ found_record = true
211
+ @_cursor = cursor
212
+ @current_object = object
213
+
214
+ interrupt_job = interrupted? || should_interrupt?
215
+ if mono_now - state_flushed_at >= STATE_FLUSH_INTERVAL || interrupt_job
216
+ _, _, cancelled = flush_state
217
+ state_flushed_at = mono_now
218
+ if cancelled
219
+ @_cancelled = true
220
+ on_cancel
221
+ logger.info { "Job cancelled" }
222
+ return true
223
+ end
224
+ end
225
+
226
+ return false if interrupt_job
227
+
228
+ verify_iteration_time(time_limit) do
229
+ around_iteration do
230
+ each_iteration(object, *arguments)
231
+ rescue Exception
232
+ flush_state
233
+ raise
234
+ end
235
+ end
236
+ end
237
+
238
+ logger.debug("Enumerator found nothing to iterate!") unless found_record
239
+ true
240
+ ensure
241
+ @_runtime += (mono_now - @_start_time)
242
+ end
243
+
244
+ def verify_iteration_time(time_limit)
245
+ start = mono_now
246
+ yield
247
+ finish = mono_now
248
+ total = finish - start
249
+ if total > time_limit
250
+ logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d). This can lead to job processing problems during deploys" % [total, time_limit] }
251
+ end
252
+ end
253
+
254
+ def reenqueue_iteration_job
255
+ flush_state
256
+ logger.debug { "Interrupting job (cursor=#{@_cursor.inspect})" }
257
+
258
+ raise Interrupted
259
+ end
260
+
261
+ def assert_enumerator!(enum)
262
+ unless enum.is_a?(Enumerator)
263
+ raise ArgumentError, <<~MSG
264
+ #build_enumerator must return an Enumerator, but returned #{enum.class}.
265
+ Example:
266
+ def build_enumerator(params, cursor:)
267
+ active_record_records_enumerator(
268
+ Shop.find(params["shop_id"]).products,
269
+ cursor: cursor
270
+ )
271
+ end
272
+ MSG
273
+ end
274
+ end
275
+
276
+ def should_interrupt?
277
+ max_iteration_runtime = Sidekiq.default_configuration[:max_iteration_runtime]
278
+ max_iteration_runtime && (mono_now - @_start_time > max_iteration_runtime)
279
+ end
280
+
281
+ def flush_state
282
+ key = iteration_key
283
+ state = {
284
+ "ex" => @_executions,
285
+ "c" => Sidekiq.dump_json(@_cursor),
286
+ "rt" => @_runtime
287
+ }
288
+
289
+ Sidekiq.redis do |conn|
290
+ conn.multi do |pipe|
291
+ pipe.hset(key, state)
292
+ pipe.expire(key, STATE_TTL, "nx")
293
+ pipe.hget(key, "cancelled")
294
+ end
295
+ end
296
+ end
297
+
298
+ def cleanup
299
+ logger.debug {
300
+ format("Completed iteration. executions=%d runtime=%.3f", @_executions, @_runtime)
301
+ }
302
+ Sidekiq.redis { |conn| conn.unlink(iteration_key) }
303
+ end
304
+
305
+ def handle_completed(completed)
306
+ case completed
307
+ when nil, # someone aborted the job but wants to call the on_complete callback
308
+ true
309
+ true
310
+ when false
311
+ false
312
+ else
313
+ raise "Unexpected thrown value: #{completed.inspect}"
314
+ end
315
+ end
316
+
317
+ def mono_now
318
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
319
+ end
320
+ end
321
+ end
322
+ 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
 
@@ -109,7 +113,7 @@ module Sidekiq
109
113
  m = "#{name}="
110
114
  undef_method(m) if method_defined?(m) || private_method_defined?(m)
111
115
  end
112
- define_singleton_method("#{name}=") do |val|
116
+ define_singleton_method(:"#{name}=") do |val|
113
117
  singleton_class.class_eval do
114
118
  ACCESSOR_MUTEX.synchronize do
115
119
  undef_method(synchronized_getter) if method_defined?(synchronized_getter) || private_method_defined?(synchronized_getter)
@@ -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(....)
@@ -237,9 +248,9 @@ module Sidekiq
237
248
  end
238
249
  alias_method :perform_sync, :perform_inline
239
250
 
240
- def perform_bulk(args, batch_size: 1_000)
251
+ def perform_bulk(args, **options)
241
252
  client = @klass.build_client
242
- client.push_bulk(@opts.merge("class" => @klass, "args" => args, :batch_size => batch_size))
253
+ client.push_bulk(@opts.merge({"class" => @klass, "args" => args}, options))
243
254
  end
244
255
 
245
256
  # +interval+ must be a timestamp, numeric or something that acts
@@ -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
 
@@ -25,16 +26,18 @@ module Sidekiq
25
26
  # If we're using a wrapper class, like ActiveJob, use the "wrapped"
26
27
  # attribute to expose the underlying thing.
27
28
  h = {
28
- class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
29
- jid: job_hash["jid"]
29
+ jid: job_hash["jid"],
30
+ class: job_hash["wrapped"] || job_hash["class"]
30
31
  }
31
- h[:bid] = job_hash["bid"] if job_hash.has_key?("bid")
32
- h[:tags] = job_hash["tags"] if job_hash.has_key?("tags")
32
+
33
+ @config[:logged_job_attributes].each do |attr|
34
+ h[attr.to_sym] = job_hash[attr] if job_hash.has_key?(attr)
35
+ end
33
36
 
34
37
  Thread.current[:sidekiq_context] = h
35
38
  level = job_hash["log_level"]
36
- if level && @logger.respond_to?(:log_at)
37
- @logger.log_at(level, &block)
39
+ if level
40
+ @logger.with_level(level, &block)
38
41
  else
39
42
  yield
40
43
  end