sidekiq 2.15.1 → 4.2.10

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 (187) hide show
  1. checksums.yaml +7 -0
  2. data/.github/contributing.md +32 -0
  3. data/.github/issue_template.md +9 -0
  4. data/.gitignore +1 -0
  5. data/.travis.yml +16 -17
  6. data/3.0-Upgrade.md +70 -0
  7. data/4.0-Upgrade.md +53 -0
  8. data/COMM-LICENSE +56 -44
  9. data/Changes.md +644 -1
  10. data/Ent-Changes.md +173 -0
  11. data/Gemfile +27 -0
  12. data/LICENSE +1 -1
  13. data/Pro-2.0-Upgrade.md +138 -0
  14. data/Pro-3.0-Upgrade.md +44 -0
  15. data/Pro-Changes.md +457 -3
  16. data/README.md +46 -29
  17. data/Rakefile +6 -3
  18. data/bin/sidekiq +4 -0
  19. data/bin/sidekiqctl +41 -20
  20. data/bin/sidekiqload +154 -0
  21. data/code_of_conduct.md +50 -0
  22. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  25. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  26. data/lib/sidekiq.rb +141 -29
  27. data/lib/sidekiq/api.rb +540 -106
  28. data/lib/sidekiq/cli.rb +131 -71
  29. data/lib/sidekiq/client.rb +168 -96
  30. data/lib/sidekiq/core_ext.rb +36 -8
  31. data/lib/sidekiq/exception_handler.rb +20 -28
  32. data/lib/sidekiq/extensions/action_mailer.rb +25 -5
  33. data/lib/sidekiq/extensions/active_record.rb +8 -4
  34. data/lib/sidekiq/extensions/class_methods.rb +9 -5
  35. data/lib/sidekiq/extensions/generic_proxy.rb +1 -0
  36. data/lib/sidekiq/fetch.rb +45 -101
  37. data/lib/sidekiq/launcher.rb +144 -30
  38. data/lib/sidekiq/logging.rb +69 -12
  39. data/lib/sidekiq/manager.rb +90 -140
  40. data/lib/sidekiq/middleware/chain.rb +18 -5
  41. data/lib/sidekiq/middleware/i18n.rb +9 -2
  42. data/lib/sidekiq/middleware/server/active_record.rb +1 -1
  43. data/lib/sidekiq/middleware/server/logging.rb +11 -11
  44. data/lib/sidekiq/middleware/server/retry_jobs.rb +98 -44
  45. data/lib/sidekiq/paginator.rb +20 -8
  46. data/lib/sidekiq/processor.rb +157 -96
  47. data/lib/sidekiq/rails.rb +109 -5
  48. data/lib/sidekiq/redis_connection.rb +70 -24
  49. data/lib/sidekiq/scheduled.rb +122 -50
  50. data/lib/sidekiq/testing.rb +171 -31
  51. data/lib/sidekiq/testing/inline.rb +1 -0
  52. data/lib/sidekiq/util.rb +31 -5
  53. data/lib/sidekiq/version.rb +2 -1
  54. data/lib/sidekiq/web.rb +136 -263
  55. data/lib/sidekiq/web/action.rb +93 -0
  56. data/lib/sidekiq/web/application.rb +336 -0
  57. data/lib/sidekiq/web/helpers.rb +278 -0
  58. data/lib/sidekiq/web/router.rb +100 -0
  59. data/lib/sidekiq/worker.rb +40 -7
  60. data/sidekiq.gemspec +18 -14
  61. data/web/assets/images/favicon.ico +0 -0
  62. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  63. data/web/assets/javascripts/application.js +67 -19
  64. data/web/assets/javascripts/dashboard.js +138 -29
  65. data/web/assets/stylesheets/application.css +267 -406
  66. data/web/assets/stylesheets/bootstrap.css +4 -8
  67. data/web/locales/cs.yml +78 -0
  68. data/web/locales/da.yml +9 -1
  69. data/web/locales/de.yml +18 -9
  70. data/web/locales/el.yml +68 -0
  71. data/web/locales/en.yml +19 -4
  72. data/web/locales/es.yml +10 -1
  73. data/web/locales/fa.yml +79 -0
  74. data/web/locales/fr.yml +50 -32
  75. data/web/locales/hi.yml +75 -0
  76. data/web/locales/it.yml +27 -18
  77. data/web/locales/ja.yml +27 -12
  78. data/web/locales/ko.yml +8 -3
  79. data/web/locales/{no.yml → nb.yml} +19 -5
  80. data/web/locales/nl.yml +8 -3
  81. data/web/locales/pl.yml +0 -1
  82. data/web/locales/pt-br.yml +11 -4
  83. data/web/locales/pt.yml +8 -1
  84. data/web/locales/ru.yml +39 -21
  85. data/web/locales/sv.yml +68 -0
  86. data/web/locales/ta.yml +75 -0
  87. data/web/locales/uk.yml +76 -0
  88. data/web/locales/zh-cn.yml +68 -0
  89. data/web/locales/zh-tw.yml +68 -0
  90. data/web/views/_footer.erb +17 -0
  91. data/web/views/_job_info.erb +72 -60
  92. data/web/views/_nav.erb +58 -25
  93. data/web/views/_paging.erb +5 -5
  94. data/web/views/_poll_link.erb +7 -0
  95. data/web/views/_summary.erb +20 -14
  96. data/web/views/busy.erb +94 -0
  97. data/web/views/dashboard.erb +34 -21
  98. data/web/views/dead.erb +34 -0
  99. data/web/views/layout.erb +8 -30
  100. data/web/views/morgue.erb +75 -0
  101. data/web/views/queue.erb +37 -30
  102. data/web/views/queues.erb +26 -20
  103. data/web/views/retries.erb +60 -47
  104. data/web/views/retry.erb +23 -19
  105. data/web/views/scheduled.erb +39 -35
  106. data/web/views/scheduled_job_info.erb +2 -1
  107. metadata +152 -195
  108. data/Contributing.md +0 -29
  109. data/config.ru +0 -18
  110. data/lib/sidekiq/actor.rb +0 -7
  111. data/lib/sidekiq/capistrano.rb +0 -54
  112. data/lib/sidekiq/yaml_patch.rb +0 -21
  113. data/test/config.yml +0 -11
  114. data/test/env_based_config.yml +0 -11
  115. data/test/fake_env.rb +0 -0
  116. data/test/helper.rb +0 -42
  117. data/test/test_api.rb +0 -341
  118. data/test/test_cli.rb +0 -326
  119. data/test/test_client.rb +0 -211
  120. data/test/test_exception_handler.rb +0 -124
  121. data/test/test_extensions.rb +0 -105
  122. data/test/test_fetch.rb +0 -44
  123. data/test/test_manager.rb +0 -83
  124. data/test/test_middleware.rb +0 -135
  125. data/test/test_processor.rb +0 -160
  126. data/test/test_redis_connection.rb +0 -97
  127. data/test/test_retry.rb +0 -306
  128. data/test/test_scheduled.rb +0 -86
  129. data/test/test_scheduling.rb +0 -47
  130. data/test/test_sidekiq.rb +0 -37
  131. data/test/test_testing.rb +0 -82
  132. data/test/test_testing_fake.rb +0 -265
  133. data/test/test_testing_inline.rb +0 -92
  134. data/test/test_util.rb +0 -18
  135. data/test/test_web.rb +0 -372
  136. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  137. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  138. data/web/assets/images/status/active.png +0 -0
  139. data/web/assets/images/status/idle.png +0 -0
  140. data/web/assets/javascripts/locales/README.md +0 -27
  141. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  142. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  143. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  144. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  145. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  146. data/web/assets/javascripts/locales/jquery.timeago.cz.js +0 -18
  147. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  148. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  149. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  150. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  151. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  152. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  153. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  154. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  155. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  156. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  157. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  158. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  159. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  160. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  161. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  162. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  163. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  164. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  165. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  166. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  167. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  168. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  169. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  170. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  171. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  172. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  173. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  174. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  175. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  176. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  177. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  178. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  179. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  180. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  181. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  182. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  183. data/web/assets/javascripts/locales/jquery.timeago.zh-CN.js +0 -20
  184. data/web/assets/javascripts/locales/jquery.timeago.zh-TW.js +0 -20
  185. data/web/views/_poll.erb +0 -14
  186. data/web/views/_workers.erb +0 -29
  187. data/web/views/index.erb +0 -16
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Sidekiq
2
3
  # Middleware is code configured to run before/after
3
4
  # a message is processed. It is patterned after Rack
@@ -49,13 +50,16 @@ module Sidekiq
49
50
  # end
50
51
  # end
51
52
  #
52
- # This is an example of a minimal client middleware:
53
+ # This is an example of a minimal client middleware, note
54
+ # the method must return the result or the job will not push
55
+ # to Redis:
53
56
  #
54
57
  # class MyClientHook
55
- # def call(worker_class, msg, queue)
58
+ # def call(worker_class, msg, queue, redis_pool)
56
59
  # puts "Before push"
57
- # yield
60
+ # result = yield
58
61
  # puts "After push"
62
+ # result
59
63
  # end
60
64
  # end
61
65
  #
@@ -64,6 +68,10 @@ module Sidekiq
64
68
  include Enumerable
65
69
  attr_reader :entries
66
70
 
71
+ def initialize_copy(copy)
72
+ copy.instance_variable_set(:@entries, entries.dup)
73
+ end
74
+
67
75
  def each(&block)
68
76
  entries.each(&block)
69
77
  end
@@ -82,6 +90,11 @@ module Sidekiq
82
90
  entries << Entry.new(klass, *args)
83
91
  end
84
92
 
93
+ def prepend(klass, *args)
94
+ remove(klass) if exists?(klass)
95
+ entries.insert(0, Entry.new(klass, *args))
96
+ end
97
+
85
98
  def insert_before(oldklass, newklass, *args)
86
99
  i = entries.index { |entry| entry.klass == newklass }
87
100
  new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
@@ -108,11 +121,11 @@ module Sidekiq
108
121
  entries.clear
109
122
  end
110
123
 
111
- def invoke(*args, &final_action)
124
+ def invoke(*args)
112
125
  chain = retrieve.dup
113
126
  traverse_chain = lambda do
114
127
  if chain.empty?
115
- final_action.call
128
+ yield
116
129
  else
117
130
  chain.shift.call(*args, &traverse_chain)
118
131
  end
@@ -1,8 +1,15 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Simple middleware to save the current locale and restore it when the job executes.
4
+ # Use it by requiring it in your initializer:
5
+ #
6
+ # require 'sidekiq/middleware/i18n'
7
+ #
1
8
  module Sidekiq::Middleware::I18n
2
9
  # Get the current locale and store it in the message
3
10
  # to be sent to Sidekiq.
4
11
  class Client
5
- def call(worker_class, msg, queue)
12
+ def call(worker_class, msg, queue, redis_pool)
6
13
  msg['locale'] ||= I18n.locale
7
14
  yield
8
15
  end
@@ -14,7 +21,7 @@ module Sidekiq::Middleware::I18n
14
21
  I18n.locale = msg['locale'] || I18n.default_locale
15
22
  yield
16
23
  ensure
17
- I18n.locale = nil
24
+ I18n.locale = I18n.default_locale
18
25
  end
19
26
  end
20
27
  end
@@ -5,7 +5,7 @@ module Sidekiq
5
5
  def call(*args)
6
6
  yield
7
7
  ensure
8
- ::ActiveRecord::Base.clear_active_connections! if defined?(::ActiveRecord)
8
+ ::ActiveRecord::Base.clear_active_connections!
9
9
  end
10
10
  end
11
11
  end
@@ -4,21 +4,21 @@ module Sidekiq
4
4
  class Logging
5
5
 
6
6
  def call(worker, item, queue)
7
- Sidekiq::Logging.with_context("#{worker.class.to_s} JID-#{item['jid']}") do
8
- begin
9
- start = Time.now
10
- logger.info { "start" }
11
- yield
12
- logger.info { "done: #{elapsed(start)} sec" }
13
- rescue Exception
14
- logger.info { "fail: #{elapsed(start)} sec" }
15
- raise
16
- end
7
+ begin
8
+ start = Time.now
9
+ logger.info("start".freeze)
10
+ yield
11
+ logger.info("done: #{elapsed(start)} sec")
12
+ rescue Exception
13
+ logger.info("fail: #{elapsed(start)} sec")
14
+ raise
17
15
  end
18
16
  end
19
17
 
18
+ private
19
+
20
20
  def elapsed(start)
21
- (Time.now - start).to_f.round(3)
21
+ (Time.now - start).round(3)
22
22
  end
23
23
 
24
24
  def logger
@@ -1,4 +1,5 @@
1
1
  require 'sidekiq/scheduled'
2
+ require 'sidekiq/api'
2
3
 
3
4
  module Sidekiq
4
5
  module Middleware
@@ -6,21 +7,22 @@ module Sidekiq
6
7
  ##
7
8
  # Automatically retry jobs that fail in Sidekiq.
8
9
  # Sidekiq's retry support assumes a typical development lifecycle:
9
- # 0. push some code changes with a bug in it
10
- # 1. bug causes message processing to fail, sidekiq's middleware captures
11
- # the message and pushes it onto a retry queue
12
- # 2. sidekiq retries messages in the retry queue multiple times with
13
- # an exponential delay, the message continues to fail
14
- # 3. after a few days, a developer deploys a fix. the message is
15
- # reprocessed successfully.
16
- # 4. if 3 never happens, sidekiq will eventually give up and throw the
17
- # message away. If the worker defines a method called 'retries_exhausted',
18
- # this will be called before throwing the message away. If the
19
- # 'retries_exhausted' method throws an exception, it's dropped and logged.
20
10
  #
21
- # A message looks like:
11
+ # 0. Push some code changes with a bug in it.
12
+ # 1. Bug causes job processing to fail, Sidekiq's middleware captures
13
+ # the job and pushes it onto a retry queue.
14
+ # 2. Sidekiq retries jobs in the retry queue multiple times with
15
+ # an exponential delay, the job continues to fail.
16
+ # 3. After a few days, a developer deploys a fix. The job is
17
+ # reprocessed successfully.
18
+ # 4. Once retries are exhausted, Sidekiq will give up and move the
19
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
20
+ # manually in the Web UI.
21
+ # 5. After 6 months on the DJQ, Sidekiq will discard the job.
22
22
  #
23
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'] }
23
+ # A job looks like:
24
+ #
25
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
24
26
  #
25
27
  # The 'retry' option also accepts a number (in place of 'true'):
26
28
  #
@@ -29,7 +31,7 @@ module Sidekiq
29
31
  # The job will be retried this number of times before giving up. (If simply
30
32
  # 'true', Sidekiq retries 25 times)
31
33
  #
32
- # We'll add a bit more data to the message to support retries:
34
+ # We'll add a bit more data to the job to support retries:
33
35
  #
34
36
  # * 'queue' - the queue to use
35
37
  # * 'retry_count' - number of times we've retried so far.
@@ -37,18 +39,28 @@ module Sidekiq
37
39
  # * 'error_class' - the exception class
38
40
  # * 'failed_at' - the first time it failed
39
41
  # * 'retried_at' - the last time it was retried
42
+ # * 'backtrace' - the number of lines of error backtrace to store
40
43
  #
41
- # We don't store the backtrace as that can add a lot of overhead
42
- # to the message and everyone is using Airbrake, right?
44
+ # We don't store the backtrace by default as that can add a lot of overhead
45
+ # to the job and everyone is using an error service, right?
43
46
  #
44
- # The default number of retry attempts is 25. You can pass a value for the
45
- # number of retry attempts when adding the middleware using the options hash:
47
+ # The default number of retry attempts is 25 which works out to about 3 weeks
48
+ # of retries. You can pass a value for the max number of retry attempts when
49
+ # adding the middleware using the options hash:
46
50
  #
47
51
  # Sidekiq.configure_server do |config|
48
52
  # config.server_middleware do |chain|
49
- # chain.add Middleware::Server::RetryJobs, {:max_retries => 7}
53
+ # chain.add Sidekiq::Middleware::Server::RetryJobs, :max_retries => 7
50
54
  # end
51
55
  # end
56
+ #
57
+ # or limit the number of retries for a particular worker with:
58
+ #
59
+ # class MyWorker
60
+ # include Sidekiq::Worker
61
+ # sidekiq_options :retry => 10
62
+ # end
63
+ #
52
64
  class RetryJobs
53
65
  include Sidekiq::Util
54
66
 
@@ -64,7 +76,16 @@ module Sidekiq
64
76
  # ignore, will be pushed back onto queue during hard_shutdown
65
77
  raise
66
78
  rescue Exception => e
79
+ # ignore, will be pushed back onto queue during hard_shutdown
80
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
81
+
67
82
  raise e unless msg['retry']
83
+ attempt_retry(worker, msg, queue, e)
84
+ end
85
+
86
+ private
87
+
88
+ def attempt_retry(worker, msg, queue, exception)
68
89
  max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
69
90
 
70
91
  msg['queue'] = if msg['retry_queue']
@@ -72,26 +93,35 @@ module Sidekiq
72
93
  else
73
94
  queue
74
95
  end
75
- msg['error_message'] = e.message
76
- msg['error_class'] = e.class.name
96
+
97
+ # App code can stuff all sorts of crazy binary data into the error message
98
+ # that won't convert to JSON.
99
+ m = exception.message.to_s[0, 10_000]
100
+ if m.respond_to?(:scrub!)
101
+ m.force_encoding("utf-8")
102
+ m.scrub!
103
+ end
104
+
105
+ msg['error_message'] = m
106
+ msg['error_class'] = exception.class.name
77
107
  count = if msg['retry_count']
78
- msg['retried_at'] = Time.now.utc
108
+ msg['retried_at'] = Time.now.to_f
79
109
  msg['retry_count'] += 1
80
110
  else
81
- msg['failed_at'] = Time.now.utc
111
+ msg['failed_at'] = Time.now.to_f
82
112
  msg['retry_count'] = 0
83
113
  end
84
114
 
85
115
  if msg['backtrace'] == true
86
- msg['error_backtrace'] = e.backtrace
87
- elsif msg['backtrace'] == false
116
+ msg['error_backtrace'] = exception.backtrace
117
+ elsif !msg['backtrace']
88
118
  # do nothing
89
119
  elsif msg['backtrace'].to_i != 0
90
- msg['error_backtrace'] = e.backtrace[0..msg['backtrace'].to_i]
120
+ msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
91
121
  end
92
122
 
93
123
  if count < max_retry_attempts
94
- delay = delay_for(worker, count)
124
+ delay = delay_for(worker, count, exception)
95
125
  logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
96
126
  retry_at = Time.now.to_f + delay
97
127
  payload = Sidekiq.dump_json(msg)
@@ -100,35 +130,47 @@ module Sidekiq
100
130
  end
101
131
  else
102
132
  # Goodbye dear message, you (re)tried your best I'm sure.
103
- retries_exhausted(worker, msg)
133
+ retries_exhausted(worker, msg, exception)
104
134
  end
105
135
 
106
- raise e
136
+ raise exception
107
137
  end
108
138
 
109
- def retries_exhausted(worker, msg)
110
- logger.debug { "Dropping message after hitting the retry maximum: #{msg}" }
111
- if worker.respond_to?(:retries_exhausted)
112
- logger.warn { "Defining #{worker.class.name}#retries_exhausted as a method is deprecated, use `sidekiq_retries_exhausted` callback instead http://git.io/Ijju8g" }
113
- worker.retries_exhausted(*msg['args'])
114
- elsif worker.sidekiq_retries_exhausted_block?
115
- worker.sidekiq_retries_exhausted_block.call(msg)
139
+ def retries_exhausted(worker, msg, exception)
140
+ logger.debug { "Retries exhausted for job" }
141
+ begin
142
+ block = worker.sidekiq_retries_exhausted_block || Sidekiq.default_retries_exhausted
143
+ block.call(msg, exception) if block
144
+ rescue => e
145
+ handle_exception(e, { context: "Error calling retries_exhausted for #{worker.class}", job: msg })
116
146
  end
117
147
 
118
- rescue Exception => e
119
- handle_exception(e, { :context => "Error calling retries_exhausted" })
148
+ send_to_morgue(msg) unless msg['dead'] == false
149
+ end
150
+
151
+ def send_to_morgue(msg)
152
+ Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
153
+ payload = Sidekiq.dump_json(msg)
154
+ now = Time.now.to_f
155
+ Sidekiq.redis do |conn|
156
+ conn.multi do
157
+ conn.zadd('dead', now, payload)
158
+ conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
159
+ conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
160
+ end
161
+ end
120
162
  end
121
163
 
122
164
  def retry_attempts_from(msg_retry, default)
123
- if msg_retry.is_a?(Fixnum)
165
+ if msg_retry.is_a?(Integer)
124
166
  msg_retry
125
167
  else
126
168
  default
127
169
  end
128
170
  end
129
171
 
130
- def delay_for(worker, count)
131
- worker.sidekiq_retry_in_block? && retry_in(worker, count) || seconds_to_delay(count)
172
+ def delay_for(worker, count, exception)
173
+ worker.sidekiq_retry_in_block? && retry_in(worker, count, exception) || seconds_to_delay(count)
132
174
  end
133
175
 
134
176
  # delayed_job uses the same basic formula
@@ -136,15 +178,27 @@ module Sidekiq
136
178
  (count ** 4) + 15 + (rand(30)*(count+1))
137
179
  end
138
180
 
139
- def retry_in(worker, count)
181
+ def retry_in(worker, count, exception)
140
182
  begin
141
- worker.sidekiq_retry_in_block.call(count)
183
+ worker.sidekiq_retry_in_block.call(count, exception).to_i
142
184
  rescue Exception => e
143
- logger.error { "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default: #{e.message}"}
185
+ handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
144
186
  nil
145
187
  end
146
188
  end
147
189
 
190
+ def exception_caused_by_shutdown?(e, checked_causes = [])
191
+ # In Ruby 2.1.0 only, check if exception is a result of shutdown.
192
+ return false unless defined?(e.cause)
193
+
194
+ # Handle circular causes
195
+ checked_causes << e.object_id
196
+ return false if checked_causes.include?(e.cause.object_id)
197
+
198
+ e.cause.instance_of?(Sidekiq::Shutdown) ||
199
+ exception_caused_by_shutdown?(e.cause, checked_causes)
200
+ end
201
+
148
202
  end
149
203
  end
150
204
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  module Sidekiq
2
3
  module Paginator
3
- def page(key, pageidx=1, page_size=25)
4
+
5
+ def page(key, pageidx=1, page_size=25, opts=nil)
4
6
  current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
5
7
  pageidx = current_page - 1
6
8
  total_size = 0
@@ -13,19 +15,29 @@ module Sidekiq
13
15
 
14
16
  case type
15
17
  when 'zset'
16
- total_size = conn.zcard(key)
17
- items = conn.zrange(key, starting, ending, :with_scores => true)
18
+ rev = opts && opts[:reverse]
19
+ total_size, items = conn.multi do
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
+ end
27
+ [current_page, total_size, items]
18
28
  when 'list'
19
- total_size = conn.llen(key)
20
- items = conn.lrange(key, starting, ending)
29
+ total_size, items = conn.multi do
30
+ conn.llen(key)
31
+ conn.lrange(key, starting, ending)
32
+ end
33
+ [current_page, total_size, items]
21
34
  when 'none'
22
- return [1, 0, []]
35
+ [1, 0, []]
23
36
  else
24
37
  raise "can't page a #{type}"
25
38
  end
26
39
  end
27
-
28
- [current_page, total_size, items]
29
40
  end
41
+
30
42
  end
31
43
  end
@@ -1,140 +1,201 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq/util'
2
- require 'sidekiq/actor'
3
-
4
- require 'sidekiq/middleware/server/active_record'
5
- require 'sidekiq/middleware/server/retry_jobs'
6
- require 'sidekiq/middleware/server/logging'
3
+ require 'sidekiq/fetch'
4
+ require 'thread'
5
+ require 'concurrent/map'
6
+ require 'concurrent/atomic/atomic_fixnum'
7
7
 
8
8
  module Sidekiq
9
9
  ##
10
- # The Processor receives a message from the Manager and actually
11
- # processes it. It instantiates the worker, runs the middleware
12
- # chain and then calls Sidekiq::Worker#perform.
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
+ #
13
25
  class Processor
14
- STATS_TIMEOUT = 180 * 24 * 60 * 60
15
26
 
16
27
  include Util
17
- include Actor
18
28
 
19
- def self.default_middleware
20
- Middleware::Chain.new do |m|
21
- m.add Middleware::Server::Logging
22
- m.add Middleware::Server::RetryJobs
23
- m.add Middleware::Server::ActiveRecord
24
- end
29
+ attr_reader :thread
30
+ attr_reader :job
31
+
32
+ def initialize(mgr)
33
+ @mgr = mgr
34
+ @down = false
35
+ @done = false
36
+ @job = nil
37
+ @thread = nil
38
+ @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
39
+ @reloader = Sidekiq.options[:reloader]
40
+ @executor = Sidekiq.options[:executor]
25
41
  end
26
42
 
27
- attr_accessor :proxy_id
28
-
29
- def initialize(boss)
30
- @boss = boss
43
+ def terminate(wait=false)
44
+ @done = true
45
+ return if !@thread
46
+ @thread.value if wait
31
47
  end
32
48
 
33
- def process(work)
34
- msgstr = work.message
35
- queue = work.queue_name
49
+ def kill(wait=false)
50
+ @done = true
51
+ return if !@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
36
60
 
37
- do_defer do
38
- @boss.async.real_thread(proxy_id, Thread.current)
61
+ def start
62
+ @thread ||= safe_thread("processor", &method(:run))
63
+ end
39
64
 
40
- begin
41
- msg = Sidekiq.load_json(msgstr)
42
- klass = msg['class'].constantize
43
- worker = klass.new
44
- worker.jid = msg['jid']
65
+ private unless $TESTING
45
66
 
46
- stats(worker, msg, queue) do
47
- Sidekiq.server_middleware.invoke(worker, msg, queue) do
48
- worker.perform(*cloned(msg['args']))
49
- end
50
- end
51
- rescue Sidekiq::Shutdown
52
- # Had to force kill this job because it didn't finish
53
- # within the timeout.
54
- rescue Exception => ex
55
- handle_exception(ex, msg || { :message => msgstr })
56
- raise
57
- ensure
58
- work.acknowledge
67
+ def run
68
+ begin
69
+ while !@done
70
+ process_one
59
71
  end
72
+ @mgr.processor_stopped(self)
73
+ rescue Sidekiq::Shutdown
74
+ @mgr.processor_stopped(self)
75
+ rescue Exception => ex
76
+ @mgr.processor_died(self, ex)
60
77
  end
61
-
62
- @boss.async.processor_done(current_actor)
63
78
  end
64
79
 
65
- def inspect
66
- "<Processor##{object_id.to_s(16)}>"
80
+ def process_one
81
+ @job = fetch
82
+ process(@job) if @job
83
+ @job = nil
67
84
  end
68
85
 
69
- private
70
-
71
- # We use Celluloid's defer to workaround tiny little
72
- # Fiber stacks (4kb!) in MRI 1.9.
73
- #
74
- # For some reason, Celluloid's thread dispatch, TaskThread,
75
- # is unstable under heavy concurrency but TaskFiber has proven
76
- # itself stable.
77
- NEED_DEFER = (RUBY_ENGINE == 'ruby' && RUBY_VERSION < '2.0.0')
86
+ def get_one
87
+ begin
88
+ work = @strategy.retrieve_work
89
+ (logger.info { "Redis is online, #{Time.now - @down} sec downtime" }; @down = nil) if @down
90
+ work
91
+ rescue Sidekiq::Shutdown
92
+ rescue => ex
93
+ handle_fetch_exception(ex)
94
+ end
95
+ end
78
96
 
79
- def do_defer(&block)
80
- if NEED_DEFER
81
- defer(&block)
97
+ def fetch
98
+ j = get_one
99
+ if j && @done
100
+ j.requeue
101
+ nil
82
102
  else
83
- yield
103
+ j
84
104
  end
85
105
  end
86
106
 
87
- def identity
88
- @str ||= "#{hostname}:#{process_id}-#{Thread.current.object_id}:default"
107
+ def handle_fetch_exception(ex)
108
+ if !@down
109
+ @down = Time.now
110
+ logger.error("Error fetching job: #{ex}")
111
+ ex.backtrace.each do |bt|
112
+ logger.error(bt)
113
+ end
114
+ end
115
+ sleep(1)
116
+ nil
89
117
  end
90
118
 
91
- def stats(worker, msg, queue)
92
- redis do |conn|
93
- conn.multi do
94
- conn.sadd('workers', identity)
95
- conn.setex("worker:#{identity}:started", EXPIRY, Time.now.to_s)
96
- hash = {:queue => queue, :payload => msg, :run_at => Time.now.to_i }
97
- conn.setex("worker:#{identity}", EXPIRY, Sidekiq.dump_json(hash))
119
+ def process(work)
120
+ jobstr = work.job
121
+ queue = work.queue_name
122
+
123
+ ack = false
124
+ begin
125
+ job_hash = Sidekiq.load_json(jobstr)
126
+ @reloader.call do
127
+ klass = job_hash['class'.freeze].constantize
128
+ worker = klass.new
129
+ worker.jid = job_hash['jid'.freeze]
130
+
131
+ stats(worker, job_hash, queue) do
132
+ Sidekiq::Logging.with_context(log_context(job_hash)) do
133
+ ack = true
134
+ Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
135
+ @executor.call do
136
+ # Only ack if we either attempted to start this job or
137
+ # successfully completed it. This prevents us from
138
+ # losing jobs if a middleware raises an exception before yielding
139
+ execute_job(worker, cloned(job_hash['args'.freeze]))
140
+ end
141
+ end
142
+ end
143
+ end
144
+ ack = true
98
145
  end
146
+ rescue Sidekiq::Shutdown
147
+ # Had to force kill this job because it didn't finish
148
+ # within the timeout. Don't acknowledge the work since
149
+ # we didn't properly finish it.
150
+ ack = false
151
+ rescue Exception => ex
152
+ handle_exception(ex, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
153
+ raise
154
+ ensure
155
+ work.acknowledge if ack
99
156
  end
157
+ end
158
+
159
+ # If we're using a wrapper class, like ActiveJob, use the "wrapped"
160
+ # attribute to expose the underlying thing.
161
+ def log_context(item)
162
+ klass = item['wrapped'.freeze] || item['class'.freeze]
163
+ "#{klass} JID-#{item['jid'.freeze]}#{" BID-#{item['bid'.freeze]}" if item['bid'.freeze]}"
164
+ end
165
+
166
+ def execute_job(worker, cloned_args)
167
+ worker.perform(*cloned_args)
168
+ end
169
+
170
+ def thread_identity
171
+ @str ||= Thread.current.object_id.to_s(36)
172
+ end
173
+
174
+ WORKER_STATE = Concurrent::Map.new
175
+ PROCESSED = Concurrent::AtomicFixnum.new
176
+ FAILURE = Concurrent::AtomicFixnum.new
177
+
178
+ def stats(worker, job_hash, queue)
179
+ tid = thread_identity
180
+ WORKER_STATE[tid] = {:queue => queue, :payload => cloned(job_hash), :run_at => Time.now.to_i }
100
181
 
101
182
  begin
102
183
  yield
103
184
  rescue Exception
104
- redis do |conn|
105
- failed = "stat:failed:#{Time.now.utc.to_date}"
106
- result = conn.multi do
107
- conn.incrby("stat:failed", 1)
108
- conn.incrby(failed, 1)
109
- end
110
- conn.expire(failed, STATS_TIMEOUT) if result.last == 1
111
- end
185
+ FAILURE.increment
112
186
  raise
113
187
  ensure
114
- redis do |conn|
115
- processed = "stat:processed:#{Time.now.utc.to_date}"
116
- result = conn.multi do
117
- conn.srem("workers", identity)
118
- conn.del("worker:#{identity}")
119
- conn.del("worker:#{identity}:started")
120
- conn.incrby("stat:processed", 1)
121
- conn.incrby(processed, 1)
122
- end
123
- conn.expire(processed, STATS_TIMEOUT) if result.last == 1
124
- end
188
+ WORKER_STATE.delete(tid)
189
+ PROCESSED.increment
125
190
  end
126
191
  end
127
192
 
128
- # Singleton classes are not clonable.
129
- SINGLETON_CLASSES = [ NilClass, TrueClass, FalseClass, Symbol, Fixnum, Float, Bignum ].freeze
130
-
131
- # Clone the arguments passed to the worker so that if
132
- # the message fails, what is pushed back onto Redis hasn't
193
+ # Deep clone the arguments passed to the worker so that if
194
+ # the job fails, what is pushed back onto Redis hasn't
133
195
  # been mutated by the worker.
134
196
  def cloned(ary)
135
- ary.map do |val|
136
- SINGLETON_CLASSES.include?(val.class) ? val : val.clone
137
- end
197
+ Marshal.load(Marshal.dump(ary))
138
198
  end
199
+
139
200
  end
140
201
  end