bj_fixed_for_rails3 1.0.2

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