sidekiq 6.0.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 (121) 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 +70 -0
  11. data/COMM-LICENSE +97 -0
  12. data/Changes.md +1570 -0
  13. data/Ent-2.0-Upgrade.md +37 -0
  14. data/Ent-Changes.md +250 -0
  15. data/Gemfile +24 -0
  16. data/Gemfile.lock +196 -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 +768 -0
  23. data/README.md +95 -0
  24. data/Rakefile +10 -0
  25. data/bin/sidekiq +18 -0
  26. data/bin/sidekiqload +153 -0
  27. data/bin/sidekiqmon +9 -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 +47 -0
  33. data/lib/sidekiq.rb +248 -0
  34. data/lib/sidekiq/api.rb +927 -0
  35. data/lib/sidekiq/cli.rb +380 -0
  36. data/lib/sidekiq/client.rb +242 -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 +55 -0
  45. data/lib/sidekiq/job_retry.rb +249 -0
  46. data/lib/sidekiq/launcher.rb +181 -0
  47. data/lib/sidekiq/logger.rb +69 -0
  48. data/lib/sidekiq/manager.rb +135 -0
  49. data/lib/sidekiq/middleware/chain.rb +151 -0
  50. data/lib/sidekiq/middleware/i18n.rb +40 -0
  51. data/lib/sidekiq/monitor.rb +148 -0
  52. data/lib/sidekiq/paginator.rb +42 -0
  53. data/lib/sidekiq/processor.rb +282 -0
  54. data/lib/sidekiq/rails.rb +52 -0
  55. data/lib/sidekiq/redis_connection.rb +138 -0
  56. data/lib/sidekiq/scheduled.rb +172 -0
  57. data/lib/sidekiq/testing.rb +332 -0
  58. data/lib/sidekiq/testing/inline.rb +30 -0
  59. data/lib/sidekiq/util.rb +69 -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 +356 -0
  64. data/lib/sidekiq/web/helpers.rb +324 -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-rtl.css +246 -0
  74. data/web/assets/stylesheets/application.css +1144 -0
  75. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  76. data/web/assets/stylesheets/bootstrap.css +5 -0
  77. data/web/locales/ar.yml +81 -0
  78. data/web/locales/cs.yml +78 -0
  79. data/web/locales/da.yml +68 -0
  80. data/web/locales/de.yml +69 -0
  81. data/web/locales/el.yml +68 -0
  82. data/web/locales/en.yml +81 -0
  83. data/web/locales/es.yml +70 -0
  84. data/web/locales/fa.yml +80 -0
  85. data/web/locales/fr.yml +78 -0
  86. data/web/locales/he.yml +79 -0
  87. data/web/locales/hi.yml +75 -0
  88. data/web/locales/it.yml +69 -0
  89. data/web/locales/ja.yml +81 -0
  90. data/web/locales/ko.yml +68 -0
  91. data/web/locales/nb.yml +77 -0
  92. data/web/locales/nl.yml +68 -0
  93. data/web/locales/pl.yml +59 -0
  94. data/web/locales/pt-br.yml +68 -0
  95. data/web/locales/pt.yml +67 -0
  96. data/web/locales/ru.yml +78 -0
  97. data/web/locales/sv.yml +68 -0
  98. data/web/locales/ta.yml +75 -0
  99. data/web/locales/uk.yml +76 -0
  100. data/web/locales/ur.yml +80 -0
  101. data/web/locales/zh-cn.yml +68 -0
  102. data/web/locales/zh-tw.yml +68 -0
  103. data/web/views/_footer.erb +20 -0
  104. data/web/views/_job_info.erb +88 -0
  105. data/web/views/_nav.erb +52 -0
  106. data/web/views/_paging.erb +23 -0
  107. data/web/views/_poll_link.erb +7 -0
  108. data/web/views/_status.erb +4 -0
  109. data/web/views/_summary.erb +40 -0
  110. data/web/views/busy.erb +98 -0
  111. data/web/views/dashboard.erb +75 -0
  112. data/web/views/dead.erb +34 -0
  113. data/web/views/layout.erb +40 -0
  114. data/web/views/morgue.erb +75 -0
  115. data/web/views/queue.erb +46 -0
  116. data/web/views/queues.erb +30 -0
  117. data/web/views/retries.erb +80 -0
  118. data/web/views/retry.erb +34 -0
  119. data/web/views/scheduled.erb +54 -0
  120. data/web/views/scheduled_job_info.erb +8 -0
  121. metadata +220 -0
@@ -0,0 +1,380 @@
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
+
42
+ self_read, self_write = IO.pipe
43
+ sigs = %w[INT TERM TTIN TSTP]
44
+ sigs.each do |sig|
45
+ trap sig do
46
+ self_write.write("#{sig}\n")
47
+ end
48
+ rescue ArgumentError
49
+ puts "Signal #{sig} not supported"
50
+ end
51
+
52
+ logger.info "Running in #{RUBY_DESCRIPTION}"
53
+ logger.info Sidekiq::LICENSE
54
+ logger.info "Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org" unless defined?(::Sidekiq::Pro)
55
+
56
+ # touch the connection pool so it is created before we
57
+ # fire startup and start multithreading.
58
+ ver = Sidekiq.redis_info["redis_version"]
59
+ raise "You are using Redis v#{ver}, Sidekiq requires Redis v4.0.0 or greater" if ver < "4"
60
+
61
+ # Since the user can pass us a connection pool explicitly in the initializer, we
62
+ # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
63
+ cursize = Sidekiq.redis_pool.size
64
+ needed = Sidekiq.options[:concurrency] + 2
65
+ raise "Your pool of #{cursize} Redis connections is too small, please increase the size to at least #{needed}" if cursize < needed
66
+
67
+ # cache process identity
68
+ Sidekiq.options[:identity] = identity
69
+
70
+ # Touch middleware so it isn't lazy loaded by multiple threads, #3043
71
+ Sidekiq.server_middleware
72
+
73
+ # Before this point, the process is initializing with just the main thread.
74
+ # Starting here the process will now have multiple threads running.
75
+ fire_event(:startup, reverse: false, reraise: true)
76
+
77
+ logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(", ")}" }
78
+ logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(", ")}" }
79
+
80
+ launch(self_read)
81
+ end
82
+
83
+ def launch(self_read)
84
+ if environment == "development" && $stdout.tty?
85
+ logger.info "Starting processing, hit Ctrl-C to stop"
86
+ end
87
+
88
+ @launcher = Sidekiq::Launcher.new(options)
89
+
90
+ begin
91
+ launcher.run
92
+
93
+ while (readable_io = IO.select([self_read]))
94
+ signal = readable_io.first[0].gets.strip
95
+ handle_signal(signal)
96
+ end
97
+ rescue Interrupt
98
+ logger.info "Shutting down"
99
+ launcher.stop
100
+ logger.info "Bye!"
101
+
102
+ # Explicitly exit so busy Processor threads won't block process shutdown.
103
+ #
104
+ # NB: slow at_exit handlers will prevent a timely exit if they take
105
+ # a while to run. If Sidekiq is getting here but the process isn't exiting,
106
+ # use the TTIN signal to determine where things are stuck.
107
+ exit(0)
108
+ end
109
+ end
110
+
111
+ def self.w
112
+ "\e[37m"
113
+ end
114
+
115
+ def self.r
116
+ "\e[31m"
117
+ end
118
+
119
+ def self.b
120
+ "\e[30m"
121
+ end
122
+
123
+ def self.reset
124
+ "\e[0m"
125
+ end
126
+
127
+ def self.banner
128
+ %{
129
+ #{w} m,
130
+ #{w} `$b
131
+ #{w} .ss, $$: .,d$
132
+ #{w} `$$P,d$P' .,md$P"'
133
+ #{w} ,$$$$$b#{b}/#{w}md$$$P^'
134
+ #{w} .d$$$$$$#{b}/#{w}$$$P'
135
+ #{w} $$^' `"#{b}/#{w}$$$' #{r}____ _ _ _ _
136
+ #{w} $: ,$$: #{r} / ___|(_) __| | ___| | _(_) __ _
137
+ #{w} `b :$$ #{r} \\___ \\| |/ _` |/ _ \\ |/ / |/ _` |
138
+ #{w} $$: #{r} ___) | | (_| | __/ <| | (_| |
139
+ #{w} $$ #{r}|____/|_|\\__,_|\\___|_|\\_\\_|\\__, |
140
+ #{w} .d$$ #{r} |_|
141
+ #{reset}}
142
+ end
143
+
144
+ SIGNAL_HANDLERS = {
145
+ # Ctrl-C in terminal
146
+ "INT" => ->(cli) { raise Interrupt },
147
+ # TERM is the signal that Sidekiq must exit.
148
+ # Heroku sends TERM and then waits 30 seconds for process to exit.
149
+ "TERM" => ->(cli) { raise Interrupt },
150
+ "TSTP" => ->(cli) {
151
+ Sidekiq.logger.info "Received TSTP, no longer accepting new work"
152
+ cli.launcher.quiet
153
+ },
154
+ "TTIN" => ->(cli) {
155
+ Thread.list.each do |thread|
156
+ Sidekiq.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
157
+ if thread.backtrace
158
+ Sidekiq.logger.warn thread.backtrace.join("\n")
159
+ else
160
+ Sidekiq.logger.warn "<no backtrace available>"
161
+ end
162
+ end
163
+ },
164
+ }
165
+
166
+ def handle_signal(sig)
167
+ Sidekiq.logger.debug "Got #{sig} signal"
168
+ handy = SIGNAL_HANDLERS[sig]
169
+ if handy
170
+ handy.call(self)
171
+ else
172
+ Sidekiq.logger.info { "No signal handler for #{sig}" }
173
+ end
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
+ if opts[:config_file] && !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] = Array(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
+
287
+ @parser = OptionParser.new { |o|
288
+ o.on "-c", "--concurrency INT", "processor threads to use" do |arg|
289
+ opts[:concurrency] = Integer(arg)
290
+ end
291
+
292
+ o.on "-d", "--daemon", "Daemonize process" do |arg|
293
+ puts "ERROR: Daemonization mode was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services"
294
+ end
295
+
296
+ o.on "-e", "--environment ENV", "Application environment" do |arg|
297
+ opts[:environment] = arg
298
+ end
299
+
300
+ o.on "-g", "--tag TAG", "Process tag for procline" do |arg|
301
+ opts[:tag] = arg
302
+ end
303
+
304
+ o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
305
+ queue, weight = arg.split(",")
306
+ parse_queue opts, queue, weight
307
+ end
308
+
309
+ o.on "-r", "--require [PATH|DIR]", "Location of Rails application with workers or file to require" do |arg|
310
+ opts[:require] = arg
311
+ end
312
+
313
+ o.on "-t", "--timeout NUM", "Shutdown timeout" do |arg|
314
+ opts[:timeout] = Integer(arg)
315
+ end
316
+
317
+ o.on "-v", "--verbose", "Print more verbose output" do |arg|
318
+ opts[:verbose] = arg
319
+ end
320
+
321
+ o.on "-C", "--config PATH", "path to YAML config file" do |arg|
322
+ opts[:config_file] = arg
323
+ end
324
+
325
+ o.on "-L", "--logfile PATH", "path to writable logfile" do |arg|
326
+ puts "ERROR: Logfile redirection was removed in Sidekiq 6.0, Sidekiq will only log to STDOUT"
327
+ end
328
+
329
+ o.on "-P", "--pidfile PATH", "path to pidfile" do |arg|
330
+ puts "ERROR: PID file creation was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services"
331
+ end
332
+
333
+ o.on "-V", "--version", "Print version and exit" do |arg|
334
+ puts "Sidekiq #{Sidekiq::VERSION}"
335
+ die(0)
336
+ end
337
+ }
338
+
339
+ @parser.banner = "sidekiq [options]"
340
+ @parser.on_tail "-h", "--help", "Show help" do
341
+ logger.info @parser
342
+ die 1
343
+ end
344
+
345
+ @parser.parse!(argv)
346
+
347
+ opts
348
+ end
349
+
350
+ def initialize_logger
351
+ Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose]
352
+ end
353
+
354
+ def parse_config(path)
355
+ opts = YAML.load(ERB.new(File.read(path)).result) || {}
356
+
357
+ if opts.respond_to? :deep_symbolize_keys!
358
+ opts.deep_symbolize_keys!
359
+ else
360
+ symbolize_keys_deep!(opts)
361
+ end
362
+
363
+ opts = opts.merge(opts.delete(environment.to_sym) || {})
364
+ parse_queues(opts, opts.delete(:queues) || [])
365
+
366
+ opts
367
+ end
368
+
369
+ def parse_queues(opts, queues_and_weights)
370
+ queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
371
+ end
372
+
373
+ def parse_queue(opts, queue, weight = nil)
374
+ opts[:queues] ||= []
375
+ raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
376
+ [weight.to_i, 1].max.times { opts[:queues] << queue }
377
+ opts[:strict] = false if weight.to_i > 0
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,242 @@
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
+ normed = normalize_item(items)
98
+ payloads = items["args"].map { |args|
99
+ copy = normed.merge("args" => args, "jid" => SecureRandom.hex(12), "enqueued_at" => Time.now.to_f)
100
+ result = process_single(items["class"], copy)
101
+ result || nil
102
+ }.compact
103
+
104
+ raw_push(payloads) unless payloads.empty?
105
+ payloads.collect { |payload| payload["jid"] }
106
+ end
107
+
108
+ # Allows sharding of jobs across any number of Redis instances. All jobs
109
+ # defined within the block will use the given Redis connection pool.
110
+ #
111
+ # pool = ConnectionPool.new { Redis.new }
112
+ # Sidekiq::Client.via(pool) do
113
+ # SomeWorker.perform_async(1,2,3)
114
+ # SomeOtherWorker.perform_async(1,2,3)
115
+ # end
116
+ #
117
+ # Generally this is only needed for very large Sidekiq installs processing
118
+ # thousands of jobs per second. I do not recommend sharding unless
119
+ # you cannot scale any other way (e.g. splitting your app into smaller apps).
120
+ def self.via(pool)
121
+ raise ArgumentError, "No pool given" if pool.nil?
122
+ current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
123
+ Thread.current[:sidekiq_via_pool] = pool
124
+ yield
125
+ ensure
126
+ Thread.current[:sidekiq_via_pool] = current_sidekiq_pool
127
+ end
128
+
129
+ class << self
130
+ def push(item)
131
+ new.push(item)
132
+ end
133
+
134
+ def push_bulk(items)
135
+ new.push_bulk(items)
136
+ end
137
+
138
+ # Resque compatibility helpers. Note all helpers
139
+ # should go through Worker#client_push.
140
+ #
141
+ # Example usage:
142
+ # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
143
+ #
144
+ # Messages are enqueued to the 'default' queue.
145
+ #
146
+ def enqueue(klass, *args)
147
+ klass.client_push("class" => klass, "args" => args)
148
+ end
149
+
150
+ # Example usage:
151
+ # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
152
+ #
153
+ def enqueue_to(queue, klass, *args)
154
+ klass.client_push("queue" => queue, "class" => klass, "args" => args)
155
+ end
156
+
157
+ # Example usage:
158
+ # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
159
+ #
160
+ def enqueue_to_in(queue, interval, klass, *args)
161
+ int = interval.to_f
162
+ now = Time.now.to_f
163
+ ts = (int < 1_000_000_000 ? now + int : int)
164
+
165
+ item = {"class" => klass, "args" => args, "at" => ts, "queue" => queue}
166
+ item.delete("at") if ts <= now
167
+
168
+ klass.client_push(item)
169
+ end
170
+
171
+ # Example usage:
172
+ # Sidekiq::Client.enqueue_in(3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
173
+ #
174
+ def enqueue_in(interval, klass, *args)
175
+ klass.perform_in(interval, *args)
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ def raw_push(payloads)
182
+ @redis_pool.with do |conn|
183
+ conn.multi do
184
+ atomic_push(conn, payloads)
185
+ end
186
+ end
187
+ true
188
+ end
189
+
190
+ def atomic_push(conn, payloads)
191
+ if payloads.first["at"]
192
+ conn.zadd("schedule", payloads.map { |hash|
193
+ at = hash.delete("at").to_s
194
+ [at, Sidekiq.dump_json(hash)]
195
+ })
196
+ else
197
+ queue = payloads.first["queue"]
198
+ now = Time.now.to_f
199
+ to_push = payloads.map { |entry|
200
+ entry["enqueued_at"] = now
201
+ Sidekiq.dump_json(entry)
202
+ }
203
+ conn.sadd("queues", queue)
204
+ conn.lpush("queue:#{queue}", to_push)
205
+ end
206
+ end
207
+
208
+ def process_single(worker_class, item)
209
+ queue = item["queue"]
210
+
211
+ middleware.invoke(worker_class, item, queue, @redis_pool) do
212
+ item
213
+ end
214
+ end
215
+
216
+ def normalize_item(item)
217
+ 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")
218
+ raise(ArgumentError, "Job args must be an Array") unless item["args"].is_a?(Array)
219
+ 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)
220
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp") if item.key?("at") && !item["at"].is_a?(Numeric)
221
+ # 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']
222
+
223
+ normalized_hash(item["class"])
224
+ .each { |key, value| item[key] = value if item[key].nil? }
225
+
226
+ item["class"] = item["class"].to_s
227
+ item["queue"] = item["queue"].to_s
228
+ item["jid"] ||= SecureRandom.hex(12)
229
+ item["created_at"] ||= Time.now.to_f
230
+ item
231
+ end
232
+
233
+ def normalized_hash(item_class)
234
+ if item_class.is_a?(Class)
235
+ raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?("get_sidekiq_options")
236
+ item_class.get_sidekiq_options
237
+ else
238
+ Sidekiq.default_worker_options
239
+ end
240
+ end
241
+ end
242
+ end