bjj 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/bin/bj ADDED
@@ -0,0 +1,679 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require "bj"
4
+ require "main"
5
+
6
+ Main {
7
+ usage["description"] = <<-txt
8
+ ________________________________
9
+ Overview
10
+ --------------------------------
11
+
12
+ Backgroundjob (Bj) is a brain dead simple zero admin background priority queue
13
+ for Rails. Bj is robust, platform independent (including windows), and
14
+ supports internal or external manangement of the background runner process.
15
+
16
+ Jobs can be submitted to the queue directly using the api or from the command
17
+ line using the ./script/bj:
18
+
19
+ api:
20
+ Bj.submit 'cat /etc/password'
21
+
22
+ command line:
23
+ bj submit cat /etc/password
24
+
25
+ Bj's priority queue lives in the database and is therefore durable - your jobs
26
+ will live across an app crash or machine reboot. The job management is
27
+ comprehensive capturing stdout, stderr, exit_status, and temporal statistics
28
+ about each job:
29
+
30
+ jobs = Bj.submit array_of_commands, :priority => 42
31
+
32
+ ...
33
+
34
+ jobs.each do |job|
35
+ if job.finished?
36
+ p job.stdout
37
+ p job.stderr
38
+ p job.exit_status
39
+ p job.started_at
40
+ p job.finished_at
41
+ end
42
+ end
43
+
44
+ In addition the background runner process logs all commands run and their
45
+ exit_status to a log named using the following convention:
46
+
47
+ rails_root/log/bj.\#{ HOSTNAME }.\#{ RAILS_ENV }.log
48
+
49
+ Bj allows you to submit jobs to multiple databases; for instance, if your
50
+ application is running in development mode you may do:
51
+
52
+ Bj.in :production do
53
+ Bj.submit 'my_job.exe'
54
+ end
55
+
56
+ Bj manages the ever growing list of jobs ran by automatically archiving them
57
+ into another table (by default jobs > 24 hrs old are archived) to prevent the
58
+ jobs table from becoming bloated and huge.
59
+
60
+ All Bj's tables are namespaced and accessible via the Bj module:
61
+
62
+ Bj.table.job.find(:all) # jobs table
63
+ Bj.table.job_archive.find(:all) # archived jobs
64
+ Bj.table.config.find(:all) # configuration and runner state
65
+
66
+ Bj always arranges for submitted jobs to run with a current working directory
67
+ of RAILS_ROOT and with the correct RAILS_ENV setting. For example, if you
68
+ submit a job in production it will have ENV['RAILS_ENV'] == 'production'.
69
+
70
+ When Bj manages the background runner it will never outlive the rails
71
+ application - it is started and stopped on demand as the rails app is started
72
+ and stopped. This is also true for ./script/console - Bj will automatically
73
+ fire off the background runner to process jobs submitted using the console.
74
+
75
+ Bj ensures that only one background process is running for your application -
76
+ firing up three mongrels or fcgi processes will result in only one background
77
+ runner being started. Note that the number of background runners does not
78
+ determine throughput - that is determined primarily by the nature of the jobs
79
+ themselves and how much work they perform per process.
80
+
81
+
82
+ ________________________________
83
+ Architecture
84
+ --------------------------------
85
+
86
+ If one ignores platform specific details the design of Bj is quite simple: the
87
+ main Rails application submits jobs to table, stored in the database. The act
88
+ of submitting triggers exactly one of two things to occur:
89
+
90
+ 1) a new long running background runner to be started
91
+
92
+ 2) an existing background runner to be signaled
93
+
94
+ The background runner refuses to run two copies of itself for a given
95
+ hostname/rails_env combination. For example you may only have one background
96
+ runner processing jobs on localhost in development mode.
97
+
98
+ The background runner, under normal circumstances, is managed by Bj itself -
99
+ you need do nothing to start, monitor, or stop it - it just works. However,
100
+ some people will prefer manage their own background process, see 'External
101
+ Runner' section below for more on this.
102
+
103
+ The runner simply processes each job in a highest priority oldest-in fashion,
104
+ capturing stdout, stderr, exit_status, etc. and storing the information back
105
+ into the database while logging it's actions. When there are no jobs to run
106
+ the runner goes to sleep for 42 seconds; however this sleep is interuptable,
107
+ such as when the runner is signaled that a new job has been submitted so,
108
+ under normal circumstances there will be zero lag between job submission and
109
+ job running for an empty queue.
110
+
111
+
112
+ ________________________________
113
+ External Runner / Clustering
114
+ --------------------------------
115
+
116
+ For the paranoid control freaks out there (myself included) it is quite
117
+ possible to manage and monitor the runner process manually. This can be
118
+ desirable in production setups where monitoring software may kill leaking
119
+ rails apps periodically.
120
+
121
+ Recalling that Bj will only allow one copy of itself to process jobs per
122
+ hostname/rails_env pair we can simply do something like this in cron
123
+
124
+ cmd = bj run --forever \\
125
+ --rails_env=development \\
126
+ --rails_root=/Users/ahoward/rails_root
127
+
128
+ */15 * * * * $cmd
129
+
130
+ this will simply attempt the start the background runner every 15 minutes if,
131
+ and only if, it's not *already* running.
132
+
133
+ In addtion to this you'll want to tell Bj not to manage the runner itself
134
+ using
135
+
136
+ Bj.config["production.no_tickle"] = true
137
+
138
+ Note that, for clusting setups, it's as simple as adding a crontab and config
139
+ entry like this for each host. Because Bj throttles background runners per
140
+ hostname this will allow one runner per hostname - making it quite simple to
141
+ cluster three nodes behind a besieged rails application.
142
+
143
+
144
+ ________________________________
145
+ Designing Jobs
146
+ --------------------------------
147
+
148
+ Bj runs it's jobs as command line applications. It ensures that all jobs run
149
+ in RAILS_ROOT so it's quite natural to apply a pattern such as
150
+
151
+ mkdir ./jobs
152
+ edit ./jobs/background_job_to_run
153
+
154
+ ...
155
+
156
+ Bj.submit "./jobs/background_job_to_run"
157
+
158
+ If you need to run you jobs under an entire rails environment you'll need to
159
+ do this:
160
+
161
+ Bj.submit "./script/runner ./jobs/background_job_to_run"
162
+
163
+ Obviously "./script/runner" loads the rails environment for you. It's worth
164
+ noting that this happens for each job and that this is by design: the reason
165
+ is that most rails applications leak memory like a sieve so, if one were to
166
+ spawn a long running process that used the application code base you'd have a
167
+ lovely doubling of memory usage on you app servers. Although loading the
168
+ rails environment for each background job requires a little time, a little
169
+ cpu, and a lot less memory. A future version of Bj will provide a way to load
170
+ the rails environment once and to process background jobs in this environment,
171
+ but anyone wanting to use this in production will be required to duct tape
172
+ their entire chest and have a team of oxen rip off the tape without screaming
173
+ to prove steelyness of spirit and profound understanding of the other side.
174
+
175
+ Don't forget that you can submit jobs with command line arguments:
176
+
177
+ Bj.submit "./jobs/a.rb 1 foobar --force"
178
+
179
+ and that you can do powerful things by passing stdin to a job that powers
180
+ through a list of work. For instance, assume a "./jobs/bulkmail" job
181
+ resembling
182
+
183
+ STDIN.each do |line|
184
+ address = line.strip
185
+ mail_message_to address
186
+ end
187
+
188
+ then you could
189
+
190
+ stdin = [
191
+ "foo@bar.com",
192
+ "bar@foo.com",
193
+ "ara.t.howard@codeforpeople.com",
194
+ ]
195
+
196
+ Bj.submit "./script/runner ./jobs/bulkmail", :stdin => stdin
197
+
198
+ and all those emails would be sent in the background.
199
+
200
+ Bj's power is putting jobs in the background in a simple and robust fashion.
201
+ It's your task to build intelligent jobs that leverage batch processing, and
202
+ other, possibilities. The upshot of building tasks this way is that they are
203
+ quite easy to test before submitting them from inside your application.
204
+
205
+
206
+ ________________________________
207
+ Install
208
+ --------------------------------
209
+
210
+ Bj can be installed two ways: as a plugin or via rubygems
211
+
212
+ plugin:
213
+ 1) ./script/plugin install http://codeforpeople.rubyforge.org/svn/rails/plugins/bj
214
+ 2) ./script/bj setup
215
+
216
+ gem:
217
+ 1) $sudo gem install bj
218
+ 2) add "require 'bj'" to config/environment.rb
219
+ 3) bj setup
220
+
221
+ ________________________________
222
+ Api
223
+ --------------------------------
224
+
225
+ submit jobs for background processing. 'jobs' can be a string or array of
226
+ strings. options are applied to each job in the 'jobs', and the list of
227
+ submitted jobs is always returned. options (string or symbol) can be
228
+
229
+ :rails_env => production|development|key_in_database_yml
230
+ when given this keyword causes bj to submit jobs to the
231
+ specified database. default is RAILS_ENV.
232
+
233
+ :priority => any number, including negative ones. default is zero.
234
+
235
+ :tag => a tag added to the job. simply makes searching easier.
236
+
237
+ :env => a hash specifying any additional environment vars the background
238
+ process should have.
239
+
240
+ :stdin => any stdin the background process should have. must respond_to
241
+ to_s
242
+
243
+ eg:
244
+
245
+ jobs = Bj.submit 'echo foobar', :tag => 'simple job'
246
+
247
+ jobs = Bj.submit '/bin/cat', :stdin => 'in the hat', :priority => 42
248
+
249
+ jobs = Bj.submit './script/runner ./scripts/a.rb', :rails_env => 'production'
250
+
251
+ jobs = Bj.submit './script/runner /dev/stdin',
252
+ :stdin => 'p RAILS_ENV',
253
+ :tag => 'dynamic ruby code'
254
+
255
+ jobs Bj.submit array_of_commands, :priority => 451
256
+
257
+ when jobs are run, they are run in RAILS_ROOT. various attributes are
258
+ available *only* once the job has finished. you can check whether or not a
259
+ job is finished by using the #finished method, which simple does a reload and
260
+ checks to see if the exit_status is non-nil.
261
+
262
+ eg:
263
+
264
+ jobs = Bj.submit list_of_jobs, :tag => 'important'
265
+ ...
266
+
267
+ jobs.each do |job|
268
+ if job.finished?
269
+ p job.exit_status
270
+ p job.stdout
271
+ p job.stderr
272
+ end
273
+ end
274
+
275
+ See lib/bj/api.rb for more details.
276
+
277
+ ________________________________
278
+ Sponsors
279
+ --------------------------------
280
+ http://quintess.com/
281
+ http://www.engineyard.com/
282
+ http://igicom.com/
283
+ http://eparklabs.com/
284
+
285
+ http://your_company.com/ <<-- (targeted marketing aimed at *you*)
286
+
287
+ ________________________________
288
+ Version
289
+ --------------------------------
290
+ #{ Bj.version }
291
+ txt
292
+
293
+ usage["uris"] = <<-txt
294
+ http://codeforpeople.com/lib/ruby/
295
+ http://rubyforge.org/projects/codeforpeople/
296
+ http://codeforpeople.rubyforge.org/svn/rails/plugins/
297
+ txt
298
+
299
+ author "ara.t.howard@gmail.com"
300
+
301
+ option("rails_root", "R"){
302
+ description "the rails_root will be guessed unless you set this"
303
+ argument_required
304
+ default RAILS_ROOT
305
+ }
306
+
307
+ option("rails_env", "E"){
308
+ description "set the rails_env"
309
+ argument_required
310
+ default RAILS_ENV
311
+ }
312
+
313
+ option("log", "l"){
314
+ description "set the logfile"
315
+ argument_required
316
+ default STDERR
317
+ }
318
+
319
+
320
+ mode "migration_code" do
321
+ description "dump migration code on stdout"
322
+
323
+ def run
324
+ puts Bj.table.migration_code
325
+ end
326
+ end
327
+
328
+ mode "generate_migration" do
329
+ description "generate a migration"
330
+
331
+ def run
332
+ Bj.generate_migration
333
+ end
334
+ end
335
+
336
+ mode "migrate" do
337
+ description "migrate the db"
338
+
339
+ def run
340
+ Bj.migrate
341
+ end
342
+ end
343
+
344
+ mode "setup" do
345
+ description "generate a migration and migrate"
346
+
347
+ def run
348
+ set_rails_env(argv.first) if argv.first
349
+ Bj.setup
350
+ end
351
+ end
352
+
353
+ mode "plugin" do
354
+ description "dump the plugin into rails_root"
355
+
356
+ def run
357
+ Bj.plugin
358
+ end
359
+ end
360
+
361
+ mode "run" do
362
+ description "start a job runnner, possibly as a daemon"
363
+
364
+ option("--forever"){}
365
+ option("--ppid"){
366
+ argument :required
367
+ cast :integer
368
+ }
369
+ option("--wait"){
370
+ argument :required
371
+ cast :integer
372
+ }
373
+ option("--limit"){
374
+ argument :required
375
+ cast :integer
376
+ }
377
+ option("--redirect"){
378
+ argument :required
379
+ }
380
+ option("--daemon"){}
381
+
382
+ def run
383
+ options = {}
384
+
385
+ =begin
386
+ %w[ forever ].each do |key|
387
+ options[key.to_sym] = true if param[key].given?
388
+ end
389
+ =end
390
+
391
+ %w[ forever ppid wait limit ].each do |key|
392
+ options[key.to_sym] = param[key].value if param[key].given?
393
+ end
394
+
395
+ #p options
396
+ #exit
397
+ if param["redirect"].given?
398
+ open(param["redirect"].value, "a+") do |fd|
399
+ STDERR.reopen fd
400
+ STDOUT.reopen fd
401
+ end
402
+ STDERR.sync = true
403
+ STDOUT.sync = true
404
+ end
405
+
406
+ trap("SIGTERM"){
407
+ info{ "SIGTERM" }
408
+ exit
409
+ }
410
+
411
+ if param["daemon"].given?
412
+ daemon{ Bj.run options }
413
+ else
414
+ Bj.run options
415
+ end
416
+ end
417
+ end
418
+
419
+ mode "submit" do
420
+ keyword("file"){
421
+ argument :required
422
+ attr
423
+ }
424
+
425
+ def run
426
+ joblist = Bj.joblist.for argv.join(' ')
427
+
428
+ case file
429
+ when "-"
430
+ joblist.push(Bj.joblist.jobs_from_io(STDIN))
431
+ when "--", "---"
432
+ joblist.push(Bj.joblist.jobs_from_yaml(STDIN))
433
+ else
434
+ open(file){|io| joblist.push(Bj.joblist.jobs_from_io(io)) }
435
+ end
436
+
437
+ jobs = Bj.submit joblist, :no_tickle => true
438
+
439
+ oh = lambda{|job| OrderedHash["id", job.id, "command", job.command]}
440
+
441
+ y jobs.map{|job| oh[job]}
442
+ end
443
+ end
444
+
445
+ mode "list" do
446
+ def run
447
+ Bj.transaction do
448
+ y Bj::Table::Job.find(:all).map(&:to_hash)
449
+ end
450
+ end
451
+ end
452
+
453
+ mode "set" do
454
+ argument("key"){ attr }
455
+
456
+ argument("value"){ attr }
457
+
458
+ option("hostname", "H"){
459
+ argument :required
460
+ default Bj.hostname
461
+ attr
462
+ }
463
+
464
+ option("cast", "c"){
465
+ argument :required
466
+ default "to_s"
467
+ attr
468
+ }
469
+
470
+ def run
471
+ Bj.transaction do
472
+ Bj.config.set(key, value, :hostname => hostname, :cast => cast)
473
+ y Bj.table.config.for(:hostname => hostname)
474
+ end
475
+ end
476
+ end
477
+
478
+ mode "config" do
479
+ option("hostname", "H"){
480
+ argument :required
481
+ default Bj.hostname
482
+ }
483
+
484
+ def run
485
+ Bj.transaction do
486
+ y Bj.table.config.for(:hostname => param["hostname"].value)
487
+ end
488
+ end
489
+ end
490
+
491
+ mode "pid" do
492
+ option("hostname", "H"){
493
+ argument :required
494
+ default Bj.hostname
495
+ }
496
+
497
+ def run
498
+ Bj.transaction do
499
+ config = Bj.table.config.for(:hostname => param["hostname"].value)
500
+ puts config[ "#{ RAILS_ENV }.pid" ] if config
501
+ end
502
+ end
503
+ end
504
+
505
+
506
+ def run
507
+ help!
508
+ end
509
+
510
+ def before_run
511
+ self.logger = param["log"].value
512
+ Bj.logger = logger
513
+ set_rails_root(param["rails_root"].value) if param["rails_root"].given?
514
+ set_rails_env(param["rails_env"].value) if param["rails_env"].given?
515
+ end
516
+
517
+ def set_rails_root rails_root
518
+ ENV["RAILS_ROOT"] = rails_root
519
+ ::Object.instance_eval do
520
+ remove_const :RAILS_ROOT
521
+ const_set :RAILS_ROOT, rails_root
522
+ end
523
+ end
524
+
525
+ def set_rails_env rails_env
526
+ ENV["RAILS_ENV"] = rails_env
527
+ ::Object.instance_eval do
528
+ remove_const :RAILS_ENV
529
+ const_set :RAILS_ENV, rails_env
530
+ end
531
+ end
532
+
533
+ def daemon
534
+ ra, wa = IO.pipe
535
+ rb, wb = IO.pipe
536
+ if fork
537
+ at_exit{ exit! }
538
+ wa.close
539
+ r = ra
540
+ rb.close
541
+ w = wb
542
+ pid = r.gets
543
+ w.puts pid
544
+ Integer pid.strip
545
+ else
546
+ ra.close
547
+ w = wa
548
+ wb.close
549
+ r = rb
550
+ open("/dev/null", "r+") do |fd|
551
+ STDIN.reopen fd
552
+ STDOUT.reopen fd
553
+ STDERR.reopen fd
554
+ end
555
+ Process::setsid rescue nil
556
+ pid =
557
+ fork do
558
+ Dir::chdir RAILS_ROOT
559
+ File::umask 0
560
+ $DAEMON = true
561
+ yield
562
+ exit!
563
+ end
564
+ w.puts pid
565
+ r.gets
566
+ exit!
567
+ end
568
+ end
569
+ }
570
+
571
+
572
+
573
+
574
+
575
+ #
576
+ # we setup a few things so the script works regardless of whether it was
577
+ # called out of /usr/local/bin, ./script, or wherever. note that the script
578
+ # does *not* require the entire rails application to be loaded into memory!
579
+ # we could just load boot.rb and environment.rb, but this method let's
580
+ # submitting and running jobs be infinitely more lightweight.
581
+ #
582
+
583
+ BEGIN {
584
+ #
585
+ # see if we're running out of RAILS_ROOT/script/
586
+ #
587
+ unless defined?(::BJ_SCRIPT)
588
+ ::BJ_SCRIPT =
589
+ if %w[ script config app ].map{|d| test ?d, "#{ File.dirname __FILE__ }/../#{ d }"}.all?
590
+ __FILE__
591
+ else
592
+ nil
593
+ end
594
+ end
595
+ #
596
+ # setup RAILS_ROOT
597
+ #
598
+ unless defined?(::RAILS_ROOT)
599
+ ### grab env var first
600
+ rails_root = ENV["RAILS_ROOT"]
601
+
602
+ ### commandline usage clobbers
603
+ kv = nil
604
+ ARGV.delete_if{|arg| arg =~ %r/^RAILS_ROOT=/ and kv = arg}
605
+ rails_root = kv.split(%r/=/,2).last if kv
606
+
607
+ ### we know the rails_root if we are in RAILS_ROOT/script/
608
+ unless rails_root
609
+ if ::BJ_SCRIPT
610
+ rails_root = File.expand_path "#{ File.dirname __FILE__ }/.."
611
+ end
612
+ end
613
+
614
+ ### perhaps the current directory is a rails_root?
615
+ unless rails_root
616
+ if %w[ script config app ].map{|d| test(?d, d)}.all?
617
+ rails_root = File.expand_path "."
618
+ end
619
+ end
620
+
621
+ ### bootstrap
622
+ ::RAILS_ROOT = rails_root
623
+ end
624
+ #
625
+ # setup RAILS_ENV
626
+ #
627
+ unless defined?(::RAILS_ENV)
628
+ ### grab env var first
629
+ rails_env = ENV["RAILS_ENV"]
630
+
631
+ ### commandline usage clobbers
632
+ kv = nil
633
+ ARGV.delete_if{|arg| arg =~ %r/^RAILS_ENV=/ and kv = arg}
634
+ rails_env = kv.split(%r/=/,2).last if kv
635
+
636
+ ### fallback to development
637
+ unless rails_env
638
+ rails_env = "development"
639
+ end
640
+
641
+ ### bootstrap
642
+ ::RAILS_ENV = rails_env
643
+ end
644
+ #
645
+ # ensure that rubygems is loaded
646
+ #
647
+ begin
648
+ require "rubygems"
649
+ rescue
650
+ 42
651
+ end
652
+ #
653
+ # load gems from plugin dir iff installed as plugin - otherwise load normally
654
+ #
655
+ if ::RAILS_ROOT and ::BJ_SCRIPT
656
+ =begin
657
+ dir = Gem.dir
658
+ path = Gem.path
659
+ gem_home = File.join RAILS_ROOT, "vendor", "plugins", "bj", "gem_home"
660
+ gem_path = [gem_home]
661
+ Gem.send :use_paths, gem_home, gem_path
662
+ gem "bj"
663
+ require "bj"
664
+ gem "main"
665
+ require "main"
666
+ =end
667
+ libdir = File.join(::RAILS_ROOT, "vendor", "plugins", "bj", "lib")
668
+ $LOAD_PATH.unshift libdir
669
+ end
670
+ #
671
+ # hack of #to_s of STDERR/STDOUT for nice help messages
672
+ #
673
+ class << STDERR
674
+ def to_s() 'STDERR' end
675
+ end
676
+ class << STDOUT
677
+ def to_s() 'STDOUT' end
678
+ end
679
+ }