sidekiq 3.4.1 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (235) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +1118 -4
  3. data/LICENSE.txt +9 -0
  4. data/README.md +55 -47
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +26 -3
  7. data/bin/sidekiqload +247 -0
  8. data/bin/sidekiqmon +11 -0
  9. data/lib/generators/sidekiq/job_generator.rb +57 -0
  10. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  11. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  12. data/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
  13. data/lib/sidekiq/api.rb +714 -312
  14. data/lib/sidekiq/capsule.rb +130 -0
  15. data/lib/sidekiq/cli.rb +275 -241
  16. data/lib/sidekiq/client.rb +141 -110
  17. data/lib/sidekiq/component.rb +68 -0
  18. data/lib/sidekiq/config.rb +291 -0
  19. data/lib/sidekiq/deploy.rb +62 -0
  20. data/lib/sidekiq/embedded.rb +61 -0
  21. data/lib/sidekiq/fetch.rb +53 -121
  22. data/lib/sidekiq/iterable_job.rb +53 -0
  23. data/lib/sidekiq/job/interrupt_handler.rb +22 -0
  24. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  25. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  26. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  27. data/lib/sidekiq/job/iterable.rb +231 -0
  28. data/lib/sidekiq/job.rb +385 -0
  29. data/lib/sidekiq/job_logger.rb +64 -0
  30. data/lib/sidekiq/job_retry.rb +305 -0
  31. data/lib/sidekiq/job_util.rb +107 -0
  32. data/lib/sidekiq/launcher.rb +241 -66
  33. data/lib/sidekiq/logger.rb +131 -0
  34. data/lib/sidekiq/manager.rb +91 -192
  35. data/lib/sidekiq/metrics/query.rb +156 -0
  36. data/lib/sidekiq/metrics/shared.rb +95 -0
  37. data/lib/sidekiq/metrics/tracking.rb +140 -0
  38. data/lib/sidekiq/middleware/chain.rb +114 -56
  39. data/lib/sidekiq/middleware/current_attributes.rb +111 -0
  40. data/lib/sidekiq/middleware/i18n.rb +8 -7
  41. data/lib/sidekiq/middleware/modules.rb +21 -0
  42. data/lib/sidekiq/monitor.rb +146 -0
  43. data/lib/sidekiq/paginator.rb +29 -16
  44. data/lib/sidekiq/processor.rb +248 -112
  45. data/lib/sidekiq/rails.rb +61 -27
  46. data/lib/sidekiq/redis_client_adapter.rb +114 -0
  47. data/lib/sidekiq/redis_connection.rb +68 -48
  48. data/lib/sidekiq/ring_buffer.rb +29 -0
  49. data/lib/sidekiq/scheduled.rb +173 -52
  50. data/lib/sidekiq/sd_notify.rb +149 -0
  51. data/lib/sidekiq/systemd.rb +24 -0
  52. data/lib/sidekiq/testing/inline.rb +7 -5
  53. data/lib/sidekiq/testing.rb +206 -65
  54. data/lib/sidekiq/transaction_aware_client.rb +51 -0
  55. data/lib/sidekiq/version.rb +4 -1
  56. data/lib/sidekiq/web/action.rb +99 -0
  57. data/lib/sidekiq/web/application.rb +479 -0
  58. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  59. data/lib/sidekiq/web/helpers.rb +415 -0
  60. data/lib/sidekiq/web/router.rb +104 -0
  61. data/lib/sidekiq/web.rb +158 -200
  62. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  63. data/lib/sidekiq.rb +100 -132
  64. data/sidekiq.gemspec +27 -23
  65. data/web/assets/images/apple-touch-icon.png +0 -0
  66. data/web/assets/images/favicon.ico +0 -0
  67. data/web/assets/javascripts/application.js +177 -72
  68. data/web/assets/javascripts/base-charts.js +106 -0
  69. data/web/assets/javascripts/chart.min.js +13 -0
  70. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  71. data/web/assets/javascripts/dashboard-charts.js +192 -0
  72. data/web/assets/javascripts/dashboard.js +37 -286
  73. data/web/assets/javascripts/metrics.js +298 -0
  74. data/web/assets/stylesheets/application-dark.css +147 -0
  75. data/web/assets/stylesheets/application-rtl.css +163 -0
  76. data/web/assets/stylesheets/application.css +228 -247
  77. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  78. data/web/assets/stylesheets/bootstrap.css +4 -8
  79. data/web/locales/ar.yml +87 -0
  80. data/web/locales/cs.yml +62 -52
  81. data/web/locales/da.yml +60 -53
  82. data/web/locales/de.yml +65 -53
  83. data/web/locales/el.yml +43 -24
  84. data/web/locales/en.yml +86 -61
  85. data/web/locales/es.yml +70 -53
  86. data/web/locales/fa.yml +80 -0
  87. data/web/locales/fr.yml +86 -56
  88. data/web/locales/gd.yml +99 -0
  89. data/web/locales/he.yml +80 -0
  90. data/web/locales/hi.yml +59 -59
  91. data/web/locales/it.yml +53 -53
  92. data/web/locales/ja.yml +78 -56
  93. data/web/locales/ko.yml +52 -52
  94. data/web/locales/lt.yml +83 -0
  95. data/web/locales/{no.yml → nb.yml} +62 -54
  96. data/web/locales/nl.yml +52 -52
  97. data/web/locales/pl.yml +45 -45
  98. data/web/locales/pt-br.yml +83 -55
  99. data/web/locales/pt.yml +51 -51
  100. data/web/locales/ru.yml +68 -60
  101. data/web/locales/sv.yml +53 -53
  102. data/web/locales/ta.yml +60 -60
  103. data/web/locales/tr.yml +101 -0
  104. data/web/locales/uk.yml +77 -0
  105. data/web/locales/ur.yml +80 -0
  106. data/web/locales/vi.yml +83 -0
  107. data/web/locales/zh-cn.yml +43 -16
  108. data/web/locales/zh-tw.yml +42 -8
  109. data/web/views/_footer.erb +22 -9
  110. data/web/views/_job_info.erb +27 -6
  111. data/web/views/_metrics_period_select.erb +12 -0
  112. data/web/views/_nav.erb +8 -22
  113. data/web/views/_paging.erb +3 -1
  114. data/web/views/_poll_link.erb +4 -0
  115. data/web/views/_summary.erb +7 -7
  116. data/web/views/busy.erb +91 -31
  117. data/web/views/dashboard.erb +52 -22
  118. data/web/views/dead.erb +5 -4
  119. data/web/views/filtering.erb +7 -0
  120. data/web/views/layout.erb +19 -7
  121. data/web/views/metrics.erb +91 -0
  122. data/web/views/metrics_for_job.erb +59 -0
  123. data/web/views/morgue.erb +26 -20
  124. data/web/views/queue.erb +36 -25
  125. data/web/views/queues.erb +24 -7
  126. data/web/views/retries.erb +29 -21
  127. data/web/views/retry.erb +6 -5
  128. data/web/views/scheduled.erb +20 -17
  129. data/web/views/scheduled_job_info.erb +2 -1
  130. metadata +101 -232
  131. data/.gitignore +0 -12
  132. data/.travis.yml +0 -16
  133. data/3.0-Upgrade.md +0 -70
  134. data/COMM-LICENSE +0 -85
  135. data/Contributing.md +0 -32
  136. data/Gemfile +0 -22
  137. data/LICENSE +0 -9
  138. data/Pro-2.0-Upgrade.md +0 -138
  139. data/Pro-Changes.md +0 -412
  140. data/Rakefile +0 -9
  141. data/bin/sidekiqctl +0 -93
  142. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  143. data/lib/generators/sidekiq/templates/worker_test.rb.erb +0 -8
  144. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  145. data/lib/sidekiq/actor.rb +0 -39
  146. data/lib/sidekiq/core_ext.rb +0 -105
  147. data/lib/sidekiq/exception_handler.rb +0 -30
  148. data/lib/sidekiq/extensions/action_mailer.rb +0 -56
  149. data/lib/sidekiq/extensions/active_record.rb +0 -39
  150. data/lib/sidekiq/extensions/class_methods.rb +0 -39
  151. data/lib/sidekiq/extensions/generic_proxy.rb +0 -24
  152. data/lib/sidekiq/logging.rb +0 -104
  153. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  154. data/lib/sidekiq/middleware/server/logging.rb +0 -35
  155. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -206
  156. data/lib/sidekiq/util.rb +0 -55
  157. data/lib/sidekiq/web_helpers.rb +0 -234
  158. data/lib/sidekiq/worker.rb +0 -89
  159. data/test/config.yml +0 -9
  160. data/test/env_based_config.yml +0 -11
  161. data/test/fake_env.rb +0 -0
  162. data/test/fixtures/en.yml +0 -2
  163. data/test/helper.rb +0 -39
  164. data/test/test_api.rb +0 -494
  165. data/test/test_cli.rb +0 -365
  166. data/test/test_client.rb +0 -269
  167. data/test/test_exception_handler.rb +0 -55
  168. data/test/test_extensions.rb +0 -120
  169. data/test/test_fetch.rb +0 -104
  170. data/test/test_logging.rb +0 -34
  171. data/test/test_manager.rb +0 -164
  172. data/test/test_middleware.rb +0 -159
  173. data/test/test_processor.rb +0 -166
  174. data/test/test_redis_connection.rb +0 -127
  175. data/test/test_retry.rb +0 -373
  176. data/test/test_scheduled.rb +0 -120
  177. data/test/test_scheduling.rb +0 -71
  178. data/test/test_sidekiq.rb +0 -69
  179. data/test/test_testing.rb +0 -82
  180. data/test/test_testing_fake.rb +0 -271
  181. data/test/test_testing_inline.rb +0 -93
  182. data/test/test_web.rb +0 -594
  183. data/test/test_web_helpers.rb +0 -52
  184. data/test/test_worker_generator.rb +0 -17
  185. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  186. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  187. data/web/assets/images/status/active.png +0 -0
  188. data/web/assets/images/status/idle.png +0 -0
  189. data/web/assets/javascripts/locales/README.md +0 -27
  190. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  191. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  192. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  193. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  194. data/web/assets/javascripts/locales/jquery.timeago.cs.js +0 -18
  195. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  196. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  197. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  198. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  199. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  200. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  201. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  202. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  203. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  204. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  205. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  206. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  207. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  208. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  209. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  210. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  211. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  212. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  213. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  214. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  215. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  216. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  217. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  218. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  219. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  220. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  221. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  222. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  223. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  224. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  225. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  226. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  227. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  228. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  229. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  230. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  231. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  232. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +0 -20
  233. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +0 -20
  234. data/web/views/_poll.erb +0 -10
  235. /data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
@@ -0,0 +1,146 @@
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
+ end
20
+
21
+ def all
22
+ version
23
+ puts
24
+ overview
25
+ puts
26
+ processes
27
+ puts
28
+ queues
29
+ end
30
+
31
+ def version
32
+ puts "Sidekiq #{Sidekiq::VERSION}"
33
+ puts Time.now.utc
34
+ end
35
+
36
+ def overview
37
+ puts "---- Overview ----"
38
+ puts " Processed: #{delimit stats.processed}"
39
+ puts " Failed: #{delimit stats.failed}"
40
+ puts " Busy: #{delimit stats.workers_size}"
41
+ puts " Enqueued: #{delimit stats.enqueued}"
42
+ puts " Retries: #{delimit stats.retry_size}"
43
+ puts " Scheduled: #{delimit stats.scheduled_size}"
44
+ puts " Dead: #{delimit stats.dead_size}"
45
+ end
46
+
47
+ def processes
48
+ puts "---- Processes (#{process_set.size}) ----"
49
+ process_set.each_with_index do |process, index|
50
+ # Keep compatibility with legacy versions since we don't want to break sidekiqmon during rolling upgrades or downgrades.
51
+ #
52
+ # Before:
53
+ # ["default", "critical"]
54
+ #
55
+ # After:
56
+ # {"default" => 1, "critical" => 10}
57
+ queues =
58
+ if process["weights"]
59
+ process["weights"].sort_by { |queue| queue[0] }.map { |capsule| capsule.map { |name, weight| (weight > 0) ? "#{name}: #{weight}" : name }.join(", ") }
60
+ else
61
+ process["queues"].sort
62
+ end
63
+
64
+ puts "#{process["identity"]} #{tags_for(process)}"
65
+ puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})"
66
+ puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)"
67
+ puts " Queues: #{split_multiline(queues, pad: 11)}"
68
+ puts " Version: #{process["version"] || "Unknown"}" if process["version"] != Sidekiq::VERSION
69
+ puts "" unless (index + 1) == process_set.size
70
+ end
71
+ end
72
+
73
+ def queues
74
+ puts "---- Queues (#{queue_data.size}) ----"
75
+ columns = {
76
+ name: [:ljust, (["name"] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
77
+ size: [:rjust, (["size"] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
78
+ latency: [:rjust, (["latency"] + queue_data.map(&:latency)).map(&:length).max + COL_PAD]
79
+ }
80
+ columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
81
+ puts
82
+ queue_data.each do |q|
83
+ columns.each do |col, (dir, width)|
84
+ print q.send(col).public_send(dir, width)
85
+ end
86
+ puts
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def delimit(number)
93
+ number.to_s.reverse.scan(/.{1,3}/).join(",").reverse
94
+ end
95
+
96
+ def split_multiline(values, opts = {})
97
+ return "none" unless values
98
+ pad = opts[:pad] || 0
99
+ max_length = opts[:max_length] || (80 - pad)
100
+ out = []
101
+ line = ""
102
+ values.each do |value|
103
+ if (line.length + value.length) > max_length
104
+ out << line
105
+ line = " " * pad
106
+ end
107
+ line << value + ", "
108
+ end
109
+ out << line[0..-3]
110
+ out.join("\n")
111
+ end
112
+
113
+ def tags_for(process)
114
+ tags = [
115
+ process["tag"],
116
+ process["labels"],
117
+ ((process["quiet"] == "true") ? "quiet" : nil)
118
+ ].flatten.compact
119
+ tags.any? ? "[#{tags.join("] [")}]" : nil
120
+ end
121
+
122
+ def time_ago(timestamp)
123
+ seconds = Time.now - Time.at(timestamp)
124
+ return "just now" if seconds < 60
125
+ return "a minute ago" if seconds < 120
126
+ return "#{seconds.floor / 60} minutes ago" if seconds < 3600
127
+ return "an hour ago" if seconds < 7200
128
+ "#{seconds.floor / 60 / 60} hours ago"
129
+ end
130
+
131
+ QUEUE_STRUCT = Struct.new(:name, :size, :latency)
132
+ def queue_data
133
+ @queue_data ||= Sidekiq::Queue.all.map { |q|
134
+ QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf("%#.2f", q.latency))
135
+ }
136
+ end
137
+
138
+ def process_set
139
+ @process_set ||= Sidekiq::ProcessSet.new
140
+ end
141
+
142
+ def stats
143
+ @stats ||= Sidekiq::Stats.new
144
+ end
145
+ end
146
+ end
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sidekiq
2
4
  module Paginator
3
-
4
- def page(key, pageidx=1, page_size=25, opts=nil)
5
- current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
5
+ def page(key, pageidx = 1, page_size = 25, opts = nil)
6
+ current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
6
7
  pageidx = current_page - 1
7
8
  total_size = 0
8
9
  items = []
@@ -11,26 +12,31 @@ module Sidekiq
11
12
 
12
13
  Sidekiq.redis do |conn|
13
14
  type = conn.type(key)
15
+ rev = opts && opts[:reverse]
14
16
 
15
17
  case type
16
- when 'zset'
17
- rev = opts && opts[:reverse]
18
- total_size, items = conn.multi do
19
- conn.zcard(key)
18
+ when "zset"
19
+ total_size, items = conn.multi { |transaction|
20
+ transaction.zcard(key)
20
21
  if rev
21
- conn.zrevrange(key, starting, ending, :with_scores => true)
22
+ transaction.zrange(key, starting, ending, "REV", "withscores")
22
23
  else
23
- conn.zrange(key, starting, ending, :with_scores => true)
24
+ transaction.zrange(key, starting, ending, "withscores")
24
25
  end
25
- end
26
+ }
26
27
  [current_page, total_size, items]
27
- when 'list'
28
- total_size, items = conn.multi do
29
- conn.llen(key)
30
- conn.lrange(key, starting, ending)
31
- end
28
+ when "list"
29
+ total_size, items = conn.multi { |transaction|
30
+ transaction.llen(key)
31
+ if rev
32
+ transaction.lrange(key, -ending - 1, -starting - 1)
33
+ else
34
+ transaction.lrange(key, starting, ending)
35
+ end
36
+ }
37
+ items.reverse! if rev
32
38
  [current_page, total_size, items]
33
- when 'none'
39
+ when "none"
34
40
  [1, 0, []]
35
41
  else
36
42
  raise "can't page a #{type}"
@@ -38,5 +44,12 @@ module Sidekiq
38
44
  end
39
45
  end
40
46
 
47
+ def page_items(items, pageidx = 1, page_size = 25)
48
+ current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
49
+ pageidx = current_page - 1
50
+ starting = pageidx * page_size
51
+ items = items.to_a
52
+ [current_page, items.size, items[starting, page_size]]
53
+ end
41
54
  end
42
55
  end
@@ -1,155 +1,291 @@
1
- require 'sidekiq/util'
2
- require 'sidekiq/actor'
1
+ # frozen_string_literal: true
3
2
 
4
- require 'sidekiq/middleware/server/retry_jobs'
5
- require 'sidekiq/middleware/server/logging'
3
+ require "sidekiq/fetch"
4
+ require "sidekiq/job_logger"
5
+ require "sidekiq/job_retry"
6
6
 
7
7
  module Sidekiq
8
8
  ##
9
- # The Processor receives a message from the Manager and actually
10
- # processes it. It instantiates the worker, runs the middleware
11
- # chain and then calls Sidekiq::Worker#perform.
9
+ # The Processor is a standalone thread which:
10
+ #
11
+ # 1. fetches a job from Redis
12
+ # 2. executes the job
13
+ # a. instantiate the job class
14
+ # b. run the middleware chain
15
+ # c. call #perform
16
+ #
17
+ # A Processor can exit due to shutdown or due to
18
+ # an error during job execution.
19
+ #
20
+ # If an error occurs in the job execution, the
21
+ # Processor calls the Manager to create a new one
22
+ # to replace itself and exits.
23
+ #
12
24
  class Processor
13
- # To prevent a memory leak, ensure that stats expire. However, they should take up a minimal amount of storage
14
- # so keep them around for a long time
15
- STATS_TIMEOUT = 24 * 60 * 60 * 365 * 5
16
-
17
- include Util
18
- include Actor
19
-
20
- def self.default_middleware
21
- Middleware::Chain.new do |m|
22
- m.add Middleware::Server::Logging
23
- m.add Middleware::Server::RetryJobs
24
- if defined?(::ActiveRecord::Base)
25
- require 'sidekiq/middleware/server/active_record'
26
- m.add Sidekiq::Middleware::Server::ActiveRecord
27
- end
28
- end
25
+ include Sidekiq::Component
26
+
27
+ attr_reader :thread
28
+ attr_reader :job
29
+ attr_reader :capsule
30
+
31
+ def initialize(capsule, &block)
32
+ @config = @capsule = capsule
33
+ @callback = block
34
+ @down = false
35
+ @done = false
36
+ @job = nil
37
+ @thread = nil
38
+ @reloader = Sidekiq.default_configuration[:reloader]
39
+ @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(capsule.config)
40
+ @retrier = Sidekiq::JobRetry.new(capsule)
29
41
  end
30
42
 
31
- attr_accessor :proxy_id
43
+ def terminate(wait = false)
44
+ @done = true
45
+ return unless @thread
46
+ @thread.value if wait
47
+ end
32
48
 
33
- def initialize(boss)
34
- @boss = boss
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
35
59
  end
36
60
 
37
- def process(work)
38
- msgstr = work.message
39
- queue = work.queue_name
61
+ def stopping?
62
+ @done
63
+ end
40
64
 
41
- @boss.async.real_thread(proxy_id, Thread.current)
65
+ def start
66
+ @thread ||= safe_thread("#{config.name}/processor", &method(:run))
67
+ end
42
68
 
43
- ack = true
44
- begin
45
- msg = Sidekiq.load_json(msgstr)
46
- klass = msg['class'].constantize
47
- worker = klass.new
48
- worker.jid = msg['jid']
49
-
50
- stats(worker, msg, queue) do
51
- Sidekiq.server_middleware.invoke(worker, msg, queue) do
52
- execute_job(worker, cloned(msg['args']))
53
- end
54
- end
55
- rescue Sidekiq::Shutdown
56
- # Had to force kill this job because it didn't finish
57
- # within the timeout. Don't acknowledge the work since
58
- # we didn't properly finish it.
59
- ack = false
60
- rescue Exception => ex
61
- handle_exception(ex, msg || { :message => msgstr })
62
- raise
63
- ensure
64
- work.acknowledge if ack
65
- end
69
+ private unless $TESTING
70
+
71
+ def run
72
+ # By setting this thread-local, Sidekiq.redis will access +Sidekiq::Capsule#redis_pool+
73
+ # instead of the global pool in +Sidekiq::Config#redis_pool+.
74
+ Thread.current[:sidekiq_capsule] = @capsule
66
75
 
67
- @boss.async.processor_done(current_actor)
76
+ process_one until @done
77
+ @callback.call(self)
78
+ rescue Sidekiq::Shutdown
79
+ @callback.call(self)
80
+ rescue Exception => ex
81
+ @callback.call(self, ex)
68
82
  end
69
83
 
70
- def inspect
71
- "<Processor##{object_id.to_s(16)}>"
84
+ def process_one(&block)
85
+ @job = fetch
86
+ process(@job) if @job
87
+ @job = nil
72
88
  end
73
89
 
74
- def execute_job(worker, cloned_args)
75
- worker.perform(*cloned_args)
90
+ def get_one
91
+ uow = capsule.fetcher.retrieve_work
92
+ if @down
93
+ logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" }
94
+ @down = nil
95
+ end
96
+ uow
97
+ rescue Sidekiq::Shutdown
98
+ rescue => ex
99
+ handle_fetch_exception(ex)
76
100
  end
77
101
 
78
- private
102
+ def fetch
103
+ j = get_one
104
+ if j && @done
105
+ j.requeue
106
+ nil
107
+ else
108
+ j
109
+ end
110
+ end
79
111
 
80
- def thread_identity
81
- @str ||= Thread.current.object_id.to_s(36)
112
+ def handle_fetch_exception(ex)
113
+ unless @down
114
+ @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
115
+ logger.error("Error fetching job: #{ex}")
116
+ handle_exception(ex)
117
+ end
118
+ sleep(1)
119
+ nil
82
120
  end
83
121
 
84
- def stats(worker, msg, queue)
85
- # Do not conflate errors from the job with errors caused by updating
86
- # stats so calling code can react appropriately
87
- retry_and_suppress_exceptions do
88
- hash = Sidekiq.dump_json({:queue => queue, :payload => msg, :run_at => Time.now.to_i })
89
- Sidekiq.redis do |conn|
90
- conn.multi do
91
- conn.hmset("#{identity}:workers", thread_identity, hash)
92
- conn.expire("#{identity}:workers", 60*60*4)
122
+ def dispatch(job_hash, queue, jobstr)
123
+ # since middleware can mutate the job hash
124
+ # we need to clone it to report the original
125
+ # job structure to the Web UI
126
+ # or to push back to redis when retrying.
127
+ # To avoid costly and, most of the time, useless cloning here,
128
+ # we pass original String of JSON to respected methods
129
+ # to re-parse it there if we need access to the original, untouched job
130
+
131
+ @job_logger.prepare(job_hash) do
132
+ @retrier.global(jobstr, queue) do
133
+ @job_logger.call(job_hash, queue) do
134
+ stats(jobstr, queue) do
135
+ # Rails 5 requires a Reloader to wrap code execution. In order to
136
+ # constantize the worker and instantiate an instance, we have to call
137
+ # the Reloader. It handles code loading, db connection management, etc.
138
+ # Effectively this block denotes a "unit of work" to Rails.
139
+ @reloader.call do
140
+ klass = Object.const_get(job_hash["class"])
141
+ inst = klass.new
142
+ inst.jid = job_hash["jid"]
143
+ inst._context = self
144
+ @retrier.local(inst, jobstr, queue) do
145
+ yield inst
146
+ end
147
+ end
148
+ end
93
149
  end
94
150
  end
95
151
  end
152
+ end
153
+
154
+ IGNORE_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :never}
155
+ private_constant :IGNORE_SHUTDOWN_INTERRUPTS
156
+ ALLOW_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :immediate}
157
+ private_constant :ALLOW_SHUTDOWN_INTERRUPTS
158
+
159
+ def process(uow)
160
+ jobstr = uow.job
161
+ queue = uow.queue_name
96
162
 
163
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
164
+ job_hash = nil
97
165
  begin
98
- yield
99
- rescue Exception
100
- retry_and_suppress_exceptions do
101
- failed = "stat:failed:#{Time.now.utc.to_date}"
102
- Sidekiq.redis do |conn|
103
- conn.multi do
104
- conn.incrby("stat:failed", 1)
105
- conn.incrby(failed, 1)
106
- conn.expire(failed, STATS_TIMEOUT)
107
- end
166
+ job_hash = Sidekiq.load_json(jobstr)
167
+ rescue => ex
168
+ handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
169
+ now = Time.now.to_f
170
+ redis do |conn|
171
+ conn.multi do |xa|
172
+ xa.zadd("dead", now.to_s, jobstr)
173
+ xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
174
+ xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
108
175
  end
109
176
  end
110
- raise
111
- ensure
112
- retry_and_suppress_exceptions do
113
- processed = "stat:processed:#{Time.now.utc.to_date}"
114
- Sidekiq.redis do |conn|
115
- conn.multi do
116
- conn.hdel("#{identity}:workers", thread_identity)
117
- conn.incrby("stat:processed", 1)
118
- conn.incrby(processed, 1)
119
- conn.expire(processed, STATS_TIMEOUT)
177
+ return uow.acknowledge
178
+ end
179
+
180
+ ack = false
181
+ Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
182
+ Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
183
+ dispatch(job_hash, queue, jobstr) do |inst|
184
+ config.server_middleware.invoke(inst, job_hash, queue) do
185
+ execute_job(inst, job_hash["args"])
120
186
  end
121
187
  end
188
+ ack = true
189
+ rescue Sidekiq::Shutdown
190
+ # Had to force kill this job because it didn't finish
191
+ # within the timeout. Don't acknowledge the work since
192
+ # we didn't properly finish it.
193
+ rescue Sidekiq::JobRetry::Skip => s
194
+ # Skip means we handled this error elsewhere. We don't
195
+ # need to log or report the error.
196
+ ack = true
197
+ raise s
198
+ rescue Sidekiq::JobRetry::Handled => h
199
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
200
+ # signals that we created a retry successfully. We can acknowledge the job.
201
+ ack = true
202
+ e = h.cause || h
203
+ handle_exception(e, {context: "Job raised exception", job: job_hash})
204
+ raise e
205
+ rescue Exception => ex
206
+ # Unexpected error! This is very bad and indicates an exception that got past
207
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
208
+ # so it can be rescued when using Sidekiq Pro.
209
+ handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
210
+ raise ex
211
+ end
212
+ ensure
213
+ if ack
214
+ uow.acknowledge
122
215
  end
123
216
  end
124
217
  end
125
218
 
126
- # Deep clone the arguments passed to the worker so that if
127
- # the message fails, what is pushed back onto Redis hasn't
128
- # been mutated by the worker.
129
- def cloned(ary)
130
- Marshal.load(Marshal.dump(ary))
219
+ def execute_job(inst, cloned_args)
220
+ inst.perform(*cloned_args)
131
221
  end
132
222
 
133
- # If an exception occurs in the block passed to this method, that block will be retried up to max_retries times.
134
- # All exceptions will be swallowed and logged.
135
- def retry_and_suppress_exceptions(max_retries = 5)
136
- retry_count = 0
137
- begin
138
- yield
139
- rescue => e
140
- retry_count += 1
141
- if retry_count <= max_retries
142
- Sidekiq.logger.debug {"Suppressing and retrying error: #{e.inspect}"}
143
- pause_for_recovery(retry_count)
144
- retry
145
- else
146
- handle_exception(e, { :message => "Exhausted #{max_retries} retries"})
147
- end
223
+ # Ruby doesn't provide atomic counters out of the box so we'll
224
+ # implement something simple ourselves.
225
+ # https://bugs.ruby-lang.org/issues/14706
226
+ class Counter
227
+ def initialize
228
+ @value = 0
229
+ @lock = Mutex.new
230
+ end
231
+
232
+ def incr(amount = 1)
233
+ @lock.synchronize { @value += amount }
234
+ end
235
+
236
+ def reset
237
+ @lock.synchronize {
238
+ val = @value
239
+ @value = 0
240
+ val
241
+ }
242
+ end
243
+ end
244
+
245
+ # jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here
246
+ class SharedWorkState
247
+ def initialize
248
+ @work_state = {}
249
+ @lock = Mutex.new
250
+ end
251
+
252
+ def set(tid, hash)
253
+ @lock.synchronize { @work_state[tid] = hash }
254
+ end
255
+
256
+ def delete(tid)
257
+ @lock.synchronize { @work_state.delete(tid) }
258
+ end
259
+
260
+ def dup
261
+ @lock.synchronize { @work_state.dup }
262
+ end
263
+
264
+ def size
265
+ @lock.synchronize { @work_state.size }
266
+ end
267
+
268
+ def clear
269
+ @lock.synchronize { @work_state.clear }
148
270
  end
149
271
  end
150
272
 
151
- def pause_for_recovery(retry_count)
152
- sleep(retry_count)
273
+ PROCESSED = Counter.new
274
+ FAILURE = Counter.new
275
+ WORK_STATE = SharedWorkState.new
276
+
277
+ def stats(jobstr, queue)
278
+ WORK_STATE.set(tid, {queue: queue, payload: jobstr, run_at: Time.now.to_i})
279
+
280
+ begin
281
+ yield
282
+ rescue Exception
283
+ FAILURE.incr
284
+ raise
285
+ ensure
286
+ WORK_STATE.delete(tid)
287
+ PROCESSED.incr
288
+ end
153
289
  end
154
290
  end
155
291
  end