sidekiq 6.1.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 (127) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +71 -0
  3. data/.github/contributing.md +32 -0
  4. data/.github/issue_template.md +11 -0
  5. data/.gitignore +13 -0
  6. data/.standard.yml +20 -0
  7. data/3.0-Upgrade.md +70 -0
  8. data/4.0-Upgrade.md +53 -0
  9. data/5.0-Upgrade.md +56 -0
  10. data/6.0-Upgrade.md +72 -0
  11. data/COMM-LICENSE +97 -0
  12. data/Changes.md +1718 -0
  13. data/Ent-2.0-Upgrade.md +37 -0
  14. data/Ent-Changes.md +269 -0
  15. data/Gemfile +24 -0
  16. data/Gemfile.lock +208 -0
  17. data/LICENSE +9 -0
  18. data/Pro-2.0-Upgrade.md +138 -0
  19. data/Pro-3.0-Upgrade.md +44 -0
  20. data/Pro-4.0-Upgrade.md +35 -0
  21. data/Pro-5.0-Upgrade.md +25 -0
  22. data/Pro-Changes.md +790 -0
  23. data/README.md +94 -0
  24. data/Rakefile +10 -0
  25. data/bin/sidekiq +42 -0
  26. data/bin/sidekiqload +157 -0
  27. data/bin/sidekiqmon +8 -0
  28. data/code_of_conduct.md +50 -0
  29. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  30. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  31. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  32. data/lib/generators/sidekiq/worker_generator.rb +57 -0
  33. data/lib/sidekiq.rb +262 -0
  34. data/lib/sidekiq/api.rb +960 -0
  35. data/lib/sidekiq/cli.rb +401 -0
  36. data/lib/sidekiq/client.rb +263 -0
  37. data/lib/sidekiq/delay.rb +41 -0
  38. data/lib/sidekiq/exception_handler.rb +27 -0
  39. data/lib/sidekiq/extensions/action_mailer.rb +47 -0
  40. data/lib/sidekiq/extensions/active_record.rb +43 -0
  41. data/lib/sidekiq/extensions/class_methods.rb +43 -0
  42. data/lib/sidekiq/extensions/generic_proxy.rb +31 -0
  43. data/lib/sidekiq/fetch.rb +82 -0
  44. data/lib/sidekiq/job_logger.rb +63 -0
  45. data/lib/sidekiq/job_retry.rb +262 -0
  46. data/lib/sidekiq/launcher.rb +206 -0
  47. data/lib/sidekiq/logger.rb +165 -0
  48. data/lib/sidekiq/manager.rb +135 -0
  49. data/lib/sidekiq/middleware/chain.rb +160 -0
  50. data/lib/sidekiq/middleware/i18n.rb +40 -0
  51. data/lib/sidekiq/monitor.rb +133 -0
  52. data/lib/sidekiq/paginator.rb +47 -0
  53. data/lib/sidekiq/processor.rb +280 -0
  54. data/lib/sidekiq/rails.rb +50 -0
  55. data/lib/sidekiq/redis_connection.rb +146 -0
  56. data/lib/sidekiq/scheduled.rb +173 -0
  57. data/lib/sidekiq/sd_notify.rb +149 -0
  58. data/lib/sidekiq/systemd.rb +24 -0
  59. data/lib/sidekiq/testing.rb +344 -0
  60. data/lib/sidekiq/testing/inline.rb +30 -0
  61. data/lib/sidekiq/util.rb +67 -0
  62. data/lib/sidekiq/version.rb +5 -0
  63. data/lib/sidekiq/web.rb +213 -0
  64. data/lib/sidekiq/web/action.rb +93 -0
  65. data/lib/sidekiq/web/application.rb +357 -0
  66. data/lib/sidekiq/web/csrf_protection.rb +153 -0
  67. data/lib/sidekiq/web/helpers.rb +333 -0
  68. data/lib/sidekiq/web/router.rb +101 -0
  69. data/lib/sidekiq/worker.rb +244 -0
  70. data/sidekiq.gemspec +20 -0
  71. data/web/assets/images/favicon.ico +0 -0
  72. data/web/assets/images/logo.png +0 -0
  73. data/web/assets/images/status.png +0 -0
  74. data/web/assets/javascripts/application.js +95 -0
  75. data/web/assets/javascripts/dashboard.js +296 -0
  76. data/web/assets/stylesheets/application-dark.css +133 -0
  77. data/web/assets/stylesheets/application-rtl.css +246 -0
  78. data/web/assets/stylesheets/application.css +1158 -0
  79. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  80. data/web/assets/stylesheets/bootstrap.css +5 -0
  81. data/web/locales/ar.yml +81 -0
  82. data/web/locales/cs.yml +78 -0
  83. data/web/locales/da.yml +68 -0
  84. data/web/locales/de.yml +81 -0
  85. data/web/locales/el.yml +68 -0
  86. data/web/locales/en.yml +83 -0
  87. data/web/locales/es.yml +70 -0
  88. data/web/locales/fa.yml +80 -0
  89. data/web/locales/fr.yml +78 -0
  90. data/web/locales/he.yml +79 -0
  91. data/web/locales/hi.yml +75 -0
  92. data/web/locales/it.yml +69 -0
  93. data/web/locales/ja.yml +83 -0
  94. data/web/locales/ko.yml +68 -0
  95. data/web/locales/lt.yml +83 -0
  96. data/web/locales/nb.yml +77 -0
  97. data/web/locales/nl.yml +68 -0
  98. data/web/locales/pl.yml +59 -0
  99. data/web/locales/pt-br.yml +68 -0
  100. data/web/locales/pt.yml +67 -0
  101. data/web/locales/ru.yml +78 -0
  102. data/web/locales/sv.yml +68 -0
  103. data/web/locales/ta.yml +75 -0
  104. data/web/locales/uk.yml +76 -0
  105. data/web/locales/ur.yml +80 -0
  106. data/web/locales/vi.yml +83 -0
  107. data/web/locales/zh-cn.yml +68 -0
  108. data/web/locales/zh-tw.yml +68 -0
  109. data/web/views/_footer.erb +20 -0
  110. data/web/views/_job_info.erb +89 -0
  111. data/web/views/_nav.erb +52 -0
  112. data/web/views/_paging.erb +23 -0
  113. data/web/views/_poll_link.erb +7 -0
  114. data/web/views/_status.erb +4 -0
  115. data/web/views/_summary.erb +40 -0
  116. data/web/views/busy.erb +101 -0
  117. data/web/views/dashboard.erb +75 -0
  118. data/web/views/dead.erb +34 -0
  119. data/web/views/layout.erb +41 -0
  120. data/web/views/morgue.erb +78 -0
  121. data/web/views/queue.erb +55 -0
  122. data/web/views/queues.erb +38 -0
  123. data/web/views/retries.erb +83 -0
  124. data/web/views/retry.erb +34 -0
  125. data/web/views/scheduled.erb +57 -0
  126. data/web/views/scheduled_job_info.erb +8 -0
  127. metadata +212 -0
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ # Middleware is code configured to run before/after
5
+ # a message is processed. It is patterned after Rack
6
+ # middleware. Middleware exists for the client side
7
+ # (pushing jobs onto the queue) as well as the server
8
+ # side (when jobs are actually processed).
9
+ #
10
+ # To add middleware for the client:
11
+ #
12
+ # Sidekiq.configure_client do |config|
13
+ # config.client_middleware do |chain|
14
+ # chain.add MyClientHook
15
+ # end
16
+ # end
17
+ #
18
+ # To modify middleware for the server, just call
19
+ # with another block:
20
+ #
21
+ # Sidekiq.configure_server do |config|
22
+ # config.server_middleware do |chain|
23
+ # chain.add MyServerHook
24
+ # chain.remove ActiveRecord
25
+ # end
26
+ # end
27
+ #
28
+ # To insert immediately preceding another entry:
29
+ #
30
+ # Sidekiq.configure_client do |config|
31
+ # config.client_middleware do |chain|
32
+ # chain.insert_before ActiveRecord, MyClientHook
33
+ # end
34
+ # end
35
+ #
36
+ # To insert immediately after another entry:
37
+ #
38
+ # Sidekiq.configure_client do |config|
39
+ # config.client_middleware do |chain|
40
+ # chain.insert_after ActiveRecord, MyClientHook
41
+ # end
42
+ # end
43
+ #
44
+ # This is an example of a minimal server middleware:
45
+ #
46
+ # class MyServerHook
47
+ # def call(worker_instance, msg, queue)
48
+ # puts "Before work"
49
+ # yield
50
+ # puts "After work"
51
+ # end
52
+ # end
53
+ #
54
+ # This is an example of a minimal client middleware, note
55
+ # the method must return the result or the job will not push
56
+ # to Redis:
57
+ #
58
+ # class MyClientHook
59
+ # def call(worker_class, msg, queue, redis_pool)
60
+ # puts "Before push"
61
+ # result = yield
62
+ # puts "After push"
63
+ # result
64
+ # end
65
+ # end
66
+ #
67
+ module Middleware
68
+ class Chain
69
+ include Enumerable
70
+
71
+ def initialize_copy(copy)
72
+ copy.instance_variable_set(:@entries, entries.dup)
73
+ end
74
+
75
+ def each(&block)
76
+ entries.each(&block)
77
+ end
78
+
79
+ def initialize
80
+ @entries = nil
81
+ yield self if block_given?
82
+ end
83
+
84
+ def entries
85
+ @entries ||= []
86
+ end
87
+
88
+ def remove(klass)
89
+ entries.delete_if { |entry| entry.klass == klass }
90
+ end
91
+
92
+ def add(klass, *args)
93
+ remove(klass) if exists?(klass)
94
+ entries << Entry.new(klass, *args)
95
+ end
96
+
97
+ def prepend(klass, *args)
98
+ remove(klass) if exists?(klass)
99
+ entries.insert(0, Entry.new(klass, *args))
100
+ end
101
+
102
+ def insert_before(oldklass, newklass, *args)
103
+ i = entries.index { |entry| entry.klass == newklass }
104
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
105
+ i = entries.index { |entry| entry.klass == oldklass } || 0
106
+ entries.insert(i, new_entry)
107
+ end
108
+
109
+ def insert_after(oldklass, newklass, *args)
110
+ i = entries.index { |entry| entry.klass == newklass }
111
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
112
+ i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
113
+ entries.insert(i + 1, new_entry)
114
+ end
115
+
116
+ def exists?(klass)
117
+ any? { |entry| entry.klass == klass }
118
+ end
119
+
120
+ def empty?
121
+ @entries.nil? || @entries.empty?
122
+ end
123
+
124
+ def retrieve
125
+ map(&:make_new)
126
+ end
127
+
128
+ def clear
129
+ entries.clear
130
+ end
131
+
132
+ def invoke(*args)
133
+ return yield if empty?
134
+
135
+ chain = retrieve.dup
136
+ traverse_chain = lambda do
137
+ if chain.empty?
138
+ yield
139
+ else
140
+ chain.shift.call(*args, &traverse_chain)
141
+ end
142
+ end
143
+ traverse_chain.call
144
+ end
145
+ end
146
+
147
+ class Entry
148
+ attr_reader :klass
149
+
150
+ def initialize(klass, *args)
151
+ @klass = klass
152
+ @args = args
153
+ end
154
+
155
+ def make_new
156
+ @klass.new(*@args)
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Simple middleware to save the current locale and restore it when the job executes.
5
+ # Use it by requiring it in your initializer:
6
+ #
7
+ # require 'sidekiq/middleware/i18n'
8
+ #
9
+ module Sidekiq::Middleware::I18n
10
+ # Get the current locale and store it in the message
11
+ # to be sent to Sidekiq.
12
+ class Client
13
+ def call(_worker, msg, _queue, _redis)
14
+ msg["locale"] ||= I18n.locale
15
+ yield
16
+ end
17
+ end
18
+
19
+ # Pull the msg locale out and set the current thread to use it.
20
+ class Server
21
+ def call(_worker, msg, _queue, &block)
22
+ I18n.with_locale(msg.fetch("locale", I18n.default_locale), &block)
23
+ end
24
+ end
25
+ end
26
+
27
+ Sidekiq.configure_client do |config|
28
+ config.client_middleware do |chain|
29
+ chain.add Sidekiq::Middleware::I18n::Client
30
+ end
31
+ end
32
+
33
+ Sidekiq.configure_server do |config|
34
+ config.client_middleware do |chain|
35
+ chain.add Sidekiq::Middleware::I18n::Client
36
+ end
37
+ config.server_middleware do |chain|
38
+ chain.add Sidekiq::Middleware::I18n::Server
39
+ end
40
+ 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
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Paginator
5
+ def page(key, pageidx = 1, page_size = 25, opts = nil)
6
+ current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
7
+ pageidx = current_page - 1
8
+ total_size = 0
9
+ items = []
10
+ starting = pageidx * page_size
11
+ ending = starting + page_size - 1
12
+
13
+ Sidekiq.redis do |conn|
14
+ type = conn.type(key)
15
+ rev = opts && opts[:reverse]
16
+
17
+ case type
18
+ when "zset"
19
+ total_size, items = conn.multi {
20
+ conn.zcard(key)
21
+ if rev
22
+ conn.zrevrange(key, starting, ending, with_scores: true)
23
+ else
24
+ conn.zrange(key, starting, ending, with_scores: true)
25
+ end
26
+ }
27
+ [current_page, total_size, items]
28
+ when "list"
29
+ total_size, items = conn.multi {
30
+ conn.llen(key)
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
38
+ [current_page, total_size, items]
39
+ when "none"
40
+ [1, 0, []]
41
+ else
42
+ raise "can't page a #{type}"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/util"
4
+ require "sidekiq/fetch"
5
+ require "sidekiq/job_logger"
6
+ require "sidekiq/job_retry"
7
+
8
+ module Sidekiq
9
+ ##
10
+ # The Processor is a standalone thread which:
11
+ #
12
+ # 1. fetches a job from Redis
13
+ # 2. executes the job
14
+ # a. instantiate the Worker
15
+ # b. run the middleware chain
16
+ # c. call #perform
17
+ #
18
+ # A Processor can exit due to shutdown (processor_stopped)
19
+ # or due to an error during job execution (processor_died)
20
+ #
21
+ # If an error occurs in the job execution, the
22
+ # Processor calls the Manager to create a new one
23
+ # to replace itself and exits.
24
+ #
25
+ class Processor
26
+ include Util
27
+
28
+ attr_reader :thread
29
+ attr_reader :job
30
+
31
+ def initialize(mgr, options)
32
+ @mgr = mgr
33
+ @down = false
34
+ @done = false
35
+ @job = nil
36
+ @thread = nil
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
+ end
42
+
43
+ def terminate(wait = false)
44
+ @done = true
45
+ return unless @thread
46
+ @thread.value if wait
47
+ end
48
+
49
+ def kill(wait = false)
50
+ @done = true
51
+ return unless @thread
52
+ # unlike the other actors, terminate does not wait
53
+ # for the thread to finish because we don't know how
54
+ # long the job will take to finish. Instead we
55
+ # provide a `kill` method to call after the shutdown
56
+ # timeout passes.
57
+ @thread.raise ::Sidekiq::Shutdown
58
+ @thread.value if wait
59
+ end
60
+
61
+ def start
62
+ @thread ||= safe_thread("processor", &method(:run))
63
+ end
64
+
65
+ private unless $TESTING
66
+
67
+ def run
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)
74
+ end
75
+
76
+ def process_one
77
+ @job = fetch
78
+ process(@job) if @job
79
+ @job = nil
80
+ end
81
+
82
+ def get_one
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
87
+ end
88
+ work
89
+ rescue Sidekiq::Shutdown
90
+ rescue => ex
91
+ handle_fetch_exception(ex)
92
+ end
93
+
94
+ def fetch
95
+ j = get_one
96
+ if j && @done
97
+ j.requeue
98
+ nil
99
+ else
100
+ j
101
+ end
102
+ end
103
+
104
+ def handle_fetch_exception(ex)
105
+ unless @down
106
+ @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
107
+ logger.error("Error fetching job: #{ex}")
108
+ handle_exception(ex)
109
+ end
110
+ sleep(1)
111
+ nil
112
+ end
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
+
145
+ def process(work)
146
+ jobstr = work.job
147
+ queue = work.queue_name
148
+
149
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
150
+ job_hash = nil
151
+ begin
152
+ job_hash = Sidekiq.load_json(jobstr)
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"])
165
+ end
166
+ end
167
+ ack = true
168
+ rescue Sidekiq::Shutdown
169
+ # Had to force kill this job because it didn't finish
170
+ # within the timeout. Don't acknowledge the work since
171
+ # we didn't properly finish it.
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
179
+ rescue Exception => ex
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
185
+ ensure
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
192
+ end
193
+ end
194
+
195
+ def execute_job(worker, cloned_args)
196
+ worker.perform(*cloned_args)
197
+ end
198
+
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
247
+ end
248
+
249
+ PROCESSED = Counter.new
250
+ FAILURE = Counter.new
251
+ WORKER_STATE = SharedWorkerState.new
252
+
253
+ def stats(jobstr, queue)
254
+ WORKER_STATE.set(tid, {queue: queue, payload: jobstr, run_at: Time.now.to_i})
255
+
256
+ begin
257
+ yield
258
+ rescue Exception
259
+ FAILURE.incr
260
+ raise
261
+ ensure
262
+ WORKER_STATE.delete(tid)
263
+ PROCESSED.incr
264
+ end
265
+ end
266
+
267
+ def constantize(str)
268
+ return Object.const_get(str) unless str.include?("::")
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
279
+ end
280
+ end