sidekiq 4.2.10 → 6.1.2

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 (106) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
  3. data/.github/workflows/ci.yml +41 -0
  4. data/.gitignore +2 -1
  5. data/.standard.yml +20 -0
  6. data/5.0-Upgrade.md +56 -0
  7. data/6.0-Upgrade.md +72 -0
  8. data/COMM-LICENSE +12 -10
  9. data/Changes.md +354 -1
  10. data/Ent-2.0-Upgrade.md +37 -0
  11. data/Ent-Changes.md +111 -3
  12. data/Gemfile +16 -21
  13. data/Gemfile.lock +192 -0
  14. data/LICENSE +1 -1
  15. data/Pro-4.0-Upgrade.md +35 -0
  16. data/Pro-5.0-Upgrade.md +25 -0
  17. data/Pro-Changes.md +181 -4
  18. data/README.md +19 -33
  19. data/Rakefile +6 -8
  20. data/bin/sidekiq +26 -2
  21. data/bin/sidekiqload +37 -34
  22. data/bin/sidekiqmon +8 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  25. data/lib/generators/sidekiq/worker_generator.rb +21 -13
  26. data/lib/sidekiq.rb +86 -61
  27. data/lib/sidekiq/api.rb +320 -209
  28. data/lib/sidekiq/cli.rb +207 -217
  29. data/lib/sidekiq/client.rb +78 -51
  30. data/lib/sidekiq/delay.rb +41 -0
  31. data/lib/sidekiq/exception_handler.rb +12 -16
  32. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  33. data/lib/sidekiq/extensions/active_record.rb +13 -10
  34. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  35. data/lib/sidekiq/extensions/generic_proxy.rb +10 -4
  36. data/lib/sidekiq/fetch.rb +29 -30
  37. data/lib/sidekiq/job_logger.rb +63 -0
  38. data/lib/sidekiq/job_retry.rb +262 -0
  39. data/lib/sidekiq/launcher.rb +102 -69
  40. data/lib/sidekiq/logger.rb +165 -0
  41. data/lib/sidekiq/manager.rb +16 -19
  42. data/lib/sidekiq/middleware/chain.rb +15 -5
  43. data/lib/sidekiq/middleware/i18n.rb +5 -7
  44. data/lib/sidekiq/monitor.rb +133 -0
  45. data/lib/sidekiq/paginator.rb +18 -14
  46. data/lib/sidekiq/processor.rb +161 -82
  47. data/lib/sidekiq/rails.rb +27 -100
  48. data/lib/sidekiq/redis_connection.rb +60 -20
  49. data/lib/sidekiq/scheduled.rb +61 -35
  50. data/lib/sidekiq/sd_notify.rb +149 -0
  51. data/lib/sidekiq/systemd.rb +24 -0
  52. data/lib/sidekiq/testing.rb +48 -28
  53. data/lib/sidekiq/testing/inline.rb +2 -1
  54. data/lib/sidekiq/util.rb +20 -16
  55. data/lib/sidekiq/version.rb +2 -1
  56. data/lib/sidekiq/web.rb +57 -57
  57. data/lib/sidekiq/web/action.rb +14 -14
  58. data/lib/sidekiq/web/application.rb +103 -84
  59. data/lib/sidekiq/web/csrf_protection.rb +158 -0
  60. data/lib/sidekiq/web/helpers.rb +126 -71
  61. data/lib/sidekiq/web/router.rb +18 -17
  62. data/lib/sidekiq/worker.rb +164 -41
  63. data/sidekiq.gemspec +15 -27
  64. data/web/assets/javascripts/application.js +25 -27
  65. data/web/assets/javascripts/dashboard.js +33 -37
  66. data/web/assets/stylesheets/application-dark.css +143 -0
  67. data/web/assets/stylesheets/application-rtl.css +246 -0
  68. data/web/assets/stylesheets/application.css +385 -10
  69. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  70. data/web/assets/stylesheets/bootstrap.css +2 -2
  71. data/web/locales/ar.yml +81 -0
  72. data/web/locales/de.yml +14 -2
  73. data/web/locales/en.yml +4 -0
  74. data/web/locales/es.yml +4 -3
  75. data/web/locales/fa.yml +1 -0
  76. data/web/locales/fr.yml +2 -2
  77. data/web/locales/he.yml +79 -0
  78. data/web/locales/ja.yml +9 -4
  79. data/web/locales/lt.yml +83 -0
  80. data/web/locales/pl.yml +4 -4
  81. data/web/locales/ru.yml +4 -0
  82. data/web/locales/ur.yml +80 -0
  83. data/web/locales/vi.yml +83 -0
  84. data/web/views/_footer.erb +5 -2
  85. data/web/views/_job_info.erb +2 -1
  86. data/web/views/_nav.erb +4 -18
  87. data/web/views/_paging.erb +1 -1
  88. data/web/views/busy.erb +15 -8
  89. data/web/views/dashboard.erb +1 -1
  90. data/web/views/dead.erb +2 -2
  91. data/web/views/layout.erb +12 -2
  92. data/web/views/morgue.erb +9 -6
  93. data/web/views/queue.erb +18 -8
  94. data/web/views/queues.erb +11 -1
  95. data/web/views/retries.erb +14 -7
  96. data/web/views/retry.erb +2 -2
  97. data/web/views/scheduled.erb +7 -4
  98. metadata +41 -188
  99. data/.github/issue_template.md +0 -9
  100. data/.travis.yml +0 -18
  101. data/bin/sidekiqctl +0 -99
  102. data/lib/sidekiq/core_ext.rb +0 -119
  103. data/lib/sidekiq/logging.rb +0 -106
  104. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  105. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  106. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
@@ -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
@@ -66,7 +67,6 @@ module Sidekiq
66
67
  module Middleware
67
68
  class Chain
68
69
  include Enumerable
69
- attr_reader :entries
70
70
 
71
71
  def initialize_copy(copy)
72
72
  copy.instance_variable_set(:@entries, entries.dup)
@@ -77,10 +77,14 @@ module Sidekiq
77
77
  end
78
78
 
79
79
  def initialize
80
- @entries = []
80
+ @entries = nil
81
81
  yield self if block_given?
82
82
  end
83
83
 
84
+ def entries
85
+ @entries ||= []
86
+ end
87
+
84
88
  def remove(klass)
85
89
  entries.delete_if { |entry| entry.klass == klass }
86
90
  end
@@ -106,13 +110,17 @@ module Sidekiq
106
110
  i = entries.index { |entry| entry.klass == newklass }
107
111
  new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
108
112
  i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
109
- entries.insert(i+1, new_entry)
113
+ entries.insert(i + 1, new_entry)
110
114
  end
111
115
 
112
116
  def exists?(klass)
113
117
  any? { |entry| entry.klass == klass }
114
118
  end
115
119
 
120
+ def empty?
121
+ @entries.nil? || @entries.empty?
122
+ end
123
+
116
124
  def retrieve
117
125
  map(&:make_new)
118
126
  end
@@ -122,8 +130,10 @@ module Sidekiq
122
130
  end
123
131
 
124
132
  def invoke(*args)
133
+ return yield if empty?
134
+
125
135
  chain = retrieve.dup
126
- traverse_chain = lambda do
136
+ traverse_chain = proc do
127
137
  if chain.empty?
128
138
  yield
129
139
  else
@@ -139,7 +149,7 @@ module Sidekiq
139
149
 
140
150
  def initialize(klass, *args)
141
151
  @klass = klass
142
- @args = args
152
+ @args = args
143
153
  end
144
154
 
145
155
  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,133 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "fileutils"
4
+ require "sidekiq/api"
5
+
6
+ class Sidekiq::Monitor
7
+ class Status
8
+ VALID_SECTIONS = %w[all version overview processes queues]
9
+ COL_PAD = 2
10
+
11
+ def display(section = nil)
12
+ section ||= "all"
13
+ unless VALID_SECTIONS.include? section
14
+ puts "I don't know how to check the status of '#{section}'!"
15
+ puts "Try one of these: #{VALID_SECTIONS.join(", ")}"
16
+ return
17
+ end
18
+ send(section)
19
+ rescue => e
20
+ puts "Couldn't get status: #{e}"
21
+ end
22
+
23
+ def all
24
+ version
25
+ puts
26
+ overview
27
+ puts
28
+ processes
29
+ puts
30
+ queues
31
+ end
32
+
33
+ def version
34
+ puts "Sidekiq #{Sidekiq::VERSION}"
35
+ puts Time.now.utc
36
+ end
37
+
38
+ def overview
39
+ puts "---- Overview ----"
40
+ puts " Processed: #{delimit stats.processed}"
41
+ puts " Failed: #{delimit stats.failed}"
42
+ puts " Busy: #{delimit stats.workers_size}"
43
+ puts " Enqueued: #{delimit stats.enqueued}"
44
+ puts " Retries: #{delimit stats.retry_size}"
45
+ puts " Scheduled: #{delimit stats.scheduled_size}"
46
+ puts " Dead: #{delimit stats.dead_size}"
47
+ end
48
+
49
+ def processes
50
+ puts "---- Processes (#{process_set.size}) ----"
51
+ process_set.each_with_index do |process, index|
52
+ puts "#{process["identity"]} #{tags_for(process)}"
53
+ puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})"
54
+ puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)"
55
+ puts " Queues: #{split_multiline(process["queues"].sort, pad: 11)}"
56
+ puts "" unless (index + 1) == process_set.size
57
+ end
58
+ end
59
+
60
+ def queues
61
+ puts "---- Queues (#{queue_data.size}) ----"
62
+ columns = {
63
+ name: [:ljust, (["name"] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
64
+ size: [:rjust, (["size"] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
65
+ latency: [:rjust, (["latency"] + queue_data.map(&:latency)).map(&:length).max + COL_PAD]
66
+ }
67
+ columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
68
+ puts
69
+ queue_data.each do |q|
70
+ columns.each do |col, (dir, width)|
71
+ print q.send(col).public_send(dir, width)
72
+ end
73
+ puts
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def delimit(number)
80
+ number.to_s.reverse.scan(/.{1,3}/).join(",").reverse
81
+ end
82
+
83
+ def split_multiline(values, opts = {})
84
+ return "none" unless values
85
+ pad = opts[:pad] || 0
86
+ max_length = opts[:max_length] || (80 - pad)
87
+ out = []
88
+ line = ""
89
+ values.each do |value|
90
+ if (line.length + value.length) > max_length
91
+ out << line
92
+ line = " " * pad
93
+ end
94
+ line << value + ", "
95
+ end
96
+ out << line[0..-3]
97
+ out.join("\n")
98
+ end
99
+
100
+ def tags_for(process)
101
+ tags = [
102
+ process["tag"],
103
+ process["labels"],
104
+ (process["quiet"] == "true" ? "quiet" : nil)
105
+ ].flatten.compact
106
+ tags.any? ? "[#{tags.join("] [")}]" : nil
107
+ end
108
+
109
+ def time_ago(timestamp)
110
+ seconds = Time.now - Time.at(timestamp)
111
+ return "just now" if seconds < 60
112
+ return "a minute ago" if seconds < 120
113
+ return "#{seconds.floor / 60} minutes ago" if seconds < 3600
114
+ return "an hour ago" if seconds < 7200
115
+ "#{seconds.floor / 60 / 60} hours ago"
116
+ end
117
+
118
+ QUEUE_STRUCT = Struct.new(:name, :size, :latency)
119
+ def queue_data
120
+ @queue_data ||= Sidekiq::Queue.all.map { |q|
121
+ QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf("%#.2f", q.latency))
122
+ }
123
+ end
124
+
125
+ def process_set
126
+ @process_set ||= Sidekiq::ProcessSet.new
127
+ end
128
+
129
+ def stats
130
+ @stats ||= Sidekiq::Stats.new
131
+ end
132
+ end
133
+ 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
@@ -12,32 +12,36 @@ module Sidekiq
12
12
 
13
13
  Sidekiq.redis do |conn|
14
14
  type = conn.type(key)
15
+ rev = opts && opts[:reverse]
15
16
 
16
17
  case type
17
- when 'zset'
18
- rev = opts && opts[:reverse]
19
- total_size, items = conn.multi do
18
+ when "zset"
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
- conn.lrange(key, starting, ending)
32
- end
31
+ if rev
32
+ conn.lrange(key, -ending - 1, -starting - 1)
33
+ else
34
+ conn.lrange(key, starting, ending)
35
+ end
36
+ }
37
+ items.reverse! if rev
33
38
  [current_page, total_size, items]
34
- when 'none'
39
+ when "none"
35
40
  [1, 0, []]
36
41
  else
37
42
  raise "can't page a #{type}"
38
43
  end
39
44
  end
40
45
  end
41
-
42
46
  end
43
47
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/util'
3
- require 'sidekiq/fetch'
4
- require 'thread'
5
- require 'concurrent/map'
6
- require 'concurrent/atomic/atomic_fixnum'
2
+
3
+ require "sidekiq/util"
4
+ require "sidekiq/fetch"
5
+ require "sidekiq/job_logger"
6
+ require "sidekiq/job_retry"
7
7
 
8
8
  module Sidekiq
9
9
  ##
@@ -23,32 +23,32 @@ module Sidekiq
23
23
  # to replace itself and exits.
24
24
  #
25
25
  class Processor
26
-
27
26
  include Util
28
27
 
29
28
  attr_reader :thread
30
29
  attr_reader :job
31
30
 
32
- def initialize(mgr)
31
+ def initialize(mgr, options)
33
32
  @mgr = mgr
34
33
  @down = false
35
34
  @done = false
36
35
  @job = nil
37
36
  @thread = nil
38
- @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
39
- @reloader = Sidekiq.options[:reloader]
40
- @executor = Sidekiq.options[:executor]
37
+ @strategy = options[:fetch]
38
+ @reloader = options[:reloader] || proc { |&block| block.call }
39
+ @job_logger = (options[:job_logger] || Sidekiq::JobLogger).new
40
+ @retrier = Sidekiq::JobRetry.new
41
41
  end
42
42
 
43
- def terminate(wait=false)
43
+ def terminate(wait = false)
44
44
  @done = true
45
- return if !@thread
45
+ return unless @thread
46
46
  @thread.value if wait
47
47
  end
48
48
 
49
- def kill(wait=false)
49
+ def kill(wait = false)
50
50
  @done = true
51
- return if !@thread
51
+ return unless @thread
52
52
  # unlike the other actors, terminate does not wait
53
53
  # for the thread to finish because we don't know how
54
54
  # long the job will take to finish. Instead we
@@ -65,16 +65,12 @@ module Sidekiq
65
65
  private unless $TESTING
66
66
 
67
67
  def run
68
- begin
69
- while !@done
70
- process_one
71
- end
72
- @mgr.processor_stopped(self)
73
- rescue Sidekiq::Shutdown
74
- @mgr.processor_stopped(self)
75
- rescue Exception => ex
76
- @mgr.processor_died(self, ex)
77
- 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)
78
74
  end
79
75
 
80
76
  def process_one
@@ -84,14 +80,15 @@ module Sidekiq
84
80
  end
85
81
 
86
82
  def get_one
87
- begin
88
- work = @strategy.retrieve_work
89
- (logger.info { "Redis is online, #{Time.now - @down} sec downtime" }; @down = nil) if @down
90
- work
91
- rescue Sidekiq::Shutdown
92
- rescue => ex
93
- 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
94
87
  end
88
+ work
89
+ rescue Sidekiq::Shutdown
90
+ rescue => ex
91
+ handle_fetch_exception(ex)
95
92
  end
96
93
 
97
94
  def fetch
@@ -105,97 +102,179 @@ module Sidekiq
105
102
  end
106
103
 
107
104
  def handle_fetch_exception(ex)
108
- if !@down
109
- @down = Time.now
105
+ unless @down
106
+ @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
110
107
  logger.error("Error fetching job: #{ex}")
111
- ex.backtrace.each do |bt|
112
- logger.error(bt)
113
- end
108
+ handle_exception(ex)
114
109
  end
115
110
  sleep(1)
116
111
  nil
117
112
  end
118
113
 
114
+ def dispatch(job_hash, queue, jobstr)
115
+ # since middleware can mutate the job hash
116
+ # we need to clone it to report the original
117
+ # job structure to the Web UI
118
+ # or to push back to redis when retrying.
119
+ # To avoid costly and, most of the time, useless cloning here,
120
+ # we pass original String of JSON to respected methods
121
+ # to re-parse it there if we need access to the original, untouched job
122
+
123
+ @job_logger.prepare(job_hash) do
124
+ @retrier.global(jobstr, queue) do
125
+ @job_logger.call(job_hash, queue) do
126
+ stats(jobstr, queue) do
127
+ # Rails 5 requires a Reloader to wrap code execution. In order to
128
+ # constantize the worker and instantiate an instance, we have to call
129
+ # the Reloader. It handles code loading, db connection management, etc.
130
+ # Effectively this block denotes a "unit of work" to Rails.
131
+ @reloader.call do
132
+ klass = constantize(job_hash["class"])
133
+ worker = klass.new
134
+ worker.jid = job_hash["jid"]
135
+ @retrier.local(worker, jobstr, queue) do
136
+ yield worker
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
119
145
  def process(work)
120
146
  jobstr = work.job
121
147
  queue = work.queue_name
122
148
 
123
- ack = false
149
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
150
+ job_hash = nil
124
151
  begin
125
152
  job_hash = Sidekiq.load_json(jobstr)
126
- @reloader.call do
127
- klass = job_hash['class'.freeze].constantize
128
- worker = klass.new
129
- worker.jid = job_hash['jid'.freeze]
130
-
131
- stats(worker, job_hash, queue) do
132
- Sidekiq::Logging.with_context(log_context(job_hash)) do
133
- ack = true
134
- Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
135
- @executor.call do
136
- # Only ack if we either attempted to start this job or
137
- # successfully completed it. This prevents us from
138
- # losing jobs if a middleware raises an exception before yielding
139
- execute_job(worker, cloned(job_hash['args'.freeze]))
140
- end
141
- end
142
- end
153
+ rescue => ex
154
+ handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
155
+ # we can't notify because the job isn't a valid hash payload.
156
+ DeadSet.new.kill(jobstr, notify_failure: false)
157
+ return work.acknowledge
158
+ end
159
+
160
+ ack = false
161
+ begin
162
+ dispatch(job_hash, queue, jobstr) do |worker|
163
+ Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
164
+ execute_job(worker, job_hash["args"])
143
165
  end
144
- ack = true
145
166
  end
167
+ ack = true
146
168
  rescue Sidekiq::Shutdown
147
169
  # Had to force kill this job because it didn't finish
148
170
  # within the timeout. Don't acknowledge the work since
149
171
  # we didn't properly finish it.
150
- ack = false
172
+ rescue Sidekiq::JobRetry::Handled => h
173
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
174
+ # signals that we created a retry successfully. We can acknowlege the job.
175
+ ack = true
176
+ e = h.cause || h
177
+ handle_exception(e, {context: "Job raised exception", job: job_hash, jobstr: jobstr})
178
+ raise e
151
179
  rescue Exception => ex
152
- handle_exception(ex, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
153
- raise
180
+ # Unexpected error! This is very bad and indicates an exception that got past
181
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
182
+ # so it can be rescued when using Sidekiq Pro.
183
+ handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
184
+ raise ex
154
185
  ensure
155
- work.acknowledge if ack
186
+ if ack
187
+ # We don't want a shutdown signal to interrupt job acknowledgment.
188
+ Thread.handle_interrupt(Sidekiq::Shutdown => :never) do
189
+ work.acknowledge
190
+ end
191
+ end
156
192
  end
157
193
  end
158
194
 
159
- # If we're using a wrapper class, like ActiveJob, use the "wrapped"
160
- # attribute to expose the underlying thing.
161
- def log_context(item)
162
- klass = item['wrapped'.freeze] || item['class'.freeze]
163
- "#{klass} JID-#{item['jid'.freeze]}#{" BID-#{item['bid'.freeze]}" if item['bid'.freeze]}"
164
- end
165
-
166
195
  def execute_job(worker, cloned_args)
167
196
  worker.perform(*cloned_args)
168
197
  end
169
198
 
170
- def thread_identity
171
- @str ||= Thread.current.object_id.to_s(36)
199
+ # Ruby doesn't provide atomic counters out of the box so we'll
200
+ # implement something simple ourselves.
201
+ # https://bugs.ruby-lang.org/issues/14706
202
+ class Counter
203
+ def initialize
204
+ @value = 0
205
+ @lock = Mutex.new
206
+ end
207
+
208
+ def incr(amount = 1)
209
+ @lock.synchronize { @value += amount }
210
+ end
211
+
212
+ def reset
213
+ @lock.synchronize {
214
+ val = @value
215
+ @value = 0
216
+ val
217
+ }
218
+ end
219
+ end
220
+
221
+ # jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here
222
+ class SharedWorkerState
223
+ def initialize
224
+ @worker_state = {}
225
+ @lock = Mutex.new
226
+ end
227
+
228
+ def set(tid, hash)
229
+ @lock.synchronize { @worker_state[tid] = hash }
230
+ end
231
+
232
+ def delete(tid)
233
+ @lock.synchronize { @worker_state.delete(tid) }
234
+ end
235
+
236
+ def dup
237
+ @lock.synchronize { @worker_state.dup }
238
+ end
239
+
240
+ def size
241
+ @lock.synchronize { @worker_state.size }
242
+ end
243
+
244
+ def clear
245
+ @lock.synchronize { @worker_state.clear }
246
+ end
172
247
  end
173
248
 
174
- WORKER_STATE = Concurrent::Map.new
175
- PROCESSED = Concurrent::AtomicFixnum.new
176
- FAILURE = Concurrent::AtomicFixnum.new
249
+ PROCESSED = Counter.new
250
+ FAILURE = Counter.new
251
+ WORKER_STATE = SharedWorkerState.new
177
252
 
178
- def stats(worker, job_hash, queue)
179
- tid = thread_identity
180
- WORKER_STATE[tid] = {:queue => queue, :payload => cloned(job_hash), :run_at => Time.now.to_i }
253
+ def stats(jobstr, queue)
254
+ WORKER_STATE.set(tid, {queue: queue, payload: jobstr, run_at: Time.now.to_i})
181
255
 
182
256
  begin
183
257
  yield
184
258
  rescue Exception
185
- FAILURE.increment
259
+ FAILURE.incr
186
260
  raise
187
261
  ensure
188
262
  WORKER_STATE.delete(tid)
189
- PROCESSED.increment
263
+ PROCESSED.incr
190
264
  end
191
265
  end
192
266
 
193
- # Deep clone the arguments passed to the worker so that if
194
- # the job fails, what is pushed back onto Redis hasn't
195
- # been mutated by the worker.
196
- def cloned(ary)
197
- Marshal.load(Marshal.dump(ary))
198
- end
267
+ def constantize(str)
268
+ return Object.const_get(str) unless str.include?("::")
199
269
 
270
+ names = str.split("::")
271
+ names.shift if names.empty? || names.first.empty?
272
+
273
+ names.inject(Object) do |constant, name|
274
+ # the false flag limits search for name to under the constant namespace
275
+ # which mimics Rails' behaviour
276
+ constant.const_get(name, false)
277
+ end
278
+ end
200
279
  end
201
280
  end