roundhouse-x 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (168) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +16 -0
  4. data/3.0-Upgrade.md +70 -0
  5. data/Changes.md +1127 -0
  6. data/Gemfile +27 -0
  7. data/LICENSE +7 -0
  8. data/README.md +52 -0
  9. data/Rakefile +9 -0
  10. data/bin/roundhouse +19 -0
  11. data/bin/roundhousectl +93 -0
  12. data/lib/generators/roundhouse/templates/worker.rb.erb +9 -0
  13. data/lib/generators/roundhouse/templates/worker_spec.rb.erb +6 -0
  14. data/lib/generators/roundhouse/templates/worker_test.rb.erb +8 -0
  15. data/lib/generators/roundhouse/worker_generator.rb +49 -0
  16. data/lib/roundhouse/actor.rb +39 -0
  17. data/lib/roundhouse/api.rb +859 -0
  18. data/lib/roundhouse/cli.rb +396 -0
  19. data/lib/roundhouse/client.rb +210 -0
  20. data/lib/roundhouse/core_ext.rb +105 -0
  21. data/lib/roundhouse/exception_handler.rb +30 -0
  22. data/lib/roundhouse/fetch.rb +154 -0
  23. data/lib/roundhouse/launcher.rb +98 -0
  24. data/lib/roundhouse/logging.rb +104 -0
  25. data/lib/roundhouse/manager.rb +236 -0
  26. data/lib/roundhouse/middleware/chain.rb +149 -0
  27. data/lib/roundhouse/middleware/i18n.rb +41 -0
  28. data/lib/roundhouse/middleware/server/active_record.rb +13 -0
  29. data/lib/roundhouse/middleware/server/logging.rb +40 -0
  30. data/lib/roundhouse/middleware/server/retry_jobs.rb +206 -0
  31. data/lib/roundhouse/monitor.rb +124 -0
  32. data/lib/roundhouse/paginator.rb +42 -0
  33. data/lib/roundhouse/processor.rb +159 -0
  34. data/lib/roundhouse/rails.rb +24 -0
  35. data/lib/roundhouse/redis_connection.rb +77 -0
  36. data/lib/roundhouse/scheduled.rb +115 -0
  37. data/lib/roundhouse/testing/inline.rb +28 -0
  38. data/lib/roundhouse/testing.rb +193 -0
  39. data/lib/roundhouse/util.rb +68 -0
  40. data/lib/roundhouse/version.rb +3 -0
  41. data/lib/roundhouse/web.rb +264 -0
  42. data/lib/roundhouse/web_helpers.rb +249 -0
  43. data/lib/roundhouse/worker.rb +90 -0
  44. data/lib/roundhouse.rb +177 -0
  45. data/roundhouse.gemspec +27 -0
  46. data/test/config.yml +9 -0
  47. data/test/env_based_config.yml +11 -0
  48. data/test/fake_env.rb +0 -0
  49. data/test/fixtures/en.yml +2 -0
  50. data/test/helper.rb +49 -0
  51. data/test/test_api.rb +521 -0
  52. data/test/test_cli.rb +389 -0
  53. data/test/test_client.rb +294 -0
  54. data/test/test_exception_handler.rb +55 -0
  55. data/test/test_fetch.rb +206 -0
  56. data/test/test_logging.rb +34 -0
  57. data/test/test_manager.rb +169 -0
  58. data/test/test_middleware.rb +160 -0
  59. data/test/test_monitor.rb +258 -0
  60. data/test/test_processor.rb +176 -0
  61. data/test/test_rails.rb +23 -0
  62. data/test/test_redis_connection.rb +127 -0
  63. data/test/test_retry.rb +390 -0
  64. data/test/test_roundhouse.rb +87 -0
  65. data/test/test_scheduled.rb +120 -0
  66. data/test/test_scheduling.rb +75 -0
  67. data/test/test_testing.rb +78 -0
  68. data/test/test_testing_fake.rb +240 -0
  69. data/test/test_testing_inline.rb +65 -0
  70. data/test/test_util.rb +18 -0
  71. data/test/test_web.rb +605 -0
  72. data/test/test_web_helpers.rb +52 -0
  73. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  74. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  75. data/web/assets/images/logo.png +0 -0
  76. data/web/assets/images/status/active.png +0 -0
  77. data/web/assets/images/status/idle.png +0 -0
  78. data/web/assets/images/status-sd8051fd480.png +0 -0
  79. data/web/assets/javascripts/application.js +83 -0
  80. data/web/assets/javascripts/dashboard.js +300 -0
  81. data/web/assets/javascripts/locales/README.md +27 -0
  82. data/web/assets/javascripts/locales/jquery.timeago.ar.js +96 -0
  83. data/web/assets/javascripts/locales/jquery.timeago.bg.js +18 -0
  84. data/web/assets/javascripts/locales/jquery.timeago.bs.js +49 -0
  85. data/web/assets/javascripts/locales/jquery.timeago.ca.js +18 -0
  86. data/web/assets/javascripts/locales/jquery.timeago.cs.js +18 -0
  87. data/web/assets/javascripts/locales/jquery.timeago.cy.js +20 -0
  88. data/web/assets/javascripts/locales/jquery.timeago.da.js +18 -0
  89. data/web/assets/javascripts/locales/jquery.timeago.de.js +18 -0
  90. data/web/assets/javascripts/locales/jquery.timeago.el.js +18 -0
  91. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +20 -0
  92. data/web/assets/javascripts/locales/jquery.timeago.en.js +20 -0
  93. data/web/assets/javascripts/locales/jquery.timeago.es.js +18 -0
  94. data/web/assets/javascripts/locales/jquery.timeago.et.js +18 -0
  95. data/web/assets/javascripts/locales/jquery.timeago.fa.js +22 -0
  96. data/web/assets/javascripts/locales/jquery.timeago.fi.js +28 -0
  97. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +16 -0
  98. data/web/assets/javascripts/locales/jquery.timeago.fr.js +17 -0
  99. data/web/assets/javascripts/locales/jquery.timeago.he.js +18 -0
  100. data/web/assets/javascripts/locales/jquery.timeago.hr.js +49 -0
  101. data/web/assets/javascripts/locales/jquery.timeago.hu.js +18 -0
  102. data/web/assets/javascripts/locales/jquery.timeago.hy.js +18 -0
  103. data/web/assets/javascripts/locales/jquery.timeago.id.js +18 -0
  104. data/web/assets/javascripts/locales/jquery.timeago.it.js +16 -0
  105. data/web/assets/javascripts/locales/jquery.timeago.ja.js +19 -0
  106. data/web/assets/javascripts/locales/jquery.timeago.ko.js +17 -0
  107. data/web/assets/javascripts/locales/jquery.timeago.lt.js +20 -0
  108. data/web/assets/javascripts/locales/jquery.timeago.mk.js +20 -0
  109. data/web/assets/javascripts/locales/jquery.timeago.nl.js +20 -0
  110. data/web/assets/javascripts/locales/jquery.timeago.no.js +18 -0
  111. data/web/assets/javascripts/locales/jquery.timeago.pl.js +31 -0
  112. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +16 -0
  113. data/web/assets/javascripts/locales/jquery.timeago.pt.js +16 -0
  114. data/web/assets/javascripts/locales/jquery.timeago.ro.js +18 -0
  115. data/web/assets/javascripts/locales/jquery.timeago.rs.js +49 -0
  116. data/web/assets/javascripts/locales/jquery.timeago.ru.js +34 -0
  117. data/web/assets/javascripts/locales/jquery.timeago.sk.js +18 -0
  118. data/web/assets/javascripts/locales/jquery.timeago.sl.js +44 -0
  119. data/web/assets/javascripts/locales/jquery.timeago.sv.js +18 -0
  120. data/web/assets/javascripts/locales/jquery.timeago.th.js +20 -0
  121. data/web/assets/javascripts/locales/jquery.timeago.tr.js +16 -0
  122. data/web/assets/javascripts/locales/jquery.timeago.uk.js +34 -0
  123. data/web/assets/javascripts/locales/jquery.timeago.uz.js +19 -0
  124. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +20 -0
  125. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +20 -0
  126. data/web/assets/stylesheets/application.css +746 -0
  127. data/web/assets/stylesheets/bootstrap.css +9 -0
  128. data/web/locales/cs.yml +68 -0
  129. data/web/locales/da.yml +68 -0
  130. data/web/locales/de.yml +69 -0
  131. data/web/locales/el.yml +68 -0
  132. data/web/locales/en.yml +77 -0
  133. data/web/locales/es.yml +69 -0
  134. data/web/locales/fr.yml +69 -0
  135. data/web/locales/hi.yml +75 -0
  136. data/web/locales/it.yml +69 -0
  137. data/web/locales/ja.yml +69 -0
  138. data/web/locales/ko.yml +68 -0
  139. data/web/locales/nl.yml +68 -0
  140. data/web/locales/no.yml +69 -0
  141. data/web/locales/pl.yml +59 -0
  142. data/web/locales/pt-br.yml +68 -0
  143. data/web/locales/pt.yml +67 -0
  144. data/web/locales/ru.yml +75 -0
  145. data/web/locales/sv.yml +68 -0
  146. data/web/locales/ta.yml +75 -0
  147. data/web/locales/zh-cn.yml +68 -0
  148. data/web/locales/zh-tw.yml +68 -0
  149. data/web/views/_footer.erb +22 -0
  150. data/web/views/_job_info.erb +84 -0
  151. data/web/views/_nav.erb +66 -0
  152. data/web/views/_paging.erb +23 -0
  153. data/web/views/_poll_js.erb +5 -0
  154. data/web/views/_poll_link.erb +7 -0
  155. data/web/views/_status.erb +4 -0
  156. data/web/views/_summary.erb +40 -0
  157. data/web/views/busy.erb +90 -0
  158. data/web/views/dashboard.erb +75 -0
  159. data/web/views/dead.erb +34 -0
  160. data/web/views/layout.erb +31 -0
  161. data/web/views/morgue.erb +71 -0
  162. data/web/views/queue.erb +45 -0
  163. data/web/views/queues.erb +27 -0
  164. data/web/views/retries.erb +74 -0
  165. data/web/views/retry.erb +34 -0
  166. data/web/views/scheduled.erb +54 -0
  167. data/web/views/scheduled_job_info.erb +8 -0
  168. metadata +404 -0
@@ -0,0 +1,396 @@
1
+ # encoding: utf-8
2
+ $stdout.sync = true
3
+
4
+ require 'yaml'
5
+ require 'singleton'
6
+ require 'optparse'
7
+ require 'erb'
8
+ require 'fileutils'
9
+
10
+ require 'roundhouse'
11
+ require 'roundhouse/util'
12
+
13
+ module Roundhouse
14
+ # We are shutting down Roundhouse but what about workers that
15
+ # are working on some long job? This error is
16
+ # raised in workers that have not finished within the hard
17
+ # timeout limit. This is needed to rollback db transactions,
18
+ # otherwise Ruby's Thread#kill will commit. See #377.
19
+ # DO NOT RESCUE THIS ERROR.
20
+ class Shutdown < Interrupt; end
21
+
22
+ class CLI
23
+ include Util
24
+ include Singleton unless $TESTING
25
+
26
+ # Used for CLI testing
27
+ attr_accessor :code
28
+ attr_accessor :launcher
29
+ attr_accessor :environment
30
+
31
+ def initialize
32
+ @code = nil
33
+ end
34
+
35
+ def parse(args=ARGV)
36
+ @code = nil
37
+
38
+ setup_options(args)
39
+ initialize_logger
40
+ validate!
41
+ daemonize
42
+ write_pid
43
+ load_celluloid
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 Roundhouse::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 Roundhouse::LICENSE
67
+ logger.info "Upgrade to Roundhouse Pro for more features and support: http://roundhouse.org" unless defined?(::Roundhouse::Pro)
68
+
69
+ fire_event(:startup)
70
+
71
+ logger.debug {
72
+ "Middleware: #{Roundhouse.server_middleware.map(&:klass).join(', ')}"
73
+ }
74
+
75
+ Roundhouse.redis do |conn|
76
+ # touch the connection pool so it is created before we
77
+ # launch the actors.
78
+ end
79
+
80
+ if !options[:daemon]
81
+ logger.info 'Starting processing, hit Ctrl-C to stop'
82
+ end
83
+
84
+ require 'roundhouse/launcher'
85
+ @launcher = Roundhouse::Launcher.new(options)
86
+
87
+ begin
88
+ launcher.run
89
+
90
+ while readable_io = IO.select([self_read])
91
+ signal = readable_io.first[0].gets.strip
92
+ handle_signal(signal)
93
+ end
94
+ rescue Interrupt
95
+ logger.info 'Shutting down'
96
+ launcher.stop
97
+ fire_event(:shutdown, true)
98
+ # Explicitly exit so busy Processor threads can't block
99
+ # process shutdown.
100
+ exit(0)
101
+ end
102
+ end
103
+
104
+ def self.banner
105
+ %q{ s
106
+ ss
107
+ sss sss ss
108
+ s sss s ssss sss ____ _ _ _ _
109
+ s sssss ssss / ___|(_) __| | ___| | _(_) __ _
110
+ s sss \___ \| |/ _` |/ _ \ |/ / |/ _` |
111
+ s sssss s ___) | | (_| | __/ <| | (_| |
112
+ ss s s |____/|_|\__,_|\___|_|\_\_|\__, |
113
+ s s s |_|
114
+ s s
115
+ sss
116
+ sss }
117
+ end
118
+
119
+ def handle_signal(sig)
120
+ Roundhouse.logger.debug "Got #{sig} signal"
121
+ case sig
122
+ when 'INT'
123
+ # Handle Ctrl-C in JRuby like MRI
124
+ # http://jira.codehaus.org/browse/JRUBY-4637
125
+ raise Interrupt
126
+ when 'TERM'
127
+ # Heroku sends TERM and then waits 10 seconds for process to exit.
128
+ raise Interrupt
129
+ when 'USR1'
130
+ Roundhouse.logger.info "Received USR1, no longer accepting new work"
131
+ launcher.manager.async.stop
132
+ fire_event(:quiet, true)
133
+ when 'USR2'
134
+ if Roundhouse.options[:logfile]
135
+ Roundhouse.logger.info "Received USR2, reopening log file"
136
+ Roundhouse::Logging.reopen_logs
137
+ end
138
+ when 'TTIN'
139
+ Thread.list.each do |thread|
140
+ Roundhouse.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
141
+ if thread.backtrace
142
+ Roundhouse.logger.warn thread.backtrace.join("\n")
143
+ else
144
+ Roundhouse.logger.warn "<no backtrace available>"
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ private
151
+
152
+ def print_banner
153
+ # Print logo and banner for development
154
+ if environment == 'development' && $stdout.tty?
155
+ puts "\e[#{31}m"
156
+ puts Roundhouse::CLI.banner
157
+ puts "\e[0m"
158
+ end
159
+ end
160
+
161
+ def load_celluloid
162
+ raise "Celluloid cannot be required until here, or it will break Roundhouse's daemonization" if defined?(::Celluloid) && options[:daemon]
163
+
164
+ # Celluloid can't be loaded until after we've daemonized
165
+ # because it spins up threads and creates locks which get
166
+ # into a very bad state if forked.
167
+ require 'celluloid/current'
168
+ Celluloid.logger = (options[:verbose] ? Roundhouse.logger : nil)
169
+
170
+ require 'roundhouse/manager'
171
+ require 'roundhouse/scheduled'
172
+ end
173
+
174
+ def daemonize
175
+ return unless options[:daemon]
176
+
177
+ raise ArgumentError, "You really should set a logfile if you're going to daemonize" unless options[:logfile]
178
+ files_to_reopen = []
179
+ ObjectSpace.each_object(File) do |file|
180
+ files_to_reopen << file unless file.closed?
181
+ end
182
+
183
+ ::Process.daemon(true, true)
184
+
185
+ files_to_reopen.each do |file|
186
+ begin
187
+ file.reopen file.path, "a+"
188
+ file.sync = true
189
+ rescue ::Exception
190
+ end
191
+ end
192
+
193
+ [$stdout, $stderr].each do |io|
194
+ File.open(options[:logfile], 'ab') do |f|
195
+ io.reopen(f)
196
+ end
197
+ io.sync = true
198
+ end
199
+ $stdin.reopen('/dev/null')
200
+
201
+ initialize_logger
202
+ end
203
+
204
+ def set_environment(cli_env)
205
+ @environment = cli_env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
206
+ end
207
+
208
+ alias_method :die, :exit
209
+ alias_method :☠, :exit
210
+
211
+ def setup_options(args)
212
+ opts = parse_options(args)
213
+ set_environment opts[:environment]
214
+
215
+ cfile = opts[:config_file]
216
+ opts = parse_config(cfile).merge(opts) if cfile
217
+
218
+ opts[:strict] = true if opts[:strict].nil?
219
+
220
+ options.merge!(opts)
221
+ end
222
+
223
+ def options
224
+ Roundhouse.options
225
+ end
226
+
227
+ def boot_system
228
+ ENV['RACK_ENV'] = ENV['RAILS_ENV'] = environment
229
+
230
+ raise ArgumentError, "#{options[:require]} does not exist" unless File.exist?(options[:require])
231
+
232
+ if File.directory?(options[:require])
233
+ require 'rails'
234
+ if ::Rails::VERSION::MAJOR < 4
235
+ require 'roundhouse/rails'
236
+ require File.expand_path("#{options[:require]}/config/environment.rb")
237
+ ::Rails.application.eager_load!
238
+ else
239
+ # Painful contortions, see 1791 for discussion
240
+ require File.expand_path("#{options[:require]}/config/application.rb")
241
+ ::Rails::Application.initializer "roundhouse.eager_load" do
242
+ ::Rails.application.config.eager_load = true
243
+ end
244
+ require 'roundhouse/rails'
245
+ require File.expand_path("#{options[:require]}/config/environment.rb")
246
+ end
247
+ options[:tag] ||= default_tag
248
+ else
249
+ require options[:require]
250
+ end
251
+ end
252
+
253
+ def default_tag
254
+ dir = ::Rails.root
255
+ name = File.basename(dir)
256
+ if name.to_i != 0 && prevdir = File.dirname(dir) # Capistrano release directory?
257
+ if File.basename(prevdir) == 'releases'
258
+ return File.basename(File.dirname(prevdir))
259
+ end
260
+ end
261
+ name
262
+ end
263
+
264
+ def validate!
265
+ options[:queues] << 'default' if options[:queues].empty?
266
+
267
+ if !File.exist?(options[:require]) ||
268
+ (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
269
+ logger.info "=================================================================="
270
+ logger.info " Please point roundhouse to a Rails 3/4 application or a Ruby file "
271
+ logger.info " to load your worker classes with -r [DIR|FILE]."
272
+ logger.info "=================================================================="
273
+ logger.info @parser
274
+ die(1)
275
+ end
276
+
277
+ [:concurrency, :timeout].each do |opt|
278
+ raise ArgumentError, "#{opt}: #{options[opt]} is not a valid value" if options.has_key?(opt) && options[opt].to_i <= 0
279
+ end
280
+ end
281
+
282
+ def parse_options(argv)
283
+ opts = {}
284
+
285
+ @parser = OptionParser.new do |o|
286
+ o.on '-c', '--concurrency INT', "processor threads to use" do |arg|
287
+ opts[:concurrency] = Integer(arg)
288
+ end
289
+
290
+ o.on '-d', '--daemon', "Daemonize process" do |arg|
291
+ opts[:daemon] = arg
292
+ end
293
+
294
+ o.on '-e', '--environment ENV', "Application environment" do |arg|
295
+ opts[:environment] = arg
296
+ end
297
+
298
+ o.on '-g', '--tag TAG', "Process tag for procline" do |arg|
299
+ opts[:tag] = arg
300
+ end
301
+
302
+ o.on '-i', '--index INT', "unique process index on this machine" do |arg|
303
+ opts[:index] = Integer(arg.match(/\d+/)[0])
304
+ end
305
+
306
+ o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
307
+ queue, weight = arg.split(",")
308
+ parse_queue opts, queue, weight
309
+ end
310
+
311
+ o.on '-r', '--require [PATH|DIR]', "Location of Rails application with workers or file to require" do |arg|
312
+ opts[:require] = arg
313
+ end
314
+
315
+ o.on '-t', '--timeout NUM', "Shutdown timeout" do |arg|
316
+ opts[:timeout] = Integer(arg)
317
+ end
318
+
319
+ o.on "-v", "--verbose", "Print more verbose output" do |arg|
320
+ opts[:verbose] = arg
321
+ end
322
+
323
+ o.on '-C', '--config PATH', "path to YAML config file" do |arg|
324
+ opts[:config_file] = arg
325
+ end
326
+
327
+ o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
328
+ opts[:logfile] = arg
329
+ end
330
+
331
+ o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
332
+ opts[:pidfile] = arg
333
+ end
334
+
335
+ o.on '-V', '--version', "Print version and exit" do |arg|
336
+ puts "Roundhouse #{Roundhouse::VERSION}"
337
+ die(0)
338
+ end
339
+ end
340
+
341
+ @parser.banner = "roundhouse [options]"
342
+ @parser.on_tail "-h", "--help", "Show help" do
343
+ logger.info @parser
344
+ die 1
345
+ end
346
+ @parser.parse!(argv)
347
+ opts[:config_file] ||= 'config/roundhouse.yml' if File.exist?('config/roundhouse.yml')
348
+ opts
349
+ end
350
+
351
+ def initialize_logger
352
+ Roundhouse::Logging.initialize_logger(options[:logfile]) if options[:logfile]
353
+
354
+ Roundhouse.logger.level = ::Logger::DEBUG if options[:verbose]
355
+ end
356
+
357
+ def write_pid
358
+ if path = options[:pidfile]
359
+ pidfile = File.expand_path(path)
360
+ File.open(pidfile, 'w') do |f|
361
+ f.puts ::Process.pid
362
+ end
363
+ end
364
+ end
365
+
366
+ def parse_config(cfile)
367
+ opts = {}
368
+ if File.exist?(cfile)
369
+ opts = YAML.load(ERB.new(IO.read(cfile)).result) || opts
370
+ opts = opts.merge(opts.delete(environment) || {})
371
+ parse_queues(opts, opts.delete(:queues) || [])
372
+ else
373
+ # allow a non-existent config file so Roundhouse
374
+ # can be deployed by cap with just the defaults.
375
+ end
376
+ ns = opts.delete(:namespace)
377
+ if ns
378
+ # logger hasn't been initialized yet, puts is all we have.
379
+ puts("namespace should be set in your ruby initializer, is ignored in config file")
380
+ puts("config.redis = { :url => ..., :namespace => '#{ns}' }")
381
+ end
382
+ opts
383
+ end
384
+
385
+ def parse_queues(opts, queues_and_weights)
386
+ queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
387
+ end
388
+
389
+ def parse_queue(opts, q, weight=nil)
390
+ [weight.to_i, 1].max.times do
391
+ (opts[:queues] ||= []) << q
392
+ end
393
+ opts[:strict] = false if weight.to_i > 0
394
+ end
395
+ end
396
+ end
@@ -0,0 +1,210 @@
1
+ require 'securerandom'
2
+ require 'roundhouse/monitor'
3
+ require 'roundhouse/middleware/chain'
4
+
5
+ module Roundhouse
6
+ class Client
7
+
8
+ ##
9
+ # Define client-side middleware:
10
+ #
11
+ # client = Roundhouse::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
+ # Roundhouse.client_middleware but you can change as necessary.
19
+ #
20
+ def middleware(&block)
21
+ @chain ||= Roundhouse.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
+ # Roundhouse::Client normally uses the default Redis pool but you may
32
+ # pass a custom ConnectionPool if you want to shard your
33
+ # Roundhouse jobs across several Redis instances (for scalability
34
+ # reasons, e.g.)
35
+ #
36
+ # Roundhouse::Client.new(ConnectionPool.new { Redis.new })
37
+ #
38
+ # Generally this is only needed for very large Roundhouse installs processing
39
+ # more than thousands jobs per second. I do not recommend sharding unless
40
+ # you truly cannot scale any other way (e.g. splitting your app into smaller apps).
41
+ # Some features, like the API, do not support sharding: they are designed to work
42
+ # against a single Redis instance only.
43
+ def initialize(redis_pool=nil)
44
+ @redis_pool = redis_pool || Thread.current[:roundhouse_via_pool] || Roundhouse.redis_pool
45
+ end
46
+
47
+ ##
48
+ # The main method used to push a job to Redis. Accepts a number of options:
49
+ #
50
+ # queue_id - integer queue_id (required, no default)
51
+ # class - the worker class to call, required
52
+ # args - an array of simple arguments to the perform method, must be JSON-serializable
53
+ # retry - whether to retry this job if it fails, true or false, default true
54
+ # backtrace - whether to save any error backtrace, default false
55
+ #
56
+ # All options must be strings, not symbols. NB: because we are serializing to JSON, all
57
+ # symbols in 'args' will be converted to strings.
58
+ #
59
+ # Returns a unique Job ID. If middleware stops the job, nil will be returned instead.
60
+ #
61
+ # Example:
62
+ # push('queue' => 1, 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
63
+ #
64
+ def push(item)
65
+ normed = normalize_item(item)
66
+ payload = process_single(item['class'], normed)
67
+
68
+ if payload
69
+ raw_push([payload])
70
+ payload['jid']
71
+ end
72
+ end
73
+
74
+ ##
75
+ # Push a large number of jobs to Redis. In practice this method is only
76
+ # useful if you are pushing tens of thousands of jobs or more, or if you need
77
+ # to ensure that a batch doesn't complete prematurely. This method
78
+ # basically cuts down on the redis round trip latency.
79
+ #
80
+ # Note: Roundhouse implementation does not use MULTI, so this is not going
81
+ # to be as fast as Sidekiq. As such, this is not officially supported.
82
+ #
83
+ # Takes the same arguments as #push except that args is expected to be
84
+ # an Array of Arrays. All other keys are duplicated for each job. Each job
85
+ # is run through the client middleware pipeline and each job gets its own Job ID
86
+ # as normal.
87
+ #
88
+ # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
89
+ # than the number given if the middleware stopped processing for one or more jobs.
90
+ def push_bulk(items)
91
+ Roundhouse.logger.warn '#push_bulk is not officially supported. Use at your own risk.'
92
+ normed = normalize_item(items)
93
+ payloads = items['args'].map do |args|
94
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !args.is_a?(Array)
95
+ process_single(items['class'], normed.merge('args' => args, 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f))
96
+ end.compact
97
+
98
+ raw_push(payloads) if !payloads.empty?
99
+ payloads.collect { |payload| payload['jid'] }
100
+ end
101
+
102
+ # Allows sharding of jobs across any number of Redis instances. All jobs
103
+ # defined within the block will use the given Redis connection pool.
104
+ #
105
+ # pool = ConnectionPool.new { Redis.new }
106
+ # Roundhouse::Client.via(pool) do
107
+ # SomeWorker.perform_async(1,2,3)
108
+ # SomeOtherWorker.perform_async(1,2,3)
109
+ # end
110
+ #
111
+ # Generally this is only needed for very large Roundhouse installs processing
112
+ # more than thousands jobs per second. I do not recommend sharding unless
113
+ # you truly cannot scale any other way (e.g. splitting your app into smaller apps).
114
+ # Some features, like the API, do not support sharding: they are designed to work
115
+ # against a single Redis instance.
116
+ def self.via(pool)
117
+ raise NotImplementedError, 'Roundhouse does not support sharding at this point.'
118
+
119
+ raise ArgumentError, "No pool given" if pool.nil?
120
+ raise RuntimeError, "Roundhouse::Client.via is not re-entrant" if x = Thread.current[:roundhouse_via_pool] && x != pool
121
+ Thread.current[:roundhouse_via_pool] = pool
122
+ yield
123
+ ensure
124
+ Thread.current[:roundhouse_via_pool] = nil
125
+ end
126
+
127
+ class << self
128
+
129
+ # deprecated
130
+ def default
131
+ @default ||= new
132
+ end
133
+
134
+ def push(item)
135
+ new.push(item)
136
+ end
137
+
138
+ def push_bulk(items)
139
+ new.push_bulk(items)
140
+ end
141
+
142
+ # Resque compatibility helpers. Note all helpers
143
+ # should go through Worker#client_push.
144
+
145
+ # Example usage:
146
+ # Roundhouse::Client.enqueue_to(queue_id, MyWorker, 'foo', 1, :bat => 'bar')
147
+ #
148
+ def enqueue_to(queue_id, klass, *args)
149
+ klass.client_push('queue_id' => queue_id, 'class' => klass, 'args' => args)
150
+ end
151
+
152
+ # Example usage:
153
+ # Roundhouse::Client.enqueue_to_in(queue_id, 3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
154
+ #
155
+ def enqueue_to_in(queue_id, interval, klass, *args)
156
+ int = interval.to_f
157
+ now = Time.now.to_f
158
+ ts = (int < 1_000_000_000 ? now + int : int)
159
+
160
+ item = { 'class' => klass, 'args' => args, 'at' => ts, 'queue_id' => queue_id }
161
+ item.delete('at'.freeze) if ts <= now
162
+
163
+ klass.client_push(item)
164
+ end
165
+
166
+ end
167
+
168
+ private
169
+
170
+ def raw_push(payloads)
171
+ @redis_pool.with do |conn|
172
+ Roundhouse::Monitor.push_job(conn, payloads)
173
+ end
174
+ true
175
+ end
176
+
177
+ def process_single(worker_class, item)
178
+ queue_id = item['queue_id']
179
+
180
+ middleware.invoke(worker_class, item, queue_id, @redis_pool) do
181
+ item
182
+ end
183
+ end
184
+
185
+ def normalize_item(item)
186
+ 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)
187
+ raise(ArgumentError, "Queue ID must be an integer") unless item['queue_id'.freeze].is_a?(Fixnum)
188
+ raise(ArgumentError, "Job args must be an Array") unless item['args'].is_a?(Array)
189
+ 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)
190
+
191
+ normalized_hash(item['class'.freeze])
192
+ .each{ |key, value| item[key] = value if item[key].nil? }
193
+
194
+ item['class'.freeze] = item['class'.freeze].to_s
195
+ item['queue_id'.freeze] = item['queue_id'.freeze].to_i
196
+ item['jid'.freeze] ||= SecureRandom.hex(12)
197
+ item['created_at'.freeze] ||= Time.now.to_f
198
+ item
199
+ end
200
+
201
+ def normalized_hash(item_class)
202
+ if item_class.is_a?(Class)
203
+ raise(ArgumentError, "Message must include a Roundhouse::Worker class, not class name: #{item_class.ancestors.inspect}") if !item_class.respond_to?('get_roundhouse_options'.freeze)
204
+ item_class.get_roundhouse_options
205
+ else
206
+ Roundhouse.default_worker_options
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,105 @@
1
+ begin
2
+ require 'active_support/core_ext/class/attribute'
3
+ rescue LoadError
4
+
5
+ # A dumbed down version of ActiveSupport's
6
+ # Class#class_attribute helper.
7
+ class Class
8
+ def class_attribute(*attrs)
9
+ instance_writer = true
10
+
11
+ attrs.each do |name|
12
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
+ def self.#{name}() nil end
14
+ def self.#{name}?() !!#{name} end
15
+
16
+ def self.#{name}=(val)
17
+ singleton_class.class_eval do
18
+ define_method(:#{name}) { val }
19
+ end
20
+
21
+ if singleton_class?
22
+ class_eval do
23
+ def #{name}
24
+ defined?(@#{name}) ? @#{name} : singleton_class.#{name}
25
+ end
26
+ end
27
+ end
28
+ val
29
+ end
30
+
31
+ def #{name}
32
+ defined?(@#{name}) ? @#{name} : self.class.#{name}
33
+ end
34
+
35
+ def #{name}?
36
+ !!#{name}
37
+ end
38
+ RUBY
39
+
40
+ attr_writer name if instance_writer
41
+ end
42
+ end
43
+
44
+ private
45
+ def singleton_class?
46
+ ancestors.first != self
47
+ end
48
+ end
49
+ end
50
+
51
+ begin
52
+ require 'active_support/core_ext/hash/keys'
53
+ require 'active_support/core_ext/hash/deep_merge'
54
+ rescue LoadError
55
+ class Hash
56
+ def stringify_keys
57
+ keys.each do |key|
58
+ self[key.to_s] = delete(key)
59
+ end
60
+ self
61
+ end if !{}.respond_to?(:stringify_keys)
62
+
63
+ def symbolize_keys
64
+ keys.each do |key|
65
+ self[(key.to_sym rescue key) || key] = delete(key)
66
+ end
67
+ self
68
+ end if !{}.respond_to?(:symbolize_keys)
69
+
70
+ def deep_merge(other_hash, &block)
71
+ dup.deep_merge!(other_hash, &block)
72
+ end if !{}.respond_to?(:deep_merge)
73
+
74
+ def deep_merge!(other_hash, &block)
75
+ other_hash.each_pair do |k,v|
76
+ tv = self[k]
77
+ if tv.is_a?(Hash) && v.is_a?(Hash)
78
+ self[k] = tv.deep_merge(v, &block)
79
+ else
80
+ self[k] = block && tv ? block.call(k, tv, v) : v
81
+ end
82
+ end
83
+ self
84
+ end if !{}.respond_to?(:deep_merge!)
85
+ end
86
+ end
87
+
88
+ begin
89
+ require 'active_support/core_ext/string/inflections'
90
+ rescue LoadError
91
+ class String
92
+ def constantize
93
+ names = self.split('::')
94
+ names.shift if names.empty? || names.first.empty?
95
+
96
+ constant = Object
97
+ names.each do |name|
98
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
99
+ end
100
+ constant
101
+ end
102
+ end if !"".respond_to?(:constantize)
103
+ end
104
+
105
+