bj_fixed_for_rails3 1.0.2

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