sweatshop 1.4.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.
- data/History.txt +6 -0
- data/LICENSE +20 -0
- data/README.markdown +103 -0
- data/Rakefile +43 -0
- data/VERSION.yml +4 -0
- data/bin/sweatd +3 -0
- data/config/defaults.yml +8 -0
- data/config/sweatshop.yml +15 -0
- data/install.rb +20 -0
- data/lib/message_queue/base.rb +17 -0
- data/lib/message_queue/kestrel.rb +32 -0
- data/lib/message_queue/rabbit.rb +108 -0
- data/lib/sweat_shop.rb +179 -0
- data/lib/sweat_shop/daemoned.rb +405 -0
- data/lib/sweat_shop/metaid.rb +5 -0
- data/lib/sweat_shop/sweatd.rb +76 -0
- data/lib/sweat_shop/worker.rb +162 -0
- data/script/initd.sh +108 -0
- data/script/kestrel +93 -0
- data/script/kestrel.sh +93 -0
- data/script/sweatshop +17 -0
- data/test/hello_worker.rb +13 -0
- data/test/test_functional_worker.rb +72 -0
- data/test/test_helper.rb +4 -0
- data/test/test_sweatshop.rb +78 -0
- metadata +91 -0
@@ -0,0 +1,405 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module Daemoned
|
5
|
+
class DieTime < StandardError; end
|
6
|
+
class TimeoutError < StandardError; end
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend ClassMethods
|
10
|
+
base.initialize_options
|
11
|
+
end
|
12
|
+
|
13
|
+
class Config
|
14
|
+
METHODS = [:script_path]
|
15
|
+
CONFIG = {}
|
16
|
+
def method_missing(name, *args)
|
17
|
+
name = name.to_s.upcase.to_sym
|
18
|
+
if name.to_s =~ /^(.*)=$/
|
19
|
+
name = $1.to_sym
|
20
|
+
CONFIG[name] = args.first
|
21
|
+
else
|
22
|
+
CONFIG[name]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
|
29
|
+
def initialize_options
|
30
|
+
@@config = Config.new
|
31
|
+
@@config.script_path = File.expand_path(File.dirname($0))
|
32
|
+
$0 = script_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_options
|
36
|
+
opts = OptionParser.new do |opt|
|
37
|
+
opt.banner = "Usage: #{script_name} [options] [start|stop]"
|
38
|
+
|
39
|
+
opt.on_tail('-h', '--help', 'Show this message') do
|
40
|
+
puts opt
|
41
|
+
exit(1)
|
42
|
+
end
|
43
|
+
|
44
|
+
opt.on('--loop-every=SECONDS', 'How long to sleep between each loop') do |value|
|
45
|
+
options[:loop_every] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
opt.on('-t', '--ontop', 'Stay on top (does not daemonize)') do
|
49
|
+
options[:ontop] = true
|
50
|
+
end
|
51
|
+
|
52
|
+
opt.on('--instances=NUM', 'Allow multiple instances to run simultaneously? 0 for infinite. default: 1') do |value|
|
53
|
+
self.instances = value.to_i
|
54
|
+
end
|
55
|
+
|
56
|
+
opt.on('--log-file=LOGFILE', 'Logfile to log to') do |value|
|
57
|
+
options[:log_file] = File.expand_path(value)
|
58
|
+
end
|
59
|
+
|
60
|
+
opt.on('--pid-file=PIDFILE', 'Location of pidfile') do |value|
|
61
|
+
options[:pid_file] = File.expand_path(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
opt.on('--no-log-prefix', 'Do not prefix PID and date/time in log file.') do
|
65
|
+
options[:log_prefix] = false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
extra_args.each do |arg|
|
70
|
+
opts.on(*arg.first) do |value|
|
71
|
+
arg.last.call(value) if arg.last
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.parse!
|
76
|
+
|
77
|
+
if ARGV.include?('stop')
|
78
|
+
stop
|
79
|
+
elsif ARGV.include?('reload')
|
80
|
+
kill('HUP')
|
81
|
+
exit
|
82
|
+
elsif not ARGV.include?('start') and not ontop?
|
83
|
+
puts opts.help
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def arg(*args, &block)
|
88
|
+
self.extra_args << [args, block]
|
89
|
+
end
|
90
|
+
|
91
|
+
def extra_args
|
92
|
+
@extra_args ||= []
|
93
|
+
end
|
94
|
+
|
95
|
+
def callbacks
|
96
|
+
@callbacks ||= {}
|
97
|
+
end
|
98
|
+
|
99
|
+
def options
|
100
|
+
@options ||= {}
|
101
|
+
end
|
102
|
+
|
103
|
+
def options=(options)
|
104
|
+
@options = options
|
105
|
+
end
|
106
|
+
|
107
|
+
def config
|
108
|
+
yield @@config
|
109
|
+
end
|
110
|
+
|
111
|
+
def before(&block)
|
112
|
+
callbacks[:before] = block
|
113
|
+
end
|
114
|
+
|
115
|
+
def after(&block)
|
116
|
+
callbacks[:after] = block
|
117
|
+
end
|
118
|
+
|
119
|
+
def sig(*signals, &block)
|
120
|
+
signals.each do |s|
|
121
|
+
callbacks["sig_#{s}".to_sym] = block
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def die_if(method=nil,&block)
|
126
|
+
options[:die_if] = method || block
|
127
|
+
end
|
128
|
+
|
129
|
+
def exit_if(method=nil,&block)
|
130
|
+
options[:exit_if] = method || block
|
131
|
+
end
|
132
|
+
|
133
|
+
def callback!(callback)
|
134
|
+
callbacks[callback].call if callbacks[callback]
|
135
|
+
end
|
136
|
+
|
137
|
+
# options may include:
|
138
|
+
#
|
139
|
+
# <tt>:loop_every</tt> Fixnum (DEFAULT 0)
|
140
|
+
# How many seconds to sleep between calls to your block
|
141
|
+
#
|
142
|
+
# <tt>:timeout</tt> Fixnum (DEFAULT 0)
|
143
|
+
# Timeout in if block does not execute withing passed number of seconds
|
144
|
+
#
|
145
|
+
# <tt>:kill_timeout</tt> Fixnum (DEFAULT 120)
|
146
|
+
# Wait number of seconds before using kill -9 on daemon
|
147
|
+
#
|
148
|
+
# <tt>:die_on_timeout</tt> BOOL (DEFAULT False)
|
149
|
+
# Should the daemon continue running if a block times out, or just run the block again
|
150
|
+
#
|
151
|
+
# <tt>:ontop</tt> BOOL (DEFAULT False)
|
152
|
+
# Do not daemonize. Run in current process
|
153
|
+
#
|
154
|
+
# <tt>:before</tt> BLOCK
|
155
|
+
# Run this block after daemonizing but before begining the daemonize loop.
|
156
|
+
# You can also define the before block by putting a before do/end block in your class.
|
157
|
+
#
|
158
|
+
# <tt>:after</tt> BLOCK
|
159
|
+
# Run this block before program exists.
|
160
|
+
# You can also define the after block by putting an after do/end block in your class.
|
161
|
+
#
|
162
|
+
# <tt>:die_if</tt> BLOCK
|
163
|
+
# Run this check after each iteration of the loop. If the block returns true, throw a DieTime exception and exit
|
164
|
+
# You can also define the after block by putting an die_if do/end block in your class.
|
165
|
+
#
|
166
|
+
# <tt>:exit_if</tt> BLOCK
|
167
|
+
# Run this check after each iteration of the loop. If the block returns true, exit gracefully
|
168
|
+
# You can also define the after block by putting an exit_if do/end block in your class.
|
169
|
+
#
|
170
|
+
# <tt>:log_prefix</tt> BOOL (DEFAULT true)
|
171
|
+
# Prefix log file entries with PID and timestamp
|
172
|
+
def daemonize(opts={}, &block)
|
173
|
+
self.options = opts
|
174
|
+
parse_options
|
175
|
+
return unless ok_to_start?
|
176
|
+
|
177
|
+
puts "Starting #{script_name}..."
|
178
|
+
puts "Logging to: #{log_file}" unless ontop?
|
179
|
+
|
180
|
+
unless ontop?
|
181
|
+
safefork do
|
182
|
+
open(pid_file, 'w'){|f| f << Process.pid }
|
183
|
+
at_exit { remove_pid! }
|
184
|
+
|
185
|
+
trap('TERM') { callback!(:sig_term) }
|
186
|
+
trap('INT') { callback!(:sig_int) ; Process.kill('TERM', $$) }
|
187
|
+
trap('HUP') { callback!(:sig_hup) }
|
188
|
+
|
189
|
+
sess_id = Process.setsid
|
190
|
+
reopen_filehandes
|
191
|
+
|
192
|
+
begin
|
193
|
+
at_exit { callback!(:after) }
|
194
|
+
callback!(:before)
|
195
|
+
run_block(&block)
|
196
|
+
rescue SystemExit
|
197
|
+
rescue Exception => e
|
198
|
+
$stdout.puts "Something bad happened #{e.inspect} #{e.backtrace.join("\n")}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
else
|
202
|
+
begin
|
203
|
+
callback!(:before)
|
204
|
+
run_block(&block)
|
205
|
+
rescue SystemExit, Interrupt
|
206
|
+
callback!(:after)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def run_block(&block)
|
214
|
+
loop do
|
215
|
+
if options[:timeout]
|
216
|
+
begin
|
217
|
+
Timeout::timeout(options[:timeout].to_i) do
|
218
|
+
block.call if block
|
219
|
+
end
|
220
|
+
rescue Timeout::Error => e
|
221
|
+
if options[:die_on_timeout]
|
222
|
+
raise TimeoutError.new("#{self} timed out after #{options[:timeout]} seconds while executing block in loop")
|
223
|
+
else
|
224
|
+
$stderr.puts "#{self} timed out after #{options[:timeout]} seconds while executing block in loop #{e.backtrace.join("\n")}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
else
|
228
|
+
block.call if block
|
229
|
+
end
|
230
|
+
|
231
|
+
if options[:loop_every]
|
232
|
+
sleep options[:loop_every].to_i
|
233
|
+
elsif not block
|
234
|
+
sleep 0.1
|
235
|
+
end
|
236
|
+
|
237
|
+
break if should_exit?
|
238
|
+
raise DieTime.new('Die if conditions were met!') if should_die?
|
239
|
+
end
|
240
|
+
exit(0)
|
241
|
+
end
|
242
|
+
|
243
|
+
def should_die?
|
244
|
+
die_if = options[:die_if]
|
245
|
+
if die_if
|
246
|
+
if die_if.is_a?(Symbol) or die_if.is_a?(String)
|
247
|
+
self.send(die_if)
|
248
|
+
elsif die_if.is_a?(Proc)
|
249
|
+
die_if.call
|
250
|
+
end
|
251
|
+
else
|
252
|
+
false
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def should_exit?
|
257
|
+
exit_if = options[:exit_if]
|
258
|
+
if exit_if
|
259
|
+
if exit_if.is_a?(Symbol) or exit_if.is_a?(String)
|
260
|
+
self.send(exit_if.to_sym)
|
261
|
+
elsif exit_if.is_a?(Proc)
|
262
|
+
exit_if.call
|
263
|
+
end
|
264
|
+
else
|
265
|
+
false
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def ok_to_start?
|
270
|
+
return true if pid.nil?
|
271
|
+
|
272
|
+
if process_alive?
|
273
|
+
$stderr.puts "#{script_name} is already running"
|
274
|
+
return false
|
275
|
+
else
|
276
|
+
$stderr.puts "Removing stale pid: #{pid}..."
|
277
|
+
end
|
278
|
+
|
279
|
+
true
|
280
|
+
end
|
281
|
+
|
282
|
+
def stop
|
283
|
+
puts "Stopping #{script_name}..."
|
284
|
+
kill
|
285
|
+
exit
|
286
|
+
end
|
287
|
+
|
288
|
+
def kill(signal = 'TERM')
|
289
|
+
if pid.nil?
|
290
|
+
$stderr.puts "#{script_name} doesn't appear to be running"
|
291
|
+
exit(1)
|
292
|
+
end
|
293
|
+
$stdout.puts("Sending pid #{pid} signal #{signal}...")
|
294
|
+
begin
|
295
|
+
Process.kill(signal, pid)
|
296
|
+
return if signal == 'HUP'
|
297
|
+
if pid_running?(options[:kill_timeout] || 120)
|
298
|
+
$stdout.puts("Using kill -9 #{pid}")
|
299
|
+
Process.kill(9, pid)
|
300
|
+
else
|
301
|
+
$stdout.puts("Process #{pid} stopped")
|
302
|
+
end
|
303
|
+
rescue Errno::ESRCH
|
304
|
+
$stdout.puts("Couldn't #{signal} #{pid} as it wasn't running")
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def pid_running?(time_to_wait = 0)
|
309
|
+
times_to_check = 1
|
310
|
+
if time_to_wait > 0.5
|
311
|
+
times_to_check = (time_to_wait / 0.5).to_i
|
312
|
+
end
|
313
|
+
times_to_check.times do
|
314
|
+
return false unless process_alive?
|
315
|
+
sleep 0.5
|
316
|
+
end
|
317
|
+
true
|
318
|
+
end
|
319
|
+
|
320
|
+
def safefork(&block)
|
321
|
+
fork_tries ||= 0
|
322
|
+
fork(&block)
|
323
|
+
rescue Errno::EWOULDBLOCK
|
324
|
+
raise if fork_tries >= 20
|
325
|
+
fork_tries += 1
|
326
|
+
sleep 5
|
327
|
+
retry
|
328
|
+
end
|
329
|
+
|
330
|
+
def process_alive?
|
331
|
+
Process.kill(0, pid)
|
332
|
+
true
|
333
|
+
rescue Errno::ESRCH => e
|
334
|
+
false
|
335
|
+
end
|
336
|
+
|
337
|
+
LOG_FORMAT = '%-6d %-19s %s'
|
338
|
+
TIME_FORMAT = '%Y/%m/%d %H:%M:%S'
|
339
|
+
def reopen_filehandes
|
340
|
+
STDIN.reopen('/dev/null')
|
341
|
+
STDOUT.reopen(log_file, 'a')
|
342
|
+
STDOUT.sync = true
|
343
|
+
STDERR.reopen(STDOUT)
|
344
|
+
if log_prefix?
|
345
|
+
def STDOUT.write(string)
|
346
|
+
if @no_prefix
|
347
|
+
@no_prefix = false if string[-1, 1] == "\n"
|
348
|
+
else
|
349
|
+
string = LOG_FORMAT % [$$, Time.now.strftime(TIME_FORMAT), string]
|
350
|
+
@no_prefix = true
|
351
|
+
end
|
352
|
+
super(string)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def remove_pid!
|
358
|
+
if File.file?(pid_file) and File.read(pid_file).to_i == $$
|
359
|
+
File.unlink(pid_file)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def ontop?
|
364
|
+
options[:ontop]
|
365
|
+
end
|
366
|
+
|
367
|
+
def log_prefix?
|
368
|
+
options[:log_prefix] || true
|
369
|
+
end
|
370
|
+
|
371
|
+
LOG_PATHS = ['log/', 'logs/', '../log/', '../logs/', '../../log', '../../logs', '.']
|
372
|
+
LOG_PATHS.unshift("#{RAILS_ROOT}/log") if defined?(RAILS_ROOT)
|
373
|
+
def log_dir
|
374
|
+
options[:log_dir] ||= begin
|
375
|
+
LOG_PATHS.detect do |path|
|
376
|
+
File.exists?(File.expand_path(path))
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def log_file
|
382
|
+
options[:log_file] ||= File.expand_path("#{log_dir}/#{script_name}.log")
|
383
|
+
end
|
384
|
+
|
385
|
+
def pid_dir
|
386
|
+
options[:pid_dir] ||= log_dir
|
387
|
+
end
|
388
|
+
|
389
|
+
def pid_file
|
390
|
+
options[:pid_file] ||= File.expand_path("#{pid_dir}/#{script_name}.pid")
|
391
|
+
end
|
392
|
+
|
393
|
+
def pid
|
394
|
+
@pid ||= File.file?(pid_file) ? File.read(pid_file).to_i : nil
|
395
|
+
end
|
396
|
+
|
397
|
+
def script_name
|
398
|
+
@script_name ||= File.basename($0).gsub('.rb', '')
|
399
|
+
end
|
400
|
+
|
401
|
+
def script_name=(script_name)
|
402
|
+
@script_name = script_name
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/daemoned'
|
2
|
+
|
3
|
+
module SweatShop
|
4
|
+
class Sweatd
|
5
|
+
include Daemoned
|
6
|
+
queues = []
|
7
|
+
groups = []
|
8
|
+
rails_root = nil
|
9
|
+
start_cmd = "ruby #{__FILE__} start #{ARGV.reject{|a| a == 'start'}.join(' ')}"
|
10
|
+
|
11
|
+
arg '--workers=Worker,Worker', 'Workers to service (Default is all)' do |value|
|
12
|
+
queues = value.split(',')
|
13
|
+
end
|
14
|
+
|
15
|
+
arg '--groups=GROUP,GROUP', 'Groups of queues to service' do |value|
|
16
|
+
groups = value.split(',').collect{|g| g.to_sym}
|
17
|
+
end
|
18
|
+
|
19
|
+
arg '--worker-file=WORKERFILE', 'Worker file to load' do |value|
|
20
|
+
require value
|
21
|
+
end
|
22
|
+
|
23
|
+
arg '--worker-dir=WORKERDIR', 'Directory containing workers' do |value|
|
24
|
+
Dir.glob(value + '*.rb').each{|worker| require worker}
|
25
|
+
end
|
26
|
+
|
27
|
+
arg '--rails=DIR', 'Pass in RAILS_ROOT to run this daemon in a rails environment' do |value|
|
28
|
+
rails_root = value
|
29
|
+
end
|
30
|
+
|
31
|
+
sig(:term, :int) do
|
32
|
+
puts "Shutting down sweatd..."
|
33
|
+
SweatShop.stop
|
34
|
+
end
|
35
|
+
|
36
|
+
sig(:hup) do
|
37
|
+
puts "Received HUP"
|
38
|
+
SweatShop.stop
|
39
|
+
remove_pid!
|
40
|
+
puts "Restarting sweatd with #{start_cmd}..."
|
41
|
+
`#{start_cmd}`
|
42
|
+
end
|
43
|
+
|
44
|
+
before do
|
45
|
+
if rails_root
|
46
|
+
puts "Loading Rails..."
|
47
|
+
require rails_root + '/config/environment'
|
48
|
+
end
|
49
|
+
require File.dirname(__FILE__) + '/../sweat_shop'
|
50
|
+
end
|
51
|
+
|
52
|
+
daemonize(:kill_timeout => 20) do
|
53
|
+
workers = []
|
54
|
+
|
55
|
+
if groups.any?
|
56
|
+
workers += SweatShop.workers_in_group(groups)
|
57
|
+
end
|
58
|
+
|
59
|
+
if queues.any?
|
60
|
+
workers += queues.collect{|q| Object.module_eval(q)}
|
61
|
+
end
|
62
|
+
|
63
|
+
if workers.any?
|
64
|
+
worker_str = workers.join(',')
|
65
|
+
puts "Starting #{worker_str}..."
|
66
|
+
$0 = "Sweatd: #{worker_str}"
|
67
|
+
SweatShop.do_tasks(workers)
|
68
|
+
else
|
69
|
+
puts "Starting all workers..."
|
70
|
+
$0 = 'Sweatd: all'
|
71
|
+
SweatShop.do_all_tasks
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|