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
@@ -1,155 +1,80 @@
1
- # encoding: utf-8
2
- require 'sidekiq/util'
3
- require 'sidekiq/actor'
4
- require 'sidekiq/processor'
5
- require 'sidekiq/fetch'
1
+ # frozen_string_literal: true
6
2
 
7
- module Sidekiq
3
+ require "sidekiq/processor"
4
+ require "set"
8
5
 
6
+ module Sidekiq
9
7
  ##
10
- # The main router in the system. This
11
- # manages the processor state and accepts messages
12
- # from Redis to be dispatched to an idle processor.
8
+ # The Manager is the central coordination point in Sidekiq, controlling
9
+ # the lifecycle of the Processors.
10
+ #
11
+ # Tasks:
12
+ #
13
+ # 1. start: Spin up Processors.
14
+ # 3. processor_died: Handle job failure, throw away Processor, create new one.
15
+ # 4. quiet: shutdown idle Processors.
16
+ # 5. stop: hard stop the Processors by deadline.
17
+ #
18
+ # Note that only the last task requires its own Thread since it has to monitor
19
+ # the shutdown process. The other tasks are performed by other threads.
13
20
  #
14
21
  class Manager
15
- include Util
16
- include Actor
17
- trap_exit :processor_died
18
-
19
- attr_reader :ready
20
- attr_reader :busy
21
- attr_accessor :fetcher
22
-
23
- SPIN_TIME_FOR_GRACEFUL_SHUTDOWN = 1
24
- JVM_RESERVED_SIGNALS = ['USR1', 'USR2'] # Don't Process#kill if we get these signals via the API
25
-
26
- def initialize(condvar, options={})
27
- logger.debug { options.inspect }
28
- @options = options
29
- @count = options[:concurrency] || 25
30
- @done_callback = nil
31
- @finished = condvar
32
-
33
- @in_progress = {}
34
- @threads = {}
35
- @done = false
36
- @busy = []
37
- @ready = @count.times.map do
38
- p = Processor.new_link(current_actor)
39
- p.proxy_id = p.object_id
40
- p
41
- end
42
- end
43
-
44
- def stop(options={})
45
- watchdog('Manager#stop died') do
46
- should_shutdown = options[:shutdown]
47
- timeout = options[:timeout]
48
-
49
- @done = true
22
+ include Sidekiq::Component
50
23
 
51
- logger.info { "Terminating #{@ready.size} quiet workers" }
52
- @ready.each { |x| x.terminate if x.alive? }
53
- @ready.clear
24
+ attr_reader :workers
25
+ attr_reader :capsule
54
26
 
55
- return if clean_up_for_graceful_shutdown
27
+ def initialize(capsule)
28
+ @config = @capsule = capsule
29
+ @count = capsule.concurrency
30
+ raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
56
31
 
57
- hard_shutdown_in timeout if should_shutdown
58
- end
59
- end
60
-
61
- def clean_up_for_graceful_shutdown
62
- if @busy.empty?
63
- shutdown
64
- return true
32
+ @done = false
33
+ @workers = Set.new
34
+ @plock = Mutex.new
35
+ @count.times do
36
+ @workers << Processor.new(@config, &method(:processor_result))
65
37
  end
66
-
67
- after(SPIN_TIME_FOR_GRACEFUL_SHUTDOWN) { clean_up_for_graceful_shutdown }
68
- false
69
38
  end
70
39
 
71
40
  def start
72
- @ready.each { dispatch }
41
+ @workers.each(&:start)
73
42
  end
74
43
 
75
- def when_done(&blk)
76
- @done_callback = blk
77
- end
44
+ def quiet
45
+ return if @done
46
+ @done = true
78
47
 
79
- def processor_done(processor)
80
- watchdog('Manager#processor_done died') do
81
- @done_callback.call(processor) if @done_callback
82
- @in_progress.delete(processor.object_id)
83
- @threads.delete(processor.object_id)
84
- @busy.delete(processor)
85
- if stopped?
86
- processor.terminate if processor.alive?
87
- shutdown if @busy.empty?
88
- else
89
- @ready << processor if processor.alive?
90
- end
91
- dispatch
92
- end
48
+ logger.info { "Terminating quiet threads for #{capsule.name} capsule" }
49
+ @workers.each(&:terminate)
93
50
  end
94
51
 
95
- def processor_died(processor, reason)
96
- watchdog("Manager#processor_died died") do
97
- @in_progress.delete(processor.object_id)
98
- @threads.delete(processor.object_id)
99
- @busy.delete(processor)
100
-
101
- unless stopped?
102
- p = Processor.new_link(current_actor)
103
- p.proxy_id = p.object_id
104
- @ready << p
105
- dispatch
106
- else
107
- shutdown if @busy.empty?
108
- end
109
- end
110
- end
52
+ def stop(deadline)
53
+ quiet
111
54
 
112
- def assign(work)
113
- watchdog("Manager#assign died") do
114
- if stopped?
115
- # Race condition between Manager#stop if Fetcher
116
- # is blocked on redis and gets a message after
117
- # all the ready Processors have been stopped.
118
- # Push the message back to redis.
119
- work.requeue
120
- else
121
- processor = @ready.pop
122
- @in_progress[processor.object_id] = work
123
- @busy << processor
124
- processor.async.process(work)
125
- end
126
- end
127
- end
55
+ # some of the shutdown events can be async,
56
+ # we don't have any way to know when they're done but
57
+ # give them a little time to take effect
58
+ sleep PAUSE_TIME
59
+ return if @workers.empty?
128
60
 
129
- # A hack worthy of Rube Goldberg. We need to be able
130
- # to hard stop a working thread. But there's no way for us to
131
- # get handle to the underlying thread performing work for a processor
132
- # so we have it call us and tell us.
133
- def real_thread(proxy_id, thr)
134
- @threads[proxy_id] = thr
61
+ logger.info { "Pausing to allow jobs to finish..." }
62
+ wait_for(deadline) { @workers.empty? }
63
+ return if @workers.empty?
64
+
65
+ hard_shutdown
66
+ ensure
67
+ capsule.stop
135
68
  end
136
69
 
137
- PROCTITLES = [
138
- proc { 'sidekiq'.freeze },
139
- proc { Sidekiq::VERSION },
140
- proc { |mgr, data| data['tag'] },
141
- proc { |mgr, data| "[#{mgr.busy.size} of #{data['concurrency']} busy]" },
142
- proc { |mgr, data| "stopping" if mgr.stopped? },
143
- ]
144
-
145
- def heartbeat(key, data, json)
146
- results = PROCTITLES.map {|x| x.(self, data) }
147
- results.compact!
148
- $0 = results.join(' ')
149
-
150
- ❤(key, json)
151
- after(5) do
152
- heartbeat(key, data, json)
70
+ def processor_result(processor, reason = nil)
71
+ @plock.synchronize do
72
+ @workers.delete(processor)
73
+ unless @done
74
+ p = Processor.new(@config, &method(:processor_result))
75
+ @workers << p
76
+ p.start
77
+ end
153
78
  end
154
79
  end
155
80
 
@@ -159,77 +84,51 @@ module Sidekiq
159
84
 
160
85
  private
161
86
 
162
- def ❤(key, json)
163
- begin
164
- _, _, _, msg = Sidekiq.redis do |conn|
165
- conn.multi do
166
- conn.sadd('processes', key)
167
- conn.hmset(key, 'info', json, 'busy', @busy.size, 'beat', Time.now.to_f)
168
- conn.expire(key, 60)
169
- conn.rpop("#{key}-signals")
170
- end
171
- end
172
-
173
- return unless msg
174
-
175
- if JVM_RESERVED_SIGNALS.include?(msg)
176
- Sidekiq::CLI.instance.handle_signal(msg)
177
- else
178
- ::Process.kill(msg, $$)
179
- end
180
- rescue => e
181
- # ignore all redis/network issues
182
- logger.error("heartbeat: #{e.message}")
87
+ def hard_shutdown
88
+ # We've reached the timeout and we still have busy threads.
89
+ # They must die but their jobs shall live on.
90
+ cleanup = nil
91
+ @plock.synchronize do
92
+ cleanup = @workers.dup
183
93
  end
184
- end
185
-
186
- def hard_shutdown_in(delay)
187
- logger.info { "Pausing up to #{delay} seconds to allow workers to finish..." }
188
94
 
189
- after(delay) do
190
- watchdog("Manager#hard_shutdown_in died") do
191
- # We've reached the timeout and we still have busy workers.
192
- # They must die but their messages shall live on.
193
- logger.warn { "Terminating #{@busy.size} busy worker threads" }
194
- logger.warn { "Work still in progress #{@in_progress.values.inspect}" }
95
+ if cleanup.size > 0
96
+ jobs = cleanup.map { |p| p.job }.compact
195
97
 
196
- requeue
98
+ logger.warn { "Terminating #{cleanup.size} busy threads" }
99
+ logger.debug { "Jobs still in progress #{jobs.inspect}" }
197
100
 
198
- @busy.each do |processor|
199
- if processor.alive? && t = @threads.delete(processor.object_id)
200
- t.raise Shutdown
201
- end
202
- end
203
-
204
- @finished.signal
205
- end
101
+ # Re-enqueue unfinished jobs
102
+ # NOTE: You may notice that we may push a job back to redis before
103
+ # the thread is terminated. This is ok because Sidekiq's
104
+ # contract says that jobs are run AT LEAST once. Process termination
105
+ # is delayed until we're certain the jobs are back in Redis because
106
+ # it is worse to lose a job than to run it twice.
107
+ capsule.fetcher.bulk_requeue(jobs)
206
108
  end
207
- end
208
109
 
209
- def dispatch
210
- return if stopped?
211
- # This is a safety check to ensure we haven't leaked
212
- # processors somehow.
213
- raise "BUG: No processors, cannot continue!" if @ready.empty? && @busy.empty?
214
- raise "No ready processor!?" if @ready.empty?
110
+ cleanup.each do |processor|
111
+ processor.kill
112
+ end
215
113
 
216
- @fetcher.async.fetch
114
+ # when this method returns, we immediately call `exit` which may not give
115
+ # the remaining threads time to run `ensure` blocks, etc. We pause here up
116
+ # to 3 seconds to give threads a minimal amount of time to run `ensure` blocks.
117
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + 3
118
+ wait_for(deadline) { @workers.empty? }
217
119
  end
218
120
 
219
- def shutdown
220
- requeue
221
- @finished.signal
222
- end
121
+ # hack for quicker development / testing environment #2774
122
+ PAUSE_TIME = $stdout.tty? ? 0.1 : 0.5
223
123
 
224
- def requeue
225
- # Re-enqueue terminated jobs
226
- # NOTE: You may notice that we may push a job back to redis before
227
- # the worker thread is terminated. This is ok because Sidekiq's
228
- # contract says that jobs are run AT LEAST once. Process termination
229
- # is delayed until we're certain the jobs are back in Redis because
230
- # it is worse to lose a job than to run it twice.
231
- Sidekiq::Fetcher.strategy.bulk_requeue(@in_progress.values, @options)
232
- @in_progress.clear
124
+ # Wait for the orblock to be true or the deadline passed.
125
+ def wait_for(deadline, &condblock)
126
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
127
+ while remaining > PAUSE_TIME
128
+ return if condblock.call
129
+ sleep PAUSE_TIME
130
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
131
+ end
233
132
  end
234
133
  end
235
134
  end
@@ -0,0 +1,156 @@
1
+ require "sidekiq"
2
+ require "date"
3
+ require "set"
4
+
5
+ require "sidekiq/metrics/shared"
6
+
7
+ module Sidekiq
8
+ module Metrics
9
+ # Allows caller to query for Sidekiq execution metrics within Redis.
10
+ # Caller sets a set of attributes to act as filters. {#fetch} will call
11
+ # Redis and return a Hash of results.
12
+ #
13
+ # NB: all metrics and times/dates are UTC only. We specifically do not
14
+ # support timezones.
15
+ class Query
16
+ def initialize(pool: nil, now: Time.now)
17
+ @time = now.utc
18
+ @pool = pool || Sidekiq.default_configuration.redis_pool
19
+ @klass = nil
20
+ end
21
+
22
+ # Get metric data for all jobs from the last hour
23
+ # +class_filter+: return only results for classes matching filter
24
+ def top_jobs(class_filter: nil, minutes: 60)
25
+ result = Result.new
26
+
27
+ time = @time
28
+ redis_results = @pool.with do |conn|
29
+ conn.pipelined do |pipe|
30
+ minutes.times do |idx|
31
+ key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
32
+ pipe.hgetall key
33
+ result.prepend_bucket time
34
+ time -= 60
35
+ end
36
+ end
37
+ end
38
+
39
+ time = @time
40
+ redis_results.each do |hash|
41
+ hash.each do |k, v|
42
+ kls, metric = k.split("|")
43
+ next if class_filter && !class_filter.match?(kls)
44
+ result.job_results[kls].add_metric metric, time, v.to_i
45
+ end
46
+ time -= 60
47
+ end
48
+
49
+ result.marks = fetch_marks(result.starts_at..result.ends_at)
50
+
51
+ result
52
+ end
53
+
54
+ def for_job(klass, minutes: 60)
55
+ result = Result.new
56
+
57
+ time = @time
58
+ redis_results = @pool.with do |conn|
59
+ conn.pipelined do |pipe|
60
+ minutes.times do |idx|
61
+ key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
62
+ pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
63
+ result.prepend_bucket time
64
+ time -= 60
65
+ end
66
+ end
67
+ end
68
+
69
+ time = @time
70
+ @pool.with do |conn|
71
+ redis_results.each do |(ms, p, f)|
72
+ result.job_results[klass].add_metric "ms", time, ms.to_i if ms
73
+ result.job_results[klass].add_metric "p", time, p.to_i if p
74
+ result.job_results[klass].add_metric "f", time, f.to_i if f
75
+ result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse
76
+ time -= 60
77
+ end
78
+ end
79
+
80
+ result.marks = fetch_marks(result.starts_at..result.ends_at)
81
+
82
+ result
83
+ end
84
+
85
+ class Result < Struct.new(:starts_at, :ends_at, :size, :buckets, :job_results, :marks)
86
+ def initialize
87
+ super
88
+ self.buckets = []
89
+ self.marks = []
90
+ self.job_results = Hash.new { |h, k| h[k] = JobResult.new }
91
+ end
92
+
93
+ def prepend_bucket(time)
94
+ buckets.unshift time.strftime("%H:%M")
95
+ self.ends_at ||= time
96
+ self.starts_at = time
97
+ end
98
+ end
99
+
100
+ class JobResult < Struct.new(:series, :hist, :totals)
101
+ def initialize
102
+ super
103
+ self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
104
+ self.hist = Hash.new { |h, k| h[k] = [] }
105
+ self.totals = Hash.new(0)
106
+ end
107
+
108
+ def add_metric(metric, time, value)
109
+ totals[metric] += value
110
+ series[metric][time.strftime("%H:%M")] += value
111
+
112
+ # Include timing measurements in seconds for convenience
113
+ add_metric("s", time, value / 1000.0) if metric == "ms"
114
+ end
115
+
116
+ def add_hist(time, hist_result)
117
+ hist[time.strftime("%H:%M")] = hist_result
118
+ end
119
+
120
+ def total_avg(metric = "ms")
121
+ completed = totals["p"] - totals["f"]
122
+ return 0 if completed.zero?
123
+ totals[metric].to_f / completed
124
+ end
125
+
126
+ def series_avg(metric = "ms")
127
+ series[metric].each_with_object(Hash.new(0)) do |(bucket, value), result|
128
+ completed = series.dig("p", bucket) - series.dig("f", bucket)
129
+ result[bucket] = (completed == 0) ? 0 : value.to_f / completed
130
+ end
131
+ end
132
+ end
133
+
134
+ class MarkResult < Struct.new(:time, :label)
135
+ def bucket
136
+ time.strftime("%H:%M")
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def fetch_marks(time_range)
143
+ [].tap do |result|
144
+ marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
145
+
146
+ marks.each do |timestamp, label|
147
+ time = Time.parse(timestamp)
148
+ if time_range.cover? time
149
+ result << MarkResult.new(time, label)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,95 @@
1
+ require "concurrent"
2
+
3
+ module Sidekiq
4
+ module Metrics
5
+ # This is the only dependency on concurrent-ruby in Sidekiq but it's
6
+ # mandatory for thread-safety until MRI supports atomic operations on values.
7
+ Counter = ::Concurrent::AtomicFixnum
8
+
9
+ # Implements space-efficient but statistically useful histogram storage.
10
+ # A precise time histogram stores every time. Instead we break times into a set of
11
+ # known buckets and increment counts of the associated time bucket. Even if we call
12
+ # the histogram a million times, we'll still only store 26 buckets.
13
+ # NB: needs to be thread-safe or resiliant to races.
14
+ #
15
+ # To store this data, we use Redis' BITFIELD command to store unsigned 16-bit counters
16
+ # per bucket per klass per minute. It's unlikely that most people will be executing more
17
+ # than 1000 job/sec for a full minute of a specific type.
18
+ class Histogram
19
+ include Enumerable
20
+
21
+ # This number represents the maximum milliseconds for this bucket.
22
+ # 20 means all job executions up to 20ms, e.g. if a job takes
23
+ # 280ms, it'll increment bucket[7]. Note we can track job executions
24
+ # up to about 5.5 minutes. After that, it's assumed you're probably
25
+ # not too concerned with its performance.
26
+ BUCKET_INTERVALS = [
27
+ 20, 30, 45, 65, 100,
28
+ 150, 225, 335, 500, 750,
29
+ 1100, 1700, 2500, 3800, 5750,
30
+ 8500, 13000, 20000, 30000, 45000,
31
+ 65000, 100000, 150000, 225000, 335000,
32
+ 1e20 # the "maybe your job is too long" bucket
33
+ ].freeze
34
+ LABELS = [
35
+ "20ms", "30ms", "45ms", "65ms", "100ms",
36
+ "150ms", "225ms", "335ms", "500ms", "750ms",
37
+ "1.1s", "1.7s", "2.5s", "3.8s", "5.75s",
38
+ "8.5s", "13s", "20s", "30s", "45s",
39
+ "65s", "100s", "150s", "225s", "335s",
40
+ "Slow"
41
+ ].freeze
42
+ FETCH = "GET u16 #0 GET u16 #1 GET u16 #2 GET u16 #3 \
43
+ GET u16 #4 GET u16 #5 GET u16 #6 GET u16 #7 \
44
+ GET u16 #8 GET u16 #9 GET u16 #10 GET u16 #11 \
45
+ GET u16 #12 GET u16 #13 GET u16 #14 GET u16 #15 \
46
+ GET u16 #16 GET u16 #17 GET u16 #18 GET u16 #19 \
47
+ GET u16 #20 GET u16 #21 GET u16 #22 GET u16 #23 \
48
+ GET u16 #24 GET u16 #25".split
49
+ HISTOGRAM_TTL = 8 * 60 * 60
50
+
51
+ def each
52
+ buckets.each { |counter| yield counter.value }
53
+ end
54
+
55
+ def label(idx)
56
+ LABELS[idx]
57
+ end
58
+
59
+ attr_reader :buckets
60
+ def initialize(klass)
61
+ @klass = klass
62
+ @buckets = Array.new(BUCKET_INTERVALS.size) { Counter.new }
63
+ end
64
+
65
+ def record_time(ms)
66
+ index_to_use = BUCKET_INTERVALS.each_index do |idx|
67
+ break idx if ms < BUCKET_INTERVALS[idx]
68
+ end
69
+
70
+ @buckets[index_to_use].increment
71
+ end
72
+
73
+ def fetch(conn, now = Time.now)
74
+ window = now.utc.strftime("%d-%H:%-M")
75
+ key = "#{@klass}-#{window}"
76
+ conn.bitfield_ro(key, *FETCH)
77
+ end
78
+
79
+ def persist(conn, now = Time.now)
80
+ buckets, @buckets = @buckets, []
81
+ window = now.utc.strftime("%d-%H:%-M")
82
+ key = "#{@klass}-#{window}"
83
+ cmd = [key, "OVERFLOW", "SAT"]
84
+ buckets.each_with_index do |counter, idx|
85
+ val = counter.value
86
+ cmd << "INCRBY" << "u16" << "##{idx}" << val.to_s if val > 0
87
+ end
88
+
89
+ conn.bitfield(*cmd) if cmd.size > 3
90
+ conn.expire(key, HISTOGRAM_TTL)
91
+ key
92
+ end
93
+ end
94
+ end
95
+ end