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