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,25 +1,28 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
1
3
  $stdout.sync = true
2
4
 
3
5
  require 'yaml'
4
6
  require 'singleton'
5
7
  require 'optparse'
6
8
  require 'erb'
9
+ require 'fileutils'
7
10
 
8
11
  require 'sidekiq'
9
12
  require 'sidekiq/util'
10
13
 
11
14
  module Sidekiq
12
- # We are shutting down Sidekiq but what about workers that
13
- # are working on some long job? This error is
14
- # raised in workers that have not finished within the hard
15
- # timeout limit. This is needed to rollback db transactions,
16
- # otherwise Ruby's Thread#kill will commit. See #377.
17
- # DO NOT RESCUE THIS ERROR.
18
- class Shutdown < Interrupt; end
19
-
20
15
  class CLI
21
16
  include Util
22
- include Singleton
17
+ include Singleton unless $TESTING
18
+
19
+ PROCTITLES = [
20
+ proc { 'sidekiq'.freeze },
21
+ proc { Sidekiq::VERSION },
22
+ proc { |me, data| data['tag'] },
23
+ proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data['concurrency']} busy]" },
24
+ proc { |me, data| "stopping" if me.stopping? },
25
+ ]
23
26
 
24
27
  # Used for CLI testing
25
28
  attr_accessor :code
@@ -38,22 +41,45 @@ module Sidekiq
38
41
  validate!
39
42
  daemonize
40
43
  write_pid
41
- load_celluloid
42
- boot_system
43
44
  end
44
45
 
46
+ # Code within this method is not tested because it alters
47
+ # global process state irreversibly. PRs which improve the
48
+ # test coverage of Sidekiq::CLI are welcomed.
45
49
  def run
50
+ boot_system
51
+ print_banner
52
+
46
53
  self_read, self_write = IO.pipe
47
54
 
48
- %w(INT TERM USR1 USR2 TTIN).each do |sig|
49
- trap sig do
50
- self_write.puts(sig)
55
+ %w(INT TERM USR1 USR2 TTIN TSTP).each do |sig|
56
+ begin
57
+ trap sig do
58
+ self_write.puts(sig)
59
+ end
60
+ rescue ArgumentError
61
+ puts "Signal #{sig} not supported"
51
62
  end
52
63
  end
53
64
 
54
- redis {} # noop to connect redis and print info
55
65
  logger.info "Running in #{RUBY_DESCRIPTION}"
56
66
  logger.info Sidekiq::LICENSE
67
+ logger.info "Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org" unless defined?(::Sidekiq::Pro)
68
+
69
+ # touch the connection pool so it is created before we
70
+ # fire startup and start multithreading.
71
+ ver = Sidekiq.redis_info['redis_version']
72
+ raise "You are using Redis v#{ver}, Sidekiq requires Redis v2.8.0 or greater" if ver < '2.8'
73
+
74
+ # Touch middleware so it isn't lazy loaded by multiple threads, #3043
75
+ Sidekiq.server_middleware
76
+
77
+ # Before this point, the process is initializing with just the main thread.
78
+ # Starting here the process will now have multiple threads running.
79
+ fire_event(:startup)
80
+
81
+ logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(', ')}" }
82
+ logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(', ')}" }
57
83
 
58
84
  if !options[:daemon]
59
85
  logger.info 'Starting processing, hit Ctrl-C to stop'
@@ -61,13 +87,8 @@ module Sidekiq
61
87
 
62
88
  require 'sidekiq/launcher'
63
89
  @launcher = Sidekiq::Launcher.new(options)
64
- launcher.procline(options[:tag] ? "#{options[:tag]} " : '')
65
90
 
66
91
  begin
67
- if options[:profile]
68
- require 'ruby-prof'
69
- RubyProf.start
70
- end
71
92
  launcher.run
72
93
 
73
94
  while readable_io = IO.select([self_read])
@@ -79,23 +100,32 @@ module Sidekiq
79
100
  launcher.stop
80
101
  # Explicitly exit so busy Processor threads can't block
81
102
  # process shutdown.
103
+ logger.info "Bye!"
82
104
  exit(0)
83
105
  end
84
106
  end
85
107
 
86
- private
108
+ def self.banner
109
+ %q{
110
+ m,
111
+ `$b
112
+ .ss, $$: .,d$
113
+ `$$P,d$P' .,md$P"'
114
+ ,$$$$$bmmd$$$P^'
115
+ .d$$$$$$$$$$P'
116
+ $$^' `"^$$$' ____ _ _ _ _
117
+ $: ,$$: / ___|(_) __| | ___| | _(_) __ _
118
+ `b :$$ \___ \| |/ _` |/ _ \ |/ / |/ _` |
119
+ $$: ___) | | (_| | __/ <| | (_| |
120
+ $$ |____/|_|\__,_|\___|_|\_\_|\__, |
121
+ .d$$ |_|
122
+ }
123
+ end
87
124
 
88
125
  def handle_signal(sig)
89
126
  Sidekiq.logger.debug "Got #{sig} signal"
90
127
  case sig
91
128
  when 'INT'
92
- if Sidekiq.options[:profile]
93
- result = RubyProf.stop
94
- printer = RubyProf::GraphHtmlPrinter.new(result)
95
- File.open("profile.html", 'w') do |f|
96
- printer.print(f, :min_percent => 1)
97
- end
98
- end
99
129
  # Handle Ctrl-C in JRuby like MRI
100
130
  # http://jira.codehaus.org/browse/JRUBY-4637
101
131
  raise Interrupt
@@ -104,35 +134,37 @@ module Sidekiq
104
134
  raise Interrupt
105
135
  when 'USR1'
106
136
  Sidekiq.logger.info "Received USR1, no longer accepting new work"
107
- launcher.manager.async.stop
137
+ launcher.quiet
138
+ when 'TSTP'
139
+ # USR1 is not available on JVM, allow TSTP as an alternate signal
140
+ Sidekiq.logger.info "Received TSTP, no longer accepting new work"
141
+ launcher.quiet
108
142
  when 'USR2'
109
143
  if Sidekiq.options[:logfile]
110
144
  Sidekiq.logger.info "Received USR2, reopening log file"
111
- initialize_logger
145
+ Sidekiq::Logging.reopen_logs
112
146
  end
113
147
  when 'TTIN'
114
148
  Thread.list.each do |thread|
115
- Sidekiq.logger.info "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
149
+ Sidekiq.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['sidekiq_label']}"
116
150
  if thread.backtrace
117
- Sidekiq.logger.info thread.backtrace.join("\n")
151
+ Sidekiq.logger.warn thread.backtrace.join("\n")
118
152
  else
119
- Sidekiq.logger.info "<no backtrace available>"
153
+ Sidekiq.logger.warn "<no backtrace available>"
120
154
  end
121
155
  end
122
156
  end
123
157
  end
124
158
 
125
- def load_celluloid
126
- raise "Celluloid cannot be required until here, or it will break Sidekiq's daemonization" if defined?(::Celluloid) && options[:daemon]
127
-
128
- # Celluloid can't be loaded until after we've daemonized
129
- # because it spins up threads and creates locks which get
130
- # into a very bad state if forked.
131
- require 'celluloid/autostart'
132
- Celluloid.logger = (options[:verbose] ? Sidekiq.logger : nil)
159
+ private
133
160
 
134
- require 'sidekiq/manager'
135
- require 'sidekiq/scheduled'
161
+ def print_banner
162
+ # Print logo and banner for development
163
+ if environment == 'development' && $stdout.tty?
164
+ puts "\e[#{31}m"
165
+ puts Sidekiq::CLI.banner
166
+ puts "\e[0m"
167
+ end
136
168
  end
137
169
 
138
170
  def daemonize
@@ -144,7 +176,7 @@ module Sidekiq
144
176
  files_to_reopen << file unless file.closed?
145
177
  end
146
178
 
147
- Process.daemon(true, true)
179
+ ::Process.daemon(true, true)
148
180
 
149
181
  files_to_reopen.each do |file|
150
182
  begin
@@ -169,18 +201,21 @@ module Sidekiq
169
201
  @environment = cli_env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
170
202
  end
171
203
 
172
- def die(code)
173
- exit(code)
174
- end
204
+ alias_method :die, :exit
205
+ alias_method :☠, :exit
175
206
 
176
207
  def setup_options(args)
177
- cli = parse_options(args)
178
- set_environment cli[:environment]
208
+ opts = parse_options(args)
209
+ set_environment opts[:environment]
179
210
 
180
- cfile = cli[:config_file]
211
+ cfile = opts[:config_file]
212
+ opts = parse_config(cfile).merge(opts) if cfile
181
213
 
182
- config = (cfile ? parse_config(cfile) : {})
183
- options.merge!(config.merge(cli))
214
+ opts[:strict] = true if opts[:strict].nil?
215
+ opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if !opts[:concurrency] && ENV["RAILS_MAX_THREADS"]
216
+ opts[:identity] = identity
217
+
218
+ options.merge!(opts)
184
219
  end
185
220
 
186
221
  def options
@@ -194,12 +229,30 @@ module Sidekiq
194
229
 
195
230
  if File.directory?(options[:require])
196
231
  require 'rails'
197
- require 'sidekiq/rails'
198
- require File.expand_path("#{options[:require]}/config/environment.rb")
199
- ::Rails.application.eager_load!
232
+ if ::Rails::VERSION::MAJOR < 4
233
+ require 'sidekiq/rails'
234
+ require File.expand_path("#{options[:require]}/config/environment.rb")
235
+ ::Rails.application.eager_load!
236
+ elsif ::Rails::VERSION::MAJOR == 4
237
+ # Painful contortions, see 1791 for discussion
238
+ # No autoloading, we want to force eager load for everything.
239
+ require File.expand_path("#{options[:require]}/config/application.rb")
240
+ ::Rails::Application.initializer "sidekiq.eager_load" do
241
+ ::Rails.application.config.eager_load = true
242
+ end
243
+ require 'sidekiq/rails'
244
+ require File.expand_path("#{options[:require]}/config/environment.rb")
245
+ else
246
+ # Rails 5+ && development mode, use Reloader
247
+ require 'sidekiq/rails'
248
+ require File.expand_path("#{options[:require]}/config/environment.rb")
249
+ end
200
250
  options[:tag] ||= default_tag
201
251
  else
202
- require options[:require]
252
+ not_required_message = "#{options[:require]} was not required, you should use an explicit path: " +
253
+ "./#{options[:require]} or /path/to/#{options[:require]}"
254
+
255
+ require(options[:require]) || raise(ArgumentError, not_required_message)
203
256
  end
204
257
  end
205
258
 
@@ -226,6 +279,10 @@ module Sidekiq
226
279
  logger.info @parser
227
280
  die(1)
228
281
  end
282
+
283
+ [:concurrency, :timeout].each do |opt|
284
+ raise ArgumentError, "#{opt}: #{options[opt]} is not a valid value" if options.has_key?(opt) && options[opt].to_i <= 0
285
+ end
229
286
  end
230
287
 
231
288
  def parse_options(argv)
@@ -252,13 +309,9 @@ module Sidekiq
252
309
  opts[:index] = Integer(arg.match(/\d+/)[0])
253
310
  end
254
311
 
255
- o.on '-p', '--profile', "Profile all code run by Sidekiq" do |arg|
256
- opts[:profile] = arg
257
- end
258
-
259
- o.on "-q", "--queue QUEUE[,WEIGHT]...", "Queues to process with optional weights" do |arg|
260
- queues_and_weights = arg.scan(/([\w\.-]+),?(\d*)/)
261
- parse_queues opts, queues_and_weights
312
+ o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
313
+ queue, weight = arg.split(",")
314
+ parse_queue opts, queue, weight
262
315
  end
263
316
 
264
317
  o.on '-r', '--require [PATH|DIR]', "Location of Rails application with workers or file to require" do |arg|
@@ -297,19 +350,25 @@ module Sidekiq
297
350
  die 1
298
351
  end
299
352
  @parser.parse!(argv)
353
+
354
+ %w[config/sidekiq.yml config/sidekiq.yml.erb].each do |filename|
355
+ opts[:config_file] ||= filename if File.exist?(filename)
356
+ end
357
+
300
358
  opts
301
359
  end
302
360
 
303
361
  def initialize_logger
304
362
  Sidekiq::Logging.initialize_logger(options[:logfile]) if options[:logfile]
305
363
 
306
- Sidekiq.logger.level = Logger::DEBUG if options[:verbose]
364
+ Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose]
307
365
  end
308
366
 
309
367
  def write_pid
310
368
  if path = options[:pidfile]
311
- File.open(path, 'w') do |f|
312
- f.puts Process.pid
369
+ pidfile = File.expand_path(path)
370
+ File.open(pidfile, 'w') do |f|
371
+ f.puts ::Process.pid
313
372
  end
314
373
  end
315
374
  end
@@ -317,7 +376,7 @@ module Sidekiq
317
376
  def parse_config(cfile)
318
377
  opts = {}
319
378
  if File.exist?(cfile)
320
- opts = YAML.load(ERB.new(IO.read(cfile)).result)
379
+ opts = YAML.load(ERB.new(IO.read(cfile)).result) || opts
321
380
  opts = opts.merge(opts.delete(environment) || {})
322
381
  parse_queues(opts, opts.delete(:queues) || [])
323
382
  else
@@ -326,21 +385,22 @@ module Sidekiq
326
385
  end
327
386
  ns = opts.delete(:namespace)
328
387
  if ns
329
- Sidekiq.logger.warn("namespace should be set in your ruby initializer, is ignored in config file")
330
- Sidekiq.logger.warn("config.redis = { :url => ..., :namespace => '#{ns}' }")
388
+ # logger hasn't been initialized yet, puts is all we have.
389
+ puts("namespace should be set in your ruby initializer, is ignored in config file")
390
+ puts("config.redis = { :url => ..., :namespace => '#{ns}' }")
331
391
  end
332
392
  opts
333
393
  end
334
394
 
335
395
  def parse_queues(opts, queues_and_weights)
336
- queues_and_weights.each {|queue_and_weight| parse_queue(opts, *queue_and_weight)}
337
- opts[:strict] = queues_and_weights.all? {|_, weight| weight.to_s.empty? }
396
+ queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
338
397
  end
339
398
 
340
399
  def parse_queue(opts, q, weight=nil)
341
400
  [weight.to_i, 1].max.times do
342
401
  (opts[:queues] ||= []) << q
343
402
  end
403
+ opts[:strict] = false if weight.to_i > 0
344
404
  end
345
405
  end
346
406
  end
@@ -1,73 +1,133 @@
1
+ # frozen_string_literal: true
1
2
  require 'securerandom'
2
-
3
3
  require 'sidekiq/middleware/chain'
4
4
 
5
5
  module Sidekiq
6
6
  class Client
7
- class << self
8
7
 
9
- def default_middleware
10
- Middleware::Chain.new do
11
- end
8
+ ##
9
+ # Define client-side middleware:
10
+ #
11
+ # client = Sidekiq::Client.new
12
+ # client.middleware do |chain|
13
+ # chain.use MyClientMiddleware
14
+ # end
15
+ # client.push('class' => 'SomeWorker', 'args' => [1,2,3])
16
+ #
17
+ # All client instances default to the globally-defined
18
+ # Sidekiq.client_middleware but you can change as necessary.
19
+ #
20
+ def middleware(&block)
21
+ @chain ||= Sidekiq.client_middleware
22
+ if block_given?
23
+ @chain = @chain.dup
24
+ yield @chain
12
25
  end
26
+ @chain
27
+ end
13
28
 
14
- def registered_workers
15
- Sidekiq.redis { |x| x.smembers('workers') }
16
- end
29
+ attr_accessor :redis_pool
30
+
31
+ # Sidekiq::Client normally uses the default Redis pool but you may
32
+ # pass a custom ConnectionPool if you want to shard your
33
+ # Sidekiq jobs across several Redis instances (for scalability
34
+ # reasons, e.g.)
35
+ #
36
+ # Sidekiq::Client.new(ConnectionPool.new { Redis.new })
37
+ #
38
+ # Generally this is only needed for very large Sidekiq installs processing
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).
41
+ def initialize(redis_pool=nil)
42
+ @redis_pool = redis_pool || Thread.current[:sidekiq_via_pool] || Sidekiq.redis_pool
43
+ end
17
44
 
18
- def registered_queues
19
- Sidekiq.redis { |x| x.smembers('queues') }
45
+ ##
46
+ # The main method used to push a job to Redis. Accepts a number of options:
47
+ #
48
+ # queue - the named queue to use, default 'default'
49
+ # class - the worker class to call, required
50
+ # args - an array of simple arguments to the perform method, must be JSON-serializable
51
+ # retry - whether to retry this job if it fails, default true or an integer number of retries
52
+ # backtrace - whether to save any error backtrace, default false
53
+ #
54
+ # All options must be strings, not symbols. NB: because we are serializing to JSON, all
55
+ # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
56
+ # space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful.
57
+ #
58
+ # Returns a unique Job ID. If middleware stops the job, nil will be returned instead.
59
+ #
60
+ # Example:
61
+ # push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
62
+ #
63
+ def push(item)
64
+ normed = normalize_item(item)
65
+ payload = process_single(item['class'], normed)
66
+
67
+ if payload
68
+ raw_push([payload])
69
+ payload['jid']
20
70
  end
71
+ end
21
72
 
22
- ##
23
- # The main method used to push a job to Redis. Accepts a number of options:
24
- #
25
- # queue - the named queue to use, default 'default'
26
- # class - the worker class to call, required
27
- # args - an array of simple arguments to the perform method, must be JSON-serializable
28
- # retry - whether to retry this job if it fails, true or false, default true
29
- # backtrace - whether to save any error backtrace, default false
30
- #
31
- # All options must be strings, not symbols. NB: because we are serializing to JSON, all
32
- # symbols in 'args' will be converted to strings.
33
- #
34
- # Returns nil if not pushed to Redis or a unique Job ID if pushed.
35
- #
36
- # Example:
37
- # Sidekiq::Client.push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
38
- #
39
- def push(item)
40
- normed = normalize_item(item)
41
- payload = process_single(item['class'], normed)
73
+ ##
74
+ # Push a large number of jobs to Redis. In practice this method is only
75
+ # useful if you are pushing thousands of jobs or more. This method
76
+ # cuts out the redis network round trip latency.
77
+ #
78
+ # Takes the same arguments as #push except that args is expected to be
79
+ # an Array of Arrays. All other keys are duplicated for each job. Each job
80
+ # is run through the client middleware pipeline and each job gets its own Job ID
81
+ # as normal.
82
+ #
83
+ # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
84
+ # than the number given if the middleware stopped processing for one or more jobs.
85
+ def push_bulk(items)
86
+ arg = items['args'].first
87
+ return [] unless arg # no jobs to push
88
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !arg.is_a?(Array)
89
+
90
+ normed = normalize_item(items)
91
+ payloads = items['args'].map do |args|
92
+ copy = normed.merge('args' => args, 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
93
+ result = process_single(items['class'], copy)
94
+ result ? result : nil
95
+ end.compact
96
+
97
+ raw_push(payloads) if !payloads.empty?
98
+ payloads.collect { |payload| payload['jid'] }
99
+ end
100
+
101
+ # Allows sharding of jobs across any number of Redis instances. All jobs
102
+ # defined within the block will use the given Redis connection pool.
103
+ #
104
+ # pool = ConnectionPool.new { Redis.new }
105
+ # Sidekiq::Client.via(pool) do
106
+ # SomeWorker.perform_async(1,2,3)
107
+ # SomeOtherWorker.perform_async(1,2,3)
108
+ # end
109
+ #
110
+ # Generally this is only needed for very large Sidekiq installs processing
111
+ # thousands of jobs per second. I do not recommend sharding unless
112
+ # you cannot scale any other way (e.g. splitting your app into smaller apps).
113
+ def self.via(pool)
114
+ raise ArgumentError, "No pool given" if pool.nil?
115
+ current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
116
+ raise RuntimeError, "Sidekiq::Client.via is not re-entrant" if current_sidekiq_pool && current_sidekiq_pool != pool
117
+ Thread.current[:sidekiq_via_pool] = pool
118
+ yield
119
+ ensure
120
+ Thread.current[:sidekiq_via_pool] = nil
121
+ end
122
+
123
+ class << self
42
124
 
43
- pushed = false
44
- pushed = raw_push([payload]) if payload
45
- pushed ? payload['jid'] : nil
125
+ def push(item)
126
+ new.push(item)
46
127
  end
47
128
 
48
- ##
49
- # Push a large number of jobs to Redis. In practice this method is only
50
- # useful if you are pushing tens of thousands of jobs or more. This method
51
- # basically cuts down on the redis round trip latency.
52
- #
53
- # Takes the same arguments as Client.push except that args is expected to be
54
- # an Array of Arrays. All other keys are duplicated for each job. Each job
55
- # is run through the client middleware pipeline and each job gets its own Job ID
56
- # as normal.
57
- #
58
- # Returns the number of jobs pushed or nil if the pushed failed. The number of jobs
59
- # pushed can be less than the number given if the middleware stopped processing for one
60
- # or more jobs.
61
129
  def push_bulk(items)
62
- normed = normalize_item(items)
63
- payloads = items['args'].map do |args|
64
- raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !args.is_a?(Array)
65
- process_single(items['class'], normed.merge('args' => args, 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f))
66
- end.compact
67
-
68
- pushed = false
69
- pushed = raw_push(payloads) if !payloads.empty?
70
- pushed ? payloads.size : nil
130
+ new.push_bulk(items)
71
131
  end
72
132
 
73
133
  # Resque compatibility helpers. Note all helpers
@@ -98,7 +158,7 @@ module Sidekiq
98
158
  ts = (int < 1_000_000_000 ? now + int : int)
99
159
 
100
160
  item = { 'class' => klass, 'args' => args, 'at' => ts, 'queue' => queue }
101
- item.delete('at') if ts <= now
161
+ item.delete('at'.freeze) if ts <= now
102
162
 
103
163
  klass.client_push(item)
104
164
  end
@@ -109,56 +169,68 @@ module Sidekiq
109
169
  def enqueue_in(interval, klass, *args)
110
170
  klass.perform_in(interval, *args)
111
171
  end
172
+ end
112
173
 
113
- private
114
-
115
- def raw_push(payloads)
116
- pushed = false
117
- Sidekiq.redis do |conn|
118
- if payloads.first['at']
119
- pushed = conn.zadd('schedule', payloads.map do |hash|
120
- at = hash.delete('at').to_s
121
- [at, Sidekiq.dump_json(hash)]
122
- end)
123
- else
124
- q = payloads.first['queue']
125
- to_push = payloads.map { |entry| Sidekiq.dump_json(entry) }
126
- _, pushed = conn.multi do
127
- conn.sadd('queues', q)
128
- conn.lpush("queue:#{q}", to_push)
129
- end
130
- end
174
+ private
175
+
176
+ def raw_push(payloads)
177
+ @redis_pool.with do |conn|
178
+ conn.multi do
179
+ atomic_push(conn, payloads)
131
180
  end
132
- pushed
133
181
  end
182
+ true
183
+ end
134
184
 
135
- def process_single(worker_class, item)
136
- queue = item['queue']
137
-
138
- Sidekiq.client_middleware.invoke(worker_class, item, queue) do
139
- item
185
+ def atomic_push(conn, payloads)
186
+ if payloads.first['at']
187
+ conn.zadd('schedule'.freeze, payloads.map do |hash|
188
+ at = hash.delete('at'.freeze).to_s
189
+ [at, Sidekiq.dump_json(hash)]
190
+ end)
191
+ else
192
+ q = payloads.first['queue']
193
+ now = Time.now.to_f
194
+ to_push = payloads.map do |entry|
195
+ entry['enqueued_at'.freeze] = now
196
+ Sidekiq.dump_json(entry)
140
197
  end
198
+ conn.sadd('queues'.freeze, q)
199
+ conn.lpush("queue:#{q}", to_push)
141
200
  end
201
+ end
142
202
 
143
- def normalize_item(item)
144
- raise(ArgumentError, "Message must be a Hash of the form: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash)
145
- raise(ArgumentError, "Message must include a class and set of arguments: #{item.inspect}") if !item['class'] || !item['args']
146
- raise(ArgumentError, "Message args must be an Array") unless item['args'].is_a?(Array)
147
- raise(ArgumentError, "Message class must be either a Class or String representation of the class name") unless item['class'].is_a?(Class) || item['class'].is_a?(String)
148
-
149
- if item['class'].is_a?(Class)
150
- raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item['class'].ancestors.inspect}") if !item['class'].respond_to?('get_sidekiq_options')
151
- normalized_item = item['class'].get_sidekiq_options.merge(item)
152
- normalized_item['class'] = normalized_item['class'].to_s
153
- else
154
- normalized_item = Sidekiq.default_worker_options.merge(item)
155
- end
203
+ def process_single(worker_class, item)
204
+ queue = item['queue']
156
205
 
157
- normalized_item['jid'] ||= SecureRandom.hex(12)
158
- normalized_item['enqueued_at'] ||= Time.now.to_f
159
- normalized_item
206
+ middleware.invoke(worker_class, item, queue, @redis_pool) do
207
+ item
160
208
  end
209
+ end
210
+
211
+ def normalize_item(item)
212
+ 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)
213
+ raise(ArgumentError, "Job args must be an Array") unless item['args'].is_a?(Array)
214
+ 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)
215
+ #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']
161
216
 
217
+ normalized_hash(item['class'.freeze])
218
+ .each{ |key, value| item[key] = value if item[key].nil? }
219
+
220
+ item['class'.freeze] = item['class'.freeze].to_s
221
+ item['queue'.freeze] = item['queue'.freeze].to_s
222
+ item['jid'.freeze] ||= SecureRandom.hex(12)
223
+ item['created_at'.freeze] ||= Time.now.to_f
224
+ item
225
+ end
226
+
227
+ def normalized_hash(item_class)
228
+ if item_class.is_a?(Class)
229
+ raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") if !item_class.respond_to?('get_sidekiq_options'.freeze)
230
+ item_class.get_sidekiq_options
231
+ else
232
+ Sidekiq.default_worker_options
233
+ end
162
234
  end
163
235
  end
164
236
  end