jcarley-simplews 1.11.0
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/LICENSE +20 -0
- data/README.rdoc +22 -0
- data/bin/start_jobs_ws +53 -0
- data/bin/start_ws +48 -0
- data/lib/rake_pipeline.rb +237 -0
- data/lib/simplews.rb +425 -0
- data/lib/simplews/jobs.rb +502 -0
- data/lib/simplews/notifier.rb +138 -0
- data/lib/simplews/rake.rb +72 -0
- metadata +90 -0
@@ -0,0 +1,502 @@
|
|
1
|
+
require File.join(File.dirname(File.dirname(__FILE__)) + '/simplews')
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'singleton'
|
5
|
+
require 'rand'
|
6
|
+
require 'zlib'
|
7
|
+
require 'base64'
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
class SimpleWS::Jobs < SimpleWS
|
12
|
+
class JobNotFound < Exception; end
|
13
|
+
class ResultNotFound < Exception; end
|
14
|
+
class Aborted < Exception; end
|
15
|
+
|
16
|
+
|
17
|
+
SLEEP_TIMES = {
|
18
|
+
:job_info => 1,
|
19
|
+
:monitor => 2,
|
20
|
+
} unless defined? SLEEP_TIMES
|
21
|
+
|
22
|
+
INHERITED_TASKS = {} unless defined? INHERITED_TASKS
|
23
|
+
|
24
|
+
|
25
|
+
#{{{ Scheduler
|
26
|
+
module Scheduler
|
27
|
+
include Process
|
28
|
+
|
29
|
+
@@task_results = {}
|
30
|
+
|
31
|
+
@@names = []
|
32
|
+
@@pids = {}
|
33
|
+
|
34
|
+
@@queue = []
|
35
|
+
@@max_jobs = 3
|
36
|
+
|
37
|
+
def self.queue_size(size)
|
38
|
+
@@max_jobs = size
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def self.random_name(s="", num=20)
|
43
|
+
num.times{
|
44
|
+
r = rand
|
45
|
+
if r < 0.3
|
46
|
+
s << (rand * 10).to_i.to_s
|
47
|
+
elsif r < 0.6
|
48
|
+
s << (rand * (?z - ?a) + ?a).to_i.chr
|
49
|
+
else
|
50
|
+
s << (rand * (?Z - ?A) + ?A).to_i.chr
|
51
|
+
end
|
52
|
+
}
|
53
|
+
s.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.make_name(name = "")
|
57
|
+
name = Scheduler::random_name("job-") unless name =~ /\w/
|
58
|
+
|
59
|
+
taken = @@names.select{|n| n =~ /^#{ Regexp.quote name }(?:-\d+)?$/}
|
60
|
+
taken += Job.taken(name)
|
61
|
+
taken = taken.sort.uniq
|
62
|
+
if taken.any?
|
63
|
+
if taken.length == 1
|
64
|
+
return name + '-2'
|
65
|
+
else
|
66
|
+
last = taken.collect{|s|
|
67
|
+
if s.match(/-(\d+)$/)
|
68
|
+
$1.to_i
|
69
|
+
else
|
70
|
+
1
|
71
|
+
end
|
72
|
+
}.sort.last
|
73
|
+
return name + '-' + (last + 1).to_s
|
74
|
+
end
|
75
|
+
else
|
76
|
+
return name
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.configure(name, value)
|
81
|
+
Job::configure(name, value)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.helper(name, block)
|
85
|
+
Job.send :define_method, name, block
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.task(name, results, block)
|
89
|
+
@@task_results[name] = results
|
90
|
+
Job.send :define_method, name, block
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.dequeue
|
94
|
+
if @@pids.length < @@max_jobs && @@queue.any?
|
95
|
+
job_info = @@queue.pop
|
96
|
+
|
97
|
+
pid = Job.new.run(job_info[:task], job_info[:name], @@task_results[job_info[:task]], *job_info[:args])
|
98
|
+
|
99
|
+
@@pids[job_info[:name]] = pid
|
100
|
+
pid
|
101
|
+
else
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.queue
|
107
|
+
@@queue
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.run(task, *args)
|
111
|
+
suggested_name = *args.pop
|
112
|
+
name = make_name(suggested_name)
|
113
|
+
@@names << name
|
114
|
+
|
115
|
+
@@queue.push( {:name => name, :task => task, :args => args})
|
116
|
+
state = {
|
117
|
+
:name => name,
|
118
|
+
:status => :queued,
|
119
|
+
:messages => [],
|
120
|
+
:info => {},
|
121
|
+
}
|
122
|
+
Job.save(name,state)
|
123
|
+
|
124
|
+
name
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.clean_job(pid)
|
128
|
+
name = @@pids.select{|name, p| p == pid}.first
|
129
|
+
return if name.nil?
|
130
|
+
name = name.first
|
131
|
+
puts "Job #{ name } with pid #{ pid } finished with exitstatus #{$?.exitstatus}"
|
132
|
+
state = Job.job_info(name)
|
133
|
+
if ![:error, :done, :aborted].include?(state[:status])
|
134
|
+
state[:status] = :error
|
135
|
+
state[:messages] << "Job finished for unknown reasons"
|
136
|
+
Job.save(name, state)
|
137
|
+
end
|
138
|
+
@@pids.delete(name)
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.job_monitor
|
142
|
+
Thread.new{
|
143
|
+
while true
|
144
|
+
begin
|
145
|
+
pid = dequeue
|
146
|
+
if pid.nil?
|
147
|
+
if @@pids.any?
|
148
|
+
pid_exit = Process.wait(-1, Process::WNOHANG)
|
149
|
+
if pid_exit
|
150
|
+
clean_job(pid_exit)
|
151
|
+
else
|
152
|
+
sleep SimpleWS::Jobs::SLEEP_TIMES[:monitor]
|
153
|
+
end
|
154
|
+
else
|
155
|
+
sleep SimpleWS::Jobs::SLEEP_TIMES[:monitor]
|
156
|
+
end
|
157
|
+
else
|
158
|
+
sleep SimpleWS::Jobs::SLEEP_TIMES[:monitor]
|
159
|
+
end
|
160
|
+
rescue
|
161
|
+
puts $!.message
|
162
|
+
puts $!.backtrace.join("\n")
|
163
|
+
sleep 2
|
164
|
+
end
|
165
|
+
end
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.abort(name)
|
170
|
+
Process.kill("INT", @@pids[name]) if @@pids[name]
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.abort_jobs
|
174
|
+
@@pids.values{|pid|
|
175
|
+
Process.kill "INT", pid
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.job_info(name)
|
180
|
+
Job.job_info(name)
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.workdir=(workdir)
|
184
|
+
Job.workdir = workdir
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.job_results(name)
|
188
|
+
Job.results(name)
|
189
|
+
end
|
190
|
+
|
191
|
+
#{{{ Job
|
192
|
+
|
193
|
+
class Job
|
194
|
+
def self.workdir=(workdir)
|
195
|
+
@@workdir = workdir
|
196
|
+
@@savedir = File.join(@@workdir, '.save')
|
197
|
+
FileUtils.mkdir_p @@workdir unless File.exist? @@workdir
|
198
|
+
FileUtils.mkdir_p @@savedir unless File.exist? @@savedir
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.taken(name = "")
|
202
|
+
Dir.glob(@@savedir + "/#{ name }*.marshal").
|
203
|
+
collect{|n| n.match(/\/(#{ Regexp.quote name }(?:-\d+)?).marshal/); $1}.compact
|
204
|
+
end
|
205
|
+
def self.path(file, name)
|
206
|
+
if file =~ /^\/|#{@@workdir}/
|
207
|
+
file.gsub(/\{JOB\}/, name)
|
208
|
+
else
|
209
|
+
File.join(@@workdir, file.gsub(/\{JOB\}/,name))
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def self.save(name, state)
|
214
|
+
fout = File.open(File.join(@@savedir,name + '.marshal'),'w')
|
215
|
+
fout.write Marshal::dump(state)
|
216
|
+
fout.close
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.job_info(name)
|
220
|
+
info = nil
|
221
|
+
|
222
|
+
retries = 2
|
223
|
+
begin
|
224
|
+
info = Marshal::load(File.open(File.join(@@savedir,name + '.marshal')))
|
225
|
+
raise Exception unless info.is_a?(Hash) && info[:info]
|
226
|
+
rescue Exception
|
227
|
+
if retries > 0
|
228
|
+
retries -= 1
|
229
|
+
sleep SimpleWS::Jobs::SLEEP_TIMES[:job_info]
|
230
|
+
retry
|
231
|
+
end
|
232
|
+
info = nil
|
233
|
+
end
|
234
|
+
|
235
|
+
raise JobNotFound, "Job with name '#{ name }' was not found" if info.nil?
|
236
|
+
|
237
|
+
if info[:queued] && !@@queue.collect{|info| info[:name]}.include?(name)
|
238
|
+
FileUtils.rm(File.join(@@savedir, name + '.marshal'))
|
239
|
+
raise Aborted, "Job #{ name } has been removed from the queue"
|
240
|
+
end
|
241
|
+
|
242
|
+
info
|
243
|
+
end
|
244
|
+
|
245
|
+
def self.results(name)
|
246
|
+
job_info(name)[:results].collect{|file|
|
247
|
+
code = Scheduler.random_name("res-")
|
248
|
+
[code, file]
|
249
|
+
}
|
250
|
+
end
|
251
|
+
|
252
|
+
@@config = {}
|
253
|
+
def self.configure(name, value)
|
254
|
+
@@config[name] = value
|
255
|
+
end
|
256
|
+
|
257
|
+
def workdir
|
258
|
+
@@workdir
|
259
|
+
end
|
260
|
+
|
261
|
+
def config
|
262
|
+
@@config
|
263
|
+
end
|
264
|
+
|
265
|
+
def path(file)
|
266
|
+
Job.path(file, @name)
|
267
|
+
end
|
268
|
+
|
269
|
+
def save
|
270
|
+
Job.save(@name, @state)
|
271
|
+
end
|
272
|
+
|
273
|
+
def write(file, contents)
|
274
|
+
path = Job.path(file, @name)
|
275
|
+
directory = File.dirname(File.expand_path(path))
|
276
|
+
FileUtils.mkdir_p directory unless File.exists? directory
|
277
|
+
File.open(path,'w') do |fout|
|
278
|
+
fout.write contents
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def message(message)
|
283
|
+
@state[:messages] << message
|
284
|
+
save
|
285
|
+
end
|
286
|
+
def step(status, message = nil)
|
287
|
+
@state[:status] = status
|
288
|
+
@state[:messages] << message if message && message != ""
|
289
|
+
save
|
290
|
+
end
|
291
|
+
|
292
|
+
def error(message = nil)
|
293
|
+
step(:error, message)
|
294
|
+
save
|
295
|
+
end
|
296
|
+
|
297
|
+
def info(info = {})
|
298
|
+
@state[:info].merge!(info)
|
299
|
+
save
|
300
|
+
@state[:info]
|
301
|
+
end
|
302
|
+
|
303
|
+
def results(results)
|
304
|
+
@state[:results] = results.collect{|file| path(file)}
|
305
|
+
save
|
306
|
+
end
|
307
|
+
|
308
|
+
def result_filenames
|
309
|
+
@state[:results]
|
310
|
+
end
|
311
|
+
|
312
|
+
def abort
|
313
|
+
raise SimpleWS::Jobs::Aborted
|
314
|
+
save
|
315
|
+
end
|
316
|
+
|
317
|
+
def job_name
|
318
|
+
@name
|
319
|
+
end
|
320
|
+
|
321
|
+
def run(task, name, results, *args)
|
322
|
+
@name = name
|
323
|
+
@state = {
|
324
|
+
:name => @name,
|
325
|
+
:status => :prepared,
|
326
|
+
:messages => [],
|
327
|
+
:info => {},
|
328
|
+
:results => results.collect{|file| path(file)},
|
329
|
+
}
|
330
|
+
save
|
331
|
+
@pid = Process.fork do
|
332
|
+
begin
|
333
|
+
puts "Job #{@name} starting with pid #{Process.pid}"
|
334
|
+
|
335
|
+
trap(:INT) { raise SimpleWS::Jobs::Aborted }
|
336
|
+
self.send task, *args
|
337
|
+
step :done
|
338
|
+
exit(0)
|
339
|
+
rescue SimpleWS::Jobs::Aborted
|
340
|
+
step(:aborted, "Job Aborted")
|
341
|
+
exit(-1)
|
342
|
+
rescue Exception
|
343
|
+
if !$!.kind_of? SystemExit
|
344
|
+
error($!.message)
|
345
|
+
puts "Error in job #{ @name }"
|
346
|
+
puts $!.message
|
347
|
+
puts $!.backtrace
|
348
|
+
exit(-1)
|
349
|
+
else
|
350
|
+
exit($!.status)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
@pid
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
|
361
|
+
def self.helper(name, &block)
|
362
|
+
Scheduler.helper name, block
|
363
|
+
end
|
364
|
+
|
365
|
+
def helper(name,&block)
|
366
|
+
Scheduler.helper name, block
|
367
|
+
end
|
368
|
+
|
369
|
+
def self.configure(name, value)
|
370
|
+
Scheduler.configure(name, value)
|
371
|
+
end
|
372
|
+
|
373
|
+
def configure(name, value)
|
374
|
+
self.class.configure(name, value)
|
375
|
+
end
|
376
|
+
|
377
|
+
def task(name, params=[], types={}, results = [], &block)
|
378
|
+
@@last_param_description['return'] ||= 'Job identifier' if @@last_param_description
|
379
|
+
@@last_param_description['suggested_name'] ||= 'Suggested job id' if @@last_param_description
|
380
|
+
|
381
|
+
Scheduler.task name, results, block
|
382
|
+
serve name.to_s, params + ['suggested_name'], types.merge(:suggested_name => 'string', :return => :string) do |*args|
|
383
|
+
Scheduler.run name, *args
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
@@tasks = {}
|
388
|
+
def self.task(name, params=[], types={}, results =[], &block)
|
389
|
+
INHERITED_TASKS[name] = {:params => params, :types => types, :results => results, :block => block,
|
390
|
+
:description => @@last_description, :param_description => @@last_param_description};
|
391
|
+
|
392
|
+
@@last_description = nil
|
393
|
+
@@last_param_description = nil
|
394
|
+
end
|
395
|
+
|
396
|
+
def abort_jobs
|
397
|
+
Scheduler.abort_jobs
|
398
|
+
end
|
399
|
+
|
400
|
+
|
401
|
+
def workdir
|
402
|
+
@workdir
|
403
|
+
end
|
404
|
+
|
405
|
+
def initialize(name = nil, description = nil, host = nil, port = nil, workdir = nil, *args)
|
406
|
+
super(name, description, host, port, *args)
|
407
|
+
|
408
|
+
@workdir = workdir || "/tmp/#{ name }"
|
409
|
+
Scheduler.workdir = @workdir
|
410
|
+
@results = {}
|
411
|
+
INHERITED_TASKS.each{|task,info|
|
412
|
+
@@last_description = info[:description]
|
413
|
+
@@last_param_description = info[:param_description]
|
414
|
+
task(task, info[:params], info[:types], info[:results], &info[:block])
|
415
|
+
}
|
416
|
+
|
417
|
+
|
418
|
+
desc "Job management: Return the names of the jobs in the queue"
|
419
|
+
param_desc :return => "Array of job names"
|
420
|
+
serve :queue, [], :return => :array do
|
421
|
+
Scheduler.queue.collect{|info| info[:name]}
|
422
|
+
end
|
423
|
+
|
424
|
+
desc "Job management: Check the status of a job"
|
425
|
+
param_desc :job => "Job identifier", :return => "Status code. Special status codes are: 'queue', 'done', 'error', and 'aborted'"
|
426
|
+
serve :status, ['job'], :job => :string, :return => :string do |job|
|
427
|
+
Scheduler.job_info(job)[:status].to_s
|
428
|
+
end
|
429
|
+
|
430
|
+
desc "Job management: Return an array with the messages issued by the job"
|
431
|
+
param_desc :job => "Job identifier", :return => "Array with message strings"
|
432
|
+
serve :messages, ['job'], :job => :string, :return => :array do |job|
|
433
|
+
Scheduler.job_info(job)[:messages]
|
434
|
+
end
|
435
|
+
|
436
|
+
desc "Job management: Return a YAML string containing arbitrary information set up by the job"
|
437
|
+
param_desc :job => "Job identifier", :return => "Hash with arbitrary values in YAML format"
|
438
|
+
serve :info, ['job'], :job => :string, :return => :string do |job|
|
439
|
+
Scheduler.job_info(job)[:info].to_yaml
|
440
|
+
end
|
441
|
+
|
442
|
+
desc "Job management: Abort the job"
|
443
|
+
param_desc :job => "Job identifier"
|
444
|
+
serve :abort, %w(job), :job => :string, :return => false do |job|
|
445
|
+
Scheduler.abort(job)
|
446
|
+
end
|
447
|
+
|
448
|
+
desc "Job management: Check if the job is done. Could have finished successfully, with error, or have been aborted"
|
449
|
+
param_desc :job => "Job identifier", :return => "True if the job has status 'done', 'error' or 'aborted'"
|
450
|
+
serve :done, %w(job), :job => :string, :return => :boolean do |job|
|
451
|
+
[:done, :error, :aborted].include? Scheduler.job_info(job)[:status].to_sym
|
452
|
+
end
|
453
|
+
|
454
|
+
desc "Job management: Check if the job has finished with error. The last message is the error message"
|
455
|
+
param_desc :job => "Job identifier", :return => "True if the job has status 'error'"
|
456
|
+
serve :error, %w(job), :job => :string, :return => :boolean do |job|
|
457
|
+
Scheduler.job_info(job)[:status] == :error
|
458
|
+
end
|
459
|
+
|
460
|
+
desc "Job management: Check if the job has been aborted"
|
461
|
+
param_desc :job => "Job identifier", :return => "True if the job has status 'aborted'"
|
462
|
+
serve :aborted, %w(job), :job => :string, :return => :boolean do |job|
|
463
|
+
Scheduler.job_info(job)[:status] == :aborted
|
464
|
+
end
|
465
|
+
|
466
|
+
desc "Job management: Return an array with result identifiers to be used with the 'result' operation. The content of the results depends
|
467
|
+
on the task"
|
468
|
+
param_desc :job => "Job identifier", :return => "Array of result identifiers"
|
469
|
+
serve :results, %w(job), :return => :array do |job|
|
470
|
+
results = Scheduler.job_results(job)
|
471
|
+
@results.merge! Hash[*results.flatten]
|
472
|
+
results.collect{|p| p[0]}
|
473
|
+
end
|
474
|
+
|
475
|
+
desc "Job management: Return the content of the result specified by the result identifier. These identifiers are retrieve using the 'results' operation. Results are Base64 encoded to allow binary data"
|
476
|
+
param_desc :result => "Result identifier", :return => "Content of the result file, in Base64 encoding for compatibility"
|
477
|
+
serve :result, %w(result), :return => :binary do |result|
|
478
|
+
path = @results[result]
|
479
|
+
raise ResultNotFound if path.nil? || ! File.exist?(path)
|
480
|
+
Base64.encode64 File.open(path).read
|
481
|
+
end
|
482
|
+
|
483
|
+
end
|
484
|
+
|
485
|
+
alias_method :old_start, :start
|
486
|
+
def start(*args)
|
487
|
+
Scheduler.job_monitor
|
488
|
+
old_start(*args)
|
489
|
+
end
|
490
|
+
|
491
|
+
alias_method :old_shutdown, :shutdown
|
492
|
+
def shutdown(*args)
|
493
|
+
Scheduler.abort_jobs
|
494
|
+
old_shutdown(*args)
|
495
|
+
end
|
496
|
+
|
497
|
+
|
498
|
+
|
499
|
+
end
|
500
|
+
|
501
|
+
|
502
|
+
|