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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +16 -0
- data/3.0-Upgrade.md +70 -0
- data/Changes.md +1127 -0
- data/Gemfile +27 -0
- data/LICENSE +7 -0
- data/README.md +52 -0
- data/Rakefile +9 -0
- data/bin/roundhouse +19 -0
- data/bin/roundhousectl +93 -0
- data/lib/generators/roundhouse/templates/worker.rb.erb +9 -0
- data/lib/generators/roundhouse/templates/worker_spec.rb.erb +6 -0
- data/lib/generators/roundhouse/templates/worker_test.rb.erb +8 -0
- data/lib/generators/roundhouse/worker_generator.rb +49 -0
- data/lib/roundhouse/actor.rb +39 -0
- data/lib/roundhouse/api.rb +859 -0
- data/lib/roundhouse/cli.rb +396 -0
- data/lib/roundhouse/client.rb +210 -0
- data/lib/roundhouse/core_ext.rb +105 -0
- data/lib/roundhouse/exception_handler.rb +30 -0
- data/lib/roundhouse/fetch.rb +154 -0
- data/lib/roundhouse/launcher.rb +98 -0
- data/lib/roundhouse/logging.rb +104 -0
- data/lib/roundhouse/manager.rb +236 -0
- data/lib/roundhouse/middleware/chain.rb +149 -0
- data/lib/roundhouse/middleware/i18n.rb +41 -0
- data/lib/roundhouse/middleware/server/active_record.rb +13 -0
- data/lib/roundhouse/middleware/server/logging.rb +40 -0
- data/lib/roundhouse/middleware/server/retry_jobs.rb +206 -0
- data/lib/roundhouse/monitor.rb +124 -0
- data/lib/roundhouse/paginator.rb +42 -0
- data/lib/roundhouse/processor.rb +159 -0
- data/lib/roundhouse/rails.rb +24 -0
- data/lib/roundhouse/redis_connection.rb +77 -0
- data/lib/roundhouse/scheduled.rb +115 -0
- data/lib/roundhouse/testing/inline.rb +28 -0
- data/lib/roundhouse/testing.rb +193 -0
- data/lib/roundhouse/util.rb +68 -0
- data/lib/roundhouse/version.rb +3 -0
- data/lib/roundhouse/web.rb +264 -0
- data/lib/roundhouse/web_helpers.rb +249 -0
- data/lib/roundhouse/worker.rb +90 -0
- data/lib/roundhouse.rb +177 -0
- data/roundhouse.gemspec +27 -0
- data/test/config.yml +9 -0
- data/test/env_based_config.yml +11 -0
- data/test/fake_env.rb +0 -0
- data/test/fixtures/en.yml +2 -0
- data/test/helper.rb +49 -0
- data/test/test_api.rb +521 -0
- data/test/test_cli.rb +389 -0
- data/test/test_client.rb +294 -0
- data/test/test_exception_handler.rb +55 -0
- data/test/test_fetch.rb +206 -0
- data/test/test_logging.rb +34 -0
- data/test/test_manager.rb +169 -0
- data/test/test_middleware.rb +160 -0
- data/test/test_monitor.rb +258 -0
- data/test/test_processor.rb +176 -0
- data/test/test_rails.rb +23 -0
- data/test/test_redis_connection.rb +127 -0
- data/test/test_retry.rb +390 -0
- data/test/test_roundhouse.rb +87 -0
- data/test/test_scheduled.rb +120 -0
- data/test/test_scheduling.rb +75 -0
- data/test/test_testing.rb +78 -0
- data/test/test_testing_fake.rb +240 -0
- data/test/test_testing_inline.rb +65 -0
- data/test/test_util.rb +18 -0
- data/test/test_web.rb +605 -0
- data/test/test_web_helpers.rb +52 -0
- data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
- data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status/active.png +0 -0
- data/web/assets/images/status/idle.png +0 -0
- data/web/assets/images/status-sd8051fd480.png +0 -0
- data/web/assets/javascripts/application.js +83 -0
- data/web/assets/javascripts/dashboard.js +300 -0
- data/web/assets/javascripts/locales/README.md +27 -0
- data/web/assets/javascripts/locales/jquery.timeago.ar.js +96 -0
- data/web/assets/javascripts/locales/jquery.timeago.bg.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.bs.js +49 -0
- data/web/assets/javascripts/locales/jquery.timeago.ca.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.cs.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.cy.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.da.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.de.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.el.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.en-short.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.en.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.es.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.et.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.fa.js +22 -0
- data/web/assets/javascripts/locales/jquery.timeago.fi.js +28 -0
- data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.fr.js +17 -0
- data/web/assets/javascripts/locales/jquery.timeago.he.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.hr.js +49 -0
- data/web/assets/javascripts/locales/jquery.timeago.hu.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.hy.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.id.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.it.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.ja.js +19 -0
- data/web/assets/javascripts/locales/jquery.timeago.ko.js +17 -0
- data/web/assets/javascripts/locales/jquery.timeago.lt.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.mk.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.nl.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.no.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.pl.js +31 -0
- data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.pt.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.ro.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.rs.js +49 -0
- data/web/assets/javascripts/locales/jquery.timeago.ru.js +34 -0
- data/web/assets/javascripts/locales/jquery.timeago.sk.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.sl.js +44 -0
- data/web/assets/javascripts/locales/jquery.timeago.sv.js +18 -0
- data/web/assets/javascripts/locales/jquery.timeago.th.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.tr.js +16 -0
- data/web/assets/javascripts/locales/jquery.timeago.uk.js +34 -0
- data/web/assets/javascripts/locales/jquery.timeago.uz.js +19 -0
- data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +20 -0
- data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +20 -0
- data/web/assets/stylesheets/application.css +746 -0
- data/web/assets/stylesheets/bootstrap.css +9 -0
- data/web/locales/cs.yml +68 -0
- data/web/locales/da.yml +68 -0
- data/web/locales/de.yml +69 -0
- data/web/locales/el.yml +68 -0
- data/web/locales/en.yml +77 -0
- data/web/locales/es.yml +69 -0
- data/web/locales/fr.yml +69 -0
- data/web/locales/hi.yml +75 -0
- data/web/locales/it.yml +69 -0
- data/web/locales/ja.yml +69 -0
- data/web/locales/ko.yml +68 -0
- data/web/locales/nl.yml +68 -0
- data/web/locales/no.yml +69 -0
- data/web/locales/pl.yml +59 -0
- data/web/locales/pt-br.yml +68 -0
- data/web/locales/pt.yml +67 -0
- data/web/locales/ru.yml +75 -0
- data/web/locales/sv.yml +68 -0
- data/web/locales/ta.yml +75 -0
- data/web/locales/zh-cn.yml +68 -0
- data/web/locales/zh-tw.yml +68 -0
- data/web/views/_footer.erb +22 -0
- data/web/views/_job_info.erb +84 -0
- data/web/views/_nav.erb +66 -0
- data/web/views/_paging.erb +23 -0
- data/web/views/_poll_js.erb +5 -0
- data/web/views/_poll_link.erb +7 -0
- data/web/views/_status.erb +4 -0
- data/web/views/_summary.erb +40 -0
- data/web/views/busy.erb +90 -0
- data/web/views/dashboard.erb +75 -0
- data/web/views/dead.erb +34 -0
- data/web/views/layout.erb +31 -0
- data/web/views/morgue.erb +71 -0
- data/web/views/queue.erb +45 -0
- data/web/views/queues.erb +27 -0
- data/web/views/retries.erb +74 -0
- data/web/views/retry.erb +34 -0
- data/web/views/scheduled.erb +54 -0
- data/web/views/scheduled_job_info.erb +8 -0
- 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
|
+
|