sidekiq 5.2.8

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 (119) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +61 -0
  3. data/.github/contributing.md +32 -0
  4. data/.github/issue_template.md +11 -0
  5. data/.gitignore +15 -0
  6. data/.travis.yml +11 -0
  7. data/3.0-Upgrade.md +70 -0
  8. data/4.0-Upgrade.md +53 -0
  9. data/5.0-Upgrade.md +56 -0
  10. data/COMM-LICENSE +97 -0
  11. data/Changes.md +1542 -0
  12. data/Ent-Changes.md +238 -0
  13. data/Gemfile +23 -0
  14. data/LICENSE +9 -0
  15. data/Pro-2.0-Upgrade.md +138 -0
  16. data/Pro-3.0-Upgrade.md +44 -0
  17. data/Pro-4.0-Upgrade.md +35 -0
  18. data/Pro-Changes.md +759 -0
  19. data/README.md +109 -0
  20. data/Rakefile +9 -0
  21. data/bin/sidekiq +18 -0
  22. data/bin/sidekiqctl +20 -0
  23. data/bin/sidekiqload +149 -0
  24. data/code_of_conduct.md +50 -0
  25. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  26. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  27. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  28. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  29. data/lib/sidekiq.rb +237 -0
  30. data/lib/sidekiq/api.rb +940 -0
  31. data/lib/sidekiq/cli.rb +445 -0
  32. data/lib/sidekiq/client.rb +243 -0
  33. data/lib/sidekiq/core_ext.rb +1 -0
  34. data/lib/sidekiq/ctl.rb +221 -0
  35. data/lib/sidekiq/delay.rb +42 -0
  36. data/lib/sidekiq/exception_handler.rb +29 -0
  37. data/lib/sidekiq/extensions/action_mailer.rb +57 -0
  38. data/lib/sidekiq/extensions/active_record.rb +40 -0
  39. data/lib/sidekiq/extensions/class_methods.rb +40 -0
  40. data/lib/sidekiq/extensions/generic_proxy.rb +31 -0
  41. data/lib/sidekiq/fetch.rb +81 -0
  42. data/lib/sidekiq/job_logger.rb +25 -0
  43. data/lib/sidekiq/job_retry.rb +262 -0
  44. data/lib/sidekiq/launcher.rb +173 -0
  45. data/lib/sidekiq/logging.rb +122 -0
  46. data/lib/sidekiq/manager.rb +137 -0
  47. data/lib/sidekiq/middleware/chain.rb +150 -0
  48. data/lib/sidekiq/middleware/i18n.rb +42 -0
  49. data/lib/sidekiq/middleware/server/active_record.rb +23 -0
  50. data/lib/sidekiq/paginator.rb +43 -0
  51. data/lib/sidekiq/processor.rb +279 -0
  52. data/lib/sidekiq/rails.rb +58 -0
  53. data/lib/sidekiq/redis_connection.rb +144 -0
  54. data/lib/sidekiq/scheduled.rb +174 -0
  55. data/lib/sidekiq/testing.rb +333 -0
  56. data/lib/sidekiq/testing/inline.rb +29 -0
  57. data/lib/sidekiq/util.rb +66 -0
  58. data/lib/sidekiq/version.rb +4 -0
  59. data/lib/sidekiq/web.rb +213 -0
  60. data/lib/sidekiq/web/action.rb +89 -0
  61. data/lib/sidekiq/web/application.rb +353 -0
  62. data/lib/sidekiq/web/helpers.rb +325 -0
  63. data/lib/sidekiq/web/router.rb +100 -0
  64. data/lib/sidekiq/worker.rb +220 -0
  65. data/sidekiq.gemspec +21 -0
  66. data/web/assets/images/favicon.ico +0 -0
  67. data/web/assets/images/logo.png +0 -0
  68. data/web/assets/images/status.png +0 -0
  69. data/web/assets/javascripts/application.js +92 -0
  70. data/web/assets/javascripts/dashboard.js +315 -0
  71. data/web/assets/stylesheets/application-rtl.css +246 -0
  72. data/web/assets/stylesheets/application.css +1144 -0
  73. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  74. data/web/assets/stylesheets/bootstrap.css +5 -0
  75. data/web/locales/ar.yml +81 -0
  76. data/web/locales/cs.yml +78 -0
  77. data/web/locales/da.yml +68 -0
  78. data/web/locales/de.yml +69 -0
  79. data/web/locales/el.yml +68 -0
  80. data/web/locales/en.yml +81 -0
  81. data/web/locales/es.yml +70 -0
  82. data/web/locales/fa.yml +80 -0
  83. data/web/locales/fr.yml +78 -0
  84. data/web/locales/he.yml +79 -0
  85. data/web/locales/hi.yml +75 -0
  86. data/web/locales/it.yml +69 -0
  87. data/web/locales/ja.yml +80 -0
  88. data/web/locales/ko.yml +68 -0
  89. data/web/locales/nb.yml +77 -0
  90. data/web/locales/nl.yml +68 -0
  91. data/web/locales/pl.yml +59 -0
  92. data/web/locales/pt-br.yml +68 -0
  93. data/web/locales/pt.yml +67 -0
  94. data/web/locales/ru.yml +78 -0
  95. data/web/locales/sv.yml +68 -0
  96. data/web/locales/ta.yml +75 -0
  97. data/web/locales/uk.yml +76 -0
  98. data/web/locales/ur.yml +80 -0
  99. data/web/locales/zh-cn.yml +68 -0
  100. data/web/locales/zh-tw.yml +68 -0
  101. data/web/views/_footer.erb +20 -0
  102. data/web/views/_job_info.erb +88 -0
  103. data/web/views/_nav.erb +52 -0
  104. data/web/views/_paging.erb +23 -0
  105. data/web/views/_poll_link.erb +7 -0
  106. data/web/views/_status.erb +4 -0
  107. data/web/views/_summary.erb +40 -0
  108. data/web/views/busy.erb +98 -0
  109. data/web/views/dashboard.erb +75 -0
  110. data/web/views/dead.erb +34 -0
  111. data/web/views/layout.erb +40 -0
  112. data/web/views/morgue.erb +75 -0
  113. data/web/views/queue.erb +46 -0
  114. data/web/views/queues.erb +30 -0
  115. data/web/views/retries.erb +80 -0
  116. data/web/views/retry.erb +34 -0
  117. data/web/views/scheduled.erb +54 -0
  118. data/web/views/scheduled_job_info.erb +8 -0
  119. metadata +230 -0
@@ -0,0 +1,445 @@
1
+ # frozen_string_literal: true
2
+ $stdout.sync = true
3
+
4
+ require 'yaml'
5
+ require 'singleton'
6
+ require 'optparse'
7
+ require 'erb'
8
+ require 'fileutils'
9
+
10
+ require 'sidekiq'
11
+ require 'sidekiq/util'
12
+ require 'sidekiq/launcher'
13
+
14
+ module Sidekiq
15
+ class CLI
16
+ include Util
17
+ include Singleton unless $TESTING
18
+
19
+ PROCTITLES = [
20
+ proc { 'sidekiq' },
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
+ ]
26
+
27
+ attr_accessor :launcher
28
+ attr_accessor :environment
29
+
30
+ def parse(args = ARGV)
31
+ setup_options(args)
32
+ initialize_logger
33
+ validate!
34
+ end
35
+
36
+ def jruby?
37
+ defined?(::JRUBY_VERSION)
38
+ end
39
+
40
+ # Code within this method is not tested because it alters
41
+ # global process state irreversibly. PRs which improve the
42
+ # test coverage of Sidekiq::CLI are welcomed.
43
+ def run
44
+ daemonize if options[:daemon]
45
+ write_pid
46
+ boot_system
47
+ print_banner if environment == 'development' && $stdout.tty?
48
+
49
+ self_read, self_write = IO.pipe
50
+ sigs = %w(INT TERM TTIN TSTP)
51
+ # USR1 and USR2 don't work on the JVM
52
+ if !jruby?
53
+ sigs << 'USR1'
54
+ sigs << 'USR2'
55
+ end
56
+
57
+ sigs.each do |sig|
58
+ begin
59
+ trap sig do
60
+ self_write.write("#{sig}\n")
61
+ end
62
+ rescue ArgumentError
63
+ puts "Signal #{sig} not supported"
64
+ end
65
+ end
66
+
67
+ logger.info "Running in #{RUBY_DESCRIPTION}"
68
+ logger.info Sidekiq::LICENSE
69
+ logger.info "Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org" unless defined?(::Sidekiq::Pro)
70
+
71
+ # touch the connection pool so it is created before we
72
+ # fire startup and start multithreading.
73
+ ver = Sidekiq.redis_info['redis_version']
74
+ raise "You are using Redis v#{ver}, Sidekiq requires Redis v2.8.0 or greater" if ver < '2.8'
75
+ logger.warn "Sidekiq 6.0 will require Redis 4.0+, you are using Redis v#{ver}" if ver < '4'
76
+
77
+ # Since the user can pass us a connection pool explicitly in the initializer, we
78
+ # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
79
+ cursize = Sidekiq.redis_pool.size
80
+ needed = Sidekiq.options[:concurrency] + 2
81
+ raise "Your pool of #{cursize} Redis connections is too small, please increase the size to at least #{needed}" if cursize < needed
82
+
83
+ # cache process identity
84
+ Sidekiq.options[:identity] = identity
85
+
86
+ # Touch middleware so it isn't lazy loaded by multiple threads, #3043
87
+ Sidekiq.server_middleware
88
+
89
+ # Before this point, the process is initializing with just the main thread.
90
+ # Starting here the process will now have multiple threads running.
91
+ fire_event(:startup, reverse: false, reraise: true)
92
+
93
+ logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(', ')}" }
94
+ logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(', ')}" }
95
+
96
+ launch(self_read)
97
+ end
98
+
99
+ def launch(self_read)
100
+ if !options[:daemon]
101
+ logger.info 'Starting processing, hit Ctrl-C to stop'
102
+ end
103
+
104
+ @launcher = Sidekiq::Launcher.new(options)
105
+
106
+ begin
107
+ launcher.run
108
+
109
+ while readable_io = IO.select([self_read])
110
+ signal = readable_io.first[0].gets.strip
111
+ handle_signal(signal)
112
+ end
113
+ rescue Interrupt
114
+ logger.info 'Shutting down'
115
+ launcher.stop
116
+ # Explicitly exit so busy Processor threads can't block
117
+ # process shutdown.
118
+ logger.info "Bye!"
119
+ exit(0)
120
+ end
121
+ end
122
+
123
+ def self.banner
124
+ %q{
125
+ m,
126
+ `$b
127
+ .ss, $$: .,d$
128
+ `$$P,d$P' .,md$P"'
129
+ ,$$$$$bmmd$$$P^'
130
+ .d$$$$$$$$$$P'
131
+ $$^' `"^$$$' ____ _ _ _ _
132
+ $: ,$$: / ___|(_) __| | ___| | _(_) __ _
133
+ `b :$$ \___ \| |/ _` |/ _ \ |/ / |/ _` |
134
+ $$: ___) | | (_| | __/ <| | (_| |
135
+ $$ |____/|_|\__,_|\___|_|\_\_|\__, |
136
+ .d$$ |_|
137
+ }
138
+ end
139
+
140
+ SIGNAL_HANDLERS = {
141
+ # Ctrl-C in terminal
142
+ 'INT' => ->(cli) { raise Interrupt },
143
+ # TERM is the signal that Sidekiq must exit.
144
+ # Heroku sends TERM and then waits 30 seconds for process to exit.
145
+ 'TERM' => ->(cli) { raise Interrupt },
146
+ 'USR1' => ->(cli) {
147
+ Sidekiq.logger.info "Received USR1, no longer accepting new work"
148
+ cli.launcher.quiet
149
+ },
150
+ 'TSTP' => ->(cli) {
151
+ Sidekiq.logger.info "Received TSTP, no longer accepting new work"
152
+ cli.launcher.quiet
153
+ },
154
+ 'USR2' => ->(cli) {
155
+ if Sidekiq.options[:logfile]
156
+ Sidekiq.logger.info "Received USR2, reopening log file"
157
+ Sidekiq::Logging.reopen_logs
158
+ end
159
+ },
160
+ 'TTIN' => ->(cli) {
161
+ Thread.list.each do |thread|
162
+ Sidekiq.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread['sidekiq_label']}"
163
+ if thread.backtrace
164
+ Sidekiq.logger.warn thread.backtrace.join("\n")
165
+ else
166
+ Sidekiq.logger.warn "<no backtrace available>"
167
+ end
168
+ end
169
+ },
170
+ }
171
+
172
+ def handle_signal(sig)
173
+ Sidekiq.logger.debug "Got #{sig} signal"
174
+ handy = SIGNAL_HANDLERS[sig]
175
+ if handy
176
+ handy.call(self)
177
+ else
178
+ Sidekiq.logger.info { "No signal handler for #{sig}" }
179
+ end
180
+ end
181
+
182
+ private
183
+
184
+ def print_banner
185
+ puts "\e[#{31}m"
186
+ puts Sidekiq::CLI.banner
187
+ puts "\e[0m"
188
+ end
189
+
190
+ def daemonize
191
+ raise ArgumentError, "You really should set a logfile if you're going to daemonize" unless options[:logfile]
192
+
193
+ files_to_reopen = ObjectSpace.each_object(File).reject { |f| f.closed? }
194
+ ::Process.daemon(true, true)
195
+
196
+ files_to_reopen.each do |file|
197
+ begin
198
+ file.reopen file.path, "a+"
199
+ file.sync = true
200
+ rescue ::Exception
201
+ end
202
+ end
203
+
204
+ [$stdout, $stderr].each do |io|
205
+ File.open(options[:logfile], 'ab') do |f|
206
+ io.reopen(f)
207
+ end
208
+ io.sync = true
209
+ end
210
+ $stdin.reopen('/dev/null')
211
+
212
+ initialize_logger
213
+ end
214
+
215
+ def set_environment(cli_env)
216
+ @environment = cli_env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
217
+ end
218
+
219
+ def symbolize_keys_deep!(hash)
220
+ hash.keys.each do |k|
221
+ symkey = k.respond_to?(:to_sym) ? k.to_sym : k
222
+ hash[symkey] = hash.delete k
223
+ symbolize_keys_deep! hash[symkey] if hash[symkey].kind_of? Hash
224
+ end
225
+ end
226
+
227
+ alias_method :die, :exit
228
+ alias_method :☠, :exit
229
+
230
+ def setup_options(args)
231
+ # parse CLI options
232
+ opts = parse_options(args)
233
+
234
+ set_environment opts[:environment]
235
+
236
+ # check config file presence
237
+ if opts[:config_file]
238
+ if opts[:config_file] && !File.exist?(opts[:config_file])
239
+ raise ArgumentError, "No such file #{opts[:config_file]}"
240
+ end
241
+ else
242
+ config_dir = if File.directory?(opts[:require].to_s)
243
+ File.join(opts[:require], 'config')
244
+ else
245
+ File.join(options[:require], 'config')
246
+ end
247
+
248
+ %w[sidekiq.yml sidekiq.yml.erb].each do |config_file|
249
+ path = File.join(config_dir, config_file)
250
+ opts[:config_file] ||= path if File.exist?(path)
251
+ end
252
+ end
253
+
254
+ # parse config file options
255
+ opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
256
+
257
+ # set defaults
258
+ opts[:queues] = Array(opts[:queues]) << 'default' if opts[:queues].nil? || opts[:queues].empty?
259
+ opts[:strict] = true if opts[:strict].nil?
260
+ opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if opts[:concurrency].nil? && ENV["RAILS_MAX_THREADS"]
261
+
262
+ # merge with defaults
263
+ options.merge!(opts)
264
+ end
265
+
266
+ def options
267
+ Sidekiq.options
268
+ end
269
+
270
+ def boot_system
271
+ ENV['RACK_ENV'] = ENV['RAILS_ENV'] = environment
272
+
273
+ if File.directory?(options[:require])
274
+ require 'rails'
275
+ if ::Rails::VERSION::MAJOR < 4
276
+ raise "Sidekiq no longer supports this version of Rails"
277
+ elsif ::Rails::VERSION::MAJOR == 4
278
+ # Painful contortions, see 1791 for discussion
279
+ # No autoloading, we want to force eager load for everything.
280
+ require File.expand_path("#{options[:require]}/config/application.rb")
281
+ ::Rails::Application.initializer "sidekiq.eager_load" do
282
+ ::Rails.application.config.eager_load = true
283
+ end
284
+ require 'sidekiq/rails'
285
+ require File.expand_path("#{options[:require]}/config/environment.rb")
286
+ else
287
+ require 'sidekiq/rails'
288
+ require File.expand_path("#{options[:require]}/config/environment.rb")
289
+ end
290
+ options[:tag] ||= default_tag
291
+ else
292
+ require options[:require]
293
+ end
294
+ end
295
+
296
+ def default_tag
297
+ dir = ::Rails.root
298
+ name = File.basename(dir)
299
+ if name.to_i != 0 && prevdir = File.dirname(dir) # Capistrano release directory?
300
+ if File.basename(prevdir) == 'releases'
301
+ return File.basename(File.dirname(prevdir))
302
+ end
303
+ end
304
+ name
305
+ end
306
+
307
+ def validate!
308
+ if !File.exist?(options[:require]) ||
309
+ (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
310
+ logger.info "=================================================================="
311
+ logger.info " Please point sidekiq to a Rails 4/5 application or a Ruby file "
312
+ logger.info " to load your worker classes with -r [DIR|FILE]."
313
+ logger.info "=================================================================="
314
+ logger.info @parser
315
+ die(1)
316
+ end
317
+
318
+ [:concurrency, :timeout].each do |opt|
319
+ raise ArgumentError, "#{opt}: #{options[opt]} is not a valid value" if options.has_key?(opt) && options[opt].to_i <= 0
320
+ end
321
+ end
322
+
323
+ def parse_options(argv)
324
+ opts = {}
325
+
326
+ @parser = OptionParser.new do |o|
327
+ o.on '-c', '--concurrency INT', "processor threads to use" do |arg|
328
+ opts[:concurrency] = Integer(arg)
329
+ end
330
+
331
+ o.on '-d', '--daemon', "Daemonize process" do |arg|
332
+ opts[:daemon] = arg
333
+ puts "WARNING: Daemonization mode will be removed in Sidekiq 6.0, see #4045. Please use a proper process supervisor to start and manage your services"
334
+ end
335
+
336
+ o.on '-e', '--environment ENV', "Application environment" do |arg|
337
+ opts[:environment] = arg
338
+ end
339
+
340
+ o.on '-g', '--tag TAG', "Process tag for procline" do |arg|
341
+ opts[:tag] = arg
342
+ end
343
+
344
+ # this index remains here for backwards compatibility but none of the Sidekiq
345
+ # family use this value anymore. it was used by Pro's original reliable_fetch.
346
+ o.on '-i', '--index INT', "unique process index on this machine" do |arg|
347
+ opts[:index] = Integer(arg.match(/\d+/)[0])
348
+ end
349
+
350
+ o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
351
+ queue, weight = arg.split(",")
352
+ parse_queue opts, queue, weight
353
+ end
354
+
355
+ o.on '-r', '--require [PATH|DIR]', "Location of Rails application with workers or file to require" do |arg|
356
+ opts[:require] = arg
357
+ end
358
+
359
+ o.on '-t', '--timeout NUM', "Shutdown timeout" do |arg|
360
+ opts[:timeout] = Integer(arg)
361
+ end
362
+
363
+ o.on "-v", "--verbose", "Print more verbose output" do |arg|
364
+ opts[:verbose] = arg
365
+ end
366
+
367
+ o.on '-C', '--config PATH', "path to YAML config file" do |arg|
368
+ opts[:config_file] = arg
369
+ end
370
+
371
+ o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
372
+ opts[:logfile] = arg
373
+ puts "WARNING: Logfile redirection will be removed in Sidekiq 6.0, see #4045. Sidekiq will only log to STDOUT"
374
+ end
375
+
376
+ o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
377
+ opts[:pidfile] = arg
378
+ puts "WARNING: PID file creation will be removed in Sidekiq 6.0, see #4045. Please use a proper process supervisor to start and manage your services"
379
+ end
380
+
381
+ o.on '-V', '--version', "Print version and exit" do |arg|
382
+ puts "Sidekiq #{Sidekiq::VERSION}"
383
+ die(0)
384
+ end
385
+ end
386
+
387
+ @parser.banner = "sidekiq [options]"
388
+ @parser.on_tail "-h", "--help", "Show help" do
389
+ logger.info @parser
390
+ die 1
391
+ end
392
+
393
+ @parser.parse!(argv)
394
+
395
+ opts
396
+ end
397
+
398
+ def initialize_logger
399
+ Sidekiq::Logging.initialize_logger(options[:logfile]) if options[:logfile]
400
+
401
+ Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose]
402
+ end
403
+
404
+ def write_pid
405
+ if path = options[:pidfile]
406
+ pidfile = File.expand_path(path)
407
+ File.open(pidfile, 'w') do |f|
408
+ f.puts ::Process.pid
409
+ end
410
+ end
411
+ end
412
+
413
+ def parse_config(path)
414
+ opts = YAML.load(ERB.new(File.read(path)).result) || {}
415
+
416
+ if opts.respond_to? :deep_symbolize_keys!
417
+ opts.deep_symbolize_keys!
418
+ else
419
+ symbolize_keys_deep!(opts)
420
+ end
421
+
422
+ opts = opts.merge(opts.delete(environment.to_sym) || {})
423
+ parse_queues(opts, opts.delete(:queues) || [])
424
+
425
+ ns = opts.delete(:namespace)
426
+ if ns
427
+ # logger hasn't been initialized yet, puts is all we have.
428
+ puts("namespace should be set in your ruby initializer, is ignored in config file")
429
+ puts("config.redis = { :url => ..., :namespace => '#{ns}' }")
430
+ end
431
+ opts
432
+ end
433
+
434
+ def parse_queues(opts, queues_and_weights)
435
+ queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
436
+ end
437
+
438
+ def parse_queue(opts, queue, weight = nil)
439
+ opts[:queues] ||= []
440
+ raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
441
+ [weight.to_i, 1].max.times { opts[:queues] << queue }
442
+ opts[:strict] = false if weight.to_i > 0
443
+ end
444
+ end
445
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+ require 'securerandom'
3
+ require 'sidekiq/middleware/chain'
4
+
5
+ module Sidekiq
6
+ class Client
7
+
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
25
+ end
26
+ @chain
27
+ end
28
+
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
44
+
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
+ # 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
+ # backtrace - whether to save any error backtrace, default false
54
+ #
55
+ # If class is set to the class name, the jobs' options will be based on Sidekiq's default
56
+ # worker options. Otherwise, they will be based on the job class's options.
57
+ #
58
+ # Any options valid for a worker class's sidekiq_options are also available here.
59
+ #
60
+ # All options must be strings, not symbols. NB: because we are serializing to JSON, all
61
+ # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
62
+ # space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful.
63
+ #
64
+ # Returns a unique Job ID. If middleware stops the job, nil will be returned instead.
65
+ #
66
+ # Example:
67
+ # push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
68
+ #
69
+ def push(item)
70
+ normed = normalize_item(item)
71
+ payload = process_single(item['class'], normed)
72
+
73
+ if payload
74
+ raw_push([payload])
75
+ payload['jid']
76
+ end
77
+ end
78
+
79
+ ##
80
+ # Push a large number of jobs to Redis. This method cuts out the redis
81
+ # network round trip latency. I wouldn't recommend pushing more than
82
+ # 1000 per call but YMMV based on network quality, size of job args, etc.
83
+ # A large number of jobs can cause a bit of Redis command processing latency.
84
+ #
85
+ # Takes the same arguments as #push except that args is expected to be
86
+ # an Array of Arrays. All other keys are duplicated for each job. Each job
87
+ # is run through the client middleware pipeline and each job gets its own Job ID
88
+ # as normal.
89
+ #
90
+ # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
91
+ # than the number given if the middleware stopped processing for one or more jobs.
92
+ def push_bulk(items)
93
+ arg = items['args'].first
94
+ return [] unless arg # no jobs to push
95
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !arg.is_a?(Array)
96
+
97
+ normed = normalize_item(items)
98
+ payloads = items['args'].map do |args|
99
+ copy = normed.merge('args' => args, 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
100
+ result = process_single(items['class'], copy)
101
+ result ? result : nil
102
+ end.compact
103
+
104
+ raw_push(payloads) if !payloads.empty?
105
+ payloads.collect { |payload| payload['jid'] }
106
+ end
107
+
108
+ # Allows sharding of jobs across any number of Redis instances. All jobs
109
+ # defined within the block will use the given Redis connection pool.
110
+ #
111
+ # pool = ConnectionPool.new { Redis.new }
112
+ # Sidekiq::Client.via(pool) do
113
+ # SomeWorker.perform_async(1,2,3)
114
+ # SomeOtherWorker.perform_async(1,2,3)
115
+ # end
116
+ #
117
+ # Generally this is only needed for very large Sidekiq installs processing
118
+ # thousands of jobs per second. I do not recommend sharding unless
119
+ # you cannot scale any other way (e.g. splitting your app into smaller apps).
120
+ def self.via(pool)
121
+ raise ArgumentError, "No pool given" if pool.nil?
122
+ current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
123
+ Thread.current[:sidekiq_via_pool] = pool
124
+ yield
125
+ ensure
126
+ Thread.current[:sidekiq_via_pool] = current_sidekiq_pool
127
+ end
128
+
129
+ class << self
130
+
131
+ def push(item)
132
+ new.push(item)
133
+ end
134
+
135
+ def push_bulk(items)
136
+ new.push_bulk(items)
137
+ end
138
+
139
+ # Resque compatibility helpers. Note all helpers
140
+ # should go through Worker#client_push.
141
+ #
142
+ # Example usage:
143
+ # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
144
+ #
145
+ # Messages are enqueued to the 'default' queue.
146
+ #
147
+ def enqueue(klass, *args)
148
+ klass.client_push('class' => klass, 'args' => args)
149
+ end
150
+
151
+ # Example usage:
152
+ # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
153
+ #
154
+ def enqueue_to(queue, klass, *args)
155
+ klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
156
+ end
157
+
158
+ # Example usage:
159
+ # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
160
+ #
161
+ def enqueue_to_in(queue, interval, klass, *args)
162
+ int = interval.to_f
163
+ now = Time.now.to_f
164
+ ts = (int < 1_000_000_000 ? now + int : int)
165
+
166
+ item = { 'class' => klass, 'args' => args, 'at' => ts, 'queue' => queue }
167
+ item.delete('at') if ts <= now
168
+
169
+ klass.client_push(item)
170
+ end
171
+
172
+ # Example usage:
173
+ # Sidekiq::Client.enqueue_in(3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
174
+ #
175
+ def enqueue_in(interval, klass, *args)
176
+ klass.perform_in(interval, *args)
177
+ end
178
+ end
179
+
180
+ private
181
+
182
+ def raw_push(payloads)
183
+ @redis_pool.with do |conn|
184
+ conn.multi do
185
+ atomic_push(conn, payloads)
186
+ end
187
+ end
188
+ true
189
+ end
190
+
191
+ def atomic_push(conn, payloads)
192
+ if payloads.first['at']
193
+ conn.zadd('schedule', payloads.map do |hash|
194
+ at = hash.delete('at').to_s
195
+ [at, Sidekiq.dump_json(hash)]
196
+ end)
197
+ else
198
+ q = payloads.first['queue']
199
+ now = Time.now.to_f
200
+ to_push = payloads.map do |entry|
201
+ entry['enqueued_at'] = now
202
+ Sidekiq.dump_json(entry)
203
+ end
204
+ conn.sadd('queues', q)
205
+ conn.lpush("queue:#{q}", to_push)
206
+ end
207
+ end
208
+
209
+ def process_single(worker_class, item)
210
+ queue = item['queue']
211
+
212
+ middleware.invoke(worker_class, item, queue, @redis_pool) do
213
+ item
214
+ end
215
+ end
216
+
217
+ def normalize_item(item)
218
+ 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') && item.has_key?('args')
219
+ raise(ArgumentError, "Job args must be an Array") unless item['args'].is_a?(Array)
220
+ raise(ArgumentError, "Job class must be either a Class or String representation of the class name") unless item['class'].is_a?(Class) || item['class'].is_a?(String)
221
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp") if item.has_key?('at') && !item['at'].is_a?(Numeric)
222
+ #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']
223
+
224
+ normalized_hash(item['class'])
225
+ .each{ |key, value| item[key] = value if item[key].nil? }
226
+
227
+ item['class'] = item['class'].to_s
228
+ item['queue'] = item['queue'].to_s
229
+ item['jid'] ||= SecureRandom.hex(12)
230
+ item['created_at'] ||= Time.now.to_f
231
+ item
232
+ end
233
+
234
+ def normalized_hash(item_class)
235
+ if item_class.is_a?(Class)
236
+ raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") if !item_class.respond_to?('get_sidekiq_options')
237
+ item_class.get_sidekiq_options
238
+ else
239
+ Sidekiq.default_worker_options
240
+ end
241
+ end
242
+ end
243
+ end