sidekiq 6.0.7 → 6.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +209 -2
  3. data/LICENSE +3 -3
  4. data/README.md +11 -10
  5. data/bin/sidekiq +8 -3
  6. data/bin/sidekiqload +70 -66
  7. data/bin/sidekiqmon +1 -1
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +180 -123
  13. data/lib/sidekiq/cli.rb +80 -45
  14. data/lib/sidekiq/client.rb +52 -71
  15. data/lib/sidekiq/{util.rb → component.rb} +11 -14
  16. data/lib/sidekiq/delay.rb +2 -0
  17. data/lib/sidekiq/extensions/action_mailer.rb +3 -2
  18. data/lib/sidekiq/extensions/active_record.rb +4 -3
  19. data/lib/sidekiq/extensions/class_methods.rb +5 -4
  20. data/lib/sidekiq/extensions/generic_proxy.rb +4 -2
  21. data/lib/sidekiq/fetch.rb +41 -30
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +16 -28
  24. data/lib/sidekiq/job_retry.rb +36 -36
  25. data/lib/sidekiq/job_util.rb +71 -0
  26. data/lib/sidekiq/launcher.rb +123 -63
  27. data/lib/sidekiq/logger.rb +11 -20
  28. data/lib/sidekiq/manager.rb +35 -34
  29. data/lib/sidekiq/middleware/chain.rb +28 -17
  30. data/lib/sidekiq/middleware/current_attributes.rb +61 -0
  31. data/lib/sidekiq/middleware/i18n.rb +6 -4
  32. data/lib/sidekiq/middleware/modules.rb +19 -0
  33. data/lib/sidekiq/monitor.rb +1 -1
  34. data/lib/sidekiq/paginator.rb +8 -8
  35. data/lib/sidekiq/processor.rb +41 -41
  36. data/lib/sidekiq/rails.rb +38 -22
  37. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  38. data/lib/sidekiq/redis_connection.rb +87 -53
  39. data/lib/sidekiq/ring_buffer.rb +29 -0
  40. data/lib/sidekiq/scheduled.rb +60 -24
  41. data/lib/sidekiq/sd_notify.rb +1 -1
  42. data/lib/sidekiq/testing/inline.rb +4 -4
  43. data/lib/sidekiq/testing.rb +39 -40
  44. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  45. data/lib/sidekiq/version.rb +1 -1
  46. data/lib/sidekiq/web/action.rb +2 -2
  47. data/lib/sidekiq/web/application.rb +21 -12
  48. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  49. data/lib/sidekiq/web/helpers.rb +40 -34
  50. data/lib/sidekiq/web/router.rb +5 -2
  51. data/lib/sidekiq/web.rb +36 -72
  52. data/lib/sidekiq/worker.rb +136 -16
  53. data/lib/sidekiq.rb +107 -30
  54. data/sidekiq.gemspec +11 -4
  55. data/web/assets/images/apple-touch-icon.png +0 -0
  56. data/web/assets/javascripts/application.js +113 -65
  57. data/web/assets/javascripts/dashboard.js +51 -51
  58. data/web/assets/stylesheets/application-dark.css +64 -43
  59. data/web/assets/stylesheets/application-rtl.css +0 -4
  60. data/web/assets/stylesheets/application.css +42 -239
  61. data/web/locales/ar.yml +8 -2
  62. data/web/locales/en.yml +4 -1
  63. data/web/locales/es.yml +18 -2
  64. data/web/locales/fr.yml +8 -1
  65. data/web/locales/ja.yml +3 -0
  66. data/web/locales/lt.yml +1 -1
  67. data/web/locales/pl.yml +4 -4
  68. data/web/locales/pt-br.yml +27 -9
  69. data/web/locales/ru.yml +4 -0
  70. data/web/views/_footer.erb +1 -1
  71. data/web/views/_job_info.erb +1 -1
  72. data/web/views/_poll_link.erb +2 -5
  73. data/web/views/_summary.erb +7 -7
  74. data/web/views/busy.erb +51 -20
  75. data/web/views/dashboard.erb +22 -14
  76. data/web/views/dead.erb +1 -1
  77. data/web/views/layout.erb +2 -1
  78. data/web/views/morgue.erb +6 -6
  79. data/web/views/queue.erb +11 -11
  80. data/web/views/queues.erb +4 -4
  81. data/web/views/retries.erb +7 -7
  82. data/web/views/retry.erb +1 -1
  83. data/web/views/scheduled.erb +1 -1
  84. metadata +29 -51
  85. data/.circleci/config.yml +0 -60
  86. data/.github/contributing.md +0 -32
  87. data/.github/issue_template.md +0 -11
  88. data/.gitignore +0 -13
  89. data/.standard.yml +0 -20
  90. data/3.0-Upgrade.md +0 -70
  91. data/4.0-Upgrade.md +0 -53
  92. data/5.0-Upgrade.md +0 -56
  93. data/6.0-Upgrade.md +0 -72
  94. data/COMM-LICENSE +0 -97
  95. data/Ent-2.0-Upgrade.md +0 -37
  96. data/Ent-Changes.md +0 -256
  97. data/Gemfile +0 -24
  98. data/Gemfile.lock +0 -208
  99. data/Pro-2.0-Upgrade.md +0 -138
  100. data/Pro-3.0-Upgrade.md +0 -44
  101. data/Pro-4.0-Upgrade.md +0 -35
  102. data/Pro-5.0-Upgrade.md +0 -25
  103. data/Pro-Changes.md +0 -782
  104. data/Rakefile +0 -10
  105. data/code_of_conduct.md +0 -50
  106. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  107. data/lib/sidekiq/exception_handler.rb +0 -27
data/lib/sidekiq/fetch.rb CHANGED
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq"
4
+ require "sidekiq/component"
4
5
 
5
6
  module Sidekiq
6
7
  class BasicFetch
8
+ include Sidekiq::Component
7
9
  # We want the fetch operation to timeout every few seconds so the thread
8
10
  # can check if the process is shutting down.
9
11
  TIMEOUT = 2
10
12
 
11
- UnitOfWork = Struct.new(:queue, :job) {
13
+ UnitOfWork = Struct.new(:queue, :job, :config) {
12
14
  def acknowledge
13
15
  # nothing to do
14
16
  end
@@ -18,15 +20,17 @@ module Sidekiq
18
20
  end
19
21
 
20
22
  def requeue
21
- Sidekiq.redis do |conn|
23
+ config.redis do |conn|
22
24
  conn.rpush(queue, job)
23
25
  end
24
26
  end
25
27
  }
26
28
 
27
- def initialize(options)
28
- @strictly_ordered_queues = !!options[:strict]
29
- @queues = options[:queues].map { |q| "queue:#{q}" }
29
+ def initialize(config)
30
+ raise ArgumentError, "missing queue list" unless config[:queues]
31
+ @config = config
32
+ @strictly_ordered_queues = !!@config[:strict]
33
+ @queues = @config[:queues].map { |q| "queue:#{q}" }
30
34
  if @strictly_ordered_queues
31
35
  @queues.uniq!
32
36
  @queues << TIMEOUT
@@ -34,47 +38,54 @@ module Sidekiq
34
38
  end
35
39
 
36
40
  def retrieve_work
37
- work = Sidekiq.redis { |conn| conn.brpop(*queues_cmd) }
38
- UnitOfWork.new(*work) if work
39
- end
40
-
41
- # Creating the Redis#brpop command takes into account any
42
- # configured queue weights. By default Redis#brpop returns
43
- # data from the first queue that has pending elements. We
44
- # recreate the queue command each time we invoke Redis#brpop
45
- # to honor weights and avoid queue starvation.
46
- def queues_cmd
47
- if @strictly_ordered_queues
48
- @queues
49
- else
50
- queues = @queues.shuffle!.uniq
51
- queues << TIMEOUT
52
- queues
41
+ qs = queues_cmd
42
+ # 4825 Sidekiq Pro with all queues paused will return an
43
+ # empty set of queues with a trailing TIMEOUT value.
44
+ if qs.size <= 1
45
+ sleep(TIMEOUT)
46
+ return nil
53
47
  end
48
+
49
+ queue, job = redis { |conn| conn.brpop(*qs) }
50
+ UnitOfWork.new(queue, job, config) if queue
54
51
  end
55
52
 
56
- # By leaving this as a class method, it can be pluggable and used by the Manager actor. Making it
57
- # an instance method will make it async to the Fetcher actor
58
- def self.bulk_requeue(inprogress, options)
53
+ def bulk_requeue(inprogress, options)
59
54
  return if inprogress.empty?
60
55
 
61
- Sidekiq.logger.debug { "Re-queueing terminated jobs" }
56
+ logger.debug { "Re-queueing terminated jobs" }
62
57
  jobs_to_requeue = {}
63
58
  inprogress.each do |unit_of_work|
64
59
  jobs_to_requeue[unit_of_work.queue] ||= []
65
60
  jobs_to_requeue[unit_of_work.queue] << unit_of_work.job
66
61
  end
67
62
 
68
- Sidekiq.redis do |conn|
69
- conn.pipelined do
63
+ redis do |conn|
64
+ conn.pipelined do |pipeline|
70
65
  jobs_to_requeue.each do |queue, jobs|
71
- conn.rpush(queue, jobs)
66
+ pipeline.rpush(queue, jobs)
72
67
  end
73
68
  end
74
69
  end
75
- Sidekiq.logger.info("Pushed #{inprogress.size} jobs back to Redis")
70
+ logger.info("Pushed #{inprogress.size} jobs back to Redis")
76
71
  rescue => ex
77
- Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
72
+ logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
73
+ end
74
+
75
+ # Creating the Redis#brpop command takes into account any
76
+ # configured queue weights. By default Redis#brpop returns
77
+ # data from the first queue that has pending elements. We
78
+ # recreate the queue command each time we invoke Redis#brpop
79
+ # to honor weights and avoid queue starvation.
80
+ def queues_cmd
81
+ if @strictly_ordered_queues
82
+ @queues
83
+ else
84
+ permute = @queues.shuffle
85
+ permute.uniq!
86
+ permute << TIMEOUT
87
+ permute
88
+ end
78
89
  end
79
90
  end
80
91
  end
@@ -0,0 +1,13 @@
1
+ require "sidekiq/worker"
2
+
3
+ module Sidekiq
4
+ # Sidekiq::Job is a new alias for Sidekiq::Worker as of Sidekiq 6.3.0.
5
+ # Use `include Sidekiq::Job` rather than `include Sidekiq::Worker`.
6
+ #
7
+ # The term "worker" is too generic and overly confusing, used in several
8
+ # different contexts meaning different things. Many people call a Sidekiq
9
+ # process a "worker". Some people call the thread that executes jobs a
10
+ # "worker". This change brings Sidekiq closer to ActiveJob where your job
11
+ # classes extend ApplicationJob.
12
+ Job = Worker
13
+ end
@@ -12,46 +12,34 @@ module Sidekiq
12
12
 
13
13
  yield
14
14
 
15
- with_elapsed_time_context(start) do
16
- @logger.info("done")
17
- end
15
+ Sidekiq::Context.add(:elapsed, elapsed(start))
16
+ @logger.info("done")
18
17
  rescue Exception
19
- with_elapsed_time_context(start) do
20
- @logger.info("fail")
21
- end
18
+ Sidekiq::Context.add(:elapsed, elapsed(start))
19
+ @logger.info("fail")
22
20
 
23
21
  raise
24
22
  end
25
23
 
26
24
  def prepare(job_hash, &block)
27
- level = job_hash["log_level"]
28
- if level
29
- @logger.log_at(level) do
30
- Sidekiq::Context.with(job_hash_context(job_hash), &block)
31
- end
32
- else
33
- Sidekiq::Context.with(job_hash_context(job_hash), &block)
34
- end
35
- end
36
-
37
- def job_hash_context(job_hash)
38
25
  # If we're using a wrapper class, like ActiveJob, use the "wrapped"
39
26
  # attribute to expose the underlying thing.
40
27
  h = {
41
- class: job_hash["wrapped"] || job_hash["class"],
28
+ class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
42
29
  jid: job_hash["jid"]
43
30
  }
44
- h[:bid] = job_hash["bid"] if job_hash["bid"]
45
- h[:tags] = job_hash["tags"] if job_hash["tags"]
46
- h
47
- end
48
-
49
- def with_elapsed_time_context(start, &block)
50
- Sidekiq::Context.with(elapsed_time_context(start), &block)
51
- end
31
+ h[:bid] = job_hash["bid"] if job_hash.has_key?("bid")
32
+ h[:tags] = job_hash["tags"] if job_hash.has_key?("tags")
52
33
 
53
- def elapsed_time_context(start)
54
- {elapsed: elapsed(start).to_s}
34
+ Thread.current[:sidekiq_context] = h
35
+ level = job_hash["log_level"]
36
+ if level
37
+ @logger.log_at(level, &block)
38
+ else
39
+ yield
40
+ end
41
+ ensure
42
+ Thread.current[:sidekiq_context] = nil
55
43
  end
56
44
 
57
45
  private
@@ -25,18 +25,19 @@ module Sidekiq
25
25
  #
26
26
  # A job looks like:
27
27
  #
28
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
28
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => true }
29
29
  #
30
30
  # The 'retry' option also accepts a number (in place of 'true'):
31
31
  #
32
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
32
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => 5 }
33
33
  #
34
34
  # The job will be retried this number of times before giving up. (If simply
35
35
  # 'true', Sidekiq retries 25 times)
36
36
  #
37
- # We'll add a bit more data to the job to support retries:
37
+ # Relevant options for job retries:
38
38
  #
39
- # * 'queue' - the queue to use
39
+ # * 'queue' - the queue for the initial job
40
+ # * 'retry_queue' - if job retries should be pushed to a different (e.g. lower priority) queue
40
41
  # * 'retry_count' - number of times we've retried so far.
41
42
  # * 'error_message' - the message from the exception
42
43
  # * 'error_class' - the exception class
@@ -52,28 +53,31 @@ module Sidekiq
52
53
  #
53
54
  # Sidekiq.options[:max_retries] = 7
54
55
  #
55
- # or limit the number of retries for a particular worker with:
56
+ # or limit the number of retries for a particular job and send retries to
57
+ # a low priority queue with:
56
58
  #
57
- # class MyWorker
58
- # include Sidekiq::Worker
59
- # sidekiq_options :retry => 10
59
+ # class MyJob
60
+ # include Sidekiq::Job
61
+ # sidekiq_options retry: 10, retry_queue: 'low'
60
62
  # end
61
63
  #
62
64
  class JobRetry
63
65
  class Handled < ::RuntimeError; end
66
+
64
67
  class Skip < Handled; end
65
68
 
66
- include Sidekiq::Util
69
+ include Sidekiq::Component
67
70
 
68
71
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
69
72
 
70
- def initialize(options = {})
71
- @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
73
+ def initialize(options)
74
+ @config = options
75
+ @max_retries = @config[:max_retries] || DEFAULT_MAX_RETRY_ATTEMPTS
72
76
  end
73
77
 
74
78
  # The global retry handler requires only the barest of data.
75
79
  # We want to be able to retry as much as possible so we don't
76
- # require the worker to be instantiated.
80
+ # require the job to be instantiated.
77
81
  def global(jobstr, queue)
78
82
  yield
79
83
  rescue Handled => ex
@@ -100,14 +104,14 @@ module Sidekiq
100
104
  end
101
105
 
102
106
  # The local retry support means that any errors that occur within
103
- # this block can be associated with the given worker instance.
107
+ # this block can be associated with the given job instance.
104
108
  # This is required to support the `sidekiq_retries_exhausted` block.
105
109
  #
106
110
  # Note that any exception from the block is wrapped in the Skip
107
111
  # exception so the global block does not reprocess the error. The
108
112
  # Skip exception is unwrapped within Sidekiq::Processor#process before
109
113
  # calling the handle_exception handlers.
110
- def local(worker, jobstr, queue)
114
+ def local(jobinst, jobstr, queue)
111
115
  yield
112
116
  rescue Handled => ex
113
117
  raise ex
@@ -120,11 +124,11 @@ module Sidekiq
120
124
 
121
125
  msg = Sidekiq.load_json(jobstr)
122
126
  if msg["retry"].nil?
123
- msg["retry"] = worker.class.get_sidekiq_options["retry"]
127
+ msg["retry"] = jobinst.class.get_sidekiq_options["retry"]
124
128
  end
125
129
 
126
130
  raise e unless msg["retry"]
127
- attempt_retry(worker, msg, queue, e)
131
+ attempt_retry(jobinst, msg, queue, e)
128
132
  # We've handled this error associated with this job, don't
129
133
  # need to handle it at the global level
130
134
  raise Skip
@@ -132,10 +136,10 @@ module Sidekiq
132
136
 
133
137
  private
134
138
 
135
- # Note that +worker+ can be nil here if an error is raised before we can
136
- # instantiate the worker instance. All access must be guarded and
139
+ # Note that +jobinst+ can be nil here if an error is raised before we can
140
+ # instantiate the job instance. All access must be guarded and
137
141
  # best effort.
138
- def attempt_retry(worker, msg, queue, exception)
142
+ def attempt_retry(jobinst, msg, queue, exception)
139
143
  max_retry_attempts = retry_attempts_from(msg["retry"], @max_retries)
140
144
 
141
145
  msg["queue"] = (msg["retry_queue"] || queue)
@@ -167,7 +171,7 @@ module Sidekiq
167
171
  end
168
172
 
169
173
  if count < max_retry_attempts
170
- delay = delay_for(worker, count, exception)
174
+ delay = delay_for(jobinst, count, exception)
171
175
  # Logging here can break retries if the logging device raises ENOSPC #3979
172
176
  # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
173
177
  retry_at = Time.now.to_f + delay
@@ -177,13 +181,13 @@ module Sidekiq
177
181
  end
178
182
  else
179
183
  # Goodbye dear message, you (re)tried your best I'm sure.
180
- retries_exhausted(worker, msg, exception)
184
+ retries_exhausted(jobinst, msg, exception)
181
185
  end
182
186
  end
183
187
 
184
- def retries_exhausted(worker, msg, exception)
188
+ def retries_exhausted(jobinst, msg, exception)
185
189
  begin
186
- block = worker&.sidekiq_retries_exhausted_block
190
+ block = jobinst&.sidekiq_retries_exhausted_block
187
191
  block&.call(msg, exception)
188
192
  rescue => e
189
193
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
@@ -212,23 +216,19 @@ module Sidekiq
212
216
  end
213
217
  end
214
218
 
215
- def delay_for(worker, count, exception)
216
- if worker&.sidekiq_retry_in_block
217
- custom_retry_in = retry_in(worker, count, exception).to_i
218
- return custom_retry_in if custom_retry_in > 0
219
+ def delay_for(jobinst, count, exception)
220
+ jitter = rand(10) * (count + 1)
221
+ if jobinst&.sidekiq_retry_in_block
222
+ custom_retry_in = retry_in(jobinst, count, exception).to_i
223
+ return custom_retry_in + jitter if custom_retry_in > 0
219
224
  end
220
- seconds_to_delay(count)
221
- end
222
-
223
- # delayed_job uses the same basic formula
224
- def seconds_to_delay(count)
225
- (count**4) + 15 + (rand(30) * (count + 1))
225
+ (count**4) + 15 + jitter
226
226
  end
227
227
 
228
- def retry_in(worker, count, exception)
229
- worker.sidekiq_retry_in_block.call(count, exception)
228
+ def retry_in(jobinst, count, exception)
229
+ jobinst.sidekiq_retry_in_block.call(count, exception)
230
230
  rescue Exception => e
231
- handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default"})
231
+ handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{jobinst.class.name}, falling back to default"})
232
232
  nil
233
233
  end
234
234
 
@@ -0,0 +1,71 @@
1
+ require "securerandom"
2
+ require "time"
3
+
4
+ module Sidekiq
5
+ module JobUtil
6
+ # These functions encapsulate various job utilities.
7
+
8
+ TRANSIENT_ATTRIBUTES = %w[]
9
+
10
+ def validate(item)
11
+ raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
12
+ raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array)
13
+ raise(ArgumentError, "Job class must be either a Class or String representation of the class name: `#{item}`") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
14
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
15
+ raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
16
+ end
17
+
18
+ def verify_json(item)
19
+ job_class = item["wrapped"] || item["class"]
20
+ if Sidekiq[:on_complex_arguments] == :raise
21
+ msg = <<~EOM
22
+ Job arguments to #{job_class} must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices.
23
+ To disable this error, remove `Sidekiq.strict_args!` from your initializer.
24
+ EOM
25
+ raise(ArgumentError, msg) unless json_safe?(item)
26
+ elsif Sidekiq[:on_complex_arguments] == :warn
27
+ Sidekiq.logger.warn <<~EOM unless json_safe?(item)
28
+ Job arguments to #{job_class} do not serialize to JSON safely. This will raise an error in
29
+ Sidekiq 7.0. See https://github.com/mperham/sidekiq/wiki/Best-Practices or raise an error today
30
+ by calling `Sidekiq.strict_args!` during Sidekiq initialization.
31
+ EOM
32
+ end
33
+ end
34
+
35
+ def normalize_item(item)
36
+ validate(item)
37
+
38
+ # merge in the default sidekiq_options for the item's class and/or wrapped element
39
+ # this allows ActiveJobs to control sidekiq_options too.
40
+ defaults = normalized_hash(item["class"])
41
+ defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?(:get_sidekiq_options)
42
+ item = defaults.merge(item)
43
+
44
+ raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
45
+
46
+ # remove job attributes which aren't necessary to persist into Redis
47
+ TRANSIENT_ATTRIBUTES.each { |key| item.delete(key) }
48
+
49
+ item["jid"] ||= SecureRandom.hex(12)
50
+ item["class"] = item["class"].to_s
51
+ item["queue"] = item["queue"].to_s
52
+ item["created_at"] ||= Time.now.to_f
53
+ item
54
+ end
55
+
56
+ def normalized_hash(item_class)
57
+ if item_class.is_a?(Class)
58
+ raise(ArgumentError, "Message must include a Sidekiq::Job class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
59
+ item_class.get_sidekiq_options
60
+ else
61
+ Sidekiq.default_job_options
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def json_safe?(item)
68
+ JSON.parse(JSON.dump(item["args"])) == item["args"]
69
+ end
70
+ end
71
+ end