sidekiq 5.0.0 → 6.0.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 (79) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +61 -0
  3. data/.github/issue_template.md +3 -1
  4. data/.gitignore +1 -1
  5. data/.standard.yml +20 -0
  6. data/6.0-Upgrade.md +70 -0
  7. data/COMM-LICENSE +12 -10
  8. data/Changes.md +169 -1
  9. data/Ent-2.0-Upgrade.md +37 -0
  10. data/Ent-Changes.md +76 -0
  11. data/Gemfile +16 -21
  12. data/Gemfile.lock +196 -0
  13. data/LICENSE +1 -1
  14. data/Pro-4.0-Upgrade.md +35 -0
  15. data/Pro-5.0-Upgrade.md +25 -0
  16. data/Pro-Changes.md +137 -1
  17. data/README.md +18 -30
  18. data/Rakefile +6 -8
  19. data/bin/sidekiqload +28 -24
  20. data/bin/sidekiqmon +9 -0
  21. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  22. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  23. data/lib/generators/sidekiq/worker_generator.rb +12 -14
  24. data/lib/sidekiq.rb +69 -49
  25. data/lib/sidekiq/api.rb +216 -160
  26. data/lib/sidekiq/cli.rb +174 -207
  27. data/lib/sidekiq/client.rb +55 -51
  28. data/lib/sidekiq/delay.rb +24 -4
  29. data/lib/sidekiq/exception_handler.rb +12 -16
  30. data/lib/sidekiq/extensions/action_mailer.rb +10 -20
  31. data/lib/sidekiq/extensions/active_record.rb +9 -7
  32. data/lib/sidekiq/extensions/class_methods.rb +9 -7
  33. data/lib/sidekiq/extensions/generic_proxy.rb +4 -4
  34. data/lib/sidekiq/fetch.rb +5 -6
  35. data/lib/sidekiq/job_logger.rb +42 -14
  36. data/lib/sidekiq/job_retry.rb +71 -57
  37. data/lib/sidekiq/launcher.rb +74 -60
  38. data/lib/sidekiq/logger.rb +69 -0
  39. data/lib/sidekiq/manager.rb +12 -15
  40. data/lib/sidekiq/middleware/chain.rb +3 -2
  41. data/lib/sidekiq/middleware/i18n.rb +5 -7
  42. data/lib/sidekiq/monitor.rb +148 -0
  43. data/lib/sidekiq/paginator.rb +11 -12
  44. data/lib/sidekiq/processor.rb +126 -82
  45. data/lib/sidekiq/rails.rb +24 -32
  46. data/lib/sidekiq/redis_connection.rb +46 -14
  47. data/lib/sidekiq/scheduled.rb +50 -25
  48. data/lib/sidekiq/testing.rb +35 -27
  49. data/lib/sidekiq/testing/inline.rb +2 -1
  50. data/lib/sidekiq/util.rb +20 -14
  51. data/lib/sidekiq/version.rb +2 -1
  52. data/lib/sidekiq/web.rb +45 -53
  53. data/lib/sidekiq/web/action.rb +14 -10
  54. data/lib/sidekiq/web/application.rb +83 -58
  55. data/lib/sidekiq/web/helpers.rb +105 -67
  56. data/lib/sidekiq/web/router.rb +18 -15
  57. data/lib/sidekiq/worker.rb +144 -41
  58. data/sidekiq.gemspec +16 -27
  59. data/web/assets/javascripts/application.js +0 -0
  60. data/web/assets/javascripts/dashboard.js +21 -23
  61. data/web/assets/stylesheets/application.css +35 -2
  62. data/web/assets/stylesheets/bootstrap.css +2 -2
  63. data/web/locales/ar.yml +1 -0
  64. data/web/locales/en.yml +2 -0
  65. data/web/locales/es.yml +4 -3
  66. data/web/locales/ja.yml +7 -4
  67. data/web/views/_footer.erb +4 -1
  68. data/web/views/_nav.erb +3 -17
  69. data/web/views/busy.erb +5 -1
  70. data/web/views/layout.erb +1 -1
  71. data/web/views/queue.erb +1 -0
  72. data/web/views/queues.erb +2 -0
  73. data/web/views/retries.erb +4 -0
  74. metadata +25 -171
  75. data/.travis.yml +0 -18
  76. data/bin/sidekiqctl +0 -99
  77. data/lib/sidekiq/core_ext.rb +0 -119
  78. data/lib/sidekiq/logging.rb +0 -106
  79. data/lib/sidekiq/middleware/server/active_record.rb +0 -22
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "time"
5
+
6
+ module Sidekiq
7
+ class Logger < ::Logger
8
+ def initialize(*args)
9
+ super
10
+
11
+ self.formatter = Sidekiq.log_formatter
12
+ end
13
+
14
+ def with_context(hash)
15
+ ctx.merge!(hash)
16
+ yield
17
+ ensure
18
+ hash.keys.each { |key| ctx.delete(key) }
19
+ end
20
+
21
+ def ctx
22
+ Thread.current[:sidekiq_context] ||= {}
23
+ end
24
+
25
+ module Formatters
26
+ class Base < ::Logger::Formatter
27
+ def tid
28
+ Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
29
+ end
30
+
31
+ def ctx
32
+ Thread.current[:sidekiq_context] ||= {}
33
+ end
34
+
35
+ def format_context
36
+ " " + ctx.compact.map { |k, v| "#{k}=#{v}" }.join(" ") if ctx.any?
37
+ end
38
+ end
39
+
40
+ class Pretty < Base
41
+ def call(severity, time, program_name, message)
42
+ "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
43
+ end
44
+ end
45
+
46
+ class WithoutTimestamp < Pretty
47
+ def call(severity, time, program_name, message)
48
+ "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
49
+ end
50
+ end
51
+
52
+ class JSON < Base
53
+ def call(severity, time, program_name, message)
54
+ hash = {
55
+ ts: time.utc.iso8601(3),
56
+ pid: ::Process.pid,
57
+ tid: tid,
58
+ lvl: severity,
59
+ msg: message,
60
+ }
61
+ c = ctx
62
+ hash["ctx"] = c unless c.empty?
63
+
64
+ Sidekiq.dump_json(hash) << "\n"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,13 +1,11 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
- require 'sidekiq/util'
4
- require 'sidekiq/processor'
5
- require 'sidekiq/fetch'
6
- require 'thread'
7
- require 'set'
8
2
 
9
- module Sidekiq
3
+ require "sidekiq/util"
4
+ require "sidekiq/processor"
5
+ require "sidekiq/fetch"
6
+ require "set"
10
7
 
8
+ module Sidekiq
11
9
  ##
12
10
  # The Manager is the central coordination point in Sidekiq, controlling
13
11
  # the lifecycle of the Processors.
@@ -28,10 +26,10 @@ module Sidekiq
28
26
  attr_reader :workers
29
27
  attr_reader :options
30
28
 
31
- def initialize(options={})
29
+ def initialize(options = {})
32
30
  logger.debug { options.inspect }
33
31
  @options = options
34
- @count = options[:concurrency] || 25
32
+ @count = options[:concurrency] || 10
35
33
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
36
34
 
37
35
  @done = false
@@ -54,7 +52,7 @@ module Sidekiq
54
52
 
55
53
  logger.info { "Terminating quiet workers" }
56
54
  @workers.each { |x| x.terminate }
57
- fire_event(:quiet, true)
55
+ fire_event(:quiet, reverse: true)
58
56
  end
59
57
 
60
58
  # hack for quicker development / testing environment #2774
@@ -62,7 +60,7 @@ module Sidekiq
62
60
 
63
61
  def stop(deadline)
64
62
  quiet
65
- fire_event(:shutdown, true)
63
+ fire_event(:shutdown, reverse: true)
66
64
 
67
65
  # some of the shutdown events can be async,
68
66
  # we don't have any way to know when they're done but
@@ -71,11 +69,11 @@ module Sidekiq
71
69
  return if @workers.empty?
72
70
 
73
71
  logger.info { "Pausing to allow workers to finish..." }
74
- remaining = deadline - Time.now
72
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
75
73
  while remaining > PAUSE_TIME
76
74
  return if @workers.empty?
77
75
  sleep PAUSE_TIME
78
- remaining = deadline - Time.now
76
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
79
77
  end
80
78
  return if @workers.empty?
81
79
 
@@ -114,7 +112,7 @@ module Sidekiq
114
112
  end
115
113
 
116
114
  if cleanup.size > 0
117
- jobs = cleanup.map {|p| p.job }.compact
115
+ jobs = cleanup.map { |p| p.job }.compact
118
116
 
119
117
  logger.warn { "Terminating #{cleanup.size} busy worker threads" }
120
118
  logger.warn { "Work still in progress #{jobs.inspect}" }
@@ -133,6 +131,5 @@ module Sidekiq
133
131
  processor.kill
134
132
  end
135
133
  end
136
-
137
134
  end
138
135
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Sidekiq
3
4
  # Middleware is code configured to run before/after
4
5
  # a message is processed. It is patterned after Rack
@@ -106,7 +107,7 @@ module Sidekiq
106
107
  i = entries.index { |entry| entry.klass == newklass }
107
108
  new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
108
109
  i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
109
- entries.insert(i+1, new_entry)
110
+ entries.insert(i + 1, new_entry)
110
111
  end
111
112
 
112
113
  def exists?(klass)
@@ -139,7 +140,7 @@ module Sidekiq
139
140
 
140
141
  def initialize(klass, *args)
141
142
  @klass = klass
142
- @args = args
143
+ @args = args
143
144
  end
144
145
 
145
146
  def make_new
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # Simple middleware to save the current locale and restore it when the job executes.
4
5
  # Use it by requiring it in your initializer:
@@ -9,19 +10,16 @@ module Sidekiq::Middleware::I18n
9
10
  # Get the current locale and store it in the message
10
11
  # to be sent to Sidekiq.
11
12
  class Client
12
- def call(worker_class, msg, queue, redis_pool)
13
- msg['locale'] ||= I18n.locale
13
+ def call(_worker, msg, _queue, _redis)
14
+ msg["locale"] ||= I18n.locale
14
15
  yield
15
16
  end
16
17
  end
17
18
 
18
19
  # Pull the msg locale out and set the current thread to use it.
19
20
  class Server
20
- def call(worker, msg, queue)
21
- I18n.locale = msg['locale'] || I18n.default_locale
22
- yield
23
- ensure
24
- I18n.locale = I18n.default_locale
21
+ def call(_worker, msg, _queue, &block)
22
+ I18n.with_locale(msg.fetch("locale", I18n.default_locale), &block)
25
23
  end
26
24
  end
27
25
  end
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "fileutils"
4
+ require "sidekiq/api"
5
+
6
+ class Sidekiq::Monitor
7
+ CMD = File.basename($PROGRAM_NAME)
8
+
9
+ attr_reader :stage
10
+
11
+ def self.print_usage
12
+ puts "#{CMD} - monitor Sidekiq from the command line."
13
+ puts
14
+ puts "Usage: #{CMD} status <section>"
15
+ puts
16
+ puts " <section> (optional) view a specific section of the status output"
17
+ puts " Valid sections are: #{Sidekiq::Monitor::Status::VALID_SECTIONS.join(", ")}"
18
+ puts
19
+ puts "Set REDIS_URL to the location of your Redis server if not monitoring localhost."
20
+ end
21
+
22
+ class Status
23
+ VALID_SECTIONS = %w[all version overview processes queues]
24
+ COL_PAD = 2
25
+
26
+ def display(section = nil)
27
+ section ||= "all"
28
+ unless VALID_SECTIONS.include? section
29
+ puts "I don't know how to check the status of '#{section}'!"
30
+ puts "Try one of these: #{VALID_SECTIONS.join(", ")}"
31
+ return
32
+ end
33
+ send(section)
34
+ rescue => e
35
+ puts "Couldn't get status: #{e}"
36
+ end
37
+
38
+ def all
39
+ version
40
+ puts
41
+ overview
42
+ puts
43
+ processes
44
+ puts
45
+ queues
46
+ end
47
+
48
+ def version
49
+ puts "Sidekiq #{Sidekiq::VERSION}"
50
+ puts Time.now
51
+ end
52
+
53
+ def overview
54
+ puts "---- Overview ----"
55
+ puts " Processed: #{delimit stats.processed}"
56
+ puts " Failed: #{delimit stats.failed}"
57
+ puts " Busy: #{delimit stats.workers_size}"
58
+ puts " Enqueued: #{delimit stats.enqueued}"
59
+ puts " Retries: #{delimit stats.retry_size}"
60
+ puts " Scheduled: #{delimit stats.scheduled_size}"
61
+ puts " Dead: #{delimit stats.dead_size}"
62
+ end
63
+
64
+ def processes
65
+ puts "---- Processes (#{process_set.size}) ----"
66
+ process_set.each_with_index do |process, index|
67
+ puts "#{process["identity"]} #{tags_for(process)}"
68
+ puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})"
69
+ puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)"
70
+ puts " Queues: #{split_multiline(process["queues"].sort, pad: 11)}"
71
+ puts "" unless (index + 1) == process_set.size
72
+ end
73
+ end
74
+
75
+ def queues
76
+ puts "---- Queues (#{queue_data.size}) ----"
77
+ columns = {
78
+ name: [:ljust, (["name"] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
79
+ size: [:rjust, (["size"] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
80
+ latency: [:rjust, (["latency"] + queue_data.map(&:latency)).map(&:length).max + COL_PAD],
81
+ }
82
+ columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
83
+ puts
84
+ queue_data.each do |q|
85
+ columns.each do |col, (dir, width)|
86
+ print q.send(col).public_send(dir, width)
87
+ end
88
+ puts
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def delimit(number)
95
+ number.to_s.reverse.scan(/.{1,3}/).join(",").reverse
96
+ end
97
+
98
+ def split_multiline(values, opts = {})
99
+ return "none" unless values
100
+ pad = opts[:pad] || 0
101
+ max_length = opts[:max_length] || (80 - pad)
102
+ out = []
103
+ line = ""
104
+ values.each do |value|
105
+ if (line.length + value.length) > max_length
106
+ out << line
107
+ line = " " * pad
108
+ end
109
+ line << value + ", "
110
+ end
111
+ out << line[0..-3]
112
+ out.join("\n")
113
+ end
114
+
115
+ def tags_for(process)
116
+ tags = [
117
+ process["tag"],
118
+ process["labels"],
119
+ (process["quiet"] == "true" ? "quiet" : nil),
120
+ ].flatten.compact
121
+ tags.any? ? "[#{tags.join("] [")}]" : nil
122
+ end
123
+
124
+ def time_ago(timestamp)
125
+ seconds = Time.now - Time.at(timestamp)
126
+ return "just now" if seconds < 60
127
+ return "a minute ago" if seconds < 120
128
+ return "#{seconds.floor / 60} minutes ago" if seconds < 3600
129
+ return "an hour ago" if seconds < 7200
130
+ "#{seconds.floor / 60 / 60} hours ago"
131
+ end
132
+
133
+ QUEUE_STRUCT = Struct.new(:name, :size, :latency)
134
+ def queue_data
135
+ @queue_data ||= Sidekiq::Queue.all.map { |q|
136
+ QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf("%#.2f", q.latency))
137
+ }
138
+ end
139
+
140
+ def process_set
141
+ @process_set ||= Sidekiq::ProcessSet.new
142
+ end
143
+
144
+ def stats
145
+ @stats ||= Sidekiq::Stats.new
146
+ end
147
+ end
148
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Sidekiq
3
4
  module Paginator
4
-
5
- def page(key, pageidx=1, page_size=25, opts=nil)
5
+ def page(key, pageidx = 1, page_size = 25, opts = nil)
6
6
  current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
7
7
  pageidx = current_page - 1
8
8
  total_size = 0
@@ -14,30 +14,29 @@ module Sidekiq
14
14
  type = conn.type(key)
15
15
 
16
16
  case type
17
- when 'zset'
17
+ when "zset"
18
18
  rev = opts && opts[:reverse]
19
- total_size, items = conn.multi do
19
+ total_size, items = conn.multi {
20
20
  conn.zcard(key)
21
21
  if rev
22
- conn.zrevrange(key, starting, ending, :with_scores => true)
22
+ conn.zrevrange(key, starting, ending, with_scores: true)
23
23
  else
24
- conn.zrange(key, starting, ending, :with_scores => true)
24
+ conn.zrange(key, starting, ending, with_scores: true)
25
25
  end
26
- end
26
+ }
27
27
  [current_page, total_size, items]
28
- when 'list'
29
- total_size, items = conn.multi do
28
+ when "list"
29
+ total_size, items = conn.multi {
30
30
  conn.llen(key)
31
31
  conn.lrange(key, starting, ending)
32
- end
32
+ }
33
33
  [current_page, total_size, items]
34
- when 'none'
34
+ when "none"
35
35
  [1, 0, []]
36
36
  else
37
37
  raise "can't page a #{type}"
38
38
  end
39
39
  end
40
40
  end
41
-
42
41
  end
43
42
  end
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/util'
3
- require 'sidekiq/fetch'
4
- require 'sidekiq/job_logger'
5
- require 'sidekiq/job_retry'
6
- require 'thread'
7
- require 'concurrent/map'
8
- require 'concurrent/atomic/atomic_fixnum'
2
+
3
+ require "sidekiq/util"
4
+ require "sidekiq/fetch"
5
+ require "sidekiq/job_logger"
6
+ require "sidekiq/job_retry"
9
7
 
10
8
  module Sidekiq
11
9
  ##
@@ -25,7 +23,6 @@ module Sidekiq
25
23
  # to replace itself and exits.
26
24
  #
27
25
  class Processor
28
-
29
26
  include Util
30
27
 
31
28
  attr_reader :thread
@@ -39,19 +36,19 @@ module Sidekiq
39
36
  @thread = nil
40
37
  @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
41
38
  @reloader = Sidekiq.options[:reloader]
42
- @logging = Sidekiq::JobLogger.new
39
+ @job_logger = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
43
40
  @retrier = Sidekiq::JobRetry.new
44
41
  end
45
42
 
46
- def terminate(wait=false)
43
+ def terminate(wait = false)
47
44
  @done = true
48
- return if !@thread
45
+ return unless @thread
49
46
  @thread.value if wait
50
47
  end
51
48
 
52
- def kill(wait=false)
49
+ def kill(wait = false)
53
50
  @done = true
54
- return if !@thread
51
+ return unless @thread
55
52
  # unlike the other actors, terminate does not wait
56
53
  # for the thread to finish because we don't know how
57
54
  # long the job will take to finish. Instead we
@@ -68,16 +65,12 @@ module Sidekiq
68
65
  private unless $TESTING
69
66
 
70
67
  def run
71
- begin
72
- while !@done
73
- process_one
74
- end
75
- @mgr.processor_stopped(self)
76
- rescue Sidekiq::Shutdown
77
- @mgr.processor_stopped(self)
78
- rescue Exception => ex
79
- @mgr.processor_died(self, ex)
80
- end
68
+ process_one until @done
69
+ @mgr.processor_stopped(self)
70
+ rescue Sidekiq::Shutdown
71
+ @mgr.processor_stopped(self)
72
+ rescue Exception => ex
73
+ @mgr.processor_died(self, ex)
81
74
  end
82
75
 
83
76
  def process_one
@@ -87,14 +80,15 @@ module Sidekiq
87
80
  end
88
81
 
89
82
  def get_one
90
- begin
91
- work = @strategy.retrieve_work
92
- (logger.info { "Redis is online, #{Time.now - @down} sec downtime" }; @down = nil) if @down
93
- work
94
- rescue Sidekiq::Shutdown
95
- rescue => ex
96
- handle_fetch_exception(ex)
83
+ work = @strategy.retrieve_work
84
+ if @down
85
+ logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" }
86
+ @down = nil
97
87
  end
88
+ work
89
+ rescue Sidekiq::Shutdown
90
+ rescue => ex
91
+ handle_fetch_exception(ex)
98
92
  end
99
93
 
100
94
  def fetch
@@ -108,12 +102,10 @@ module Sidekiq
108
102
  end
109
103
 
110
104
  def handle_fetch_exception(ex)
111
- if !@down
112
- @down = Time.now
105
+ unless @down
106
+ @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
113
107
  logger.error("Error fetching job: #{ex}")
114
- ex.backtrace.each do |bt|
115
- logger.error(bt)
116
- end
108
+ handle_exception(ex)
117
109
  end
118
110
  sleep(1)
119
111
  nil
@@ -125,24 +117,19 @@ module Sidekiq
125
117
  # job structure to the Web UI
126
118
  pristine = cloned(job_hash)
127
119
 
128
- # If we're using a wrapper class, like ActiveJob, use the "wrapped"
129
- # attribute to expose the underlying thing.
130
- klass = job_hash['wrapped'.freeze] || job_hash["class".freeze]
131
- ctx = "#{klass} JID-#{job_hash['jid'.freeze]}#{" BID-#{job_hash['bid'.freeze]}" if job_hash['bid'.freeze]}"
132
-
133
- Sidekiq::Logging.with_context(ctx) do
134
- @retrier.global(job_hash, queue) do
135
- @logging.call(job_hash, queue) do
120
+ @job_logger.with_job_hash_context(job_hash) do
121
+ @retrier.global(pristine, queue) do
122
+ @job_logger.call(job_hash, queue) do
136
123
  stats(pristine, queue) do
137
124
  # Rails 5 requires a Reloader to wrap code execution. In order to
138
125
  # constantize the worker and instantiate an instance, we have to call
139
126
  # the Reloader. It handles code loading, db connection management, etc.
140
127
  # Effectively this block denotes a "unit of work" to Rails.
141
128
  @reloader.call do
142
- klass = job_hash['class'.freeze].constantize
129
+ klass = constantize(job_hash["class"])
143
130
  worker = klass.new
144
- worker.jid = job_hash['jid'.freeze]
145
- @retrier.local(worker, job_hash, queue) do
131
+ worker.jid = job_hash["jid"]
132
+ @retrier.local(worker, pristine, queue) do
146
133
  yield worker
147
134
  end
148
135
  end
@@ -156,46 +143,48 @@ module Sidekiq
156
143
  jobstr = work.job
157
144
  queue = work.queue_name
158
145
 
159
- ack = false
146
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
147
+ job_hash = nil
160
148
  begin
161
- # Treat malformed JSON as a special case: job goes straight to the morgue.
162
- job_hash = nil
163
- begin
164
- job_hash = Sidekiq.load_json(jobstr)
165
- rescue => ex
166
- handle_exception(ex, { :context => "Invalid JSON for job", :jobstr => jobstr })
167
- send_to_morgue(jobstr)
168
- ack = true
169
- raise
170
- end
149
+ job_hash = Sidekiq.load_json(jobstr)
150
+ rescue => ex
151
+ handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
152
+ # we can't notify because the job isn't a valid hash payload.
153
+ DeadSet.new.kill(jobstr, notify_failure: false)
154
+ return work.acknowledge
155
+ end
171
156
 
172
- ack = true
157
+ ack = false
158
+ begin
173
159
  dispatch(job_hash, queue) do |worker|
174
160
  Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
175
- execute_job(worker, cloned(job_hash['args'.freeze]))
161
+ execute_job(worker, cloned(job_hash["args"]))
176
162
  end
177
163
  end
164
+ ack = true
178
165
  rescue Sidekiq::Shutdown
179
166
  # Had to force kill this job because it didn't finish
180
167
  # within the timeout. Don't acknowledge the work since
181
168
  # we didn't properly finish it.
182
- ack = false
169
+ rescue Sidekiq::JobRetry::Handled => h
170
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
171
+ # signals that we created a retry successfully. We can acknowlege the job.
172
+ ack = true
173
+ e = h.cause || h
174
+ handle_exception(e, {context: "Job raised exception", job: job_hash, jobstr: jobstr})
175
+ raise e
183
176
  rescue Exception => ex
184
- e = ex.is_a?(::Sidekiq::JobRetry::Skip) && ex.cause ? ex.cause : ex
185
- handle_exception(e, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
177
+ # Unexpected error! This is very bad and indicates an exception that got past
178
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
179
+ # so it can be rescued when using Sidekiq Pro.
180
+ handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
186
181
  raise e
187
182
  ensure
188
- work.acknowledge if ack
189
- end
190
- end
191
-
192
- def send_to_morgue(msg)
193
- now = Time.now.to_f
194
- Sidekiq.redis do |conn|
195
- conn.multi do
196
- conn.zadd('dead', now, msg)
197
- conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
198
- conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
183
+ if ack
184
+ # We don't want a shutdown signal to interrupt job acknowledgment.
185
+ Thread.handle_interrupt(Sidekiq::Shutdown => :never) do
186
+ work.acknowledge
187
+ end
199
188
  end
200
189
  end
201
190
  end
@@ -204,26 +193,71 @@ module Sidekiq
204
193
  worker.perform(*cloned_args)
205
194
  end
206
195
 
207
- def thread_identity
208
- @str ||= Thread.current.object_id.to_s(36)
196
+ # Ruby doesn't provide atomic counters out of the box so we'll
197
+ # implement something simple ourselves.
198
+ # https://bugs.ruby-lang.org/issues/14706
199
+ class Counter
200
+ def initialize
201
+ @value = 0
202
+ @lock = Mutex.new
203
+ end
204
+
205
+ def incr(amount = 1)
206
+ @lock.synchronize { @value += amount }
207
+ end
208
+
209
+ def reset
210
+ @lock.synchronize {
211
+ val = @value
212
+ @value = 0
213
+ val
214
+ }
215
+ end
209
216
  end
210
217
 
211
- WORKER_STATE = Concurrent::Map.new
212
- PROCESSED = Concurrent::AtomicFixnum.new
213
- FAILURE = Concurrent::AtomicFixnum.new
218
+ # jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here
219
+ class SharedWorkerState
220
+ def initialize
221
+ @worker_state = {}
222
+ @lock = Mutex.new
223
+ end
224
+
225
+ def set(tid, hash)
226
+ @lock.synchronize { @worker_state[tid] = hash }
227
+ end
228
+
229
+ def delete(tid)
230
+ @lock.synchronize { @worker_state.delete(tid) }
231
+ end
232
+
233
+ def dup
234
+ @lock.synchronize { @worker_state.dup }
235
+ end
236
+
237
+ def size
238
+ @lock.synchronize { @worker_state.size }
239
+ end
240
+
241
+ def clear
242
+ @lock.synchronize { @worker_state.clear }
243
+ end
244
+ end
245
+
246
+ PROCESSED = Counter.new
247
+ FAILURE = Counter.new
248
+ WORKER_STATE = SharedWorkerState.new
214
249
 
215
250
  def stats(job_hash, queue)
216
- tid = thread_identity
217
- WORKER_STATE[tid] = {:queue => queue, :payload => job_hash, :run_at => Time.now.to_i }
251
+ WORKER_STATE.set(tid, {queue: queue, payload: job_hash, run_at: Time.now.to_i})
218
252
 
219
253
  begin
220
254
  yield
221
255
  rescue Exception
222
- FAILURE.increment
256
+ FAILURE.incr
223
257
  raise
224
258
  ensure
225
259
  WORKER_STATE.delete(tid)
226
- PROCESSED.increment
260
+ PROCESSED.incr
227
261
  end
228
262
  end
229
263
 
@@ -234,5 +268,15 @@ module Sidekiq
234
268
  Marshal.load(Marshal.dump(thing))
235
269
  end
236
270
 
271
+ def constantize(str)
272
+ names = str.split("::")
273
+ names.shift if names.empty? || names.first.empty?
274
+
275
+ names.inject(Object) do |constant, name|
276
+ # the false flag limits search for name to under the constant namespace
277
+ # which mimics Rails' behaviour
278
+ constant.const_defined?(name, false) ? constant.const_get(name, false) : constant.const_missing(name)
279
+ end
280
+ end
237
281
  end
238
282
  end