sidekiq 6.0.4

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 (122) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +82 -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 +1666 -0
  13. data/Ent-2.0-Upgrade.md +37 -0
  14. data/Ent-Changes.md +256 -0
  15. data/Gemfile +24 -0
  16. data/Gemfile.lock +199 -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 +776 -0
  23. data/README.md +97 -0
  24. data/Rakefile +10 -0
  25. data/bin/sidekiq +18 -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 +260 -0
  34. data/lib/sidekiq/api.rb +960 -0
  35. data/lib/sidekiq/cli.rb +387 -0
  36. data/lib/sidekiq/client.rb +256 -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 +42 -0
  41. data/lib/sidekiq/extensions/class_methods.rb +42 -0
  42. data/lib/sidekiq/extensions/generic_proxy.rb +31 -0
  43. data/lib/sidekiq/fetch.rb +80 -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 +179 -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 +52 -0
  55. data/lib/sidekiq/redis_connection.rb +141 -0
  56. data/lib/sidekiq/scheduled.rb +173 -0
  57. data/lib/sidekiq/testing.rb +344 -0
  58. data/lib/sidekiq/testing/inline.rb +30 -0
  59. data/lib/sidekiq/util.rb +67 -0
  60. data/lib/sidekiq/version.rb +5 -0
  61. data/lib/sidekiq/web.rb +205 -0
  62. data/lib/sidekiq/web/action.rb +93 -0
  63. data/lib/sidekiq/web/application.rb +359 -0
  64. data/lib/sidekiq/web/helpers.rb +336 -0
  65. data/lib/sidekiq/web/router.rb +103 -0
  66. data/lib/sidekiq/worker.rb +247 -0
  67. data/sidekiq.gemspec +21 -0
  68. data/web/assets/images/favicon.ico +0 -0
  69. data/web/assets/images/logo.png +0 -0
  70. data/web/assets/images/status.png +0 -0
  71. data/web/assets/javascripts/application.js +92 -0
  72. data/web/assets/javascripts/dashboard.js +296 -0
  73. data/web/assets/stylesheets/application-dark.css +125 -0
  74. data/web/assets/stylesheets/application-rtl.css +246 -0
  75. data/web/assets/stylesheets/application.css +1153 -0
  76. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  77. data/web/assets/stylesheets/bootstrap.css +5 -0
  78. data/web/locales/ar.yml +81 -0
  79. data/web/locales/cs.yml +78 -0
  80. data/web/locales/da.yml +68 -0
  81. data/web/locales/de.yml +81 -0
  82. data/web/locales/el.yml +68 -0
  83. data/web/locales/en.yml +83 -0
  84. data/web/locales/es.yml +70 -0
  85. data/web/locales/fa.yml +80 -0
  86. data/web/locales/fr.yml +78 -0
  87. data/web/locales/he.yml +79 -0
  88. data/web/locales/hi.yml +75 -0
  89. data/web/locales/it.yml +69 -0
  90. data/web/locales/ja.yml +81 -0
  91. data/web/locales/ko.yml +68 -0
  92. data/web/locales/nb.yml +77 -0
  93. data/web/locales/nl.yml +68 -0
  94. data/web/locales/pl.yml +59 -0
  95. data/web/locales/pt-br.yml +68 -0
  96. data/web/locales/pt.yml +67 -0
  97. data/web/locales/ru.yml +78 -0
  98. data/web/locales/sv.yml +68 -0
  99. data/web/locales/ta.yml +75 -0
  100. data/web/locales/uk.yml +76 -0
  101. data/web/locales/ur.yml +80 -0
  102. data/web/locales/zh-cn.yml +68 -0
  103. data/web/locales/zh-tw.yml +68 -0
  104. data/web/views/_footer.erb +20 -0
  105. data/web/views/_job_info.erb +89 -0
  106. data/web/views/_nav.erb +52 -0
  107. data/web/views/_paging.erb +23 -0
  108. data/web/views/_poll_link.erb +7 -0
  109. data/web/views/_status.erb +4 -0
  110. data/web/views/_summary.erb +40 -0
  111. data/web/views/busy.erb +101 -0
  112. data/web/views/dashboard.erb +75 -0
  113. data/web/views/dead.erb +34 -0
  114. data/web/views/layout.erb +41 -0
  115. data/web/views/morgue.erb +78 -0
  116. data/web/views/queue.erb +55 -0
  117. data/web/views/queues.erb +38 -0
  118. data/web/views/retries.erb +83 -0
  119. data/web/views/retry.erb +34 -0
  120. data/web/views/scheduled.erb +57 -0
  121. data/web/views/scheduled_job_info.erb +8 -0
  122. metadata +221 -0
@@ -0,0 +1,387 @@
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: http://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
+ @environment = cli_env || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
186
+ end
187
+
188
+ def symbolize_keys_deep!(hash)
189
+ hash.keys.each do |k|
190
+ symkey = k.respond_to?(:to_sym) ? k.to_sym : k
191
+ hash[symkey] = hash.delete k
192
+ symbolize_keys_deep! hash[symkey] if hash[symkey].is_a? Hash
193
+ end
194
+ end
195
+
196
+ alias_method :die, :exit
197
+ alias_method :☠, :exit
198
+
199
+ def setup_options(args)
200
+ # parse CLI options
201
+ opts = parse_options(args)
202
+
203
+ set_environment opts[:environment]
204
+
205
+ # check config file presence
206
+ if opts[:config_file]
207
+ unless File.exist?(opts[:config_file])
208
+ raise ArgumentError, "No such file #{opts[:config_file]}"
209
+ end
210
+ else
211
+ config_dir = if File.directory?(opts[:require].to_s)
212
+ File.join(opts[:require], "config")
213
+ else
214
+ File.join(options[:require], "config")
215
+ end
216
+
217
+ %w[sidekiq.yml sidekiq.yml.erb].each do |config_file|
218
+ path = File.join(config_dir, config_file)
219
+ opts[:config_file] ||= path if File.exist?(path)
220
+ end
221
+ end
222
+
223
+ # parse config file options
224
+ opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
225
+
226
+ # set defaults
227
+ opts[:queues] = ["default"] if opts[:queues].nil? || opts[:queues].empty?
228
+ opts[:strict] = true if opts[:strict].nil?
229
+ opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if opts[:concurrency].nil? && ENV["RAILS_MAX_THREADS"]
230
+
231
+ # merge with defaults
232
+ options.merge!(opts)
233
+ end
234
+
235
+ def options
236
+ Sidekiq.options
237
+ end
238
+
239
+ def boot_system
240
+ ENV["RACK_ENV"] = ENV["RAILS_ENV"] = environment
241
+
242
+ if File.directory?(options[:require])
243
+ require "rails"
244
+ if ::Rails::VERSION::MAJOR < 5
245
+ raise "Sidekiq no longer supports this version of Rails"
246
+ else
247
+ require "sidekiq/rails"
248
+ require File.expand_path("#{options[:require]}/config/environment.rb")
249
+ end
250
+ options[:tag] ||= default_tag
251
+ else
252
+ require options[:require]
253
+ end
254
+ end
255
+
256
+ def default_tag
257
+ dir = ::Rails.root
258
+ name = File.basename(dir)
259
+ prevdir = File.dirname(dir) # Capistrano release directory?
260
+ if name.to_i != 0 && prevdir
261
+ if File.basename(prevdir) == "releases"
262
+ return File.basename(File.dirname(prevdir))
263
+ end
264
+ end
265
+ name
266
+ end
267
+
268
+ def validate!
269
+ if !File.exist?(options[:require]) ||
270
+ (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
271
+ logger.info "=================================================================="
272
+ logger.info " Please point Sidekiq to a Rails application or a Ruby file "
273
+ logger.info " to load your worker classes with -r [DIR|FILE]."
274
+ logger.info "=================================================================="
275
+ logger.info @parser
276
+ die(1)
277
+ end
278
+
279
+ [:concurrency, :timeout].each do |opt|
280
+ raise ArgumentError, "#{opt}: #{options[opt]} is not a valid value" if options.key?(opt) && options[opt].to_i <= 0
281
+ end
282
+ end
283
+
284
+ def parse_options(argv)
285
+ opts = {}
286
+ @parser = option_parser(opts)
287
+ @parser.parse!(argv)
288
+ opts
289
+ end
290
+
291
+ def option_parser(opts)
292
+ parser = OptionParser.new { |o|
293
+ o.on "-c", "--concurrency INT", "processor threads to use" do |arg|
294
+ opts[:concurrency] = Integer(arg)
295
+ end
296
+
297
+ o.on "-d", "--daemon", "Daemonize process" do |arg|
298
+ puts "ERROR: Daemonization mode was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services"
299
+ end
300
+
301
+ o.on "-e", "--environment ENV", "Application environment" do |arg|
302
+ opts[:environment] = arg
303
+ end
304
+
305
+ o.on "-g", "--tag TAG", "Process tag for procline" do |arg|
306
+ opts[:tag] = arg
307
+ end
308
+
309
+ o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
310
+ queue, weight = arg.split(",")
311
+ parse_queue opts, queue, weight
312
+ end
313
+
314
+ o.on "-r", "--require [PATH|DIR]", "Location of Rails application with workers or file to require" do |arg|
315
+ opts[:require] = arg
316
+ end
317
+
318
+ o.on "-t", "--timeout NUM", "Shutdown timeout" do |arg|
319
+ opts[:timeout] = Integer(arg)
320
+ end
321
+
322
+ o.on "-v", "--verbose", "Print more verbose output" do |arg|
323
+ opts[:verbose] = arg
324
+ end
325
+
326
+ o.on "-C", "--config PATH", "path to YAML config file" do |arg|
327
+ opts[:config_file] = arg
328
+ end
329
+
330
+ o.on "-L", "--logfile PATH", "path to writable logfile" do |arg|
331
+ puts "ERROR: Logfile redirection was removed in Sidekiq 6.0, Sidekiq will only log to STDOUT"
332
+ end
333
+
334
+ o.on "-P", "--pidfile PATH", "path to pidfile" do |arg|
335
+ puts "ERROR: PID file creation was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services"
336
+ end
337
+
338
+ o.on "-V", "--version", "Print version and exit" do |arg|
339
+ puts "Sidekiq #{Sidekiq::VERSION}"
340
+ die(0)
341
+ end
342
+ }
343
+
344
+ parser.banner = "sidekiq [options]"
345
+ parser.on_tail "-h", "--help", "Show help" do
346
+ logger.info parser
347
+ die 1
348
+ end
349
+
350
+ parser
351
+ end
352
+
353
+ def initialize_logger
354
+ Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose]
355
+ end
356
+
357
+ def parse_config(path)
358
+ opts = YAML.load(ERB.new(File.read(path)).result) || {}
359
+
360
+ if opts.respond_to? :deep_symbolize_keys!
361
+ opts.deep_symbolize_keys!
362
+ else
363
+ symbolize_keys_deep!(opts)
364
+ end
365
+
366
+ opts = opts.merge(opts.delete(environment.to_sym) || {})
367
+ parse_queues(opts, opts.delete(:queues) || [])
368
+
369
+ opts
370
+ end
371
+
372
+ def parse_queues(opts, queues_and_weights)
373
+ queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
374
+ end
375
+
376
+ def parse_queue(opts, queue, weight = nil)
377
+ opts[:queues] ||= []
378
+ raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
379
+ [weight.to_i, 1].max.times { opts[:queues] << queue }
380
+ opts[:strict] = false if weight.to_i > 0
381
+ end
382
+
383
+ def rails_app?
384
+ defined?(::Rails)
385
+ end
386
+ end
387
+ end
@@ -0,0 +1,256 @@
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
+ 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]]" unless arg.is_a?(Array)
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
+
100
+ normed = normalize_item(items)
101
+ payloads = items["args"].map.with_index { |args, index|
102
+ copy = normed.merge("args" => args, "jid" => SecureRandom.hex(12), "enqueued_at" => Time.now.to_f)
103
+ copy["at"] = (at.is_a?(Array) ? at[index] : at) if at
104
+
105
+ result = process_single(items["class"], copy)
106
+ result || nil
107
+ }.compact
108
+
109
+ raw_push(payloads) unless payloads.empty?
110
+ payloads.collect { |payload| payload["jid"] }
111
+ end
112
+
113
+ # Allows sharding of jobs across any number of Redis instances. All jobs
114
+ # defined within the block will use the given Redis connection pool.
115
+ #
116
+ # pool = ConnectionPool.new { Redis.new }
117
+ # Sidekiq::Client.via(pool) do
118
+ # SomeWorker.perform_async(1,2,3)
119
+ # SomeOtherWorker.perform_async(1,2,3)
120
+ # end
121
+ #
122
+ # Generally this is only needed for very large Sidekiq installs processing
123
+ # thousands of jobs per second. I do not recommend sharding unless
124
+ # you cannot scale any other way (e.g. splitting your app into smaller apps).
125
+ def self.via(pool)
126
+ raise ArgumentError, "No pool given" if pool.nil?
127
+ current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
128
+ Thread.current[:sidekiq_via_pool] = pool
129
+ yield
130
+ ensure
131
+ Thread.current[:sidekiq_via_pool] = current_sidekiq_pool
132
+ end
133
+
134
+ class << self
135
+ def push(item)
136
+ new.push(item)
137
+ end
138
+
139
+ def push_bulk(items)
140
+ new.push_bulk(items)
141
+ end
142
+
143
+ # Resque compatibility helpers. Note all helpers
144
+ # should go through Worker#client_push.
145
+ #
146
+ # Example usage:
147
+ # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
148
+ #
149
+ # Messages are enqueued to the 'default' queue.
150
+ #
151
+ def enqueue(klass, *args)
152
+ klass.client_push("class" => klass, "args" => args)
153
+ end
154
+
155
+ # Example usage:
156
+ # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
157
+ #
158
+ def enqueue_to(queue, klass, *args)
159
+ klass.client_push("queue" => queue, "class" => klass, "args" => args)
160
+ end
161
+
162
+ # Example usage:
163
+ # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
164
+ #
165
+ def enqueue_to_in(queue, interval, klass, *args)
166
+ int = interval.to_f
167
+ now = Time.now.to_f
168
+ ts = (int < 1_000_000_000 ? now + int : int)
169
+
170
+ item = {"class" => klass, "args" => args, "at" => ts, "queue" => queue}
171
+ item.delete("at") if ts <= now
172
+
173
+ klass.client_push(item)
174
+ end
175
+
176
+ # Example usage:
177
+ # Sidekiq::Client.enqueue_in(3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
178
+ #
179
+ def enqueue_in(interval, klass, *args)
180
+ klass.perform_in(interval, *args)
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def raw_push(payloads)
187
+ @redis_pool.with do |conn|
188
+ conn.multi do
189
+ atomic_push(conn, payloads)
190
+ end
191
+ end
192
+ true
193
+ end
194
+
195
+ def atomic_push(conn, payloads)
196
+ if payloads.first.key?("at")
197
+ conn.zadd("schedule", payloads.map { |hash|
198
+ at = hash.delete("at").to_s
199
+ [at, Sidekiq.dump_json(hash)]
200
+ })
201
+ else
202
+ queue = payloads.first["queue"]
203
+ now = Time.now.to_f
204
+ to_push = payloads.map { |entry|
205
+ entry["enqueued_at"] = now
206
+ Sidekiq.dump_json(entry)
207
+ }
208
+ conn.sadd("queues", queue)
209
+ conn.lpush("queue:#{queue}", to_push)
210
+ end
211
+ end
212
+
213
+ def process_single(worker_class, item)
214
+ queue = item["queue"]
215
+
216
+ middleware.invoke(worker_class, item, queue, @redis_pool) do
217
+ item
218
+ end
219
+ end
220
+
221
+ def normalize_item(item)
222
+ # 6.0.0 push_bulk bug, #4321
223
+ # TODO Remove after a while...
224
+ item.delete("at") if item.key?("at") && item["at"].nil?
225
+
226
+ 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.key?("class") && item.key?("args")
227
+ raise(ArgumentError, "Job args must be an Array") unless item["args"].is_a?(Array)
228
+ 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)
229
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp") if item.key?("at") && !item["at"].is_a?(Numeric)
230
+ raise(ArgumentError, "Job tags must be an Array") if item["tags"] && !item["tags"].is_a?(Array)
231
+ # 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']
232
+
233
+ # merge in the default sidekiq_options for the item's class and/or wrapped element
234
+ # this allows ActiveJobs to control sidekiq_options too.
235
+ defaults = normalized_hash(item["class"])
236
+ defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?("get_sidekiq_options")
237
+ item = defaults.merge(item)
238
+
239
+ item["class"] = item["class"].to_s
240
+ item["queue"] = item["queue"].to_s
241
+ item["jid"] ||= SecureRandom.hex(12)
242
+ item["created_at"] ||= Time.now.to_f
243
+
244
+ item
245
+ end
246
+
247
+ def normalized_hash(item_class)
248
+ if item_class.is_a?(Class)
249
+ raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?("get_sidekiq_options")
250
+ item_class.get_sidekiq_options
251
+ else
252
+ Sidekiq.default_worker_options
253
+ end
254
+ end
255
+ end
256
+ end