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