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.
@@ -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
+