rq-ruby1.8 3.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/Gemfile +22 -0
  2. data/Gemfile.lock +22 -0
  3. data/INSTALL +166 -0
  4. data/LICENSE +10 -0
  5. data/Makefile +6 -0
  6. data/README +1183 -0
  7. data/Rakefile +37 -0
  8. data/TODO +24 -0
  9. data/TUTORIAL +230 -0
  10. data/VERSION +1 -0
  11. data/bin/rq +902 -0
  12. data/bin/rqmailer +865 -0
  13. data/example/a.rb +7 -0
  14. data/extconf.rb +198 -0
  15. data/gemspec.rb +40 -0
  16. data/install.rb +210 -0
  17. data/lib/rq.rb +155 -0
  18. data/lib/rq/arrayfields.rb +371 -0
  19. data/lib/rq/backer.rb +31 -0
  20. data/lib/rq/configfile.rb +82 -0
  21. data/lib/rq/configurator.rb +40 -0
  22. data/lib/rq/creator.rb +54 -0
  23. data/lib/rq/cron.rb +144 -0
  24. data/lib/rq/defaultconfig.txt +5 -0
  25. data/lib/rq/deleter.rb +51 -0
  26. data/lib/rq/executor.rb +40 -0
  27. data/lib/rq/feeder.rb +527 -0
  28. data/lib/rq/ioviewer.rb +48 -0
  29. data/lib/rq/job.rb +51 -0
  30. data/lib/rq/jobqueue.rb +947 -0
  31. data/lib/rq/jobrunner.rb +110 -0
  32. data/lib/rq/jobrunnerdaemon.rb +193 -0
  33. data/lib/rq/lister.rb +47 -0
  34. data/lib/rq/locker.rb +43 -0
  35. data/lib/rq/lockfile.rb +564 -0
  36. data/lib/rq/logging.rb +124 -0
  37. data/lib/rq/mainhelper.rb +189 -0
  38. data/lib/rq/orderedautohash.rb +39 -0
  39. data/lib/rq/orderedhash.rb +240 -0
  40. data/lib/rq/qdb.rb +733 -0
  41. data/lib/rq/querier.rb +98 -0
  42. data/lib/rq/rails.rb +80 -0
  43. data/lib/rq/recoverer.rb +28 -0
  44. data/lib/rq/refresher.rb +80 -0
  45. data/lib/rq/relayer.rb +283 -0
  46. data/lib/rq/resource.rb +22 -0
  47. data/lib/rq/resourcemanager.rb +40 -0
  48. data/lib/rq/resubmitter.rb +100 -0
  49. data/lib/rq/rotater.rb +98 -0
  50. data/lib/rq/sleepcycle.rb +46 -0
  51. data/lib/rq/snapshotter.rb +40 -0
  52. data/lib/rq/sqlite.rb +286 -0
  53. data/lib/rq/statuslister.rb +48 -0
  54. data/lib/rq/submitter.rb +113 -0
  55. data/lib/rq/toucher.rb +182 -0
  56. data/lib/rq/updater.rb +94 -0
  57. data/lib/rq/usage.rb +1222 -0
  58. data/lib/rq/util.rb +304 -0
  59. data/rdoc.sh +17 -0
  60. data/rq-ruby1.8.gemspec +120 -0
  61. data/test/.gitignore +1 -0
  62. data/test/test_rq.rb +145 -0
  63. data/white_box/crontab +2 -0
  64. data/white_box/joblist +8 -0
  65. data/white_box/killrq +18 -0
  66. data/white_box/rq_killer +27 -0
  67. metadata +208 -0
@@ -0,0 +1,527 @@
1
+ unless defined? $__rq_feeder__
2
+ module RQ
3
+ #--{{{
4
+ LIBDIR = File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
5
+ defined? LIBDIR
6
+
7
+ require 'fileutils'
8
+
9
+ require LIBDIR + 'mainhelper'
10
+ require LIBDIR + 'job'
11
+ require LIBDIR + 'jobrunner'
12
+ require LIBDIR + 'jobrunnerdaemon'
13
+ require LIBDIR + 'jobqueue'
14
+
15
+
16
+ #
17
+ # TODO - resolve elapsed time bug with throttle/sleep
18
+ #
19
+
20
+ #
21
+ # the Feeder class is responsible for running jobs from a queue - or
22
+ # 'feeding' from that queue. the mode of operation is essentially to run
23
+ # jobs as quickly as possible, return them to the queue, and then to run
24
+ # more jobs if any exist. if no jobs exist the Feeder will periodically
25
+ # poll the queue to see if any new jobs have arrived.
26
+ #
27
+ class Feeder < MainHelper
28
+ #--{{{
29
+ DEFAULT_MIN_SLEEP = 42
30
+ DEFAULT_MAX_SLEEP = 240
31
+ DEFAULT_FEED = 2
32
+
33
+ class << self
34
+ #--{{{
35
+ attr :min_sleep, true
36
+ attr :max_sleep, true
37
+ attr :feed, true
38
+ #--}}}
39
+ end
40
+
41
+ def feed
42
+ #--{{{
43
+ daemon do
44
+ gen_pidfile
45
+ @main.init_logging
46
+ @logger = @main.logger
47
+ set_q
48
+
49
+ @pid = Process::pid
50
+ @cmd = @main.cmd
51
+ @started = Util::timestamp
52
+ @min_sleep = Integer(@options['min_sleep'] || defval('min_sleep'))
53
+ @max_sleep = Integer(@options['max_sleep'] || defval('max_sleep'))
54
+ @max_feed = Integer(@options['max_feed'] || defval('feed'))
55
+ @loops = Integer @options['loops'] rescue nil
56
+ @children = Hash::new
57
+ @jrd = JobRunnerDaemon::daemon @q
58
+
59
+ install_signal_handlers
60
+
61
+ if @daemon and not @quiet
62
+ STDOUT.puts "pid <#{ Process::pid }> started"
63
+ STDOUT.flush
64
+ end
65
+
66
+ install_redirects
67
+
68
+ info{ "** STARTED **" }
69
+ info{ "version <#{ RQ::VERSION }>" }
70
+ info{ "cmd <#{ @cmd }>" }
71
+ info{ "pid <#{ @pid }>" }
72
+ info{ "pidfile <#{ @pidfile.path }>" }
73
+ info{ "jobrunnerdaemon uri <#{ @jrd.uri }> pid <#{ @jrd.pid }>" }
74
+ info{ "qpath <#{ @qpath }>" }
75
+ debug{ "mode <#{ @mode }>" }
76
+ debug{ "max_feed <#{ @max_feed }>" }
77
+ debug{ "min_sleep <#{ @min_sleep }>" }
78
+ debug{ "max_sleep <#{ @max_sleep }>" }
79
+
80
+ transaction do
81
+ fill_morgue
82
+ reap_zombie_ios
83
+ end
84
+
85
+ looping do
86
+ handle_signal if $rq_signaled
87
+ throttle(@min_sleep) do
88
+ start_jobs unless busy?
89
+ if nothing_running?
90
+ relax
91
+ else
92
+ reap_jobs
93
+ end
94
+ end
95
+ end
96
+ end
97
+ #--}}}
98
+ end
99
+ def looping
100
+ #--{{{
101
+ @loops && @loops > 0 ? @loops.times{ yield } : loop{ yield }
102
+ #--}}}
103
+ end
104
+ def daemon
105
+ #--{{{
106
+ if @options['daemon']
107
+ fork do
108
+ Process::setsid
109
+ pid =
110
+ fork do
111
+ Dir::chdir(Util.realpath('~'))
112
+ File::umask 0
113
+ @daemon = true
114
+ yield
115
+ exit EXIT_SUCCESS
116
+ end
117
+ # STDOUT.puts "#{ pid }"
118
+ exit!
119
+ end
120
+ exit!
121
+ else
122
+ @daemon = false
123
+ yield
124
+ exit EXIT_SUCCESS
125
+ end
126
+ #--}}}
127
+ end
128
+ def gen_pidfile name = nil
129
+ #--{{{
130
+ gen_pidfilepath
131
+
132
+ begin
133
+ FileUtils::mkdir_p(File::dirname(@pidfilepath))
134
+ rescue
135
+ nil
136
+ end
137
+
138
+ locked = nil
139
+ no_other_feeder = nil
140
+
141
+ 2.times do
142
+ locked = false
143
+ no_other_feeder = false
144
+
145
+ @pidfile =
146
+ begin
147
+ open @pidfilepath, File::CREAT | File::EXCL | File::RDWR
148
+ rescue
149
+ open @pidfilepath, File::RDWR
150
+ end
151
+
152
+ ret = @pidfile.posixlock(File::LOCK_EX | File::LOCK_NB)
153
+ locked = (ret == 0)
154
+
155
+ begin
156
+ pid = Integer(IO::read(@pidfilepath)) rescue nil
157
+
158
+ unless pid
159
+ no_other_feeder = true
160
+ break
161
+ end
162
+
163
+ if Util::alive?(pid)
164
+ no_other_feeder = Process::pid == pid ? true : false
165
+ #no_other_feeder = false
166
+ #else
167
+ #no_other_feeder = false
168
+ #end
169
+ break
170
+ else
171
+ no_other_feeder = true
172
+ STDERR.puts "WARNING : process <#{ pid }> died holding lock on <#{ @pidfilepath }>"
173
+ STDERR.puts "WARNING : attempting autorecovery!"
174
+ break if locked
175
+ STDERR.puts "WARNING : your NFS locking setup is FUBAR - iptables or firewall issues!"
176
+ STDERR.puts "WARNING : attempting autorecovery!"
177
+ FileUtils::rm_f @pidfilepath
178
+ 4.times{ sleep rand }
179
+ end
180
+
181
+ rescue Exception => e
182
+ STDERR.puts "WARNING : #{ e.message } (#{ e.class })"
183
+ end
184
+ end
185
+
186
+
187
+ unless(locked and no_other_feeder)
188
+ pid = Integer(IO::read(@pidfilepath)) rescue 'UNKNOWN'
189
+ if @options['quiet']
190
+ exit EXIT_FAILURE
191
+ else
192
+ abort "process <#{ pid }> is already feeding from this queue"
193
+ end
194
+ else
195
+ @pidfile.chmod 0600 rescue nil
196
+ @pidfile.rewind
197
+ @pidfile.sync = true
198
+ @pidfile.print Process::pid
199
+ @pidfile.truncate @pidfile.pos
200
+ @pidfile.flush
201
+
202
+ at_exit do
203
+ FileUtils::rm_f @pidfilepath rescue nil
204
+ @pidfile.posixlock File::LOCK_UN rescue nil
205
+ @pidfile.close rescue nil
206
+ end
207
+ end
208
+ #--}}}
209
+ end
210
+ def gen_pidfilepath
211
+ #--{{{
212
+ # @pidfilepath ||= gen_feeder_name
213
+ @pidfilepath ||= File::join(@dot_rq_dir, 'pid')
214
+ #--}}}
215
+ end
216
+ def gen_feeder_name path = nil
217
+ #--{{{
218
+ path ||= (@options['name'] || @qpath)
219
+ path = Util::realpath(path).gsub(%r|/|o, '_')
220
+ #File::join(Util::realpath('~'), ".#{ path }.feeder")
221
+ basename = ".#{ Util::host }_#{ path }.feeder".gsub(%r/_+/,'_')
222
+ dirname = Util::realpath '~'
223
+ File::join dirname, basename
224
+ #--}}}
225
+ end
226
+ def install_signal_handlers
227
+ #--{{{
228
+ if @daemon or ENV['RQ_SIGNALS']
229
+ $rq_signaled = false
230
+ $rq_sighup = false
231
+ $rq_sigterm = false
232
+ $rq_sigint = false
233
+ trap('SIGHUP') do
234
+ $rq_signaled = $rq_sighup = 'SIGHUP'
235
+ if nothing_running?
236
+ warn{ "signal <SIGHUP>" }
237
+ handle_signal
238
+ else
239
+ warn{ "finishing running jobs before handling signal" }
240
+ end
241
+ end
242
+ trap('SIGTERM') do
243
+ $rq_signaled = $rq_sigterm = 'SIGTERM'
244
+ if nothing_running?
245
+ warn{ "signal <SIGTERM>" }
246
+ handle_signal
247
+ else
248
+ warn{ "finishing running jobs before handling signal" }
249
+ end
250
+ end
251
+ trap('SIGINT') do
252
+ $rq_signaled = $rq_sigint = 'SIGINT'
253
+ if nothing_running?
254
+ warn{ "signal <SIGINT>" }
255
+ handle_signal
256
+ else
257
+ warn{ "finishing running jobs before handling signal" }
258
+ end
259
+ end
260
+ @jrd.install_signal_handlers
261
+ else
262
+ %w(SIGHUP SIGTERM SIGINT).each do |sig|
263
+ trap(sig) do
264
+ warn{ "signal <#{ sig }>" }
265
+ warn{ "not cleaning up - only daemon mode cleans up!" }
266
+ exit
267
+ end
268
+ end
269
+ end
270
+ #--}}}
271
+ end
272
+ def install_redirects
273
+ #--{{{
274
+ if @daemon
275
+ open('/dev/null','r+') do |f|
276
+ STDIN.reopen f
277
+ STDOUT.reopen f
278
+ STDERR.reopen f
279
+ end
280
+ end
281
+ #--}}}
282
+ end
283
+ def fill_morgue
284
+ #--{{{
285
+ debug{ "filling morgue..." }
286
+ transaction do
287
+ deadjobs = @q.getdeadjobs @started
288
+ deadjobs.each do |job|
289
+ @q.jobisdead job
290
+ unless job['restartable']
291
+ info{ "burried job <#{ job['jid'] }>" }
292
+ else
293
+ warn{ "dead job <#{ job['jid'] }> will be restarted" }
294
+ end
295
+ end
296
+ end
297
+ debug{ "filled morgue" }
298
+ #--}}}
299
+ end
300
+ def reap_zombie_ios
301
+ #--{{{
302
+ debug{ "reaping zombie ios" }
303
+ begin
304
+ transaction do
305
+ stdin, stdout, stderr, data = @q.stdin, @q.stdout, @q.stderr, @q.data
306
+ jids = @q.execute("select jid from jobs").map{|tuple| Integer tuple.first}
307
+ jids = jids.inject({}){|h,jid| h.update jid => true}
308
+ %w[ stdin stdout stderr data ].each do |d|
309
+ Dir::glob(File::join(@q.send(d), "*")).each do |iof|
310
+ begin
311
+ jid = Integer iof[%r/\d+\s*$/]
312
+ unless jids[jid]
313
+ debug{ "removing zombie io <#{ iof }>" }
314
+ FileUtils::rm_rf iof
315
+ end
316
+ rescue
317
+ next
318
+ end
319
+ end
320
+ end
321
+ end
322
+ rescue Exception => e # because this is a non-essential function
323
+ warn{ e }
324
+ end
325
+ debug{ "reaped" }
326
+ #--}}}
327
+ end
328
+ def handle_signal
329
+ #--{{{
330
+ if $rq_sigterm or $rq_sigint
331
+ reap_jobs(reap_only = true) until nothing_running?
332
+ info{ "** STOPPING **" }
333
+ @jrd.shutdown rescue nil
334
+ @pidfile.posixlock File::LOCK_UN
335
+ exit EXIT_SUCCESS
336
+ end
337
+
338
+ if $rq_sighup
339
+ reap_jobs(reap_only = true) until nothing_running?
340
+ info{ "** RESTARTING **" }
341
+ info{ "** ARGV <#{ @cmd }> **" }
342
+ begin
343
+ @jrd.shutdown rescue nil
344
+ Util::uncache __FILE__
345
+ @pidfile.posixlock File::LOCK_UN
346
+ Util::exec @cmd
347
+ rescue Exception => e
348
+ fatal{"** FAILED TO RESTART! **"}
349
+ fatal{ e }
350
+ exit EXIT_FAILURE
351
+ end
352
+ end
353
+ #--}}}
354
+ end
355
+ def throttle rate = @min_sleep
356
+ #--{{{
357
+ if Numeric === rate and rate > 0
358
+ if defined? @last_throttle_time and @last_throttle_time
359
+ elapsed = Time.now - @last_throttle_time
360
+ timeout = rate - elapsed
361
+ if timeout > 0
362
+ timeout = timeout + rand(rate * 0.10)
363
+ debug{ "throttle rate of <#{ rate }> exceeded - sleeping <#{ timeout }>" }
364
+ sleep timeout
365
+ end
366
+ end
367
+ @last_throttle_time = Time.now
368
+ end
369
+ yield
370
+ #--}}}
371
+ end
372
+ def start_jobs
373
+ #--{{{
374
+ debug{ "starting jobs..." }
375
+ n_started = 0
376
+ transaction do
377
+ until busy?
378
+ break unless((job = @q.getjob))
379
+ start_job job
380
+ n_started += 1
381
+ end
382
+ end
383
+ debug{ "<#{ n_started }> jobs started" }
384
+ n_started
385
+ #--}}}
386
+ end
387
+ def start_job job
388
+ #--{{{
389
+ jid, command = job['jid'], job['command']
390
+
391
+ #
392
+ # we setup state slightly prematurely so jobrunner will have it availible
393
+ #
394
+ job['state'] = 'running'
395
+ job['started'] = Util::timestamp Time::now
396
+ job['runner'] = Util::hostname
397
+
398
+ job['stdout'] = @q.stdout4 jid
399
+ job['stderr'] = @q.stderr4 jid
400
+
401
+ jr = @jrd.runner job
402
+ cid = jr.cid
403
+
404
+ if jr and cid
405
+ jr.run
406
+ job['pid'] = cid
407
+ @children[cid] = job
408
+ @q.jobisrunning job
409
+ info{ "started - jid <#{ job['jid'] }> pid <#{ job['pid'] }> command <#{ job['command'] }>" }
410
+ else
411
+ error{ "not started - jid <#{ job['jid'] }> command <#{ job['command'] }>" }
412
+ end
413
+
414
+ cid
415
+ #--}}}
416
+ end
417
+ def nothing_running?
418
+ #--{{{
419
+ @children.size == 0
420
+ #--}}}
421
+ end
422
+ def reap_jobs reap_only = false, blocking = true
423
+ #--{{{
424
+ debug{ "reaping jobs..." }
425
+ reaped = []
426
+
427
+ cid = status = nil
428
+
429
+ if blocking
430
+ if busy? or reap_only
431
+ cid, status = @jrd.waitpid2 -1, Process::WUNTRACED
432
+ else
433
+ loop do
434
+ debug{ "not busy - busywait loop" }
435
+ cid, status = @jrd.waitpid2 -1, Process::WNOHANG | Process::WUNTRACED
436
+ break if cid
437
+ start_jobs unless $rq_signaled
438
+ break if busy?
439
+ cid, status = @jrd.waitpid2 -1, Process::WNOHANG | Process::WUNTRACED
440
+ break if cid
441
+ sleep 4.2
442
+ end
443
+ cid, status = @jrd.waitpid2 -1, Process::WUNTRACED unless cid
444
+ end
445
+ else
446
+ cid, status = @jrd.waitpid2 -1, Process::WNOHANG | Process::WUNTRACED
447
+ end
448
+
449
+ if cid and status
450
+ job = @children[cid]
451
+ finish_job job, status
452
+
453
+ transaction do
454
+ loopno = 0
455
+ loop do
456
+ @q.jobisdone job
457
+ @children.delete cid
458
+ reaped << cid
459
+
460
+ start_jobs unless reap_only or $rq_signaled
461
+
462
+ if @children.size == 0 or loopno > 42
463
+ sleep 8 if loopno > 42 # wow - we are CRANKING through jobs so BACK OFF!!
464
+ break
465
+ else
466
+ sleep 0.1
467
+ cid, status = @jrd.waitpid2 -1, Process::WNOHANG | Process::WUNTRACED
468
+ break unless cid and status
469
+ job = @children[cid]
470
+ finish_job job, status
471
+ end
472
+ loopno += 1
473
+ end
474
+ end
475
+ end
476
+ debug{ "<#{ reaped.size }> jobs reaped" }
477
+ reaped
478
+ #--}}}
479
+ end
480
+ def finish_job job, status
481
+ #--{{{
482
+ job['finished'] = Util::timestamp(Time::now)
483
+ job['elapsed'] = Util::stamptime(job['finished']) - Util::stamptime(job['started'])
484
+ t = status.exitstatus rescue nil
485
+ job['exit_status'] = t
486
+ job['state'] = 'finished'
487
+ if t and t == 0
488
+ info{ "finished - jid <#{ job['jid'] }> pid <#{ job['pid'] }> exit_status <#{ job['exit_status'] }>" }
489
+ else
490
+ warn{ "finished - jid <#{ job['jid'] }> pid <#{ job['pid'] }> exit_status <#{ job['exit_status'] }>" }
491
+ end
492
+ #--}}}
493
+ end
494
+ def transaction
495
+ #--{{{
496
+ ret = nil
497
+ if @in_transaction
498
+ ret = yield
499
+ else
500
+ begin
501
+ @in_transaction = true
502
+ @q.transaction{ ret = yield }
503
+ ensure
504
+ @in_transaction = false
505
+ end
506
+ end
507
+ ret
508
+ #--}}}
509
+ end
510
+ def busy?
511
+ #--{{{
512
+ @children.size >= @max_feed
513
+ #--}}}
514
+ end
515
+ def relax
516
+ #--{{{
517
+ seconds = rand(@max_sleep - @min_sleep + 1) + @min_sleep
518
+ debug{ "relaxing <#{ seconds }>" }
519
+ sleep seconds
520
+ #--}}}
521
+ end
522
+ #--}}}
523
+ end # class Feeder
524
+ #--}}}
525
+ end # module RQ
526
+ $__rq_feeder__ = __FILE__
527
+ end