rq-ruby1.8 3.4.3
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/Gemfile +22 -0
- data/Gemfile.lock +22 -0
- data/INSTALL +166 -0
- data/LICENSE +10 -0
- data/Makefile +6 -0
- data/README +1183 -0
- data/Rakefile +37 -0
- data/TODO +24 -0
- data/TUTORIAL +230 -0
- data/VERSION +1 -0
- data/bin/rq +902 -0
- data/bin/rqmailer +865 -0
- data/example/a.rb +7 -0
- data/extconf.rb +198 -0
- data/gemspec.rb +40 -0
- data/install.rb +210 -0
- data/lib/rq.rb +155 -0
- data/lib/rq/arrayfields.rb +371 -0
- data/lib/rq/backer.rb +31 -0
- data/lib/rq/configfile.rb +82 -0
- data/lib/rq/configurator.rb +40 -0
- data/lib/rq/creator.rb +54 -0
- data/lib/rq/cron.rb +144 -0
- data/lib/rq/defaultconfig.txt +5 -0
- data/lib/rq/deleter.rb +51 -0
- data/lib/rq/executor.rb +40 -0
- data/lib/rq/feeder.rb +527 -0
- data/lib/rq/ioviewer.rb +48 -0
- data/lib/rq/job.rb +51 -0
- data/lib/rq/jobqueue.rb +947 -0
- data/lib/rq/jobrunner.rb +110 -0
- data/lib/rq/jobrunnerdaemon.rb +193 -0
- data/lib/rq/lister.rb +47 -0
- data/lib/rq/locker.rb +43 -0
- data/lib/rq/lockfile.rb +564 -0
- data/lib/rq/logging.rb +124 -0
- data/lib/rq/mainhelper.rb +189 -0
- data/lib/rq/orderedautohash.rb +39 -0
- data/lib/rq/orderedhash.rb +240 -0
- data/lib/rq/qdb.rb +733 -0
- data/lib/rq/querier.rb +98 -0
- data/lib/rq/rails.rb +80 -0
- data/lib/rq/recoverer.rb +28 -0
- data/lib/rq/refresher.rb +80 -0
- data/lib/rq/relayer.rb +283 -0
- data/lib/rq/resource.rb +22 -0
- data/lib/rq/resourcemanager.rb +40 -0
- data/lib/rq/resubmitter.rb +100 -0
- data/lib/rq/rotater.rb +98 -0
- data/lib/rq/sleepcycle.rb +46 -0
- data/lib/rq/snapshotter.rb +40 -0
- data/lib/rq/sqlite.rb +286 -0
- data/lib/rq/statuslister.rb +48 -0
- data/lib/rq/submitter.rb +113 -0
- data/lib/rq/toucher.rb +182 -0
- data/lib/rq/updater.rb +94 -0
- data/lib/rq/usage.rb +1222 -0
- data/lib/rq/util.rb +304 -0
- data/rdoc.sh +17 -0
- data/rq-ruby1.8.gemspec +120 -0
- data/test/.gitignore +1 -0
- data/test/test_rq.rb +145 -0
- data/white_box/crontab +2 -0
- data/white_box/joblist +8 -0
- data/white_box/killrq +18 -0
- data/white_box/rq_killer +27 -0
- metadata +208 -0
data/lib/rq/ioviewer.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
unless defined? $__rq_ioviewer__
|
2
|
+
module RQ
|
3
|
+
#--{{{
|
4
|
+
LIBDIR = File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
|
5
|
+
defined? LIBDIR
|
6
|
+
|
7
|
+
require LIBDIR + 'mainhelper'
|
8
|
+
|
9
|
+
#
|
10
|
+
# the IOViewer class spawns an external editor command to view the
|
11
|
+
# stdin/stdout/stderr of a jid(s)
|
12
|
+
#
|
13
|
+
class IOViewer < MainHelper
|
14
|
+
#--{{{
|
15
|
+
def ioview
|
16
|
+
#--{{{
|
17
|
+
@infile = @options['infile']
|
18
|
+
debug{ "infile <#{ @infile }>" }
|
19
|
+
|
20
|
+
jobs = []
|
21
|
+
if @infile
|
22
|
+
open(@infile) do |f|
|
23
|
+
debug{ "reading jobs from <#{ @infile }>" }
|
24
|
+
loadio f, @infile, jobs
|
25
|
+
end
|
26
|
+
end
|
27
|
+
if stdin?
|
28
|
+
debug{ "reading jobs from <stdin>" }
|
29
|
+
loadio stdin, 'stdin', jobs
|
30
|
+
end
|
31
|
+
jobs.each{|job| @argv << Integer(job['jid'])}
|
32
|
+
|
33
|
+
editor = @options['editor'] || ENV['RQ_EDITOR'] || ENV['RQ_IOVIEW'] || 'vim -R -o'
|
34
|
+
@argv.each do |jid|
|
35
|
+
jid = Integer jid
|
36
|
+
ios = %w( stdin stdout stderr ).map{|d| File.join @qpath, d, jid.to_s}
|
37
|
+
command = "#{ editor } #{ ios.join ' ' }"
|
38
|
+
system(command) #or error{ "command <#{ command }> failed with <#{ $? }>" }
|
39
|
+
end
|
40
|
+
self
|
41
|
+
#--}}}
|
42
|
+
end
|
43
|
+
#--}}}
|
44
|
+
end # class IOViewer
|
45
|
+
#--}}}
|
46
|
+
end # module RQ
|
47
|
+
$__rq_ioviewer__ = __FILE__
|
48
|
+
end
|
data/lib/rq/job.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
unless defined? $__rq_job__
|
2
|
+
module RQ
|
3
|
+
#--{{{
|
4
|
+
LIBDIR = File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
|
5
|
+
defined? LIBDIR
|
6
|
+
|
7
|
+
require 'arrayfields'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
require LIBDIR + 'util'
|
11
|
+
require LIBDIR + 'qdb'
|
12
|
+
|
13
|
+
#
|
14
|
+
# Job is a convenience class which stamps out a QDB::tuple and extends it
|
15
|
+
# with methods that give accessor methods for each field in the hash
|
16
|
+
#
|
17
|
+
class Job < Array
|
18
|
+
#--{{{
|
19
|
+
include ArrayFields
|
20
|
+
def initialize kvs = {}
|
21
|
+
#--{{{
|
22
|
+
self.fields = QDB::FIELDS
|
23
|
+
(kvs.keys - self.fields).each{|k| self[k] = kvs[k]}
|
24
|
+
#--}}}
|
25
|
+
end
|
26
|
+
def method_missing(meth, *args, &block)
|
27
|
+
#--{{{
|
28
|
+
setpat = %r/=$/o
|
29
|
+
meth = "#{ meth }"
|
30
|
+
setter = meth =~ setpat
|
31
|
+
meth.gsub! setpat, ''
|
32
|
+
if fields.include? "#{ meth }"
|
33
|
+
if setter
|
34
|
+
self.send('[]=', meth, *args, &block)
|
35
|
+
else
|
36
|
+
self.send('[]', meth, *args, &block)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
#--}}}
|
42
|
+
end
|
43
|
+
def to_yaml
|
44
|
+
to_hash.to_yaml
|
45
|
+
end
|
46
|
+
#--}}}
|
47
|
+
end # class Job
|
48
|
+
#--}}}
|
49
|
+
end # module RQ
|
50
|
+
$__rq_job__ = __FILE__
|
51
|
+
end
|
data/lib/rq/jobqueue.rb
ADDED
@@ -0,0 +1,947 @@
|
|
1
|
+
unless defined? $__rq_jobqueue__
|
2
|
+
module RQ
|
3
|
+
#--{{{
|
4
|
+
LIBDIR = File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
|
5
|
+
defined? LIBDIR
|
6
|
+
|
7
|
+
require 'tempfile'
|
8
|
+
|
9
|
+
require LIBDIR + 'util'
|
10
|
+
require LIBDIR + 'logging'
|
11
|
+
require LIBDIR + 'qdb'
|
12
|
+
require LIBDIR + 'orderedhash'
|
13
|
+
require LIBDIR + 'orderedautohash'
|
14
|
+
|
15
|
+
#
|
16
|
+
# the JobQueue class is responsible for high level access to the job queue
|
17
|
+
#
|
18
|
+
class JobQueue
|
19
|
+
#--{{{
|
20
|
+
include Logging
|
21
|
+
include Util
|
22
|
+
class Error < StandardError; end
|
23
|
+
|
24
|
+
MAX_JID = 2 ** 20
|
25
|
+
|
26
|
+
class << self
|
27
|
+
#--{{{
|
28
|
+
def create path, opts = {}
|
29
|
+
#--{{{
|
30
|
+
FileUtils::rm_rf path
|
31
|
+
FileUtils::mkdir_p path
|
32
|
+
db = File::join path, 'db'
|
33
|
+
qdb = QDB.create db, opts
|
34
|
+
opts['qdb'] = qdb
|
35
|
+
q = new path, opts
|
36
|
+
FileUtils::mkdir_p q.bin
|
37
|
+
FileUtils::mkdir_p q.stdin
|
38
|
+
FileUtils::mkdir_p q.stdout
|
39
|
+
FileUtils::mkdir_p q.stderr
|
40
|
+
FileUtils::mkdir_p q.data
|
41
|
+
q
|
42
|
+
#--}}}
|
43
|
+
end
|
44
|
+
#--}}}
|
45
|
+
end
|
46
|
+
|
47
|
+
attr :path
|
48
|
+
attr :bin
|
49
|
+
attr :stdin
|
50
|
+
attr :stdout
|
51
|
+
attr :stderr
|
52
|
+
attr :data
|
53
|
+
attr :opts
|
54
|
+
attr :qdb
|
55
|
+
alias :db :qdb
|
56
|
+
|
57
|
+
def initialize path, opts = {}
|
58
|
+
#--{{{
|
59
|
+
@path = path # do NOT expand this or it'll be fubar from misc nfs mounts!!
|
60
|
+
@bin = File::join @path, 'bin'
|
61
|
+
@stdin = File::join @path, 'stdin'
|
62
|
+
@stdout = File::join @path, 'stdout'
|
63
|
+
@stderr = File::join @path, 'stderr'
|
64
|
+
@data = File::join @path, 'data'
|
65
|
+
@opts = opts
|
66
|
+
raise "q <#{ @path }> does not exist" unless test ?e, @path
|
67
|
+
raise "q <#{ @path }> is not a directory" unless test ?d, @path
|
68
|
+
@basename = File::basename(@path)
|
69
|
+
@dirname = File::dirname(@path)
|
70
|
+
@logger = getopt('logger', opts) || Logger::new(STDERR)
|
71
|
+
@qdb = getopt('qdb', opts) || QDB::new(File::join(@path, 'db'), 'logger' => @logger)
|
72
|
+
@in_transaction = false
|
73
|
+
@in_ro_transaction = false
|
74
|
+
#--}}}
|
75
|
+
end
|
76
|
+
def stdin4 jid
|
77
|
+
#--{{{
|
78
|
+
"stdin/#{ jid }"
|
79
|
+
#--}}}
|
80
|
+
end
|
81
|
+
def standard_in_4 jid
|
82
|
+
#--{{{
|
83
|
+
File::expand_path(File::join(path, stdin4(jid)))
|
84
|
+
#--}}}
|
85
|
+
end
|
86
|
+
def stdout4 jid
|
87
|
+
#--{{{
|
88
|
+
"stdout/#{ jid }"
|
89
|
+
#--}}}
|
90
|
+
end
|
91
|
+
def standard_out_4 jid
|
92
|
+
#--{{{
|
93
|
+
File::expand_path(File::join(path, stdout4(jid)))
|
94
|
+
#--}}}
|
95
|
+
end
|
96
|
+
def stderr4 jid
|
97
|
+
#--{{{
|
98
|
+
"stderr/#{ jid }"
|
99
|
+
#--}}}
|
100
|
+
end
|
101
|
+
def standard_err_4 jid
|
102
|
+
#--{{{
|
103
|
+
File::expand_path(File::join(path, stderr4(jid)))
|
104
|
+
#--}}}
|
105
|
+
end
|
106
|
+
def data4 jid
|
107
|
+
#--{{{
|
108
|
+
"data/#{ jid }"
|
109
|
+
#--}}}
|
110
|
+
end
|
111
|
+
def data_4 jid
|
112
|
+
#--{{{
|
113
|
+
File::expand_path(File::join(path, data4(jid)))
|
114
|
+
#--}}}
|
115
|
+
end
|
116
|
+
def submit(*jobs, &block)
|
117
|
+
#--{{{
|
118
|
+
if jobs.size == 1 and jobs.first.is_a?(String)
|
119
|
+
jobs = [ { "command" => jobs.to_s } ]
|
120
|
+
end
|
121
|
+
|
122
|
+
now = Util::timestamp Time::now
|
123
|
+
|
124
|
+
transaction do
|
125
|
+
sql = "select max(jid) from jobs"
|
126
|
+
tuple = execute(sql).first
|
127
|
+
jid = tuple.first || 0
|
128
|
+
jid = Integer(jid) + 1
|
129
|
+
|
130
|
+
jobs.each do |job|
|
131
|
+
command = job['command']
|
132
|
+
stdin = job['stdin']
|
133
|
+
data = job['data']
|
134
|
+
|
135
|
+
raise "no command for job <#{ job.inspect }>" unless command
|
136
|
+
|
137
|
+
tmp_stdin(stdin) do |ts|
|
138
|
+
tuple = QDB::tuple
|
139
|
+
|
140
|
+
tuple['command'] = command
|
141
|
+
tuple['priority'] = job['priority'] || 0
|
142
|
+
tuple['tag'] = job['tag']
|
143
|
+
tuple['runner'] = job['runner']
|
144
|
+
tuple['restartable'] = job['restartable']
|
145
|
+
tuple['state'] = 'pending'
|
146
|
+
tuple['submitted'] = now
|
147
|
+
tuple['submitter'] = Util::hostname
|
148
|
+
tuple['stdin'] = stdin4 jid
|
149
|
+
tuple['stdout'] = nil
|
150
|
+
tuple['stderr'] = nil
|
151
|
+
tuple['data'] = data4 jid
|
152
|
+
|
153
|
+
values = QDB::q tuple
|
154
|
+
|
155
|
+
sql = "insert into jobs values (#{ values.join ',' });\n"
|
156
|
+
execute(sql){}
|
157
|
+
|
158
|
+
FileUtils::rm_rf standard_in_4(jid)
|
159
|
+
FileUtils::rm_rf standard_out_4(jid)
|
160
|
+
FileUtils::rm_rf standard_err_4(jid)
|
161
|
+
FileUtils::rm_rf data_4(jid)
|
162
|
+
FileUtils::cp ts.path, standard_in_4(jid) if ts
|
163
|
+
if data
|
164
|
+
FileUtils::cp_r data, data_4(jid)
|
165
|
+
else
|
166
|
+
FileUtils::mkdir_p data_4(jid)
|
167
|
+
end
|
168
|
+
|
169
|
+
if block
|
170
|
+
sql = "select * from jobs where jid = '#{ jid }'"
|
171
|
+
execute(sql, &block)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
jid += 1
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
self
|
180
|
+
#--}}}
|
181
|
+
end
|
182
|
+
def resubmit(*jobs, &block)
|
183
|
+
#--{{{
|
184
|
+
now = Util::timestamp Time::now
|
185
|
+
|
186
|
+
transaction do
|
187
|
+
jobs.each do |job|
|
188
|
+
jid = Integer job['jid']
|
189
|
+
command = job['command']
|
190
|
+
stdin = job['stdin']
|
191
|
+
data = job['data']
|
192
|
+
|
193
|
+
raise "no jid for job <#{ job.inspect }>" unless jid
|
194
|
+
raise "no command for job <#{ job.inspect }>" unless command
|
195
|
+
|
196
|
+
tmp_stdin(stdin) do |ts|
|
197
|
+
tuple = QDB::tuple
|
198
|
+
|
199
|
+
tuple['jid'] = jid
|
200
|
+
tuple['command'] = command
|
201
|
+
tuple['priority'] = job['priority'] || 0
|
202
|
+
tuple['tag'] = job['tag']
|
203
|
+
tuple['runner'] = job['runner']
|
204
|
+
tuple['restartable'] = job['restartable']
|
205
|
+
tuple['state'] = 'pending'
|
206
|
+
tuple['submitted'] = now
|
207
|
+
tuple['submitter'] = Util::hostname
|
208
|
+
tuple['stdin'] = stdin4 jid
|
209
|
+
tuple['stdout'] = nil
|
210
|
+
tuple['stderr'] = nil
|
211
|
+
tuple['data'] = data4 jid
|
212
|
+
|
213
|
+
kvs = tuple.fields[1..-1].map{|f| "#{ f }=#{ QDB::q(tuple[ f ]) }"}
|
214
|
+
sql = "update jobs set #{ kvs.join ',' } where jid=#{ jid };\n"
|
215
|
+
|
216
|
+
execute(sql){}
|
217
|
+
|
218
|
+
FileUtils::rm_rf standard_in_4(jid)
|
219
|
+
FileUtils::rm_rf standard_out_4(jid)
|
220
|
+
FileUtils::rm_rf standard_err_4(jid)
|
221
|
+
#FileUtils::rm_rf data_4(jid)
|
222
|
+
FileUtils::cp ts.path, standard_in_4(jid) if ts
|
223
|
+
if data
|
224
|
+
FileUtils::mv data, data_4(jid)
|
225
|
+
else
|
226
|
+
FileUtils::mkdir_p data_4(jid)
|
227
|
+
end
|
228
|
+
|
229
|
+
if block
|
230
|
+
sql = "select * from jobs where jid = '#{ jid }'"
|
231
|
+
execute(sql, &block)
|
232
|
+
end
|
233
|
+
end # tmp_stdin
|
234
|
+
end # jobs.each
|
235
|
+
end # transaction
|
236
|
+
|
237
|
+
self
|
238
|
+
#--}}}
|
239
|
+
end
|
240
|
+
def tmp_stdin stdin = nil
|
241
|
+
#--{{{
|
242
|
+
#stdin = nil if stdin.to_s.empty?
|
243
|
+
if stdin.to_s.empty?
|
244
|
+
return(block_given? ? yield(nil) : nil)
|
245
|
+
end
|
246
|
+
stdin = STDIN if stdin == '-'
|
247
|
+
|
248
|
+
was_opened = false
|
249
|
+
|
250
|
+
begin
|
251
|
+
unless stdin.respond_to?('read') or stdin.nil?
|
252
|
+
stdin = stdin.to_s
|
253
|
+
# relative to queue
|
254
|
+
if stdin =~ %r|^@?stdin/\d+$|
|
255
|
+
stdin.gsub! %r|^@|, ''
|
256
|
+
stdin = File::join(path, stdin)
|
257
|
+
end
|
258
|
+
stdin = File.expand_path stdin
|
259
|
+
stdin = open stdin
|
260
|
+
was_opened = true
|
261
|
+
end
|
262
|
+
|
263
|
+
tmp = Tempfile::new "#{ Process::pid }_#{ rand }"
|
264
|
+
while((buf = stdin.read(8192))); tmp.write buf; end if stdin
|
265
|
+
tmp.close
|
266
|
+
|
267
|
+
if block_given?
|
268
|
+
begin
|
269
|
+
yield tmp
|
270
|
+
ensure
|
271
|
+
tmp.close!
|
272
|
+
end
|
273
|
+
else
|
274
|
+
return tmp
|
275
|
+
end
|
276
|
+
ensure
|
277
|
+
stdin.close if was_opened rescue nil
|
278
|
+
end
|
279
|
+
#--}}}
|
280
|
+
end
|
281
|
+
def list(*whats, &block)
|
282
|
+
#--{{{
|
283
|
+
ret = nil
|
284
|
+
|
285
|
+
whats.replace(%w( pending running finished dead )) if
|
286
|
+
whats.empty? or whats.include?('all')
|
287
|
+
|
288
|
+
whats.map! do |what|
|
289
|
+
case what
|
290
|
+
when %r/^\s*p/io
|
291
|
+
'pending'
|
292
|
+
when %r/^\s*h/io
|
293
|
+
'holding'
|
294
|
+
when %r/^\s*r/io
|
295
|
+
'running'
|
296
|
+
when %r/^\s*f/io
|
297
|
+
'finished'
|
298
|
+
when %r/^\s*d/io
|
299
|
+
'dead'
|
300
|
+
else
|
301
|
+
what
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
where_clauses = []
|
306
|
+
|
307
|
+
whats.each do |what|
|
308
|
+
case what
|
309
|
+
when Numeric
|
310
|
+
where_clauses << "jid=#{ what }\n"
|
311
|
+
else
|
312
|
+
what = "#{ what }"
|
313
|
+
if what.to_s =~ %r/^\s*\d+\s*$/o
|
314
|
+
where_clauses << "jid=#{ QDB::q what }\n"
|
315
|
+
else
|
316
|
+
where_clauses << "state=#{ QDB::q what }\n"
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
where_clause = where_clauses.join(" or \n")
|
322
|
+
|
323
|
+
sql = <<-sql
|
324
|
+
select * from jobs
|
325
|
+
where #{ where_clause }
|
326
|
+
sql
|
327
|
+
|
328
|
+
if block
|
329
|
+
ro_transaction{ execute(sql, &block) }
|
330
|
+
else
|
331
|
+
ret = ro_transaction{ execute(sql) }
|
332
|
+
end
|
333
|
+
|
334
|
+
ret
|
335
|
+
#--}}}
|
336
|
+
end
|
337
|
+
def status options = {}
|
338
|
+
#--{{{
|
339
|
+
stats = OrderedAutoHash::new
|
340
|
+
|
341
|
+
now = Time::now
|
342
|
+
|
343
|
+
hms = lambda do |t|
|
344
|
+
elapsed =
|
345
|
+
begin
|
346
|
+
Float t
|
347
|
+
rescue
|
348
|
+
now - Util::stamptime(t, 'local' => true)
|
349
|
+
end
|
350
|
+
sh, sm, ss = Util::hms elapsed.to_f
|
351
|
+
s = "#{ '%2.2d' % sh }h#{ '%2.2d' % sm }m#{ '%05.2f' % ss }s"
|
352
|
+
end
|
353
|
+
|
354
|
+
exit_code_map =
|
355
|
+
options[:exit_code_map] || options['exit_code_map'] || {}
|
356
|
+
|
357
|
+
ro_transaction do
|
358
|
+
#
|
359
|
+
# jobs stats
|
360
|
+
#
|
361
|
+
total = 0
|
362
|
+
%w( pending holding running finished dead ).each do |state|
|
363
|
+
sql = <<-sql
|
364
|
+
select count(*) from jobs
|
365
|
+
where
|
366
|
+
state='#{ state }'
|
367
|
+
sql
|
368
|
+
tuples = execute sql
|
369
|
+
tuple = tuples.first
|
370
|
+
count = (tuple ? Integer(tuple.first || 0) : 0)
|
371
|
+
stats['jobs'][state] = count
|
372
|
+
total += count
|
373
|
+
end
|
374
|
+
stats['jobs']['total'] = total
|
375
|
+
#
|
376
|
+
# temporal stats
|
377
|
+
#
|
378
|
+
metrics = OrderedAutoHash::new
|
379
|
+
metrics['pending'] = 'submitted'
|
380
|
+
metrics['holding'] = 'submitted'
|
381
|
+
metrics['running'] = 'started'
|
382
|
+
metrics['finished'] = 'elapsed'
|
383
|
+
metrics['dead'] = 'elapsed'
|
384
|
+
|
385
|
+
metrics.each do |state, metric|
|
386
|
+
sql =
|
387
|
+
unless metric == 'elapsed'
|
388
|
+
<<-sql
|
389
|
+
select min(#{ metric }) as max, max(#{ metric }) as min
|
390
|
+
from jobs where state='#{ state }'
|
391
|
+
sql
|
392
|
+
else
|
393
|
+
<<-sql
|
394
|
+
select min(#{ metric }) as min, max(#{ metric }) as max
|
395
|
+
from jobs where state='#{ state }'
|
396
|
+
sql
|
397
|
+
end
|
398
|
+
tuple = execute(sql).first
|
399
|
+
next unless tuple
|
400
|
+
|
401
|
+
%w( min max ).each do |time|
|
402
|
+
oh = nil
|
403
|
+
t = tuple[time]
|
404
|
+
if t
|
405
|
+
sql = <<-sql
|
406
|
+
select jid from jobs where #{ metric }='#{ t }' and state='#{ state }'
|
407
|
+
sql
|
408
|
+
which = execute(sql).first
|
409
|
+
jid = (which and which['jid']).to_i
|
410
|
+
if jid
|
411
|
+
oh = OrderedAutoHash::new
|
412
|
+
oh[jid] = hms[t]
|
413
|
+
oh.yaml_inline = true
|
414
|
+
end
|
415
|
+
stats['temporal'][state][time] = oh
|
416
|
+
end
|
417
|
+
end
|
418
|
+
#stats['temporal'][state] ||= nil
|
419
|
+
end
|
420
|
+
stats['temporal'] ||= nil
|
421
|
+
#
|
422
|
+
# generate performance stats
|
423
|
+
#
|
424
|
+
sql = <<-sql
|
425
|
+
select avg(elapsed) from jobs
|
426
|
+
where
|
427
|
+
state='finished'
|
428
|
+
sql
|
429
|
+
tuples = execute sql
|
430
|
+
tuple = tuples.first
|
431
|
+
avg = (tuple ? Float(tuple.first || 0) : 0)
|
432
|
+
stats['performance']['avg_time_per_job'] = hms[avg]
|
433
|
+
|
434
|
+
list = []
|
435
|
+
0.step(5){|i| list << (2 ** i)}
|
436
|
+
list << 24
|
437
|
+
list.sort!
|
438
|
+
|
439
|
+
list = 1, 12, 24
|
440
|
+
|
441
|
+
list.each do |n|
|
442
|
+
ago = now - (n * 3600)
|
443
|
+
ago = Util::timestamp ago
|
444
|
+
sql = <<-sql
|
445
|
+
select count(*) from jobs
|
446
|
+
where
|
447
|
+
state = 'finished' and
|
448
|
+
finished > '#{ ago }'
|
449
|
+
sql
|
450
|
+
tuples = execute sql
|
451
|
+
tuple = tuples.first
|
452
|
+
count = (tuple ? Integer(tuple.first || 0) : 0)
|
453
|
+
#stats['performance']["n_jobs_in_last_#{ n }_hrs"] = count
|
454
|
+
stats['performance']["n_jobs_in_last_hrs"][n] = count
|
455
|
+
end
|
456
|
+
|
457
|
+
#
|
458
|
+
# generate exit_status stats
|
459
|
+
#
|
460
|
+
#stats['exit_status'] = {}
|
461
|
+
sql = <<-sql
|
462
|
+
select count(*) from jobs
|
463
|
+
where
|
464
|
+
state='finished' and
|
465
|
+
exit_status=0
|
466
|
+
sql
|
467
|
+
tuples = execute sql
|
468
|
+
tuple = tuples.first
|
469
|
+
successes = (tuple ? Integer(tuple.first || 0) : 0)
|
470
|
+
stats['exit_status']['successes'] = successes
|
471
|
+
|
472
|
+
sql = <<-sql
|
473
|
+
select count(*) from jobs
|
474
|
+
where
|
475
|
+
(state='finished' and
|
476
|
+
exit_status!=0) or
|
477
|
+
state='dead'
|
478
|
+
sql
|
479
|
+
tuples = execute sql
|
480
|
+
tuple = tuples.first
|
481
|
+
failures = (tuple ? Integer(tuple.first || 0) : 0)
|
482
|
+
stats['exit_status']['failures'] = failures
|
483
|
+
|
484
|
+
exit_code_map.each do |which, codes|
|
485
|
+
exit_status_clause = codes.map{|code| "exit_status=#{ code }"}.join(' or ')
|
486
|
+
sql = <<-sql
|
487
|
+
select count(*) from jobs
|
488
|
+
where
|
489
|
+
(state='finished' and (#{ exit_status_clause }))
|
490
|
+
sql
|
491
|
+
tuples = execute sql
|
492
|
+
tuple = tuples.first
|
493
|
+
n = (tuple ? Integer(tuple.first || 0) : 0)
|
494
|
+
stats['exit_status'][which] = n
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
stats
|
499
|
+
#--}}}
|
500
|
+
end
|
501
|
+
def query(where_clause = nil, &block)
|
502
|
+
#--{{{
|
503
|
+
ret = nil
|
504
|
+
|
505
|
+
sql =
|
506
|
+
if where_clause
|
507
|
+
|
508
|
+
#
|
509
|
+
# turn =~ into like clauses
|
510
|
+
#
|
511
|
+
#where_clause.gsub!(/(=~\s*([^\s')(=]+))/om){q = $2.gsub(%r/'+|\s+/o,''); "like '%#{ q }%'"}
|
512
|
+
#
|
513
|
+
# quote everything on the rhs of an '=' sign - helps with shell problems...
|
514
|
+
#
|
515
|
+
#where_clause.gsub!(/(==?\s*([^\s')(=]+))/om){q = $2.gsub(%r/'+|\s+/o,''); "='#{ q }'"}
|
516
|
+
|
517
|
+
"select * from jobs where #{ where_clause };"
|
518
|
+
else
|
519
|
+
"select * from jobs;"
|
520
|
+
end
|
521
|
+
|
522
|
+
if block
|
523
|
+
ro_transaction{ execute(sql, &block) }
|
524
|
+
else
|
525
|
+
ret = ro_transaction{ execute(sql) }
|
526
|
+
end
|
527
|
+
|
528
|
+
ret
|
529
|
+
#--}}}
|
530
|
+
end
|
531
|
+
def delete(*args, &block)
|
532
|
+
#--{{{
|
533
|
+
whats, optargs = args.partition{|arg| not Hash === arg}
|
534
|
+
|
535
|
+
opts = {}
|
536
|
+
optargs.each{|oa| opts.update oa}
|
537
|
+
|
538
|
+
force = Util::getopt 'force', opts
|
539
|
+
|
540
|
+
delete_sql, select_sql = '', ''
|
541
|
+
|
542
|
+
whats << 'all' if whats.empty?
|
543
|
+
|
544
|
+
whats.each do |what|
|
545
|
+
case "#{ what }"
|
546
|
+
when %r/^\s*\d+\s*$/io # number
|
547
|
+
delete_sql << "delete from jobs where jid=#{ what } and state!='running';\n"
|
548
|
+
select_sql << "select * from jobs where jid=#{ what } and state!='running';\n"
|
549
|
+
when %r/^\s*p/io # pending
|
550
|
+
delete_sql << "delete from jobs where state='pending';\n"
|
551
|
+
select_sql << "select * from jobs where state='pending';\n"
|
552
|
+
when %r/^\s*h/io # holding
|
553
|
+
delete_sql << "delete from jobs where state='holding';\n"
|
554
|
+
select_sql << "select * from jobs where state='holding';\n"
|
555
|
+
when %r/^\s*r/io # running
|
556
|
+
delete_sql << "delete from jobs where state='running';\n" if force
|
557
|
+
select_sql << "select * from jobs where state='running';\n" if force
|
558
|
+
when %r/^\s*f/io # finished
|
559
|
+
delete_sql << "delete from jobs where state='finished';\n"
|
560
|
+
select_sql << "select * from jobs where state='finished';\n"
|
561
|
+
when %r/^\s*d/io # dead
|
562
|
+
delete_sql << "delete from jobs where state='dead';\n"
|
563
|
+
select_sql << "select * from jobs where state='dead';\n"
|
564
|
+
when %r/^\s*a/io # all
|
565
|
+
delete_sql << "delete from jobs where state!='running';\n"
|
566
|
+
select_sql << "select * from jobs where state!='running';\n"
|
567
|
+
else
|
568
|
+
raise ArgumentError, "cannot delete <#{ what.inspect }>"
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
scrub = lambda do |jid|
|
573
|
+
[standard_in_4(jid), standard_out_4(jid), standard_err_4(jid), data_4(jid)].each do |path|
|
574
|
+
FileUtils::rm_rf path
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
tuples = []
|
579
|
+
|
580
|
+
metablock =
|
581
|
+
if block
|
582
|
+
lambda do |tuple|
|
583
|
+
jid = tuple['jid']
|
584
|
+
block[tuple]
|
585
|
+
scrub[jid]
|
586
|
+
end
|
587
|
+
else
|
588
|
+
lambda do |tuple|
|
589
|
+
jid = tuple['jid']
|
590
|
+
scrub[jid]
|
591
|
+
tuples << tuple
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
# TODO - make file deletion transactional too
|
596
|
+
|
597
|
+
transaction do
|
598
|
+
execute(select_sql, &metablock)
|
599
|
+
execute(delete_sql){}
|
600
|
+
end
|
601
|
+
|
602
|
+
delete_sql = nil
|
603
|
+
select_sql = nil
|
604
|
+
|
605
|
+
block ? nil : tuples
|
606
|
+
#--}}}
|
607
|
+
end
|
608
|
+
def vacuum
|
609
|
+
#--{{{
|
610
|
+
@qdb.vacuum
|
611
|
+
#--}}}
|
612
|
+
end
|
613
|
+
def update(kvs, *jids, &block)
|
614
|
+
#--{{{
|
615
|
+
ret = nil
|
616
|
+
#
|
617
|
+
# yank out stdin - which we allow as a key
|
618
|
+
#
|
619
|
+
stdin = kvs.delete 'stdin'
|
620
|
+
data = kvs.delete 'data'
|
621
|
+
#
|
622
|
+
# validate/munge state value iff present
|
623
|
+
#
|
624
|
+
if((state = kvs['state']))
|
625
|
+
case state
|
626
|
+
when %r/^p/io
|
627
|
+
kvs['state'] = 'pending'
|
628
|
+
when %r/^h/io
|
629
|
+
kvs['state'] = 'holding'
|
630
|
+
else
|
631
|
+
raise "update of <state> = <#{ state }> not allowed (try pending or holding)"
|
632
|
+
end
|
633
|
+
end
|
634
|
+
#
|
635
|
+
# validate kvs pairs
|
636
|
+
#
|
637
|
+
allowed = %w( priority command tag runner restartable )
|
638
|
+
kvs.each do |key, val|
|
639
|
+
raise "update of <#{ key }> = <#{ val }> not allowed" unless
|
640
|
+
(allowed.include?(key)) or (key == 'state' and %w( pending holding ).include?(val))
|
641
|
+
end
|
642
|
+
#
|
643
|
+
# ensure there are acutally some jobs to update
|
644
|
+
#
|
645
|
+
raise "no jobs to update" if jids.empty?
|
646
|
+
#
|
647
|
+
# generates sql to update jids with kvs and sql to show updated tuples
|
648
|
+
#
|
649
|
+
build_sql =
|
650
|
+
lambda do |kvs, jids|
|
651
|
+
if(jids.delete('pending'))
|
652
|
+
execute("select jid from jobs where state='pending'") do |tuple|
|
653
|
+
jids << tuple['jid']
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
if(jids.delete('holding'))
|
658
|
+
execute("select jid from jobs where state='holding'") do |tuple|
|
659
|
+
jids << tuple['jid']
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
rollback_transaction "no jobs to update" if jids.empty?
|
664
|
+
|
665
|
+
update_clause = kvs.map{|k,v| v ? "#{ k }='#{ v }'" : "#{ k }=NULL" }.join(",\n")
|
666
|
+
where_clause = jids.map{|jid| "jid=#{ jid }"}.join(" or\n")
|
667
|
+
update_sql =
|
668
|
+
"update jobs\n" <<
|
669
|
+
"set\n#{ update_clause }\n" <<
|
670
|
+
"where\n(state='pending' or state='holding') and\n(#{ where_clause })"
|
671
|
+
select_sql = "select * from jobs where (state='pending' or state='holding') and\n(#{ where_clause })"
|
672
|
+
|
673
|
+
if kvs.empty?
|
674
|
+
[ nil, select_sql ]
|
675
|
+
else
|
676
|
+
[ update_sql, select_sql ]
|
677
|
+
end
|
678
|
+
end
|
679
|
+
#
|
680
|
+
# setup stdin
|
681
|
+
#
|
682
|
+
tmp_stdin(stdin) do |ts|
|
683
|
+
clobber_stdin = lambda do |job|
|
684
|
+
FileUtils::cp ts.path, standard_in_4(job['jid']) if ts
|
685
|
+
true
|
686
|
+
end
|
687
|
+
|
688
|
+
clobber_data = lambda do |job|
|
689
|
+
if data
|
690
|
+
FileUtils::rm_rf data_4(job['jid'])
|
691
|
+
FileUtils::cp_r data, data_4(job['jid'])
|
692
|
+
end
|
693
|
+
true
|
694
|
+
end
|
695
|
+
|
696
|
+
tuples = []
|
697
|
+
|
698
|
+
metablock =
|
699
|
+
if block
|
700
|
+
lambda{|job| clobber_stdin[job] and clobber_data[job] and block[job]}
|
701
|
+
else
|
702
|
+
lambda{|job| clobber_stdin[job] and clobber_data[job] and tuples << job}
|
703
|
+
end
|
704
|
+
|
705
|
+
transaction do
|
706
|
+
update_sql, select_sql = build_sql[kvs, jids]
|
707
|
+
break unless select_sql
|
708
|
+
execute(update_sql){} if update_sql
|
709
|
+
execute(select_sql, &metablock)
|
710
|
+
end
|
711
|
+
|
712
|
+
block ? nil : tuples
|
713
|
+
end
|
714
|
+
#--}}}
|
715
|
+
end
|
716
|
+
|
717
|
+
def getjob
|
718
|
+
#--{{{
|
719
|
+
sql = <<-sql
|
720
|
+
select * from jobs
|
721
|
+
where
|
722
|
+
(state='pending' or (state='dead' and (not restartable isnull))) and
|
723
|
+
(runner like '%#{ Util::host }%' or runner isnull)
|
724
|
+
order by priority desc, submitted asc, jid asc
|
725
|
+
limit 1;
|
726
|
+
sql
|
727
|
+
tuples = execute sql
|
728
|
+
job = tuples.first
|
729
|
+
job
|
730
|
+
#--}}}
|
731
|
+
end
|
732
|
+
def jobisrunning job
|
733
|
+
#--{{{
|
734
|
+
sql = <<-sql
|
735
|
+
update jobs
|
736
|
+
set
|
737
|
+
pid='#{ job['pid'] }',
|
738
|
+
state='#{ job['state'] }',
|
739
|
+
started='#{ job['started'] }',
|
740
|
+
runner='#{ job['runner'] }',
|
741
|
+
stdout='#{ job['stdout'] }',
|
742
|
+
stderr='#{ job['stderr'] }'
|
743
|
+
where jid=#{ job['jid'] };
|
744
|
+
sql
|
745
|
+
execute sql
|
746
|
+
#--}}}
|
747
|
+
end
|
748
|
+
def jobisdone job
|
749
|
+
#--{{{
|
750
|
+
sql = <<-sql
|
751
|
+
update jobs
|
752
|
+
set
|
753
|
+
state = '#{ job['state'] }',
|
754
|
+
exit_status = '#{ job['exit_status'] }',
|
755
|
+
finished = '#{ job['finished'] }',
|
756
|
+
elapsed = '#{ job['elapsed'] }'
|
757
|
+
where jid = #{ job['jid'] };
|
758
|
+
sql
|
759
|
+
execute sql
|
760
|
+
#--}}}
|
761
|
+
end
|
762
|
+
def getdeadjobs(started, &block)
|
763
|
+
#--{{{
|
764
|
+
ret = nil
|
765
|
+
sql = <<-sql
|
766
|
+
select * from jobs
|
767
|
+
where
|
768
|
+
state = 'running' and
|
769
|
+
runner='#{ Util::hostname }' and
|
770
|
+
started<='#{ started }'
|
771
|
+
sql
|
772
|
+
if block
|
773
|
+
execute(sql, &block)
|
774
|
+
else
|
775
|
+
ret = execute(sql)
|
776
|
+
end
|
777
|
+
ret
|
778
|
+
#--}}}
|
779
|
+
end
|
780
|
+
def jobisdead job
|
781
|
+
#--{{{
|
782
|
+
jid = job['jid']
|
783
|
+
if jid
|
784
|
+
sql = "update jobs set state='dead' where jid='#{ jid }'"
|
785
|
+
execute(sql){}
|
786
|
+
end
|
787
|
+
job
|
788
|
+
#--}}}
|
789
|
+
end
|
790
|
+
|
791
|
+
def transaction(*args)
|
792
|
+
#--{{{
|
793
|
+
raise "cannot upgrade ro_transaction" if @in_ro_transaction
|
794
|
+
ret = nil
|
795
|
+
if @in_transaction
|
796
|
+
ret = yield
|
797
|
+
else
|
798
|
+
begin
|
799
|
+
@in_transaction = true
|
800
|
+
@qdb.transaction(*args){ ret = yield }
|
801
|
+
ensure
|
802
|
+
@in_transaction = false
|
803
|
+
end
|
804
|
+
end
|
805
|
+
ret
|
806
|
+
#--}}}
|
807
|
+
end
|
808
|
+
def ro_transaction(*args)
|
809
|
+
#--{{{
|
810
|
+
ret = nil
|
811
|
+
if @in_ro_transaction || @in_transaction
|
812
|
+
ret = yield
|
813
|
+
else
|
814
|
+
begin
|
815
|
+
@in_ro_transaction = true
|
816
|
+
@qdb.ro_transaction(*args){ ret = yield }
|
817
|
+
ensure
|
818
|
+
@in_ro_transaction = false
|
819
|
+
end
|
820
|
+
end
|
821
|
+
ret
|
822
|
+
#--}}}
|
823
|
+
end
|
824
|
+
def execute(*args, &block)
|
825
|
+
#--{{{
|
826
|
+
@qdb.execute(*args, &block)
|
827
|
+
#--}}}
|
828
|
+
end
|
829
|
+
def integrity_check(*args, &block)
|
830
|
+
#--{{{
|
831
|
+
@qdb.integrity_check(*args, &block)
|
832
|
+
#--}}}
|
833
|
+
end
|
834
|
+
def recover!(*args, &block)
|
835
|
+
#--{{{
|
836
|
+
@qdb.recover!(*args, &block)
|
837
|
+
#--}}}
|
838
|
+
end
|
839
|
+
def lock(*args, &block)
|
840
|
+
#--{{{
|
841
|
+
@qdb.lock(*args, &block)
|
842
|
+
#--}}}
|
843
|
+
end
|
844
|
+
def abort_transaction(*a,&b)
|
845
|
+
#--{{{
|
846
|
+
@qdb.abort_transaction(*a,&b)
|
847
|
+
#--}}}
|
848
|
+
end
|
849
|
+
def rollback_transaction(*a,&b)
|
850
|
+
#--{{{
|
851
|
+
@qdb.rollback_transaction(*a,&b)
|
852
|
+
#--}}}
|
853
|
+
end
|
854
|
+
|
855
|
+
def snapshot qtmp = "#{ @basename }.snapshot", retries = nil
|
856
|
+
#--{{{
|
857
|
+
qtmp ||= "#{ @basename }.snapshot"
|
858
|
+
debug{ "snapshot <#{ @path }> -> <#{ qtmp }>" }
|
859
|
+
retries = Integer(retries || 16)
|
860
|
+
debug{ "retries <#{ retries }>" }
|
861
|
+
|
862
|
+
qss = nil
|
863
|
+
loopno = 0
|
864
|
+
|
865
|
+
take_snapshot = lambda do
|
866
|
+
FileUtils::rm_rf qtmp
|
867
|
+
FileUtils::mkdir_p qtmp
|
868
|
+
%w(db db.schema lock).each do |base|
|
869
|
+
src, dest = File::join(@path, base), File::join(qtmp, base)
|
870
|
+
debug{ "cp <#{ src }> -> <#{ dest }>" }
|
871
|
+
FileUtils::cp(src, dest)
|
872
|
+
end
|
873
|
+
ss = klass::new qtmp, @opts
|
874
|
+
if ss.integrity_check
|
875
|
+
ss
|
876
|
+
else
|
877
|
+
begin; recover! unless integrity_check; rescue; nil; end
|
878
|
+
ss.recover!
|
879
|
+
end
|
880
|
+
end
|
881
|
+
|
882
|
+
loop do
|
883
|
+
break if loopno >= retries
|
884
|
+
if((ss = take_snapshot.call))
|
885
|
+
debug{ "snapshot <#{ qtmp }> created" }
|
886
|
+
qss = ss
|
887
|
+
break
|
888
|
+
else
|
889
|
+
debug{ "failure <#{ loopno + 1}> of <#{ retries }> attempts to create snapshot <#{ qtmp }> - retrying..." }
|
890
|
+
end
|
891
|
+
loopno += 1
|
892
|
+
end
|
893
|
+
|
894
|
+
unless qss
|
895
|
+
debug{ "locking <#{ @path }> as last resort" }
|
896
|
+
@qdb.write_lock do
|
897
|
+
if((ss = take_snapshot.call))
|
898
|
+
debug{ "snapshot <#{ qtmp }> created" }
|
899
|
+
qss = ss
|
900
|
+
else
|
901
|
+
raise "failed <#{ loopno }> times to create snapshot <#{ qtmp }>"
|
902
|
+
end
|
903
|
+
end
|
904
|
+
end
|
905
|
+
|
906
|
+
qss
|
907
|
+
#--}}}
|
908
|
+
end
|
909
|
+
|
910
|
+
# TODO - use mtime to optimize checks by feeder??
|
911
|
+
def mtime
|
912
|
+
#--{{{
|
913
|
+
File::stat(@path).mtime
|
914
|
+
#--}}}
|
915
|
+
end
|
916
|
+
def []= key, value
|
917
|
+
#--{{{
|
918
|
+
sql = "select count(*) from attributes where key='#{ key }';"
|
919
|
+
tuples = @qdb.execute sql
|
920
|
+
tuple = tuples.first
|
921
|
+
count = Integer tuple['count(*)']
|
922
|
+
case count
|
923
|
+
when 0
|
924
|
+
sql = "insert into attributes values('#{ key }','#{ value }');"
|
925
|
+
@qdb.execute sql
|
926
|
+
when 1
|
927
|
+
sql = "update attributes set key='#{ key }', value='#{ value }' where key='#{ key }';"
|
928
|
+
@qdb.execute sql
|
929
|
+
else
|
930
|
+
raise "key <#{ key }> has become corrupt!"
|
931
|
+
end
|
932
|
+
#--}}}
|
933
|
+
end
|
934
|
+
def attributes
|
935
|
+
#--{{{
|
936
|
+
h = {}
|
937
|
+
tuples = @qdb.execute "select * from attributes;"
|
938
|
+
tuples.map!{|t| h[t['key']] = t['value']}
|
939
|
+
h
|
940
|
+
#--}}}
|
941
|
+
end
|
942
|
+
#--}}}
|
943
|
+
end # class JobQueue
|
944
|
+
#--}}}
|
945
|
+
end # module RQ
|
946
|
+
$__rq_jobqueue__ = __FILE__
|
947
|
+
end
|