bj 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ class Bj
2
+ class Error < ::StandardError; end
3
+ class RailsRoot < Error; end
4
+ end
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