sidekiq 6.1.1

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 +1724 -0
  13. data/Ent-2.0-Upgrade.md +37 -0
  14. data/Ent-Changes.md +275 -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 +795 -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 +963 -0
  35. data/lib/sidekiq/cli.rb +396 -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 +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 +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 +90 -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,396 @@
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(boot_app: true)
37
+ boot_application if boot_app
38
+
39
+ if environment == "development" && $stdout.tty? && Sidekiq.log_formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
40
+ print_banner
41
+ end
42
+ logger.info "Booted Rails #{::Rails.version} application in #{environment} environment" if rails_app?
43
+
44
+ self_read, self_write = IO.pipe
45
+ sigs = %w[INT TERM TTIN TSTP]
46
+ # USR1 and USR2 don't work on the JVM
47
+ sigs << "USR2" unless jruby?
48
+ sigs.each do |sig|
49
+ trap sig do
50
+ self_write.puts(sig)
51
+ end
52
+ rescue ArgumentError
53
+ puts "Signal #{sig} not supported"
54
+ end
55
+
56
+ logger.info "Running in #{RUBY_DESCRIPTION}"
57
+ logger.info Sidekiq::LICENSE
58
+ logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
59
+
60
+ # touch the connection pool so it is created before we
61
+ # fire startup and start multithreading.
62
+ ver = Sidekiq.redis_info["redis_version"]
63
+ raise "You are connecting to Redis v#{ver}, Sidekiq requires Redis v4.0.0 or greater" if ver < "4"
64
+
65
+ # Since the user can pass us a connection pool explicitly in the initializer, we
66
+ # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
67
+ cursize = Sidekiq.redis_pool.size
68
+ needed = Sidekiq.options[:concurrency] + 2
69
+ raise "Your pool of #{cursize} Redis connections is too small, please increase the size to at least #{needed}" if cursize < needed
70
+
71
+ # cache process identity
72
+ Sidekiq.options[:identity] = identity
73
+
74
+ # Touch middleware so it isn't lazy loaded by multiple threads, #3043
75
+ Sidekiq.server_middleware
76
+
77
+ # Before this point, the process is initializing with just the main thread.
78
+ # Starting here the process will now have multiple threads running.
79
+ fire_event(:startup, reverse: false, reraise: true)
80
+
81
+ logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(", ")}" }
82
+ logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(", ")}" }
83
+
84
+ launch(self_read)
85
+ end
86
+
87
+ def launch(self_read)
88
+ if environment == "development" && $stdout.tty?
89
+ logger.info "Starting processing, hit Ctrl-C to stop"
90
+ end
91
+
92
+ @launcher = Sidekiq::Launcher.new(options)
93
+
94
+ begin
95
+ launcher.run
96
+
97
+ while (readable_io = IO.select([self_read]))
98
+ signal = readable_io.first[0].gets.strip
99
+ handle_signal(signal)
100
+ end
101
+ rescue Interrupt
102
+ logger.info "Shutting down"
103
+ launcher.stop
104
+ logger.info "Bye!"
105
+
106
+ # Explicitly exit so busy Processor threads won't block process shutdown.
107
+ #
108
+ # NB: slow at_exit handlers will prevent a timely exit if they take
109
+ # a while to run. If Sidekiq is getting here but the process isn't exiting,
110
+ # use the TTIN signal to determine where things are stuck.
111
+ exit(0)
112
+ end
113
+ end
114
+
115
+ def self.w
116
+ "\e[37m"
117
+ end
118
+
119
+ def self.r
120
+ "\e[31m"
121
+ end
122
+
123
+ def self.b
124
+ "\e[30m"
125
+ end
126
+
127
+ def self.reset
128
+ "\e[0m"
129
+ end
130
+
131
+ def self.banner
132
+ %{
133
+ #{w} m,
134
+ #{w} `$b
135
+ #{w} .ss, $$: .,d$
136
+ #{w} `$$P,d$P' .,md$P"'
137
+ #{w} ,$$$$$b#{b}/#{w}md$$$P^'
138
+ #{w} .d$$$$$$#{b}/#{w}$$$P'
139
+ #{w} $$^' `"#{b}/#{w}$$$' #{r}____ _ _ _ _
140
+ #{w} $: ,$$: #{r} / ___|(_) __| | ___| | _(_) __ _
141
+ #{w} `b :$$ #{r} \\___ \\| |/ _` |/ _ \\ |/ / |/ _` |
142
+ #{w} $$: #{r} ___) | | (_| | __/ <| | (_| |
143
+ #{w} $$ #{r}|____/|_|\\__,_|\\___|_|\\_\\_|\\__, |
144
+ #{w} .d$$ #{r} |_|
145
+ #{reset}}
146
+ end
147
+
148
+ SIGNAL_HANDLERS = {
149
+ # Ctrl-C in terminal
150
+ "INT" => ->(cli) { raise Interrupt },
151
+ # TERM is the signal that Sidekiq must exit.
152
+ # Heroku sends TERM and then waits 30 seconds for process to exit.
153
+ "TERM" => ->(cli) { raise Interrupt },
154
+ "TSTP" => ->(cli) {
155
+ Sidekiq.logger.info "Received TSTP, no longer accepting new work"
156
+ cli.launcher.quiet
157
+ },
158
+ "TTIN" => ->(cli) {
159
+ Thread.list.each do |thread|
160
+ Sidekiq.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
161
+ if thread.backtrace
162
+ Sidekiq.logger.warn thread.backtrace.join("\n")
163
+ else
164
+ Sidekiq.logger.warn "<no backtrace available>"
165
+ end
166
+ end
167
+ }
168
+ }
169
+ UNHANDLED_SIGNAL_HANDLER = ->(cli) { Sidekiq.logger.info "No signal handler registered, ignoring" }
170
+ SIGNAL_HANDLERS.default = UNHANDLED_SIGNAL_HANDLER
171
+
172
+ def handle_signal(sig)
173
+ Sidekiq.logger.debug "Got #{sig} signal"
174
+ SIGNAL_HANDLERS[sig].call(self)
175
+ end
176
+
177
+ private
178
+
179
+ def print_banner
180
+ puts "\e[31m"
181
+ puts Sidekiq::CLI.banner
182
+ puts "\e[0m"
183
+ end
184
+
185
+ def set_environment(cli_env)
186
+ # See #984 for discussion.
187
+ # APP_ENV is now the preferred ENV term since it is not tech-specific.
188
+ # Both Sinatra 2.0+ and Sidekiq support this term.
189
+ # RAILS_ENV and RACK_ENV are there for legacy support.
190
+ @environment = cli_env || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
191
+ end
192
+
193
+ def symbolize_keys_deep!(hash)
194
+ hash.keys.each do |k|
195
+ symkey = k.respond_to?(:to_sym) ? k.to_sym : k
196
+ hash[symkey] = hash.delete k
197
+ symbolize_keys_deep! hash[symkey] if hash[symkey].is_a? Hash
198
+ end
199
+ end
200
+
201
+ alias_method :die, :exit
202
+ alias_method :☠, :exit
203
+
204
+ def setup_options(args)
205
+ # parse CLI options
206
+ opts = parse_options(args)
207
+
208
+ set_environment opts[:environment]
209
+
210
+ # check config file presence
211
+ if opts[:config_file]
212
+ unless File.exist?(opts[:config_file])
213
+ raise ArgumentError, "No such file #{opts[:config_file]}"
214
+ end
215
+ else
216
+ config_dir = if File.directory?(opts[:require].to_s)
217
+ File.join(opts[:require], "config")
218
+ else
219
+ File.join(options[:require], "config")
220
+ end
221
+
222
+ %w[sidekiq.yml sidekiq.yml.erb].each do |config_file|
223
+ path = File.join(config_dir, config_file)
224
+ opts[:config_file] ||= path if File.exist?(path)
225
+ end
226
+ end
227
+
228
+ # parse config file options
229
+ opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
230
+
231
+ # set defaults
232
+ opts[:queues] = ["default"] if opts[:queues].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_application
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
+ opts.delete(:strict)
372
+
373
+ parse_queues(opts, opts.delete(:queues) || [])
374
+
375
+ opts
376
+ end
377
+
378
+ def parse_queues(opts, queues_and_weights)
379
+ queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
380
+ end
381
+
382
+ def parse_queue(opts, queue, weight = nil)
383
+ opts[:queues] ||= []
384
+ opts[:strict] = true if opts[:strict].nil?
385
+ raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
386
+ [weight.to_i, 1].max.times { opts[:queues] << queue }
387
+ opts[:strict] = false if weight.to_i > 0
388
+ end
389
+
390
+ def rails_app?
391
+ defined?(::Rails) && ::Rails.respond_to?(:application)
392
+ end
393
+ end
394
+ end
395
+
396
+ 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