bj 0.0.5 → 1.0.0

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 (7) hide show
  1. data/bin/bj +227 -62
  2. data/lib/bj.rb +2 -2
  3. data/lib/bj/bj.rb +7 -6
  4. data/lib/bj/runner.rb +29 -17
  5. data/lib/bj/util.rb +25 -1
  6. data/rakefile +3 -0
  7. metadata +2 -1
data/bin/bj CHANGED
@@ -9,92 +9,250 @@ Main {
9
9
  Overview
10
10
  --------------------------------
11
11
 
12
- Backgroundjob (Bj) is a simple to use background priority queue for rails.
13
- Although not yet tested on windows, the design of bj is such that operation
14
- should be possible on any operating system, including M$.
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
15
 
16
- Jobs can be submitted to the queue directly using the api or from the
17
- commandline using the 'bj' script. For example
16
+ Jobs can be submitted to the queue directly using the api or from the command
17
+ line using the ./script/bj:
18
18
 
19
- code:
20
- Bj.submit 'cat /etc/password'
19
+ api:
20
+ Bj.submit 'cat /etc/password'
21
21
 
22
- cli:
23
- bj submit cat /etc/password
22
+ command line:
23
+ bj submit cat /etc/password
24
24
 
25
- When used from inside a rails application bj arranges that another process
26
- will always be running in the background to process the jobs that you submit.
27
- By using a separate process to run jobs bj does not impact the resource
28
- utilization of your rails application at all and enables several very cool
29
- features:
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:
30
29
 
31
- 1) Bj allows you to sumbit jobs to any of your configured databases and,
32
- in each case, spawns a separate background process to run jobs from that
33
- queue
30
+ jobs = Bj.submit array_of_commands, :priority => 42
34
31
 
35
- Bj.in :production do
36
- Bj.submit 'production_job.exe'
37
- end
32
+ ...
38
33
 
39
- Bj.in :development do
40
- Bj.submit 'development_job.exe'
41
- end
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.
42
204
 
43
- 2) Although bj ensures that a process is always running to process
44
- your jobs, you can start a proces manually. This means that any machine
45
- capable of seeing your RAILS_ROOT can run jobs for your application, allowing
46
- one to setup a cluster of machines doing the work of a single front end rails
47
- applicaiton.
48
205
 
49
206
  ________________________________
50
207
  Install
51
208
  --------------------------------
52
209
 
53
- Bj can be installed two ways: as a gem or as a plugin.
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
54
215
 
55
216
  gem:
56
217
  1) $sudo gem install bj
57
218
  2) add "require 'bj'" to config/environment.rb
58
219
  3) bj setup
59
220
 
60
- plugin:
61
- 1) ./script/plugin install http://codeforpeople.rubyforge.org/svn/rails/plugins/bj
62
- 2) ./script/bj setup
63
-
64
221
  ________________________________
65
222
  Api
66
223
  --------------------------------
67
-
68
- submit jobs for background processing. 'jobs' can be a string or array of
69
- strings. options are applied to each job in the 'jobs', and the list of
70
- submitted jobs is always returned. options (string or symbol) can be
71
-
72
- :rails_env => production|development|key_in_database_yml
73
- when given this keyword causes bj to submit jobs to the
74
- specified database. default is RAILS_ENV.
75
-
76
- :priority => any number, including negative ones. default is zero.
77
-
78
- :tag => a tag added to the job. simply makes searching easier.
79
-
80
- :env => a hash specifying any additional environment vars the background
81
- process should have.
82
-
83
- :stdin => any stdin the background process should have.
84
-
85
- eg:
86
-
87
- jobs = Bj.submit 'echo foobar', :tag => 'simple job'
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
88
228
 
89
- jobs = Bj.submit '/bin/cat', :stdin => 'in the hat', :priority => 42
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
90
242
 
91
- jobs = Bj.submit './script/runner ./scripts/a.rb', :rails_env => 'production'
92
-
93
- jobs = Bj.submit './script/runner /dev/stdin',
94
- :stdin => 'p RAILS_ENV',
95
- :tag => 'dynamic ruby code'
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'
96
254
 
97
- jobs Bj.submit array_of_commands, :priority => 451
255
+ jobs Bj.submit array_of_commands, :priority => 451
98
256
 
99
257
  when jobs are run, they are run in RAILS_ROOT. various attributes are
100
258
  available *only* once the job has finished. you can check whether or not a
@@ -122,7 +280,14 @@ Main {
122
280
  http://www.engineyard.com/
123
281
  http://quintess.com/
124
282
  http://eparklabs.com/
125
- txt
283
+
284
+ http://your_company.com/ <<-- (targeted marketing aimed at *you*)
285
+
286
+ ________________________________
287
+ Version
288
+ --------------------------------
289
+ #{ Bj.version }
290
+ txt
126
291
 
127
292
  usage["uris"] = <<-txt
128
293
  http://codeforpeople.com/lib/ruby/
data/lib/bj.rb CHANGED
@@ -3,7 +3,7 @@ unless defined? Bj
3
3
  #
4
4
  # constants and associated attrs
5
5
  #
6
- Bj::VERSION = "0.0.4" #unless defined? Bj::VERSION
6
+ Bj::VERSION = "1.0.0" #unless defined? Bj::VERSION
7
7
  def self.version() Bj::VERSION end
8
8
 
9
9
  Bj::LIBDIR = File.expand_path(File::join(File.dirname(__FILE__), "bj")) + File::SEPARATOR unless
@@ -30,7 +30,7 @@ unless defined? Bj
30
30
  require "rbconfig"
31
31
  require "set"
32
32
  require "erb"
33
- require "drb"
33
+ require "tempfile"
34
34
  #
35
35
  # bootstrap rubygems
36
36
  #
data/lib/bj/bj.rb CHANGED
@@ -8,14 +8,15 @@ class Bj
8
8
  attribute("hostname"){ Socket.gethostname }
9
9
  attribute("logger"){ Bj::Logger.off STDERR }
10
10
  attribute("ruby"){ Util.which_ruby }
11
+ attribute("rake"){ Util.which_rake }
11
12
  attribute("script"){ Util.find_script "bj" }
12
13
  attribute("ttl"){ Integer(Bj::Table::Config["ttl"] || (twenty_four_hours = 24 * 60 * 60)) }
13
- attribute('table'){ Table }
14
- attribute('config'){ table.config }
15
- attribute('util'){ Util }
16
- attribute('runner'){ Runner }
17
- attribute('joblist'){ Joblist }
18
- attribute('default_path'){ %w'/bin /usr/bin /usr/local/bin /opt/local/bin'.join(File::PATH_SEPARATOR) }
14
+ attribute("table"){ Table }
15
+ attribute("config"){ table.config }
16
+ attribute("util"){ Util }
17
+ attribute("runner"){ Runner }
18
+ attribute("joblist"){ Joblist }
19
+ attribute("default_path"){ %w'/bin /usr/bin /usr/local/bin /opt/local/bin'.join(File::PATH_SEPARATOR) }
19
20
 
20
21
  def transaction options = {}, &block
21
22
  options.to_options!
data/lib/bj/runner.rb CHANGED
@@ -20,8 +20,6 @@ class Bj
20
20
  end
21
21
 
22
22
 
23
- # TODO - this should try to use drb too
24
- # TODO - pull out win stuff using platform.rb
25
23
  # TODO - auto start runner?
26
24
 
27
25
  def new_thread
@@ -65,20 +63,31 @@ class Bj
65
63
  end
66
64
 
67
65
  def ping
68
- pid = nil
69
- uri = nil
70
- process = nil
71
- Bj.transaction do
72
- pid = Bj.config[Runner.key(Process.pid)] || Bj.config[Runner.key]
73
- uri = Bj.config["#{ pid }.uri"]
74
- process = uri ? DRbObject.new(nil, uri) : Process
75
- end
76
- return nil unless pid
77
- pid = Integer pid
78
66
  begin
79
- process.kill Runner.hup_signal, pid
80
- pid
81
- rescue Exception
67
+ pid = nil
68
+ uri = nil
69
+ process = nil
70
+ Bj.transaction do
71
+ pid = Bj.config[Runner.key(Process.pid)] || Bj.config[Runner.key]
72
+ uri = Bj.config["#{ pid }.uri"]
73
+ process =
74
+ if uri
75
+ require "drb"
76
+ # DRb.start_service "druby://localhost:0"
77
+ DRbObject.new(nil, uri)
78
+ else
79
+ Process
80
+ end
81
+ end
82
+ return nil unless pid
83
+ pid = Integer pid
84
+ begin
85
+ process.kill Runner.hup_signal, pid
86
+ pid
87
+ rescue Exception => e
88
+ false
89
+ end
90
+ rescue Exception => e
82
91
  false
83
92
  end
84
93
  end
@@ -318,8 +327,11 @@ class Bj
318
327
  pid = Bj.config[key]
319
328
  return false if Util.alive?(pid)
320
329
  Bj.config[key] = Process.pid
321
- DRb.start_service "druby://localhost:0", Process
322
- Bj.config["#{ Process.pid }.uri"] = DRb.uri
330
+ unless Bj.util.ipc_signals_supported? # not winblows
331
+ require "drb"
332
+ DRb.start_service "druby://localhost:0", Process
333
+ Bj.config["#{ Process.pid }.uri"] = DRb.uri
334
+ end
323
335
  end
324
336
  at_exit{ unregister }
325
337
  true
data/lib/bj/util.rb CHANGED
@@ -21,7 +21,7 @@ class Bj
21
21
 
22
22
  def spawn cmd, options = {}
23
23
  options.to_options!
24
- logger = options[:logger] || Bj.logger
24
+ logger = options.has_key?(:logger) ? options[:logger] : Bj.logger
25
25
  logger.info{ "cmd <#{ cmd }>" } if logger
26
26
  status = systemu cmd, 1=>(stdout=""), 2=>(stderr="")
27
27
  logger.info{ "status <#{ status.exitstatus }>" } if logger
@@ -57,6 +57,30 @@ class Bj
57
57
  ruby
58
58
  end
59
59
 
60
+ def which_rake
61
+ tmp = Tempfile.new Process.pid
62
+ tmp.write "task(:foobar){ puts 42 }"
63
+ tmp.close
64
+ bat = spawn("rake.bat -f #{ tmp.path.inspect } foobar", :logger => false) rescue false
65
+ bat ? "rake.bat" : "rake"
66
+ ensure
67
+ tmp.close! rescue nil
68
+ end
69
+
70
+ def ipc_signals_supported
71
+ @ipc_signals_supported ||=
72
+ IO.popen 'ruby', 'r+' do |ruby|
73
+ pid = ruby.pid
74
+ begin
75
+ Process.kill 'TERM', pid
76
+ true
77
+ rescue Exception
78
+ false
79
+ end
80
+ end
81
+ end
82
+ alias_method "ipc_signals_supported?", "ipc_signals_supported"
83
+
60
84
  def find_script basename
61
85
  path = ENV["PATH"] || ENV["path"] || Bj.default_path
62
86
  raise "no env PATH" unless path
data/rakefile ADDED
@@ -0,0 +1,3 @@
1
+ task :foobar do
2
+ puts 42
3
+ end
metadata CHANGED
@@ -3,7 +3,7 @@ rubygems_version: 0.9.4
3
3
  specification_version: 1
4
4
  name: bj
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.0.5
6
+ version: 1.0.0
7
7
  date: 2007-12-28 00:00:00 -07:00
8
8
  summary: bj
9
9
  require_paths:
@@ -46,6 +46,7 @@ files:
46
46
  - lib/bj/table.rb
47
47
  - lib/bj/util.rb
48
48
  - lib/bj.rb
49
+ - rakefile
49
50
  - README
50
51
  - TODO
51
52
  test_files: []