roundhouse-x 0.1.0

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 (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
+