sidekiq 6.0.5

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 +61 -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 +1679 -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 +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 +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 +391 -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 +357 -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 +83 -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,391 @@
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
+ # 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
+ # RACK_ENV and RAILS_ENV are there for legacy support.
189
+ @environment = cli_env || ENV["APP_ENV"] || ENV["RACK_ENV"] || ENV["RAILS_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? || opts[:queues].empty?
232
+ opts[:strict] = true if opts[:strict].nil?
233
+ opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if opts[:concurrency].nil? && ENV["RAILS_MAX_THREADS"]
234
+
235
+ # merge with defaults
236
+ options.merge!(opts)
237
+ end
238
+
239
+ def options
240
+ Sidekiq.options
241
+ end
242
+
243
+ def boot_system
244
+ ENV["RACK_ENV"] = ENV["RAILS_ENV"] = environment
245
+
246
+ if File.directory?(options[:require])
247
+ require "rails"
248
+ if ::Rails::VERSION::MAJOR < 5
249
+ raise "Sidekiq no longer supports this version of Rails"
250
+ else
251
+ require "sidekiq/rails"
252
+ require File.expand_path("#{options[:require]}/config/environment.rb")
253
+ end
254
+ options[:tag] ||= default_tag
255
+ else
256
+ require options[:require]
257
+ end
258
+ end
259
+
260
+ def default_tag
261
+ dir = ::Rails.root
262
+ name = File.basename(dir)
263
+ prevdir = File.dirname(dir) # Capistrano release directory?
264
+ if name.to_i != 0 && prevdir
265
+ if File.basename(prevdir) == "releases"
266
+ return File.basename(File.dirname(prevdir))
267
+ end
268
+ end
269
+ name
270
+ end
271
+
272
+ def validate!
273
+ if !File.exist?(options[:require]) ||
274
+ (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
275
+ logger.info "=================================================================="
276
+ logger.info " Please point Sidekiq to a Rails application or a Ruby file "
277
+ logger.info " to load your worker classes with -r [DIR|FILE]."
278
+ logger.info "=================================================================="
279
+ logger.info @parser
280
+ die(1)
281
+ end
282
+
283
+ [:concurrency, :timeout].each do |opt|
284
+ raise ArgumentError, "#{opt}: #{options[opt]} is not a valid value" if options.key?(opt) && options[opt].to_i <= 0
285
+ end
286
+ end
287
+
288
+ def parse_options(argv)
289
+ opts = {}
290
+ @parser = option_parser(opts)
291
+ @parser.parse!(argv)
292
+ opts
293
+ end
294
+
295
+ def option_parser(opts)
296
+ parser = OptionParser.new { |o|
297
+ o.on "-c", "--concurrency INT", "processor threads to use" do |arg|
298
+ opts[:concurrency] = Integer(arg)
299
+ end
300
+
301
+ o.on "-d", "--daemon", "Daemonize process" do |arg|
302
+ puts "ERROR: Daemonization mode was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services"
303
+ end
304
+
305
+ o.on "-e", "--environment ENV", "Application environment" do |arg|
306
+ opts[:environment] = arg
307
+ end
308
+
309
+ o.on "-g", "--tag TAG", "Process tag for procline" do |arg|
310
+ opts[:tag] = arg
311
+ end
312
+
313
+ o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
314
+ queue, weight = arg.split(",")
315
+ parse_queue opts, queue, weight
316
+ end
317
+
318
+ o.on "-r", "--require [PATH|DIR]", "Location of Rails application with workers or file to require" do |arg|
319
+ opts[:require] = arg
320
+ end
321
+
322
+ o.on "-t", "--timeout NUM", "Shutdown timeout" do |arg|
323
+ opts[:timeout] = Integer(arg)
324
+ end
325
+
326
+ o.on "-v", "--verbose", "Print more verbose output" do |arg|
327
+ opts[:verbose] = arg
328
+ end
329
+
330
+ o.on "-C", "--config PATH", "path to YAML config file" do |arg|
331
+ opts[:config_file] = arg
332
+ end
333
+
334
+ o.on "-L", "--logfile PATH", "path to writable logfile" do |arg|
335
+ puts "ERROR: Logfile redirection was removed in Sidekiq 6.0, Sidekiq will only log to STDOUT"
336
+ end
337
+
338
+ o.on "-P", "--pidfile PATH", "path to pidfile" do |arg|
339
+ puts "ERROR: PID file creation was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services"
340
+ end
341
+
342
+ o.on "-V", "--version", "Print version and exit" do |arg|
343
+ puts "Sidekiq #{Sidekiq::VERSION}"
344
+ die(0)
345
+ end
346
+ }
347
+
348
+ parser.banner = "sidekiq [options]"
349
+ parser.on_tail "-h", "--help", "Show help" do
350
+ logger.info parser
351
+ die 1
352
+ end
353
+
354
+ parser
355
+ end
356
+
357
+ def initialize_logger
358
+ Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose]
359
+ end
360
+
361
+ def parse_config(path)
362
+ opts = YAML.load(ERB.new(File.read(path)).result) || {}
363
+
364
+ if opts.respond_to? :deep_symbolize_keys!
365
+ opts.deep_symbolize_keys!
366
+ else
367
+ symbolize_keys_deep!(opts)
368
+ end
369
+
370
+ opts = opts.merge(opts.delete(environment.to_sym) || {})
371
+ parse_queues(opts, opts.delete(:queues) || [])
372
+
373
+ opts
374
+ end
375
+
376
+ def parse_queues(opts, queues_and_weights)
377
+ queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
378
+ end
379
+
380
+ def parse_queue(opts, queue, weight = nil)
381
+ opts[:queues] ||= []
382
+ raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
383
+ [weight.to_i, 1].max.times { opts[:queues] << queue }
384
+ opts[:strict] = false if weight.to_i > 0
385
+ end
386
+
387
+ def rails_app?
388
+ defined?(::Rails) && ::Rails.respond_to?(:application)
389
+ end
390
+ end
391
+ 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