sidekiq 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/{Contributing.md → .github/contributing.md} +0 -0
  3. data/.github/issue_template.md +9 -0
  4. data/.gitignore +1 -0
  5. data/.travis.yml +12 -10
  6. data/4.0-Upgrade.md +4 -1
  7. data/5.0-Upgrade.md +56 -0
  8. data/COMM-LICENSE +1 -1
  9. data/Changes.md +236 -7
  10. data/Ent-Changes.md +111 -3
  11. data/Gemfile +7 -6
  12. data/Pro-3.0-Upgrade.md +5 -7
  13. data/Pro-Changes.md +162 -5
  14. data/README.md +31 -21
  15. data/Rakefile +5 -2
  16. data/bin/sidekiq +0 -1
  17. data/bin/sidekiqctl +1 -1
  18. data/bin/sidekiqload +15 -33
  19. data/code_of_conduct.md +50 -0
  20. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +2 -2
  21. data/lib/generators/sidekiq/templates/worker_test.rb.erb +6 -6
  22. data/lib/sidekiq.rb +46 -21
  23. data/lib/sidekiq/api.rb +94 -30
  24. data/lib/sidekiq/cli.rb +38 -16
  25. data/lib/sidekiq/client.rb +32 -26
  26. data/lib/sidekiq/core_ext.rb +14 -0
  27. data/lib/sidekiq/delay.rb +21 -0
  28. data/lib/sidekiq/exception_handler.rb +2 -1
  29. data/lib/sidekiq/extensions/action_mailer.rb +1 -0
  30. data/lib/sidekiq/extensions/active_record.rb +1 -0
  31. data/lib/sidekiq/extensions/class_methods.rb +1 -0
  32. data/lib/sidekiq/extensions/generic_proxy.rb +8 -1
  33. data/lib/sidekiq/fetch.rb +2 -1
  34. data/lib/sidekiq/job_logger.rb +27 -0
  35. data/lib/sidekiq/job_retry.rb +235 -0
  36. data/lib/sidekiq/launcher.rb +42 -33
  37. data/lib/sidekiq/logging.rb +3 -1
  38. data/lib/sidekiq/manager.rb +9 -4
  39. data/lib/sidekiq/middleware/chain.rb +1 -0
  40. data/lib/sidekiq/middleware/i18n.rb +1 -0
  41. data/lib/sidekiq/middleware/server/active_record.rb +9 -0
  42. data/lib/sidekiq/paginator.rb +1 -0
  43. data/lib/sidekiq/processor.rb +73 -19
  44. data/lib/sidekiq/rails.rb +47 -25
  45. data/lib/sidekiq/redis_connection.rb +14 -3
  46. data/lib/sidekiq/scheduled.rb +12 -1
  47. data/lib/sidekiq/testing.rb +65 -13
  48. data/lib/sidekiq/testing/inline.rb +1 -0
  49. data/lib/sidekiq/util.rb +3 -15
  50. data/lib/sidekiq/version.rb +2 -1
  51. data/lib/sidekiq/web.rb +120 -184
  52. data/lib/sidekiq/web/action.rb +89 -0
  53. data/lib/sidekiq/web/application.rb +331 -0
  54. data/lib/sidekiq/{web_helpers.rb → web/helpers.rb} +57 -27
  55. data/lib/sidekiq/web/router.rb +100 -0
  56. data/lib/sidekiq/worker.rb +52 -11
  57. data/sidekiq.gemspec +12 -7
  58. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  59. data/web/assets/javascripts/application.js +24 -20
  60. data/web/assets/javascripts/dashboard.js +11 -13
  61. data/web/assets/stylesheets/application-rtl.css +246 -0
  62. data/web/assets/stylesheets/application.css +362 -5
  63. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  64. data/web/assets/stylesheets/bootstrap.css +4 -8
  65. data/web/locales/ar.yml +80 -0
  66. data/web/locales/cs.yml +11 -1
  67. data/web/locales/de.yml +1 -1
  68. data/web/locales/en.yml +2 -0
  69. data/web/locales/fa.yml +80 -0
  70. data/web/locales/fr.yml +21 -12
  71. data/web/locales/he.yml +79 -0
  72. data/web/locales/ja.yml +19 -10
  73. data/web/locales/ru.yml +3 -0
  74. data/web/locales/ur.yml +80 -0
  75. data/web/views/_footer.erb +2 -2
  76. data/web/views/_job_info.erb +5 -1
  77. data/web/views/_nav.erb +2 -2
  78. data/web/views/_paging.erb +1 -1
  79. data/web/views/busy.erb +14 -9
  80. data/web/views/dashboard.erb +5 -5
  81. data/web/views/dead.erb +1 -1
  82. data/web/views/layout.erb +13 -5
  83. data/web/views/morgue.erb +16 -12
  84. data/web/views/queue.erb +11 -11
  85. data/web/views/queues.erb +3 -3
  86. data/web/views/retries.erb +15 -13
  87. data/web/views/retry.erb +2 -2
  88. data/web/views/scheduled.erb +4 -4
  89. data/web/views/scheduled_job_info.erb +1 -1
  90. metadata +97 -148
  91. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  92. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -206
  93. data/test/config.yml +0 -9
  94. data/test/env_based_config.yml +0 -11
  95. data/test/fake_env.rb +0 -0
  96. data/test/fixtures/en.yml +0 -2
  97. data/test/helper.rb +0 -74
  98. data/test/test_actors.rb +0 -137
  99. data/test/test_api.rb +0 -494
  100. data/test/test_cli.rb +0 -335
  101. data/test/test_client.rb +0 -194
  102. data/test/test_exception_handler.rb +0 -55
  103. data/test/test_extensions.rb +0 -126
  104. data/test/test_fetch.rb +0 -49
  105. data/test/test_launcher.rb +0 -80
  106. data/test/test_logging.rb +0 -34
  107. data/test/test_manager.rb +0 -49
  108. data/test/test_middleware.rb +0 -157
  109. data/test/test_processor.rb +0 -200
  110. data/test/test_rails.rb +0 -21
  111. data/test/test_redis_connection.rb +0 -126
  112. data/test/test_retry.rb +0 -325
  113. data/test/test_scheduled.rb +0 -114
  114. data/test/test_scheduling.rb +0 -49
  115. data/test/test_sidekiq.rb +0 -99
  116. data/test/test_testing.rb +0 -142
  117. data/test/test_testing_fake.rb +0 -331
  118. data/test/test_testing_inline.rb +0 -93
  119. data/test/test_util.rb +0 -16
  120. data/test/test_web.rb +0 -608
  121. data/test/test_web_helpers.rb +0 -53
  122. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  123. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  124. data/web/assets/images/status/active.png +0 -0
  125. data/web/assets/images/status/idle.png +0 -0
  126. data/web/assets/javascripts/locales/README.md +0 -27
  127. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  128. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  129. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  130. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  131. data/web/assets/javascripts/locales/jquery.timeago.cs.js +0 -18
  132. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  133. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  134. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  135. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  136. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  137. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  138. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  139. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  140. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  141. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  142. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  143. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  144. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  145. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  146. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  147. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  148. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  149. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  150. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  151. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  152. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  153. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  154. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  155. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  156. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  157. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  158. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  159. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  160. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  161. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  162. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  163. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  164. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  165. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  166. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  167. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  168. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  169. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +0 -20
  170. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +0 -20
  171. data/web/views/_poll_js.erb +0 -5
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
  $stdout.sync = true
3
4
 
4
5
  require 'yaml'
@@ -42,6 +43,10 @@ module Sidekiq
42
43
  write_pid
43
44
  end
44
45
 
46
+ def jruby?
47
+ defined?(::JRUBY_VERSION)
48
+ end
49
+
45
50
  # Code within this method is not tested because it alters
46
51
  # global process state irreversibly. PRs which improve the
47
52
  # test coverage of Sidekiq::CLI are welcomed.
@@ -50,8 +55,14 @@ module Sidekiq
50
55
  print_banner
51
56
 
52
57
  self_read, self_write = IO.pipe
58
+ sigs = %w(INT TERM TTIN TSTP)
59
+ # USR1 and USR2 don't work on the JVM
60
+ if !jruby?
61
+ sigs << 'USR1'
62
+ sigs << 'USR2'
63
+ end
53
64
 
54
- %w(INT TERM USR1 USR2 TTIN).each do |sig|
65
+ sigs.each do |sig|
55
66
  begin
56
67
  trap sig do
57
68
  self_write.puts(sig)
@@ -65,20 +76,20 @@ module Sidekiq
65
76
  logger.info Sidekiq::LICENSE
66
77
  logger.info "Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org" unless defined?(::Sidekiq::Pro)
67
78
 
68
- Sidekiq.redis do |conn|
69
- # touch the connection pool so it is created before we
70
- # fire startup and start multithreading.
71
- ver = conn.info['redis_version']
72
- raise "You are using Redis v#{ver}, Sidekiq requires Redis v2.8.0 or greater" if ver < '2.8'
73
- end
79
+ # touch the connection pool so it is created before we
80
+ # fire startup and start multithreading.
81
+ ver = Sidekiq.redis_info['redis_version']
82
+ raise "You are using Redis v#{ver}, Sidekiq requires Redis v2.8.0 or greater" if ver < '2.8'
83
+
84
+ # Touch middleware so it isn't lazy loaded by multiple threads, #3043
85
+ Sidekiq.server_middleware
74
86
 
75
87
  # Before this point, the process is initializing with just the main thread.
76
88
  # Starting here the process will now have multiple threads running.
77
89
  fire_event(:startup)
78
90
 
79
- logger.debug {
80
- "Middleware: #{Sidekiq.server_middleware.map(&:klass).join(', ')}"
81
- }
91
+ logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(', ')}" }
92
+ logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(', ')}" }
82
93
 
83
94
  if !options[:daemon]
84
95
  logger.info 'Starting processing, hit Ctrl-C to stop'
@@ -134,6 +145,10 @@ module Sidekiq
134
145
  when 'USR1'
135
146
  Sidekiq.logger.info "Received USR1, no longer accepting new work"
136
147
  launcher.quiet
148
+ when 'TSTP'
149
+ # USR1 is not available on JVM, allow TSTP as an alternate signal
150
+ Sidekiq.logger.info "Received TSTP, no longer accepting new work"
151
+ launcher.quiet
137
152
  when 'USR2'
138
153
  if Sidekiq.options[:logfile]
139
154
  Sidekiq.logger.info "Received USR2, reopening log file"
@@ -141,7 +156,7 @@ module Sidekiq
141
156
  end
142
157
  when 'TTIN'
143
158
  Thread.list.each do |thread|
144
- Sidekiq.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
159
+ Sidekiq.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['sidekiq_label']}"
145
160
  if thread.backtrace
146
161
  Sidekiq.logger.warn thread.backtrace.join("\n")
147
162
  else
@@ -207,6 +222,8 @@ module Sidekiq
207
222
  opts = parse_config(cfile).merge(opts) if cfile
208
223
 
209
224
  opts[:strict] = true if opts[:strict].nil?
225
+ opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if !opts[:concurrency] && ENV["RAILS_MAX_THREADS"]
226
+ opts[:identity] = identity
210
227
 
211
228
  options.merge!(opts)
212
229
  end
@@ -223,21 +240,26 @@ module Sidekiq
223
240
  if File.directory?(options[:require])
224
241
  require 'rails'
225
242
  if ::Rails::VERSION::MAJOR < 4
226
- require 'sidekiq/rails'
227
- require File.expand_path("#{options[:require]}/config/environment.rb")
228
- ::Rails.application.eager_load!
229
- else
243
+ raise "Sidekiq no longer supports this version of Rails"
244
+ elsif ::Rails::VERSION::MAJOR == 4
230
245
  # Painful contortions, see 1791 for discussion
246
+ # No autoloading, we want to force eager load for everything.
231
247
  require File.expand_path("#{options[:require]}/config/application.rb")
232
248
  ::Rails::Application.initializer "sidekiq.eager_load" do
233
249
  ::Rails.application.config.eager_load = true
234
250
  end
235
251
  require 'sidekiq/rails'
236
252
  require File.expand_path("#{options[:require]}/config/environment.rb")
253
+ else
254
+ require 'sidekiq/rails'
255
+ require File.expand_path("#{options[:require]}/config/environment.rb")
237
256
  end
238
257
  options[:tag] ||= default_tag
239
258
  else
240
- require options[:require]
259
+ not_required_message = "#{options[:require]} was not required, you should use an explicit path: " +
260
+ "./#{options[:require]} or /path/to/#{options[:require]}"
261
+
262
+ require(options[:require]) || raise(ArgumentError, not_required_message)
241
263
  end
242
264
  end
243
265
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'securerandom'
2
3
  require 'sidekiq/middleware/chain'
3
4
 
@@ -35,10 +36,8 @@ module Sidekiq
35
36
  # Sidekiq::Client.new(ConnectionPool.new { Redis.new })
36
37
  #
37
38
  # Generally this is only needed for very large Sidekiq installs processing
38
- # more than thousands jobs per second. I do not recommend sharding unless
39
- # you truly cannot scale any other way (e.g. splitting your app into smaller apps).
40
- # Some features, like the API, do not support sharding: they are designed to work
41
- # against a single Redis instance only.
39
+ # thousands of jobs per second. I don't recommend sharding unless you
40
+ # cannot scale any other way (e.g. splitting your app into smaller apps).
42
41
  def initialize(redis_pool=nil)
43
42
  @redis_pool = redis_pool || Thread.current[:sidekiq_via_pool] || Sidekiq.redis_pool
44
43
  end
@@ -49,11 +48,13 @@ module Sidekiq
49
48
  # queue - the named queue to use, default 'default'
50
49
  # class - the worker class to call, required
51
50
  # args - an array of simple arguments to the perform method, must be JSON-serializable
52
- # retry - whether to retry this job if it fails, true or false, default true
51
+ # at - timestamp to schedule the job (optional), must be Numeric (e.g. Time.now.to_f)
52
+ # retry - whether to retry this job if it fails, default true or an integer number of retries
53
53
  # backtrace - whether to save any error backtrace, default false
54
54
  #
55
55
  # All options must be strings, not symbols. NB: because we are serializing to JSON, all
56
- # symbols in 'args' will be converted to strings.
56
+ # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
57
+ # space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful.
57
58
  #
58
59
  # Returns a unique Job ID. If middleware stops the job, nil will be returned instead.
59
60
  #
@@ -62,19 +63,18 @@ module Sidekiq
62
63
  #
63
64
  def push(item)
64
65
  normed = normalize_item(item)
65
- payload = process_single(item['class'], normed)
66
+ payload = process_single(item['class'.freeze], normed)
66
67
 
67
68
  if payload
68
69
  raw_push([payload])
69
- payload['jid']
70
+ payload['jid'.freeze]
70
71
  end
71
72
  end
72
73
 
73
74
  ##
74
75
  # Push a large number of jobs to Redis. In practice this method is only
75
- # useful if you are pushing tens of thousands of jobs or more, or if you need
76
- # to ensure that a batch doesn't complete prematurely. This method
77
- # basically cuts down on the redis round trip latency.
76
+ # useful if you are pushing thousands of jobs or more. This method
77
+ # cuts out the redis network round trip latency.
78
78
  #
79
79
  # Takes the same arguments as #push except that args is expected to be
80
80
  # an Array of Arrays. All other keys are duplicated for each job. Each job
@@ -84,14 +84,19 @@ module Sidekiq
84
84
  # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
85
85
  # than the number given if the middleware stopped processing for one or more jobs.
86
86
  def push_bulk(items)
87
+ arg = items['args'.freeze].first
88
+ return [] unless arg # no jobs to push
89
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !arg.is_a?(Array)
90
+
87
91
  normed = normalize_item(items)
88
- payloads = items['args'].map do |args|
89
- raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !args.is_a?(Array)
90
- process_single(items['class'], normed.merge('args' => args, 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f))
92
+ payloads = items['args'.freeze].map do |args|
93
+ copy = normed.merge('args'.freeze => args, 'jid'.freeze => SecureRandom.hex(12), 'enqueued_at'.freeze => Time.now.to_f)
94
+ result = process_single(items['class'.freeze], copy)
95
+ result ? result : nil
91
96
  end.compact
92
97
 
93
98
  raw_push(payloads) if !payloads.empty?
94
- payloads.collect { |payload| payload['jid'] }
99
+ payloads.collect { |payload| payload['jid'.freeze] }
95
100
  end
96
101
 
97
102
  # Allows sharding of jobs across any number of Redis instances. All jobs
@@ -104,13 +109,12 @@ module Sidekiq
104
109
  # end
105
110
  #
106
111
  # Generally this is only needed for very large Sidekiq installs processing
107
- # more than thousands jobs per second. I do not recommend sharding unless
108
- # you truly cannot scale any other way (e.g. splitting your app into smaller apps).
109
- # Some features, like the API, do not support sharding: they are designed to work
110
- # against a single Redis instance.
112
+ # thousands of jobs per second. I do not recommend sharding unless
113
+ # you cannot scale any other way (e.g. splitting your app into smaller apps).
111
114
  def self.via(pool)
112
115
  raise ArgumentError, "No pool given" if pool.nil?
113
- raise RuntimeError, "Sidekiq::Client.via is not re-entrant" if x = Thread.current[:sidekiq_via_pool] && x != pool
116
+ current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
117
+ raise RuntimeError, "Sidekiq::Client.via is not re-entrant" if current_sidekiq_pool && current_sidekiq_pool != pool
114
118
  Thread.current[:sidekiq_via_pool] = pool
115
119
  yield
116
120
  ensure
@@ -136,14 +140,14 @@ module Sidekiq
136
140
  # Messages are enqueued to the 'default' queue.
137
141
  #
138
142
  def enqueue(klass, *args)
139
- klass.client_push('class' => klass, 'args' => args)
143
+ klass.client_push('class'.freeze => klass, 'args'.freeze => args)
140
144
  end
141
145
 
142
146
  # Example usage:
143
147
  # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
144
148
  #
145
149
  def enqueue_to(queue, klass, *args)
146
- klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
150
+ klass.client_push('queue'.freeze => queue, 'class'.freeze => klass, 'args'.freeze => args)
147
151
  end
148
152
 
149
153
  # Example usage:
@@ -154,7 +158,7 @@ module Sidekiq
154
158
  now = Time.now.to_f
155
159
  ts = (int < 1_000_000_000 ? now + int : int)
156
160
 
157
- item = { 'class' => klass, 'args' => args, 'at' => ts, 'queue' => queue }
161
+ item = { 'class'.freeze => klass, 'args'.freeze => args, 'at'.freeze => ts, 'queue'.freeze => queue }
158
162
  item.delete('at'.freeze) if ts <= now
159
163
 
160
164
  klass.client_push(item)
@@ -180,13 +184,13 @@ module Sidekiq
180
184
  end
181
185
 
182
186
  def atomic_push(conn, payloads)
183
- if payloads.first['at']
187
+ if payloads.first['at'.freeze]
184
188
  conn.zadd('schedule'.freeze, payloads.map do |hash|
185
189
  at = hash.delete('at'.freeze).to_s
186
190
  [at, Sidekiq.dump_json(hash)]
187
191
  end)
188
192
  else
189
- q = payloads.first['queue']
193
+ q = payloads.first['queue'.freeze]
190
194
  now = Time.now.to_f
191
195
  to_push = payloads.map do |entry|
192
196
  entry['enqueued_at'.freeze] = now
@@ -198,7 +202,7 @@ module Sidekiq
198
202
  end
199
203
 
200
204
  def process_single(worker_class, item)
201
- queue = item['queue']
205
+ queue = item['queue'.freeze]
202
206
 
203
207
  middleware.invoke(worker_class, item, queue, @redis_pool) do
204
208
  item
@@ -209,6 +213,8 @@ module Sidekiq
209
213
  raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash) && item.has_key?('class'.freeze) && item.has_key?('args'.freeze)
210
214
  raise(ArgumentError, "Job args must be an Array") unless item['args'].is_a?(Array)
211
215
  raise(ArgumentError, "Job class must be either a Class or String representation of the class name") unless item['class'.freeze].is_a?(Class) || item['class'.freeze].is_a?(String)
216
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp") if item.has_key?('at'.freeze) && !item['at'].is_a?(Numeric)
217
+ #raise(ArgumentError, "Arguments must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices") unless JSON.load(JSON.dump(item['args'])) == item['args']
212
218
 
213
219
  normalized_hash(item['class'.freeze])
214
220
  .each{ |key, value| item[key] = value if item[key].nil? }
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  begin
2
3
  require 'active_support/core_ext/class/attribute'
3
4
  rescue LoadError
@@ -103,3 +104,16 @@ rescue LoadError
103
104
  end
104
105
 
105
106
 
107
+ begin
108
+ require 'active_support/core_ext/kernel/reporting'
109
+ rescue LoadError
110
+ module Kernel
111
+ module_function
112
+ def silence_warnings
113
+ old_verbose, $VERBOSE = $VERBOSE, nil
114
+ yield
115
+ ensure
116
+ $VERBOSE = old_verbose
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,21 @@
1
+ module Sidekiq
2
+ module Extensions
3
+
4
+ def self.enable_delay!
5
+ if defined?(::ActiveSupport)
6
+ ActiveSupport.on_load(:active_record) do
7
+ require 'sidekiq/extensions/active_record'
8
+ include Sidekiq::Extensions::ActiveRecord
9
+ end
10
+ ActiveSupport.on_load(:action_mailer) do
11
+ require 'sidekiq/extensions/action_mailer'
12
+ extend Sidekiq::Extensions::ActionMailer
13
+ end
14
+ end
15
+
16
+ require 'sidekiq/extensions/class_methods'
17
+ Module.__send__(:include, Sidekiq::Extensions::Klass)
18
+ end
19
+
20
+ end
21
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq'
2
3
 
3
4
  module Sidekiq
@@ -5,7 +6,7 @@ module Sidekiq
5
6
 
6
7
  class Logger
7
8
  def call(ex, ctxHash)
8
- Sidekiq.logger.warn(ctxHash) if !ctxHash.empty?
9
+ Sidekiq.logger.warn(Sidekiq.dump_json(ctxHash)) if !ctxHash.empty?
9
10
  Sidekiq.logger.warn "#{ex.class.name}: #{ex.message}"
10
11
  Sidekiq.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
11
12
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq/extensions/generic_proxy'
2
3
 
3
4
  module Sidekiq
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq/extensions/generic_proxy'
2
3
 
3
4
  module Sidekiq
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq/extensions/generic_proxy'
2
3
 
3
4
  module Sidekiq
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  require 'yaml'
2
3
 
3
4
  module Sidekiq
4
5
  module Extensions
6
+ SIZE_LIMIT = 8_192
7
+
5
8
  class Proxy < BasicObject
6
9
  def initialize(performable, target, options={})
7
10
  @performable = performable
@@ -16,7 +19,11 @@ module Sidekiq
16
19
  # to JSON and then deserialized on the other side back into a
17
20
  # Ruby object.
18
21
  obj = [@target, name, args]
19
- @performable.client_push({ 'class' => @performable, 'args' => [::YAML.dump(obj)] }.merge(@opts))
22
+ marshalled = ::YAML.dump(obj)
23
+ if marshalled.size > SIZE_LIMIT
24
+ ::Sidekiq.logger.warn { "#{@target}.#{name} job argument is #{marshalled.bytesize} bytes, you should refactor it to reduce the size" }
25
+ end
26
+ @performable.client_push({ 'class' => @performable, 'args' => [marshalled] }.merge(@opts))
20
27
  end
21
28
  end
22
29
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq'
2
3
 
3
4
  module Sidekiq
@@ -12,7 +13,7 @@ module Sidekiq
12
13
  end
13
14
 
14
15
  def queue_name
15
- queue.gsub(/.*queue:/, ''.freeze)
16
+ queue.sub(/.*queue:/, ''.freeze)
16
17
  end
17
18
 
18
19
  def requeue
@@ -0,0 +1,27 @@
1
+ module Sidekiq
2
+ class JobLogger
3
+
4
+ def call(item, queue)
5
+ begin
6
+ start = Time.now
7
+ logger.info("start".freeze)
8
+ yield
9
+ logger.info("done: #{elapsed(start)} sec")
10
+ rescue Exception
11
+ logger.info("fail: #{elapsed(start)} sec")
12
+ raise
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def elapsed(start)
19
+ (Time.now - start).round(3)
20
+ end
21
+
22
+ def logger
23
+ Sidekiq.logger
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,235 @@
1
+ require 'sidekiq/scheduled'
2
+ require 'sidekiq/api'
3
+
4
+ module Sidekiq
5
+ ##
6
+ # Automatically retry jobs that fail in Sidekiq.
7
+ # Sidekiq's retry support assumes a typical development lifecycle:
8
+ #
9
+ # 0. Push some code changes with a bug in it.
10
+ # 1. Bug causes job processing to fail, Sidekiq's middleware captures
11
+ # the job and pushes it onto a retry queue.
12
+ # 2. Sidekiq retries jobs in the retry queue multiple times with
13
+ # an exponential delay, the job continues to fail.
14
+ # 3. After a few days, a developer deploys a fix. The job is
15
+ # reprocessed successfully.
16
+ # 4. Once retries are exhausted, Sidekiq will give up and move the
17
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
18
+ # manually in the Web UI.
19
+ # 5. After 6 months on the DJQ, Sidekiq will discard the job.
20
+ #
21
+ # A job looks like:
22
+ #
23
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
24
+ #
25
+ # The 'retry' option also accepts a number (in place of 'true'):
26
+ #
27
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
28
+ #
29
+ # The job will be retried this number of times before giving up. (If simply
30
+ # 'true', Sidekiq retries 25 times)
31
+ #
32
+ # We'll add a bit more data to the job to support retries:
33
+ #
34
+ # * 'queue' - the queue to use
35
+ # * 'retry_count' - number of times we've retried so far.
36
+ # * 'error_message' - the message from the exception
37
+ # * 'error_class' - the exception class
38
+ # * 'failed_at' - the first time it failed
39
+ # * 'retried_at' - the last time it was retried
40
+ # * 'backtrace' - the number of lines of error backtrace to store
41
+ #
42
+ # We don't store the backtrace by default as that can add a lot of overhead
43
+ # to the job and everyone is using an error service, right?
44
+ #
45
+ # The default number of retries is 25 which works out to about 3 weeks
46
+ # You can change the default maximum number of retries in your initializer:
47
+ #
48
+ # Sidekiq.options[:max_retries] = 7
49
+ #
50
+ # or limit the number of retries for a particular worker with:
51
+ #
52
+ # class MyWorker
53
+ # include Sidekiq::Worker
54
+ # sidekiq_options :retry => 10
55
+ # end
56
+ #
57
+ class JobRetry
58
+ class Skip < ::RuntimeError; end
59
+
60
+ include Sidekiq::Util
61
+
62
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
63
+
64
+ def initialize(options = {})
65
+ @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
66
+ end
67
+
68
+ # The global retry handler requires only the barest of data.
69
+ # We want to be able to retry as much as possible so we don't
70
+ # require the worker to be instantiated.
71
+ def global(msg, queue)
72
+ yield
73
+ rescue Skip => ex
74
+ raise ex
75
+ rescue Sidekiq::Shutdown => ey
76
+ # ignore, will be pushed back onto queue during hard_shutdown
77
+ raise ey
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
+
82
+ raise e unless msg['retry']
83
+ attempt_retry(nil, msg, queue, e)
84
+ raise e
85
+ end
86
+
87
+
88
+ # The local retry support means that any errors that occur within
89
+ # this block can be associated with the given worker instance.
90
+ # This is required to support the `sidekiq_retries_exhausted` block.
91
+ #
92
+ # Note that any exception from the block is wrapped in the Skip
93
+ # exception so the global block does not reprocess the error. The
94
+ # Skip exception is unwrapped within Sidekiq::Processor#process before
95
+ # calling the handle_exception handlers.
96
+ def local(worker, msg, queue)
97
+ yield
98
+ rescue Skip => ex
99
+ raise ex
100
+ rescue Sidekiq::Shutdown => ey
101
+ # ignore, will be pushed back onto queue during hard_shutdown
102
+ raise ey
103
+ rescue Exception => e
104
+ # ignore, will be pushed back onto queue during hard_shutdown
105
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
106
+
107
+ if msg['retry'] == nil
108
+ msg['retry'] = worker.class.get_sidekiq_options['retry']
109
+ end
110
+
111
+ raise e unless msg['retry']
112
+ attempt_retry(worker, msg, queue, e)
113
+ # We've handled this error associated with this job, don't
114
+ # need to handle it at the global level
115
+ raise Skip
116
+ end
117
+
118
+ private
119
+
120
+ # Note that +worker+ can be nil here if an error is raised before we can
121
+ # instantiate the worker instance. All access must be guarded and
122
+ # best effort.
123
+ def attempt_retry(worker, msg, queue, exception)
124
+ max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
125
+
126
+ msg['queue'] = if msg['retry_queue']
127
+ msg['retry_queue']
128
+ else
129
+ queue
130
+ end
131
+
132
+ # App code can stuff all sorts of crazy binary data into the error message
133
+ # that won't convert to JSON.
134
+ m = exception.message.to_s[0, 10_000]
135
+ if m.respond_to?(:scrub!)
136
+ m.force_encoding("utf-8")
137
+ m.scrub!
138
+ end
139
+
140
+ msg['error_message'] = m
141
+ msg['error_class'] = exception.class.name
142
+ count = if msg['retry_count']
143
+ msg['retried_at'] = Time.now.to_f
144
+ msg['retry_count'] += 1
145
+ else
146
+ msg['failed_at'] = Time.now.to_f
147
+ msg['retry_count'] = 0
148
+ end
149
+
150
+ if msg['backtrace'] == true
151
+ msg['error_backtrace'] = exception.backtrace
152
+ elsif !msg['backtrace']
153
+ # do nothing
154
+ elsif msg['backtrace'].to_i != 0
155
+ msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
156
+ end
157
+
158
+ if count < max_retry_attempts
159
+ delay = delay_for(worker, count, exception)
160
+ logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
161
+ retry_at = Time.now.to_f + delay
162
+ payload = Sidekiq.dump_json(msg)
163
+ Sidekiq.redis do |conn|
164
+ conn.zadd('retry', retry_at.to_s, payload)
165
+ end
166
+ else
167
+ # Goodbye dear message, you (re)tried your best I'm sure.
168
+ retries_exhausted(worker, msg, exception)
169
+ end
170
+ end
171
+
172
+ def retries_exhausted(worker, msg, exception)
173
+ logger.debug { "Retries exhausted for job" }
174
+ begin
175
+ block = worker && worker.sidekiq_retries_exhausted_block || Sidekiq.default_retries_exhausted
176
+ block.call(msg, exception) if block
177
+ rescue => e
178
+ handle_exception(e, { context: "Error calling retries_exhausted for #{msg['class']}", job: msg })
179
+ end
180
+
181
+ send_to_morgue(msg) unless msg['dead'] == false
182
+ end
183
+
184
+ def send_to_morgue(msg)
185
+ Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
186
+ payload = Sidekiq.dump_json(msg)
187
+ now = Time.now.to_f
188
+ Sidekiq.redis do |conn|
189
+ conn.multi do
190
+ conn.zadd('dead', now, payload)
191
+ conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
192
+ conn.zremrangebyrank('dead', 0, -DeadSet.max_jobs)
193
+ end
194
+ end
195
+ end
196
+
197
+ def retry_attempts_from(msg_retry, default)
198
+ if msg_retry.is_a?(Integer)
199
+ msg_retry
200
+ else
201
+ default
202
+ end
203
+ end
204
+
205
+ def delay_for(worker, count, exception)
206
+ worker && worker.sidekiq_retry_in_block? && retry_in(worker, count, exception) || seconds_to_delay(count)
207
+ end
208
+
209
+ # delayed_job uses the same basic formula
210
+ def seconds_to_delay(count)
211
+ (count ** 4) + 15 + (rand(30)*(count+1))
212
+ end
213
+
214
+ def retry_in(worker, count, exception)
215
+ begin
216
+ worker.sidekiq_retry_in_block.call(count, exception).to_i
217
+ rescue Exception => e
218
+ handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
219
+ nil
220
+ end
221
+ end
222
+
223
+ def exception_caused_by_shutdown?(e, checked_causes = [])
224
+ return false unless e.cause
225
+
226
+ # Handle circular causes
227
+ checked_causes << e.object_id
228
+ return false if checked_causes.include?(e.cause.object_id)
229
+
230
+ e.cause.instance_of?(Sidekiq::Shutdown) ||
231
+ exception_caused_by_shutdown?(e.cause, checked_causes)
232
+ end
233
+
234
+ end
235
+ end