bj 0.0.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []