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.
@@ -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