rq-ruby1.8 3.4.3

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