sweatshop 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ def metaclass; class << self; self; end; end
2
+ def meta_eval(&blk); metaclass.instance_eval(&blk); end
3
+ def meta_def(name, &blk)
4
+ meta_eval { define_method name, &blk }
5
+ 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