bj 0.0.1
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/HISTORY +2 -0
- data/README +130 -0
- data/TODO +3 -0
- data/bin/bj +478 -0
- data/gemspec.rb +29 -0
- data/install.rb +210 -0
- data/lib/bj.rb +79 -0
- data/lib/bj/api.rb +144 -0
- data/lib/bj/bj.rb +92 -0
- data/lib/bj/errors.rb +4 -0
- data/lib/bj/joblist.rb +112 -0
- data/lib/bj/logger.rb +50 -0
- data/lib/bj/runner.rb +285 -0
- data/lib/bj/stdext.rb +80 -0
- data/lib/bj/table.rb +388 -0
- data/lib/bj/util.rb +81 -0
- metadata +99 -0
data/lib/bj/bj.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
class Bj
|
2
|
+
module ClassMethods
|
3
|
+
attribute("rails_root"){ Util.const_or_env("RAILS_ROOT"){ "." } }
|
4
|
+
attribute("rails_env"){ Util.const_or_env("RAILS_ENV"){ "development" } }
|
5
|
+
attribute("database_yml"){ File.join rails_root, "config", "database.yml" }
|
6
|
+
attribute("configurations"){ YAML.load IO.read(database_yml) }
|
7
|
+
attribute("tables"){ Table.list }
|
8
|
+
attribute("hostname"){ Socket.gethostname }
|
9
|
+
attribute("logger"){ Bj::Logger.off STDERR }
|
10
|
+
attribute("ruby"){ Util.which_ruby }
|
11
|
+
attribute("script"){ Util.find_script "bj" }
|
12
|
+
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
|
+
|
19
|
+
def transaction options = {}, &block
|
20
|
+
options.to_options!
|
21
|
+
options.reverse_merge! :rails_env => Bj.rails_env
|
22
|
+
bj_rails_env = Bj.rails_env
|
23
|
+
begin
|
24
|
+
Bj.rails_env = options[:rails_env].to_s
|
25
|
+
connecting(options) do
|
26
|
+
ActiveRecord::Base.transaction do
|
27
|
+
block.call ActiveRecord::Base.connection
|
28
|
+
end
|
29
|
+
end
|
30
|
+
ensure
|
31
|
+
Bj.rails_env = bj_rails_env
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def connecting options = {}, &block
|
36
|
+
options.to_options!
|
37
|
+
rails_env = (options[:rails_env] || Bj.rails_env).to_s
|
38
|
+
previous = ar_connection
|
39
|
+
ActiveRecord::Base.connection = connections[rails_env] unless ActiveRecord::Base.connection == connections[rails_env]
|
40
|
+
begin
|
41
|
+
block.call ActiveRecord::Base.connection
|
42
|
+
ensure
|
43
|
+
ActiveRecord::Base.connection = previous unless ActiveRecord::Base.connection == previous
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def connections
|
48
|
+
@connections ||= Hash.new do |hash, rails_env|
|
49
|
+
rails_env = rails_env.to_s
|
50
|
+
ActiveRecord::Base.establish_connection configurations[rails_env]
|
51
|
+
hash[rails_env] = ActiveRecord::Base.connection
|
52
|
+
end.merge(ar_connections)
|
53
|
+
@connections
|
54
|
+
end
|
55
|
+
|
56
|
+
def ar_connections
|
57
|
+
if defined?(RAILS_ENV)
|
58
|
+
{ RAILS_ENV => ar_connection }
|
59
|
+
else
|
60
|
+
{}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def ar_connection
|
65
|
+
begin
|
66
|
+
ActiveRecord::Base.connection
|
67
|
+
rescue ActiveRecord::ConnectionNotEstablished
|
68
|
+
ActiveRecord::Base.establish_connection configurations[rails_env]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def chroot options = {}, &block
|
73
|
+
if defined? @chrooted and @chrooted
|
74
|
+
return block.call(@chrooted)
|
75
|
+
end
|
76
|
+
begin
|
77
|
+
Dir.chdir @chrooted = rails_root do |pwd|
|
78
|
+
raise RailsRoot, "<#{ pwd }> is not a rails root" unless Util.valid_rails_root?(pwd)
|
79
|
+
block.call(@chrooted)
|
80
|
+
end
|
81
|
+
ensure
|
82
|
+
@chrooted = nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def boot
|
87
|
+
load File.join(rails_root, "config", "boot.rb")
|
88
|
+
load File.join(rails_root, "config", "environment.rb")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
send :extend, ClassMethods
|
92
|
+
end
|
data/lib/bj/errors.rb
ADDED
data/lib/bj/joblist.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
class Bj
|
2
|
+
class JobList < ::Array
|
3
|
+
module ClassMethods
|
4
|
+
def for jobs, options = {}
|
5
|
+
if Joblist === jobs
|
6
|
+
jobs.update options
|
7
|
+
return jobs
|
8
|
+
end
|
9
|
+
options.to_options!
|
10
|
+
jobs = [jobs].flatten.compact
|
11
|
+
list = []
|
12
|
+
jobs.each do |arg|
|
13
|
+
list.push(
|
14
|
+
case arg
|
15
|
+
when String
|
16
|
+
case arg
|
17
|
+
when %r/^\d+$/
|
18
|
+
job_from_id arg
|
19
|
+
else
|
20
|
+
job_from_string arg
|
21
|
+
end
|
22
|
+
when Hash
|
23
|
+
job_from_hash arg
|
24
|
+
when Io
|
25
|
+
jobs_from_io arg
|
26
|
+
when Fixnum, Bignum
|
27
|
+
job_from_number arg
|
28
|
+
else
|
29
|
+
job_from_string arg
|
30
|
+
end
|
31
|
+
)
|
32
|
+
end
|
33
|
+
list.flatten!
|
34
|
+
list.compact!
|
35
|
+
list.map!{|job| job.reverse_merge! options}
|
36
|
+
list
|
37
|
+
end
|
38
|
+
|
39
|
+
def job_from_hash arg
|
40
|
+
arg.to_hash.to_options!
|
41
|
+
end
|
42
|
+
|
43
|
+
def job_from_string arg
|
44
|
+
unless arg.strip.empty?
|
45
|
+
{ :command => arg.to_s }
|
46
|
+
else
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def job_from_number arg
|
52
|
+
id = arg.to_i
|
53
|
+
Table::Job.find(id).to_hash
|
54
|
+
end
|
55
|
+
|
56
|
+
def jobs_from_io arg
|
57
|
+
if arg == "-"
|
58
|
+
load_from_io STDIN
|
59
|
+
else
|
60
|
+
if arg.respond_to? :read
|
61
|
+
load_from_io arg
|
62
|
+
else
|
63
|
+
open(arg, "r"){|fd| load_from_io fd}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_from_io io
|
69
|
+
list = []
|
70
|
+
io.each do |line|
|
71
|
+
line.strip!
|
72
|
+
next if line.empty?
|
73
|
+
list << job_from_string(line)
|
74
|
+
end
|
75
|
+
list
|
76
|
+
end
|
77
|
+
|
78
|
+
def jobs_from_yaml arg
|
79
|
+
object =
|
80
|
+
if arg == "-"
|
81
|
+
YAML.load STDIN
|
82
|
+
else
|
83
|
+
if arg.respond_to? :read
|
84
|
+
YAML.load arg
|
85
|
+
else
|
86
|
+
open(arg, "r"){|fd| YAML.load fd}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
Joblist.for object
|
90
|
+
end
|
91
|
+
end
|
92
|
+
send :extend, ClassMethods
|
93
|
+
|
94
|
+
module InstanceMethods
|
95
|
+
def update options = {}
|
96
|
+
options.to_options!
|
97
|
+
each{|job| job.update options}
|
98
|
+
end
|
99
|
+
|
100
|
+
def push other
|
101
|
+
Joblist.for(other).each do |job|
|
102
|
+
super job
|
103
|
+
end
|
104
|
+
self
|
105
|
+
end
|
106
|
+
alias_method "<<", "push"
|
107
|
+
end
|
108
|
+
send :include, InstanceMethods
|
109
|
+
end
|
110
|
+
|
111
|
+
Joblist = JobList
|
112
|
+
end
|
data/lib/bj/logger.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
class Bj
|
2
|
+
class Logger < ::Logger
|
3
|
+
def self.new *a, &b
|
4
|
+
super(*a, &b).instance_eval{ @default_formatter = @formatter = Formatter.new; self }
|
5
|
+
end
|
6
|
+
def format_message(severity, datetime, progname, msg)
|
7
|
+
(@formatter || @default_formatter).call(severity, datetime, progname, msg)
|
8
|
+
end
|
9
|
+
|
10
|
+
def device
|
11
|
+
@logdev.instance_eval{ @dev }
|
12
|
+
end
|
13
|
+
|
14
|
+
def tty?
|
15
|
+
device.respond_to?('tty?') and device.tty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def turn which
|
19
|
+
@logdev.extend OnOff unless OnOff === @logdev
|
20
|
+
@logdev.turn which
|
21
|
+
end
|
22
|
+
|
23
|
+
module OnOff
|
24
|
+
def turn which
|
25
|
+
@turned = which.to_s =~ %r/on/i ? :on : :off
|
26
|
+
end
|
27
|
+
|
28
|
+
def write message
|
29
|
+
return message.to_s.size if @turned == :off
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def on
|
35
|
+
turn :on
|
36
|
+
end
|
37
|
+
alias_method "on!", "on"
|
38
|
+
def self.on *a, &b
|
39
|
+
new(*a, &b).instance_eval{ turn :on; self }
|
40
|
+
end
|
41
|
+
|
42
|
+
def off
|
43
|
+
turn :off
|
44
|
+
end
|
45
|
+
alias_method "off!", "off"
|
46
|
+
def self.off *a, &b
|
47
|
+
new(*a, &b).instance_eval{ turn :off; self }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/bj/runner.rb
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
class Bj
|
2
|
+
class Runner
|
3
|
+
class Background
|
4
|
+
def self.for(*a, &b) new(*a, &b) end
|
5
|
+
|
6
|
+
attribute "command"
|
7
|
+
attribute "thread"
|
8
|
+
attribute "pid"
|
9
|
+
|
10
|
+
def initialize command
|
11
|
+
@command = command
|
12
|
+
@thread = new_thread
|
13
|
+
end
|
14
|
+
|
15
|
+
def inspect
|
16
|
+
{
|
17
|
+
"command" => command,
|
18
|
+
"pid" => pid,
|
19
|
+
}.inspect
|
20
|
+
end
|
21
|
+
|
22
|
+
def new_thread
|
23
|
+
this = self
|
24
|
+
Thread.new do
|
25
|
+
Thread.current.abort_on_exception = true
|
26
|
+
loop do
|
27
|
+
cleanup = lambda{}
|
28
|
+
|
29
|
+
IO.popen command, "r+" do |pipe|
|
30
|
+
this.pid = pid = pipe.pid
|
31
|
+
cleanup = lambda do
|
32
|
+
cleanup = lambda{}
|
33
|
+
begin; Process.kill('SIGTERM', pid); rescue Exception; 42; end
|
34
|
+
end
|
35
|
+
at_exit &cleanup
|
36
|
+
pipe.each{|line|}
|
37
|
+
end
|
38
|
+
|
39
|
+
Bj.logger.error{ "#{ command } failed with #{ $?.inspect }" } unless
|
40
|
+
[0, 42].include?($?.exitstatus)
|
41
|
+
|
42
|
+
cleanup.call
|
43
|
+
|
44
|
+
sleep 42
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
module ClassMethods
|
51
|
+
def tickle
|
52
|
+
ping or start
|
53
|
+
end
|
54
|
+
|
55
|
+
def ping
|
56
|
+
pid = Bj.config[Runner.key]
|
57
|
+
pid ? Util.ping(pid) : nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def key
|
61
|
+
"#{ Bj.rails_env }.pid"
|
62
|
+
end
|
63
|
+
|
64
|
+
def start options = {}
|
65
|
+
options.to_options!
|
66
|
+
background.delete Bj.rails_env if options[:force]
|
67
|
+
background[Bj.rails_env] ||= Background.for(command)
|
68
|
+
end
|
69
|
+
|
70
|
+
def background
|
71
|
+
@background ||= Hash.new
|
72
|
+
end
|
73
|
+
|
74
|
+
def command
|
75
|
+
"#{ Bj.ruby } #{ Bj.script } run --forever --redirect=#{ log } --rails_env=#{ Bj.rails_env } --ppid=#{ Process.pid }"
|
76
|
+
end
|
77
|
+
|
78
|
+
def log
|
79
|
+
File.join logdir, "bj.#{ Bj.hostname }.#{ Bj.rails_env }.log"
|
80
|
+
end
|
81
|
+
|
82
|
+
def logdir
|
83
|
+
File.join File.expand_path(Bj.rails_root), 'log'
|
84
|
+
end
|
85
|
+
|
86
|
+
def run options = {}, &block
|
87
|
+
new(options, &block).run
|
88
|
+
end
|
89
|
+
end
|
90
|
+
send :extend, ClassMethods
|
91
|
+
|
92
|
+
module Instance_Methods
|
93
|
+
attribute "options"
|
94
|
+
attribute "block"
|
95
|
+
|
96
|
+
def initialize options = {}, &block
|
97
|
+
options.to_options!
|
98
|
+
@options, @block = options, block
|
99
|
+
end
|
100
|
+
|
101
|
+
def run
|
102
|
+
wait = options[:wait] || 42
|
103
|
+
limit = options[:limit]
|
104
|
+
forever = options[:forever]
|
105
|
+
ppid = options[:ppid]
|
106
|
+
|
107
|
+
limit = false if forever
|
108
|
+
wait = Integer wait
|
109
|
+
loopno = 0
|
110
|
+
thread = new_lifeline(ppid) if ppid
|
111
|
+
|
112
|
+
Bj.chroot do
|
113
|
+
register or exit!(EXIT::WARNING)
|
114
|
+
Bj.logger.info{ "START" }
|
115
|
+
fill_morgue
|
116
|
+
install_signal_handlers
|
117
|
+
|
118
|
+
loop do
|
119
|
+
loopno += 1
|
120
|
+
break if(limit and n > limit)
|
121
|
+
|
122
|
+
Bj.logger.debug{ "loopno #{ loopno }" }
|
123
|
+
|
124
|
+
sweep
|
125
|
+
|
126
|
+
catch :no_jobs do
|
127
|
+
loop do
|
128
|
+
job = thread = stdout = stderr = nil
|
129
|
+
|
130
|
+
Bj.transaction(options) do
|
131
|
+
job = Bj::Table::Job.find :first,
|
132
|
+
:conditions => "state = 'pending'",
|
133
|
+
:order => "priority DESC, submitted_at ASC",
|
134
|
+
:limit => 1
|
135
|
+
throw :no_jobs unless job
|
136
|
+
|
137
|
+
Bj.logger.info{ "running #{ job.id } (#{ job.command })" }
|
138
|
+
|
139
|
+
command = job.command
|
140
|
+
env = job.env || {}
|
141
|
+
stdin = job.stdin || ''
|
142
|
+
stdout = job.stdout || ''
|
143
|
+
stderr = job.stderr || ''
|
144
|
+
started_at = Time.now
|
145
|
+
|
146
|
+
q = Queue.new
|
147
|
+
thread = Thread.new do
|
148
|
+
Thread.current.abort_on_exception = true
|
149
|
+
systemu command, :cwd=>Bj.rails_root, :env=>env, :stdin=>stdin, :stdout=>stdout, :stderr=>stderr do |pid|
|
150
|
+
q << pid
|
151
|
+
end
|
152
|
+
end
|
153
|
+
pid = q.pop
|
154
|
+
|
155
|
+
job.state = "running"
|
156
|
+
job.runner = Bj.hostname
|
157
|
+
job.pid = pid
|
158
|
+
job.started_at = started_at
|
159
|
+
job.save!
|
160
|
+
job.update
|
161
|
+
end
|
162
|
+
|
163
|
+
exit_status = thread.value
|
164
|
+
finished_at = Time.now
|
165
|
+
|
166
|
+
Bj.transaction(options) do
|
167
|
+
job = Bj::Table::Job.find job.id
|
168
|
+
break unless job
|
169
|
+
job.state = "finished"
|
170
|
+
job.finished_at = finished_at
|
171
|
+
job.stdout = stdout
|
172
|
+
job.stderr = stderr
|
173
|
+
job.exit_status = exit_status
|
174
|
+
job.save!
|
175
|
+
job.update
|
176
|
+
Bj.logger.info{ "exit_status #{ job.exit_status }" }
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
sleep wait
|
182
|
+
|
183
|
+
break unless(limit or limit == false)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def fill_morgue options = {}
|
189
|
+
Bj.logger.debug{ "filling the morgue..." }
|
190
|
+
Bj.transaction(options) do
|
191
|
+
now = Time.now
|
192
|
+
jobs = Bj::Table::Job.find :all,
|
193
|
+
:conditions => ["state = 'running' and runner = ?", Bj.hostname]
|
194
|
+
jobs.each do |job|
|
195
|
+
Bj.logger.info{ "marking #{ job.id } (#{ job.command }) as dead." }
|
196
|
+
job.state = 'dead'
|
197
|
+
job.finished_at = now
|
198
|
+
job.save!
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def sweep options = {}
|
204
|
+
Bj.logger.debug{ "sweeping..." }
|
205
|
+
Bj.transaction(options) do
|
206
|
+
now = Time.now
|
207
|
+
too_old = now - Bj.ttl
|
208
|
+
jobs = Bj::Table::Job.find :all,
|
209
|
+
:conditions => ["(state = 'finished' or state = 'dead') and submitted_at < ?", too_old]
|
210
|
+
jobs.each do |job|
|
211
|
+
Bj.logger.info{ "archiving #{ job.id } (#{ job.command })." }
|
212
|
+
hash = job.to_hash.update(:archived_at => now)
|
213
|
+
Bj::Table::JobArchive.create! hash
|
214
|
+
job.destroy
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def new_lifeline ppid
|
220
|
+
Thread.new do
|
221
|
+
loop do
|
222
|
+
begin
|
223
|
+
Process.kill 0, ppid
|
224
|
+
rescue Errno::ESRCH
|
225
|
+
STDERR.puts "parent #{ ppid } died"
|
226
|
+
Kernel.exit 42
|
227
|
+
end
|
228
|
+
sleep 42
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
attribute 'sighup' => false
|
234
|
+
|
235
|
+
def install_signal_handlers
|
236
|
+
trap('SIGHUP') do
|
237
|
+
if sleeping?
|
238
|
+
Bj.logger.debug{ "woke up!" }
|
239
|
+
throw :wake_up
|
240
|
+
else
|
241
|
+
sighup true
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def sleep seconds
|
247
|
+
if sighup
|
248
|
+
sighup false
|
249
|
+
return
|
250
|
+
end
|
251
|
+
Bj.logger.debug{ "sleeping #{ seconds }..." }
|
252
|
+
catch :wake_up do
|
253
|
+
begin
|
254
|
+
@sleeping = true
|
255
|
+
Kernel.sleep seconds
|
256
|
+
ensure
|
257
|
+
@sleeping = false
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def sleeping?
|
263
|
+
@sleeping
|
264
|
+
end
|
265
|
+
|
266
|
+
def register options = {}
|
267
|
+
Bj.transaction(options) do
|
268
|
+
pid = Bj.config[Runner.key]
|
269
|
+
if Util.alive?(pid)
|
270
|
+
return false
|
271
|
+
end
|
272
|
+
Bj.config[Runner.key] = Process.pid
|
273
|
+
at_exit{ unregister }
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def unregister options = {}
|
278
|
+
Bj.transaction(options) do
|
279
|
+
Bj.config.delete Runner.key
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
send :include, Instance_Methods
|
284
|
+
end
|
285
|
+
end
|