sidekiq 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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +61 -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 +70 -0
  11. data/COMM-LICENSE +97 -0
  12. data/Changes.md +1570 -0
  13. data/Ent-2.0-Upgrade.md +37 -0
  14. data/Ent-Changes.md +250 -0
  15. data/Gemfile +24 -0
  16. data/Gemfile.lock +196 -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 +768 -0
  23. data/README.md +95 -0
  24. data/Rakefile +10 -0
  25. data/bin/sidekiq +18 -0
  26. data/bin/sidekiqload +153 -0
  27. data/bin/sidekiqmon +9 -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 +47 -0
  33. data/lib/sidekiq.rb +248 -0
  34. data/lib/sidekiq/api.rb +927 -0
  35. data/lib/sidekiq/cli.rb +380 -0
  36. data/lib/sidekiq/client.rb +242 -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 +42 -0
  41. data/lib/sidekiq/extensions/class_methods.rb +42 -0
  42. data/lib/sidekiq/extensions/generic_proxy.rb +31 -0
  43. data/lib/sidekiq/fetch.rb +80 -0
  44. data/lib/sidekiq/job_logger.rb +55 -0
  45. data/lib/sidekiq/job_retry.rb +249 -0
  46. data/lib/sidekiq/launcher.rb +181 -0
  47. data/lib/sidekiq/logger.rb +69 -0
  48. data/lib/sidekiq/manager.rb +135 -0
  49. data/lib/sidekiq/middleware/chain.rb +151 -0
  50. data/lib/sidekiq/middleware/i18n.rb +40 -0
  51. data/lib/sidekiq/monitor.rb +148 -0
  52. data/lib/sidekiq/paginator.rb +42 -0
  53. data/lib/sidekiq/processor.rb +282 -0
  54. data/lib/sidekiq/rails.rb +52 -0
  55. data/lib/sidekiq/redis_connection.rb +138 -0
  56. data/lib/sidekiq/scheduled.rb +172 -0
  57. data/lib/sidekiq/testing.rb +332 -0
  58. data/lib/sidekiq/testing/inline.rb +30 -0
  59. data/lib/sidekiq/util.rb +69 -0
  60. data/lib/sidekiq/version.rb +5 -0
  61. data/lib/sidekiq/web.rb +205 -0
  62. data/lib/sidekiq/web/action.rb +93 -0
  63. data/lib/sidekiq/web/application.rb +356 -0
  64. data/lib/sidekiq/web/helpers.rb +324 -0
  65. data/lib/sidekiq/web/router.rb +103 -0
  66. data/lib/sidekiq/worker.rb +247 -0
  67. data/sidekiq.gemspec +21 -0
  68. data/web/assets/images/favicon.ico +0 -0
  69. data/web/assets/images/logo.png +0 -0
  70. data/web/assets/images/status.png +0 -0
  71. data/web/assets/javascripts/application.js +92 -0
  72. data/web/assets/javascripts/dashboard.js +296 -0
  73. data/web/assets/stylesheets/application-rtl.css +246 -0
  74. data/web/assets/stylesheets/application.css +1144 -0
  75. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  76. data/web/assets/stylesheets/bootstrap.css +5 -0
  77. data/web/locales/ar.yml +81 -0
  78. data/web/locales/cs.yml +78 -0
  79. data/web/locales/da.yml +68 -0
  80. data/web/locales/de.yml +69 -0
  81. data/web/locales/el.yml +68 -0
  82. data/web/locales/en.yml +81 -0
  83. data/web/locales/es.yml +70 -0
  84. data/web/locales/fa.yml +80 -0
  85. data/web/locales/fr.yml +78 -0
  86. data/web/locales/he.yml +79 -0
  87. data/web/locales/hi.yml +75 -0
  88. data/web/locales/it.yml +69 -0
  89. data/web/locales/ja.yml +81 -0
  90. data/web/locales/ko.yml +68 -0
  91. data/web/locales/nb.yml +77 -0
  92. data/web/locales/nl.yml +68 -0
  93. data/web/locales/pl.yml +59 -0
  94. data/web/locales/pt-br.yml +68 -0
  95. data/web/locales/pt.yml +67 -0
  96. data/web/locales/ru.yml +78 -0
  97. data/web/locales/sv.yml +68 -0
  98. data/web/locales/ta.yml +75 -0
  99. data/web/locales/uk.yml +76 -0
  100. data/web/locales/ur.yml +80 -0
  101. data/web/locales/zh-cn.yml +68 -0
  102. data/web/locales/zh-tw.yml +68 -0
  103. data/web/views/_footer.erb +20 -0
  104. data/web/views/_job_info.erb +88 -0
  105. data/web/views/_nav.erb +52 -0
  106. data/web/views/_paging.erb +23 -0
  107. data/web/views/_poll_link.erb +7 -0
  108. data/web/views/_status.erb +4 -0
  109. data/web/views/_summary.erb +40 -0
  110. data/web/views/busy.erb +98 -0
  111. data/web/views/dashboard.erb +75 -0
  112. data/web/views/dead.erb +34 -0
  113. data/web/views/layout.erb +40 -0
  114. data/web/views/morgue.erb +75 -0
  115. data/web/views/queue.erb +46 -0
  116. data/web/views/queues.erb +30 -0
  117. data/web/views/retries.erb +80 -0
  118. data/web/views/retry.erb +34 -0
  119. data/web/views/scheduled.erb +54 -0
  120. data/web/views/scheduled_job_info.erb +8 -0
  121. metadata +220 -0
@@ -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,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
@@ -0,0 +1,42 @@
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
+
16
+ case type
17
+ when "zset"
18
+ rev = opts && opts[:reverse]
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
+ conn.lrange(key, starting, ending)
32
+ }
33
+ [current_page, total_size, items]
34
+ when "none"
35
+ [1, 0, []]
36
+ else
37
+ raise "can't page a #{type}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,282 @@
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)
32
+ @mgr = mgr
33
+ @down = false
34
+ @done = false
35
+ @job = nil
36
+ @thread = nil
37
+ @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
38
+ @reloader = Sidekiq.options[:reloader]
39
+ @job_logger = (mgr.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)
115
+ # since middleware can mutate the job hash
116
+ # we clone here so we report the original
117
+ # job structure to the Web UI
118
+ pristine = cloned(job_hash)
119
+
120
+ @job_logger.with_job_hash_context(job_hash) do
121
+ @retrier.global(pristine, queue) do
122
+ @job_logger.call(job_hash, queue) do
123
+ stats(pristine, queue) do
124
+ # Rails 5 requires a Reloader to wrap code execution. In order to
125
+ # constantize the worker and instantiate an instance, we have to call
126
+ # the Reloader. It handles code loading, db connection management, etc.
127
+ # Effectively this block denotes a "unit of work" to Rails.
128
+ @reloader.call do
129
+ klass = constantize(job_hash["class"])
130
+ worker = klass.new
131
+ worker.jid = job_hash["jid"]
132
+ @retrier.local(worker, pristine, queue) do
133
+ yield worker
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ def process(work)
143
+ jobstr = work.job
144
+ queue = work.queue_name
145
+
146
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
147
+ job_hash = nil
148
+ begin
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
156
+
157
+ ack = false
158
+ begin
159
+ dispatch(job_hash, queue) do |worker|
160
+ Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
161
+ execute_job(worker, cloned(job_hash["args"]))
162
+ end
163
+ end
164
+ ack = true
165
+ rescue Sidekiq::Shutdown
166
+ # Had to force kill this job because it didn't finish
167
+ # within the timeout. Don't acknowledge the work since
168
+ # we didn't properly finish it.
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
176
+ rescue Exception => ex
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})
181
+ raise e
182
+ ensure
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
188
+ end
189
+ end
190
+ end
191
+
192
+ def execute_job(worker, cloned_args)
193
+ worker.perform(*cloned_args)
194
+ end
195
+
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
216
+ end
217
+
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
249
+
250
+ def stats(job_hash, queue)
251
+ WORKER_STATE.set(tid, {queue: queue, payload: job_hash, run_at: Time.now.to_i})
252
+
253
+ begin
254
+ yield
255
+ rescue Exception
256
+ FAILURE.incr
257
+ raise
258
+ ensure
259
+ WORKER_STATE.delete(tid)
260
+ PROCESSED.incr
261
+ end
262
+ end
263
+
264
+ # Deep clone the arguments passed to the worker so that if
265
+ # the job fails, what is pushed back onto Redis hasn't
266
+ # been mutated by the worker.
267
+ def cloned(thing)
268
+ Marshal.load(Marshal.dump(thing))
269
+ end
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
281
+ end
282
+ end