sr-sidekiq 4.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/3.0-Upgrade.md +70 -0
  4. data/4.0-Upgrade.md +50 -0
  5. data/COMM-LICENSE (sidekiq) +95 -0
  6. data/Changes.md +1241 -0
  7. data/Ent-Changes.md +112 -0
  8. data/Gemfile +29 -0
  9. data/LICENSE (sidekiq) +9 -0
  10. data/LICENSE (sr-sidekiq) +5 -0
  11. data/Pro-2.0-Upgrade.md +138 -0
  12. data/Pro-3.0-Upgrade.md +44 -0
  13. data/Pro-Changes.md +539 -0
  14. data/README.md +8 -0
  15. data/Rakefile +9 -0
  16. data/bin/sidekiq +18 -0
  17. data/bin/sidekiqctl +99 -0
  18. data/bin/sidekiqload +167 -0
  19. data/code_of_conduct.md +50 -0
  20. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  21. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  22. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  23. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  24. data/lib/sidekiq.rb +237 -0
  25. data/lib/sidekiq/api.rb +844 -0
  26. data/lib/sidekiq/cli.rb +389 -0
  27. data/lib/sidekiq/client.rb +260 -0
  28. data/lib/sidekiq/core_ext.rb +106 -0
  29. data/lib/sidekiq/exception_handler.rb +31 -0
  30. data/lib/sidekiq/extensions/action_mailer.rb +57 -0
  31. data/lib/sidekiq/extensions/active_record.rb +40 -0
  32. data/lib/sidekiq/extensions/class_methods.rb +40 -0
  33. data/lib/sidekiq/extensions/generic_proxy.rb +25 -0
  34. data/lib/sidekiq/fetch.rb +81 -0
  35. data/lib/sidekiq/launcher.rb +160 -0
  36. data/lib/sidekiq/logging.rb +106 -0
  37. data/lib/sidekiq/manager.rb +137 -0
  38. data/lib/sidekiq/middleware/chain.rb +150 -0
  39. data/lib/sidekiq/middleware/i18n.rb +42 -0
  40. data/lib/sidekiq/middleware/server/active_record.rb +13 -0
  41. data/lib/sidekiq/middleware/server/logging.rb +40 -0
  42. data/lib/sidekiq/middleware/server/retry_jobs.rb +205 -0
  43. data/lib/sidekiq/paginator.rb +43 -0
  44. data/lib/sidekiq/processor.rb +186 -0
  45. data/lib/sidekiq/rails.rb +39 -0
  46. data/lib/sidekiq/redis_connection.rb +97 -0
  47. data/lib/sidekiq/scheduled.rb +146 -0
  48. data/lib/sidekiq/testing.rb +316 -0
  49. data/lib/sidekiq/testing/inline.rb +29 -0
  50. data/lib/sidekiq/util.rb +62 -0
  51. data/lib/sidekiq/version.rb +4 -0
  52. data/lib/sidekiq/web.rb +278 -0
  53. data/lib/sidekiq/web_helpers.rb +255 -0
  54. data/lib/sidekiq/worker.rb +121 -0
  55. data/sidekiq.gemspec +26 -0
  56. data/sr-sidekiq-4.1.3.gem +0 -0
  57. data/sr-sidekiq-4.1.4.gem +0 -0
  58. data/sr-sidekiq-4.1.5.gem +0 -0
  59. data/test/config.yml +9 -0
  60. data/test/env_based_config.yml +11 -0
  61. data/test/fake_env.rb +1 -0
  62. data/test/fixtures/en.yml +2 -0
  63. data/test/helper.rb +75 -0
  64. data/test/test_actors.rb +138 -0
  65. data/test/test_api.rb +528 -0
  66. data/test/test_cli.rb +406 -0
  67. data/test/test_client.rb +262 -0
  68. data/test/test_exception_handler.rb +56 -0
  69. data/test/test_extensions.rb +127 -0
  70. data/test/test_fetch.rb +50 -0
  71. data/test/test_launcher.rb +85 -0
  72. data/test/test_logging.rb +35 -0
  73. data/test/test_manager.rb +50 -0
  74. data/test/test_middleware.rb +158 -0
  75. data/test/test_processor.rb +201 -0
  76. data/test/test_rails.rb +22 -0
  77. data/test/test_redis_connection.rb +127 -0
  78. data/test/test_retry.rb +326 -0
  79. data/test/test_retry_exhausted.rb +149 -0
  80. data/test/test_scheduled.rb +115 -0
  81. data/test/test_scheduling.rb +50 -0
  82. data/test/test_sidekiq.rb +107 -0
  83. data/test/test_testing.rb +143 -0
  84. data/test/test_testing_fake.rb +357 -0
  85. data/test/test_testing_inline.rb +94 -0
  86. data/test/test_util.rb +13 -0
  87. data/test/test_web.rb +614 -0
  88. data/test/test_web_helpers.rb +54 -0
  89. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  90. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  91. data/web/assets/images/favicon.ico +0 -0
  92. data/web/assets/images/logo.png +0 -0
  93. data/web/assets/images/status-sd8051fd480.png +0 -0
  94. data/web/assets/images/status/active.png +0 -0
  95. data/web/assets/images/status/idle.png +0 -0
  96. data/web/assets/javascripts/application.js +88 -0
  97. data/web/assets/javascripts/dashboard.js +300 -0
  98. data/web/assets/javascripts/locales/README.md +27 -0
  99. data/web/assets/javascripts/locales/jquery.timeago.ar.js +96 -0
  100. data/web/assets/javascripts/locales/jquery.timeago.bg.js +18 -0
  101. data/web/assets/javascripts/locales/jquery.timeago.bs.js +49 -0
  102. data/web/assets/javascripts/locales/jquery.timeago.ca.js +18 -0
  103. data/web/assets/javascripts/locales/jquery.timeago.cs.js +18 -0
  104. data/web/assets/javascripts/locales/jquery.timeago.cy.js +20 -0
  105. data/web/assets/javascripts/locales/jquery.timeago.da.js +18 -0
  106. data/web/assets/javascripts/locales/jquery.timeago.de.js +18 -0
  107. data/web/assets/javascripts/locales/jquery.timeago.el.js +18 -0
  108. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +20 -0
  109. data/web/assets/javascripts/locales/jquery.timeago.en.js +20 -0
  110. data/web/assets/javascripts/locales/jquery.timeago.es.js +18 -0
  111. data/web/assets/javascripts/locales/jquery.timeago.et.js +18 -0
  112. data/web/assets/javascripts/locales/jquery.timeago.fa.js +22 -0
  113. data/web/assets/javascripts/locales/jquery.timeago.fi.js +28 -0
  114. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +16 -0
  115. data/web/assets/javascripts/locales/jquery.timeago.fr.js +17 -0
  116. data/web/assets/javascripts/locales/jquery.timeago.he.js +18 -0
  117. data/web/assets/javascripts/locales/jquery.timeago.hr.js +49 -0
  118. data/web/assets/javascripts/locales/jquery.timeago.hu.js +18 -0
  119. data/web/assets/javascripts/locales/jquery.timeago.hy.js +18 -0
  120. data/web/assets/javascripts/locales/jquery.timeago.id.js +18 -0
  121. data/web/assets/javascripts/locales/jquery.timeago.it.js +16 -0
  122. data/web/assets/javascripts/locales/jquery.timeago.ja.js +19 -0
  123. data/web/assets/javascripts/locales/jquery.timeago.ko.js +17 -0
  124. data/web/assets/javascripts/locales/jquery.timeago.lt.js +20 -0
  125. data/web/assets/javascripts/locales/jquery.timeago.mk.js +20 -0
  126. data/web/assets/javascripts/locales/jquery.timeago.nl.js +20 -0
  127. data/web/assets/javascripts/locales/jquery.timeago.no.js +18 -0
  128. data/web/assets/javascripts/locales/jquery.timeago.pl.js +31 -0
  129. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +16 -0
  130. data/web/assets/javascripts/locales/jquery.timeago.pt.js +16 -0
  131. data/web/assets/javascripts/locales/jquery.timeago.ro.js +18 -0
  132. data/web/assets/javascripts/locales/jquery.timeago.rs.js +49 -0
  133. data/web/assets/javascripts/locales/jquery.timeago.ru.js +34 -0
  134. data/web/assets/javascripts/locales/jquery.timeago.sk.js +18 -0
  135. data/web/assets/javascripts/locales/jquery.timeago.sl.js +44 -0
  136. data/web/assets/javascripts/locales/jquery.timeago.sv.js +18 -0
  137. data/web/assets/javascripts/locales/jquery.timeago.th.js +20 -0
  138. data/web/assets/javascripts/locales/jquery.timeago.tr.js +16 -0
  139. data/web/assets/javascripts/locales/jquery.timeago.uk.js +34 -0
  140. data/web/assets/javascripts/locales/jquery.timeago.uz.js +19 -0
  141. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +20 -0
  142. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +20 -0
  143. data/web/assets/stylesheets/application.css +754 -0
  144. data/web/assets/stylesheets/bootstrap.css +9 -0
  145. data/web/locales/cs.yml +78 -0
  146. data/web/locales/da.yml +68 -0
  147. data/web/locales/de.yml +69 -0
  148. data/web/locales/el.yml +68 -0
  149. data/web/locales/en.yml +79 -0
  150. data/web/locales/es.yml +69 -0
  151. data/web/locales/fr.yml +78 -0
  152. data/web/locales/hi.yml +75 -0
  153. data/web/locales/it.yml +69 -0
  154. data/web/locales/ja.yml +78 -0
  155. data/web/locales/ko.yml +68 -0
  156. data/web/locales/nb.yml +77 -0
  157. data/web/locales/nl.yml +68 -0
  158. data/web/locales/pl.yml +59 -0
  159. data/web/locales/pt-br.yml +68 -0
  160. data/web/locales/pt.yml +67 -0
  161. data/web/locales/ru.yml +78 -0
  162. data/web/locales/sv.yml +68 -0
  163. data/web/locales/ta.yml +75 -0
  164. data/web/locales/uk.yml +76 -0
  165. data/web/locales/zh-cn.yml +68 -0
  166. data/web/locales/zh-tw.yml +68 -0
  167. data/web/views/_footer.erb +17 -0
  168. data/web/views/_job_info.erb +88 -0
  169. data/web/views/_nav.erb +66 -0
  170. data/web/views/_paging.erb +23 -0
  171. data/web/views/_poll_js.erb +5 -0
  172. data/web/views/_poll_link.erb +7 -0
  173. data/web/views/_status.erb +4 -0
  174. data/web/views/_summary.erb +40 -0
  175. data/web/views/busy.erb +94 -0
  176. data/web/views/dashboard.erb +75 -0
  177. data/web/views/dead.erb +34 -0
  178. data/web/views/layout.erb +32 -0
  179. data/web/views/morgue.erb +71 -0
  180. data/web/views/queue.erb +45 -0
  181. data/web/views/queues.erb +28 -0
  182. data/web/views/retries.erb +74 -0
  183. data/web/views/retry.erb +34 -0
  184. data/web/views/scheduled.erb +54 -0
  185. data/web/views/scheduled_job_info.erb +8 -0
  186. metadata +408 -0
@@ -0,0 +1,389 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
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/util'
13
+
14
+ module Sidekiq
15
+ class CLI
16
+ include Util
17
+ include Singleton unless $TESTING
18
+
19
+ PROCTITLES = [
20
+ proc { 'sidekiq'.freeze },
21
+ proc { Sidekiq::VERSION },
22
+ proc { |me, data| data['tag'] },
23
+ proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data['concurrency']} busy]" },
24
+ proc { |me, data| "stopping" if me.stopping? },
25
+ ]
26
+
27
+ # Used for CLI testing
28
+ attr_accessor :code
29
+ attr_accessor :launcher
30
+ attr_accessor :environment
31
+
32
+ def initialize
33
+ @code = nil
34
+ end
35
+
36
+ def parse(args=ARGV)
37
+ @code = nil
38
+
39
+ setup_options(args)
40
+ initialize_logger
41
+ validate!
42
+ daemonize
43
+ write_pid
44
+ end
45
+
46
+ # Code within this method is not tested because it alters
47
+ # global process state irreversibly. PRs which improve the
48
+ # test coverage of Sidekiq::CLI are welcomed.
49
+ def run
50
+ boot_system
51
+ print_banner
52
+
53
+ self_read, self_write = IO.pipe
54
+
55
+ %w(INT TERM USR1 USR2 TTIN).each do |sig|
56
+ begin
57
+ trap sig do
58
+ self_write.puts(sig)
59
+ end
60
+ rescue ArgumentError
61
+ puts "Signal #{sig} not supported"
62
+ end
63
+ end
64
+
65
+ logger.info "Running in #{RUBY_DESCRIPTION}"
66
+ logger.info Sidekiq::LICENSE
67
+ logger.info "Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org" unless defined?(::Sidekiq::Pro)
68
+
69
+ # touch the connection pool so it is created before we
70
+ # fire startup and start multithreading.
71
+ ver = Sidekiq.redis_info['redis_version']
72
+ raise "You are using Redis v#{ver}, Sidekiq requires Redis v2.8.0 or greater" if ver < '2.8'
73
+
74
+ # Before this point, the process is initializing with just the main thread.
75
+ # Starting here the process will now have multiple threads running.
76
+ fire_event(:startup)
77
+
78
+ logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(', ')}" }
79
+ logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(', ')}" }
80
+
81
+ if !options[:daemon]
82
+ logger.info 'Starting processing, hit Ctrl-C to stop'
83
+ end
84
+
85
+ require 'sidekiq/launcher'
86
+ @launcher = Sidekiq::Launcher.new(options)
87
+
88
+ begin
89
+ launcher.run
90
+
91
+ while readable_io = IO.select([self_read])
92
+ signal = readable_io.first[0].gets.strip
93
+ handle_signal(signal)
94
+ end
95
+ rescue Interrupt
96
+ logger.info 'Shutting down'
97
+ launcher.stop
98
+ # Explicitly exit so busy Processor threads can't block
99
+ # process shutdown.
100
+ logger.info "Bye!"
101
+ exit(0)
102
+ end
103
+ end
104
+
105
+ def self.banner
106
+ %q{
107
+ m,
108
+ `$b
109
+ .ss, $$: .,d$
110
+ `$$P,d$P' .,md$P"'
111
+ ,$$$$$bmmd$$$P^'
112
+ .d$$$$$$$$$$P'
113
+ $$^' `"^$$$' ____ _ _ _ _
114
+ $: ,$$: / ___|(_) __| | ___| | _(_) __ _
115
+ `b :$$ \___ \| |/ _` |/ _ \ |/ / |/ _` |
116
+ $$: ___) | | (_| | __/ <| | (_| |
117
+ $$ |____/|_|\__,_|\___|_|\_\_|\__, |
118
+ .d$$ |_|
119
+ }
120
+ end
121
+
122
+ def handle_signal(sig)
123
+ Sidekiq.logger.debug "Got #{sig} signal"
124
+ case sig
125
+ when 'INT'
126
+ # Handle Ctrl-C in JRuby like MRI
127
+ # http://jira.codehaus.org/browse/JRUBY-4637
128
+ raise Interrupt
129
+ when 'TERM'
130
+ # Heroku sends TERM and then waits 10 seconds for process to exit.
131
+ raise Interrupt
132
+ when 'USR1'
133
+ Sidekiq.logger.info "Received USR1, no longer accepting new work"
134
+ launcher.quiet
135
+ when 'USR2'
136
+ if Sidekiq.options[:logfile]
137
+ Sidekiq.logger.info "Received USR2, reopening log file"
138
+ Sidekiq::Logging.reopen_logs
139
+ end
140
+ when 'TTIN'
141
+ Thread.list.each do |thread|
142
+ Sidekiq.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
143
+ if thread.backtrace
144
+ Sidekiq.logger.warn thread.backtrace.join("\n")
145
+ else
146
+ Sidekiq.logger.warn "<no backtrace available>"
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def print_banner
155
+ # Print logo and banner for development
156
+ if environment == 'development' && $stdout.tty?
157
+ puts "\e[#{31}m"
158
+ puts Sidekiq::CLI.banner
159
+ puts "\e[0m"
160
+ end
161
+ end
162
+
163
+ def daemonize
164
+ return unless options[:daemon]
165
+
166
+ raise ArgumentError, "You really should set a logfile if you're going to daemonize" unless options[:logfile]
167
+ files_to_reopen = []
168
+ ObjectSpace.each_object(File) do |file|
169
+ files_to_reopen << file unless file.closed?
170
+ end
171
+
172
+ ::Process.daemon(true, true)
173
+
174
+ files_to_reopen.each do |file|
175
+ begin
176
+ file.reopen file.path, "a+"
177
+ file.sync = true
178
+ rescue ::Exception
179
+ end
180
+ end
181
+
182
+ [$stdout, $stderr].each do |io|
183
+ File.open(options[:logfile], 'ab') do |f|
184
+ io.reopen(f)
185
+ end
186
+ io.sync = true
187
+ end
188
+ $stdin.reopen('/dev/null')
189
+
190
+ initialize_logger
191
+ end
192
+
193
+ def set_environment(cli_env)
194
+ @environment = cli_env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
195
+ end
196
+
197
+ alias_method :die, :exit
198
+ alias_method :☠, :exit
199
+
200
+ def setup_options(args)
201
+ opts = parse_options(args)
202
+ set_environment opts[:environment]
203
+
204
+ cfile = opts[:config_file]
205
+ opts = parse_config(cfile).merge(opts) if cfile
206
+
207
+ opts[:strict] = true if opts[:strict].nil?
208
+
209
+ options.merge!(opts)
210
+ end
211
+
212
+ def options
213
+ Sidekiq.options
214
+ end
215
+
216
+ def boot_system
217
+ ENV['RACK_ENV'] = ENV['RAILS_ENV'] = environment
218
+
219
+ raise ArgumentError, "#{options[:require]} does not exist" unless File.exist?(options[:require])
220
+
221
+ if File.directory?(options[:require])
222
+ require 'rails'
223
+ if ::Rails::VERSION::MAJOR < 4
224
+ require 'sidekiq/rails'
225
+ require File.expand_path("#{options[:require]}/config/environment.rb")
226
+ ::Rails.application.eager_load!
227
+ else
228
+ # Painful contortions, see 1791 for discussion
229
+ require File.expand_path("#{options[:require]}/config/application.rb")
230
+ ::Rails::Application.initializer "sidekiq.eager_load" do
231
+ ::Rails.application.config.eager_load = true
232
+ end
233
+ require 'sidekiq/rails'
234
+ require File.expand_path("#{options[:require]}/config/environment.rb")
235
+ end
236
+ options[:tag] ||= default_tag
237
+ else
238
+ require options[:require]
239
+ end
240
+ end
241
+
242
+ def default_tag
243
+ dir = ::Rails.root
244
+ name = File.basename(dir)
245
+ if name.to_i != 0 && prevdir = File.dirname(dir) # Capistrano release directory?
246
+ if File.basename(prevdir) == 'releases'
247
+ return File.basename(File.dirname(prevdir))
248
+ end
249
+ end
250
+ name
251
+ end
252
+
253
+ def validate!
254
+ options[:queues] << 'default' if options[:queues].empty?
255
+
256
+ if !File.exist?(options[:require]) ||
257
+ (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
258
+ logger.info "=================================================================="
259
+ logger.info " Please point sidekiq to a Rails 3/4 application or a Ruby file "
260
+ logger.info " to load your worker classes with -r [DIR|FILE]."
261
+ logger.info "=================================================================="
262
+ logger.info @parser
263
+ die(1)
264
+ end
265
+
266
+ [:concurrency, :timeout].each do |opt|
267
+ raise ArgumentError, "#{opt}: #{options[opt]} is not a valid value" if options.has_key?(opt) && options[opt].to_i <= 0
268
+ end
269
+ end
270
+
271
+ def parse_options(argv)
272
+ opts = {}
273
+
274
+ @parser = OptionParser.new do |o|
275
+ o.on '-c', '--concurrency INT', "processor threads to use" do |arg|
276
+ opts[:concurrency] = Integer(arg)
277
+ end
278
+
279
+ o.on '-d', '--daemon', "Daemonize process" do |arg|
280
+ opts[:daemon] = arg
281
+ end
282
+
283
+ o.on '-e', '--environment ENV', "Application environment" do |arg|
284
+ opts[:environment] = arg
285
+ end
286
+
287
+ o.on '-g', '--tag TAG', "Process tag for procline" do |arg|
288
+ opts[:tag] = arg
289
+ end
290
+
291
+ o.on '-i', '--index INT', "unique process index on this machine" do |arg|
292
+ opts[:index] = Integer(arg.match(/\d+/)[0])
293
+ end
294
+
295
+ o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
296
+ queue, weight = arg.split(",")
297
+ parse_queue opts, queue, weight
298
+ end
299
+
300
+ o.on '-r', '--require [PATH|DIR]', "Location of Rails application with workers or file to require" do |arg|
301
+ opts[:require] = arg
302
+ end
303
+
304
+ o.on '-t', '--timeout NUM', "Shutdown timeout" do |arg|
305
+ opts[:timeout] = Integer(arg)
306
+ end
307
+
308
+ o.on "-v", "--verbose", "Print more verbose output" do |arg|
309
+ opts[:verbose] = arg
310
+ end
311
+
312
+ o.on '-C', '--config PATH', "path to YAML config file" do |arg|
313
+ opts[:config_file] = arg
314
+ end
315
+
316
+ o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
317
+ opts[:logfile] = arg
318
+ end
319
+
320
+ o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
321
+ opts[:pidfile] = arg
322
+ end
323
+
324
+ o.on '-V', '--version', "Print version and exit" do |arg|
325
+ puts "Sidekiq #{Sidekiq::VERSION}"
326
+ die(0)
327
+ end
328
+ end
329
+
330
+ @parser.banner = "sidekiq [options]"
331
+ @parser.on_tail "-h", "--help", "Show help" do
332
+ logger.info @parser
333
+ die 1
334
+ end
335
+ @parser.parse!(argv)
336
+
337
+ %w[config/sidekiq.yml config/sidekiq.yml.erb].each do |filename|
338
+ opts[:config_file] ||= filename if File.exist?(filename)
339
+ end
340
+
341
+ opts
342
+ end
343
+
344
+ def initialize_logger
345
+ Sidekiq::Logging.initialize_logger(options[:logfile]) if options[:logfile]
346
+
347
+ Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose]
348
+ end
349
+
350
+ def write_pid
351
+ if path = options[:pidfile]
352
+ pidfile = File.expand_path(path)
353
+ File.open(pidfile, 'w') do |f|
354
+ f.puts ::Process.pid
355
+ end
356
+ end
357
+ end
358
+
359
+ def parse_config(cfile)
360
+ opts = {}
361
+ if File.exist?(cfile)
362
+ opts = YAML.load(ERB.new(IO.read(cfile)).result) || opts
363
+ opts = opts.merge(opts.delete(environment) || {})
364
+ parse_queues(opts, opts.delete(:queues) || [])
365
+ else
366
+ # allow a non-existent config file so Sidekiq
367
+ # can be deployed by cap with just the defaults.
368
+ end
369
+ ns = opts.delete(:namespace)
370
+ if ns
371
+ # logger hasn't been initialized yet, puts is all we have.
372
+ puts("namespace should be set in your ruby initializer, is ignored in config file")
373
+ puts("config.redis = { :url => ..., :namespace => '#{ns}' }")
374
+ end
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, q, weight=nil)
383
+ [weight.to_i, 1].max.times do
384
+ (opts[:queues] ||= []) << q
385
+ end
386
+ opts[:strict] = false if weight.to_i > 0
387
+ end
388
+ end
389
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+ require 'securerandom'
3
+ require 'sidekiq/middleware/chain'
4
+
5
+ module Sidekiq
6
+ class Client
7
+
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
+ # retry - whether to retry this job if it fails, default true or an integer number of retries
52
+ # backtrace - whether to save any error backtrace, default false
53
+ #
54
+ # All options must be strings, not symbols. NB: because we are serializing to JSON, all
55
+ # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
56
+ # space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful.
57
+ #
58
+ # Returns a unique Job ID. If middleware stops the job, nil will be returned instead.
59
+ #
60
+ # Example:
61
+ # push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
62
+ #
63
+ def push(item)
64
+ normed = normalize_item(item)
65
+ payload = process_single(item['class'], normed)
66
+
67
+ if payload
68
+ raw_push([payload])
69
+ payload['jid']
70
+ end
71
+ end
72
+
73
+ ##
74
+ # Push a large number of jobs to Redis. In practice this method is only
75
+ # useful if you are pushing thousands of jobs or more. This method
76
+ # cuts out the redis network round trip latency.
77
+ #
78
+ # Takes the same arguments as #push except that args is expected to be
79
+ # an Array of Arrays. All other keys are duplicated for each job. Each job
80
+ # is run through the client middleware pipeline and each job gets its own Job ID
81
+ # as normal.
82
+ #
83
+ # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
84
+ # than the number given if the middleware stopped processing for one or more jobs.
85
+ def push_bulk(items)
86
+ arg = items['args'].first
87
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !arg.is_a?(Array)
88
+
89
+ normed = normalize_item(items)
90
+ payloads = items['args'].map do |args|
91
+ copy = normed.merge('args' => args, 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
92
+ result = process_single(items['class'], copy)
93
+ result ? result : nil
94
+ end.compact
95
+
96
+ raw_push(payloads) if !payloads.empty?
97
+ payloads.collect { |payload| payload['jid'] }
98
+ end
99
+
100
+ # Allows sharding of jobs across any number of Redis instances. All jobs
101
+ # defined within the block will use the given Redis connection pool.
102
+ #
103
+ # pool = ConnectionPool.new { Redis.new }
104
+ # Sidekiq::Client.via(pool) do
105
+ # SomeWorker.perform_async(1,2,3)
106
+ # SomeOtherWorker.perform_async(1,2,3)
107
+ # end
108
+ #
109
+ # Generally this is only needed for very large Sidekiq installs processing
110
+ # thousands of jobs per second. I do not recommend sharding unless
111
+ # you cannot scale any other way (e.g. splitting your app into smaller apps).
112
+ def self.via(pool)
113
+ raise ArgumentError, "No pool given" if pool.nil?
114
+ current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
115
+ raise RuntimeError, "Sidekiq::Client.via is not re-entrant" if current_sidekiq_pool && current_sidekiq_pool != pool
116
+ Thread.current[:sidekiq_via_pool] = pool
117
+ yield
118
+ ensure
119
+ Thread.current[:sidekiq_via_pool] = nil
120
+ end
121
+
122
+ class << self
123
+
124
+ def push(item)
125
+ new.push(item)
126
+ end
127
+
128
+ def push_bulk(items)
129
+ new.push_bulk(items)
130
+ end
131
+
132
+ # Resque compatibility helpers. Note all helpers
133
+ # should go through Worker#client_push.
134
+ #
135
+ # Example usage:
136
+ # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
137
+ #
138
+ # Messages are enqueued to the 'default' queue.
139
+ #
140
+ def enqueue(klass, *args)
141
+ klass.client_push('class' => klass, 'args' => args)
142
+ end
143
+
144
+ # Example usage:
145
+ # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
146
+ #
147
+ def enqueue_to(queue, klass, *args)
148
+ klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
149
+ end
150
+
151
+ # Example usage:
152
+ # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
153
+ #
154
+ def enqueue_to_in(queue, interval, klass, *args)
155
+ int = interval.to_f
156
+ now = Time.now.to_f
157
+ ts = (int < 1_000_000_000 ? now + int : int)
158
+
159
+ item = { 'class' => klass, 'args' => args, 'at' => ts, 'queue' => queue }
160
+ item.delete('at'.freeze) if ts <= now
161
+
162
+ klass.client_push(item)
163
+ end
164
+
165
+ # Example usage:
166
+ # Sidekiq::Client.enqueue_in(3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
167
+ #
168
+ def enqueue_in(interval, klass, *args)
169
+ klass.perform_in(interval, *args)
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def raw_push(payloads)
176
+ @redis_pool.with do |conn|
177
+ conn.multi do
178
+ atomic_push(conn, payloads)
179
+ end
180
+ end
181
+ true
182
+ end
183
+
184
+ def atomic_push(conn, payloads)
185
+ if payloads.first['at']
186
+ conn.zadd('schedule'.freeze, payloads.map do |hash|
187
+ at = hash.delete('at'.freeze).to_s
188
+ [at, Sidekiq.dump_json(hash)]
189
+ end)
190
+ else
191
+ q = payloads.first['queue']
192
+ now = Time.now.to_f
193
+ to_push = payloads.map do |entry|
194
+ entry['enqueued_at'.freeze] = now
195
+ Sidekiq.dump_json(entry)
196
+ end
197
+ conn.sadd('queues'.freeze, q)
198
+ conn.lpush("queue:#{q}", to_push)
199
+ end
200
+ end
201
+
202
+ def process_single(worker_class, item)
203
+ queue = item['queue']
204
+
205
+ middleware.invoke(worker_class, item, queue, @redis_pool) do
206
+ item
207
+ end
208
+ end
209
+
210
+ def normalize_item(item)
211
+ 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.has_key?('class'.freeze) && item.has_key?('args'.freeze)
212
+ raise(ArgumentError, "Job args must be an Array") unless item['args'.freeze].is_a?(Array)
213
+ raise(ArgumentError, "Job class must be either a Class or String representation of the class name") unless item['class'.freeze].is_a?(Class) || item['class'.freeze].is_a?(String)
214
+ #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']
215
+
216
+ unless item['retry'.freeze]
217
+ args_to_check = if item['class'.freeze].is_a?(Class) && item['class'.freeze].name.include?('ActiveJob'.freeze)
218
+ raise(ArgumentError, "Unexpected arguments (expected array with single element): #{item['args'.freeze].inspect}") unless item['args'.freeze].length == 1
219
+ raise(ArgumentError, "Expected item['args'][0] to be a Hash: #{item['args'.freeze][0].inspect}") unless item['args'.freeze][0].is_a?(Hash)
220
+ if item['args'.freeze][0]['job_class'.freeze].include?('ActionMailer'.freeze) && item['args'.freeze][0]['arguments'.freeze].length > 3
221
+ # for ActionMailer, the first argument in the name of the mailer class, the second argument is the mail method, and the 3rd argument is the delivery method ('deliver_now').
222
+ item['args'.freeze][0]['arguments'.freeze][3..-1]
223
+ else
224
+ # check if all active job arguments are integers
225
+ item['args'.freeze][0]['arguments'.freeze]
226
+ end
227
+ else
228
+ # just check that item['args'] are all integers
229
+ item['args'.freeze]
230
+ end
231
+
232
+ args_to_check.each do |arg|
233
+ next if arg.is_a?(Fixnum)
234
+ raise(ArgumentError, "Only Fixnums, Arrays of Fixnums, or Hashes with Fixnum values are permitted as arguments. Received: #{arg}") unless arg.is_a?(Array) || arg.is_a?(Hash)
235
+ (arg.is_a?(Hash) ? arg.values : arg).each do |sub_arg|
236
+ raise(ArgumentError, "Only Fixnum arguments are permitted. Received: #{sub_arg}") unless sub_arg.is_a?(Fixnum)
237
+ end
238
+ end
239
+ end
240
+
241
+ normalized_hash(item['class'.freeze])
242
+ .each{ |key, value| item[key] = value if item[key].nil? }
243
+
244
+ item['class'.freeze] = item['class'.freeze].to_s
245
+ item['queue'.freeze] = item['queue'.freeze].to_s
246
+ item['jid'.freeze] ||= SecureRandom.hex(12)
247
+ item['created_at'.freeze] ||= Time.now.to_f
248
+ item
249
+ end
250
+
251
+ def normalized_hash(item_class)
252
+ if item_class.is_a?(Class)
253
+ raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") if !item_class.respond_to?('get_sidekiq_options'.freeze)
254
+ item_class.get_sidekiq_options
255
+ else
256
+ Sidekiq.default_worker_options
257
+ end
258
+ end
259
+ end
260
+ end