scout-gear 5.1.1 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +24 -12
  3. data/Rakefile +2 -0
  4. data/VERSION +1 -1
  5. data/bin/scout +2 -0
  6. data/lib/scout/exceptions.rb +14 -2
  7. data/lib/scout/log/color.rb +34 -10
  8. data/lib/scout/log/progress/report.rb +5 -4
  9. data/lib/scout/meta_extension.rb +4 -2
  10. data/lib/scout/misc/format.rb +16 -4
  11. data/lib/scout/misc/monitor.rb +41 -0
  12. data/lib/scout/misc.rb +1 -0
  13. data/lib/scout/open/stream.rb +31 -0
  14. data/lib/scout/path/find.rb +2 -1
  15. data/lib/scout/path.rb +1 -1
  16. data/lib/scout/persist/serialize.rb +15 -4
  17. data/lib/scout/resource/path.rb +5 -0
  18. data/lib/scout/resource/util.rb +48 -0
  19. data/lib/scout/resource.rb +2 -0
  20. data/lib/scout/semaphore.rb +148 -0
  21. data/lib/scout/simple_opt/doc.rb +26 -2
  22. data/lib/scout/work_queue/socket.rb +119 -0
  23. data/lib/scout/work_queue/worker.rb +54 -0
  24. data/lib/scout/work_queue.rb +86 -0
  25. data/lib/scout/workflow/definition.rb +8 -2
  26. data/lib/scout/workflow/documentation.rb +32 -26
  27. data/lib/scout/workflow/step/info.rb +13 -13
  28. data/lib/scout/workflow/step/load.rb +18 -0
  29. data/lib/scout/workflow/step.rb +40 -4
  30. data/lib/scout/workflow/task/inputs.rb +4 -2
  31. data/lib/scout/workflow/task.rb +15 -1
  32. data/lib/scout/workflow/usage.rb +96 -76
  33. data/lib/scout/workflow.rb +1 -0
  34. data/scout-gear.gemspec +25 -3
  35. data/scout_commands/workflow/info +29 -0
  36. data/scout_commands/workflow/list +27 -0
  37. data/scout_commands/workflow/task +32 -681
  38. data/scout_commands/workflow/task_old +706 -0
  39. data/share/color/color_names +507 -0
  40. data/share/color/diverging_colors.hex +12 -0
  41. data/test/scout/log/test_color.rb +0 -0
  42. data/test/scout/resource/test_util.rb +27 -0
  43. data/test/scout/simple_opt/test_doc.rb +16 -0
  44. data/test/scout/test_meta_extension.rb +9 -0
  45. data/test/scout/test_semaphore.rb +17 -0
  46. data/test/scout/test_work_queue.rb +93 -0
  47. data/test/scout/work_queue/test_socket.rb +46 -0
  48. data/test/scout/work_queue/test_worker.rb +99 -0
  49. data/test/scout/workflow/step/test_info.rb +17 -15
  50. data/test/scout/workflow/step/test_load.rb +65 -0
  51. data/test/scout/workflow/test_definition.rb +0 -0
  52. data/test/scout/workflow/test_documentation.rb +30 -0
  53. data/test/scout/workflow/test_task.rb +1 -0
  54. data/test/scout/workflow/test_usage.rb +12 -3
  55. metadata +24 -2
@@ -0,0 +1,148 @@
1
+ begin
2
+ require 'inline'
3
+ continue = true
4
+ rescue Exception
5
+ Log.warn "The RubyInline gem could not be loaded: semaphore synchronization will not work"
6
+ continue = false
7
+ end
8
+
9
+ if continue
10
+ module ScoutSemaphore
11
+ inline(:C) do |builder|
12
+ builder.prefix <<-EOF
13
+ #include <unistd.h>
14
+ #include <stdio.h>
15
+ #include <stdlib.h>
16
+ #include <semaphore.h>
17
+ #include <time.h>
18
+ #include <assert.h>
19
+ #include <errno.h>
20
+ #include <signal.h>
21
+ #include <fcntl.h>
22
+ EOF
23
+
24
+ builder.c_singleton <<-EOF
25
+ void create_semaphore(char* name, int value){
26
+ sem_open(name, O_CREAT, S_IRWXU|S_IRWXG|S_IRWXO, value);
27
+ }
28
+ EOF
29
+ builder.c_singleton <<-EOF
30
+ void delete_semaphore(char* name){
31
+ sem_unlink(name);
32
+ }
33
+ EOF
34
+
35
+ builder.c_singleton <<-EOF
36
+ int wait_semaphore(char* name){
37
+ int ret;
38
+ sem_t* sem;
39
+ sem = sem_open(name, 0);
40
+ ret = sem_wait(sem);
41
+ sem_close(sem);
42
+ return(ret);
43
+ }
44
+ EOF
45
+
46
+ builder.c_singleton <<-EOF
47
+ void post_semaphore(char* name){
48
+ sem_t* sem;
49
+ sem = sem_open(name, 0);
50
+ sem_post(sem);
51
+ sem_close(sem);
52
+ }
53
+ EOF
54
+ end
55
+
56
+ SEM_MUTEX = Mutex.new
57
+ def self.synchronize(sem)
58
+ ret = ScoutSemaphore.wait_semaphore(sem)
59
+ raise SemaphoreInterrupted if ret == -1
60
+ begin
61
+ yield
62
+ ensure
63
+ ScoutSemaphore.post_semaphore(sem)
64
+ end
65
+ end
66
+
67
+ def self.with_semaphore(size, file = nil)
68
+ if file.nil?
69
+ file = "/" << Misc.digest(rand(1000000000000).to_s) if file.nil?
70
+ else
71
+ file = file.gsub('/', '_') if file
72
+ end
73
+
74
+ begin
75
+ Log.debug "Creating semaphore (#{ size }): #{file}"
76
+ ScoutSemaphore.create_semaphore(file, size)
77
+ yield file
78
+ ensure
79
+ Log.debug "Removing semaphore #{ file }"
80
+ ScoutSemaphore.delete_semaphore(file)
81
+ end
82
+ end
83
+
84
+ def self.fork_each_on_semaphore(elems, size, file = nil)
85
+
86
+ TSV.traverse elems, :cpus => size, :bar => "Fork each on semaphore: #{ Misc.fingerprint elems }", :into => Set.new do |elem|
87
+ elems.annotate elem if elems.respond_to? :annotate
88
+ begin
89
+ yield elem
90
+ rescue Interrupt
91
+ Log.warn "Process #{Process.pid} was aborted"
92
+ end
93
+ nil
94
+ end
95
+ nil
96
+ end
97
+
98
+ def self.thread_each_on_semaphore(elems, size)
99
+ mutex = Mutex.new
100
+ count = 0
101
+ cv = ConditionVariable.new
102
+ wait_mutex = Mutex.new
103
+
104
+ begin
105
+
106
+ threads = []
107
+ wait_mutex.synchronize do
108
+ threads = elems.collect do |elem|
109
+ Thread.new(elem) do |elem|
110
+
111
+ continue = false
112
+ mutex.synchronize do
113
+ while not continue do
114
+ if count < size
115
+ continue = true
116
+ count += 1
117
+ end
118
+ mutex.sleep 1 unless continue
119
+ end
120
+ end
121
+
122
+ begin
123
+ yield elem
124
+ rescue Interrupt
125
+ Log.error "Thread was aborted while processing: #{Misc.fingerprint elem}"
126
+ raise $!
127
+ ensure
128
+ mutex.synchronize do
129
+ count -= 1
130
+ cv.signal if mutex.locked?
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ threads.each do |thread|
138
+ thread.join
139
+ end
140
+ rescue Exception
141
+ Log.exception $!
142
+ Log.info "Ensuring threads are dead: #{threads.length}"
143
+ threads.each do |thread| thread.kill end
144
+ end
145
+ end
146
+ end
147
+ end
148
+
@@ -42,10 +42,33 @@ module SOPT
42
42
  "=<#{ type }>"
43
43
  end
44
44
  #extra << " (default: #{Array === default ? (default.length > 3 ? default[0..2]*", " + ', ...' : default*", " ): default})" if default != nil
45
- extra << " (default: #{Misc.fingerprint(default)})" if default != nil
45
+ extra << " (default: #{Log.fingerprint(default)})" if default != nil
46
46
  input_str << Log.color(:green, extra)
47
47
  end
48
48
 
49
+ def self.input_array_doc(input_array)
50
+ input_array.collect do |name,type,description,default,options|
51
+ type = :string if type.nil?
52
+
53
+ name = name.to_s
54
+ shortcut, options = options, nil if String === options || Symbol === options
55
+
56
+ case options && options[:shortcut]
57
+ when FalseClass
58
+ shortcut = nil
59
+ when TrueClass, nil
60
+ shortcut = fix_shortcut(name[0], name)
61
+ else
62
+ shortcut = options[:shortcut]
63
+ end unless shortcut
64
+
65
+ shortcut = fix_shortcut(shortcut, name)
66
+ register(shortcut, name, type, description) unless self.inputs.include? name
67
+ name = SOPT.input_format(name, type.to_sym, default, shortcut )
68
+ Misc.format_definition_list_item(name, description)
69
+ end * "\n"
70
+ end
71
+
49
72
  def self.input_doc(inputs, input_types = nil, input_descriptions = nil, input_defaults = nil, input_shortcuts = nil)
50
73
  type = description = default = nil
51
74
  shortcut = ""
@@ -73,10 +96,11 @@ module SOPT
73
96
  register(shortcut, name, type, description) unless self.inputs.include? name
74
97
 
75
98
  name = SOPT.input_format(name, type.to_sym, default, shortcut)
76
- Misc.format_definition_list_item(name, description, 80, 31, nil)
99
+ Misc.format_definition_list_item(name, description)
77
100
  end * "\n"
78
101
  end
79
102
 
103
+
80
104
  def self.doc
81
105
  doc =<<-EOF
82
106
  #{Log.color :magenta}#{command}(1) -- #{summary}
@@ -0,0 +1,119 @@
1
+ require 'scout/open'
2
+ require 'scout/semaphore'
3
+ require 'scout/exceptions'
4
+ class WorkQueue
5
+ class Socket
6
+ attr_accessor :sread, :swrite, :write_sem, :read_sem, :cleaned
7
+ def initialize(serializer = nil)
8
+ @sread, @swrite = Open.pipe
9
+
10
+ @serializer = serializer || Marshal
11
+
12
+ @key = "/" << rand(1000000000).to_s << '.' << Process.pid.to_s;
13
+ @write_sem = @key + '.in'
14
+ @read_sem = @key + '.out'
15
+ Log.debug "Creating socket semaphores: #{@key}"
16
+ ScoutSemaphore.create_semaphore(@write_sem,1)
17
+ ScoutSemaphore.create_semaphore(@read_sem,1)
18
+ end
19
+
20
+ def clean
21
+ @cleaned = true
22
+ @sread.close unless @sread.closed?
23
+ @swrite.close unless @swrite.closed?
24
+ Log.low "Destroying socket semaphores: #{[@key] * ", "}"
25
+ ScoutSemaphore.delete_semaphore(@write_sem)
26
+ ScoutSemaphore.delete_semaphore(@read_sem)
27
+ end
28
+
29
+
30
+ def dump(obj)
31
+ stream = @swrite
32
+ obj.concurrent_stream = nil if obj.respond_to?(:concurrent_stream)
33
+ case obj
34
+ when Integer
35
+ size_head = [obj,"I"].pack 'La'
36
+ str = size_head
37
+ when nil
38
+ size_head = [0,"N"].pack 'La'
39
+ str = size_head
40
+ when String
41
+ payload = obj
42
+ size_head = [payload.bytesize,"C"].pack 'La'
43
+ str = size_head << payload
44
+ else
45
+ payload = @serializer.dump(obj)
46
+ size_head = [payload.bytesize,"S"].pack 'La'
47
+ str = size_head << payload
48
+ end
49
+
50
+ write_length = str.length
51
+ wrote = stream.write(str)
52
+ while wrote < write_length
53
+ wrote += stream.write(str[wrote..-1])
54
+ end
55
+ end
56
+
57
+ def load
58
+ stream = @sread
59
+ size_head = Open.read_stream stream, 5
60
+
61
+ size, type = size_head.unpack('La')
62
+
63
+ return nil if type == "N"
64
+ return size.to_i if type == "I"
65
+ begin
66
+ payload = Open.read_stream stream, size
67
+ case type
68
+ when "S"
69
+ begin
70
+ @serializer.load(payload)
71
+ rescue Exception
72
+ Log.exception $!
73
+ raise $!
74
+ end
75
+ when "C"
76
+ payload
77
+ end
78
+ rescue TryAgain
79
+ retry
80
+ end
81
+ end
82
+
83
+ def closed_read?
84
+ @sread.closed?
85
+ end
86
+
87
+ def closed_write?
88
+ @swrite.closed?
89
+ end
90
+
91
+ def close_write
92
+ self.dump ClosedStream.new
93
+ @swrite.close unless closed_write?
94
+ end
95
+
96
+ def close_read
97
+ @sread.close unless closed_read?
98
+ end
99
+
100
+ #{{{ ACCESSOR
101
+ def push(obj)
102
+ ScoutSemaphore.synchronize(@write_sem) do
103
+ self.dump(obj)
104
+ end
105
+ end
106
+
107
+ def pop
108
+ ScoutSemaphore.synchronize(@read_sem) do
109
+ res = self.load
110
+ raise res if ClosedStream === res
111
+ res
112
+ end
113
+ end
114
+
115
+ alias write push
116
+
117
+ alias read pop
118
+ end
119
+ end
@@ -0,0 +1,54 @@
1
+ class WorkQueue
2
+ class Worker
3
+ attr_accessor :pid, :ignore_ouput
4
+ def initialize
5
+ end
6
+
7
+ def run
8
+ @pid = Process.fork do
9
+ yield
10
+ end
11
+ end
12
+
13
+ def process(input, output, &block)
14
+ run do
15
+ begin
16
+ while obj = input.read
17
+ if DoneProcessing === obj
18
+ output.write DoneProcessing.new
19
+ raise obj
20
+ end
21
+ res = block.call obj
22
+ output.write res unless ignore_ouput || res == :ignore
23
+ end
24
+ rescue DoneProcessing
25
+ Log.log "Worker #{Process.pid} done"
26
+ rescue Exception
27
+ Log.exception $!
28
+ exit -1
29
+ end
30
+ end
31
+ end
32
+
33
+ def join
34
+ Log.log "Joining worker #{@pid}"
35
+ Process.waitpid @pid
36
+ end
37
+
38
+ def exit(status)
39
+ Log.log "Worker #{@pid} exited with status #{Log.color(:green, status)}"
40
+ end
41
+
42
+ def self.join(workers)
43
+ workers = [workers] unless Array === workers
44
+ begin
45
+ while pid = Process.wait
46
+ status = $?
47
+ worker = workers.select{|w| w.pid == pid }.first
48
+ worker.exit status.exitstatus if worker
49
+ end
50
+ rescue Errno::ECHILD
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,86 @@
1
+ require_relative 'work_queue/socket'
2
+ require_relative 'work_queue/worker'
3
+
4
+ class WorkQueue
5
+ attr_accessor :workers, :worker_proc, :callback
6
+
7
+ def initialize(workers = 0, &block)
8
+ @input = WorkQueue::Socket.new
9
+ @output = WorkQueue::Socket.new
10
+ @workers = workers.times.collect{ Worker.new }
11
+ @worker_proc = block
12
+ @worker_mutex = Mutex.new
13
+ @removed_workers = []
14
+ end
15
+
16
+ def add_worker(&block)
17
+ worker = Worker.new
18
+ @worker_mutex.synchronize do
19
+ @workers.push(worker)
20
+ if block_given?
21
+ worker.process @input, @output, &block
22
+ else
23
+ worker.process @input, @output, &@worker_proc
24
+ end
25
+ end
26
+ worker
27
+ end
28
+
29
+ def ignore_ouput
30
+ @workers.each{|w| w.ignore_ouput = true }
31
+ end
32
+
33
+ def remove_one_worker
34
+ @input.write DoneProcessing.new
35
+ end
36
+
37
+ def remove_worker(pid)
38
+ worker = @worker_mutex.synchronize do
39
+ Log.debug "Remove #{pid}"
40
+ @removed_workers.concat(@workers.delete_if{|w| w.pid == pid })
41
+ end
42
+ end
43
+
44
+ def process(&callback)
45
+ @workers.each do |w|
46
+ w.process @input, @output, &@worker_proc
47
+ end
48
+ @reader = Thread.new do
49
+ begin
50
+ while true
51
+ obj = @output.read
52
+ if DoneProcessing === obj
53
+ remove_worker obj.pid if obj.pid
54
+ else
55
+ callback.call obj if callback
56
+ end
57
+ end
58
+ rescue Aborted
59
+ end
60
+ end if @output
61
+ end
62
+
63
+ def write(obj)
64
+ @input.write obj
65
+ end
66
+
67
+ def close
68
+ while @worker_mutex.synchronize{ @workers.length } > 0
69
+ begin
70
+ @input.write DoneProcessing.new
71
+ pid = Process.wait
72
+ status = $?
73
+ worker = @worker_mutex.synchronize{ @removed_workers.delete_if{|w| w.pid == pid }.first }
74
+ worker.exit $?.exitstatus if worker
75
+ rescue Errno::ECHILD
76
+ Thread.pass until @workers.length == 0
77
+ break
78
+ end
79
+ end
80
+ @reader.raise Aborted if @reader
81
+ end
82
+
83
+ def join
84
+ @reader.join if @reader
85
+ end
86
+ end
@@ -33,6 +33,11 @@ module Workflow
33
33
  @annotate_next_task[type] << obj
34
34
  end
35
35
 
36
+ def annotate_next_task_single(type, obj)
37
+ @annotate_next_task ||= {}
38
+ @annotate_next_task[type] = obj
39
+ end
40
+
36
41
  def dep(*args, &block)
37
42
  case args.length
38
43
  when 3
@@ -60,13 +65,14 @@ module Workflow
60
65
  @tasks ||= IndiferentHash.setup({})
61
66
  begin
62
67
  @annotate_next_task ||= {}
63
- @tasks[name] = Task.setup(block, @annotate_next_task.merge(name: name, type: type, directory: directory[name]))
68
+ task = Task.setup(block, @annotate_next_task.merge(name: name, type: type, directory: directory[name]))
69
+ @tasks[name] = task
64
70
  ensure
65
71
  @annotate_next_task = {}
66
72
  end
67
73
  end
68
74
 
69
75
  def desc(description)
70
- annotate_next_task(:desc, description)
76
+ annotate_next_task_single(:description, description)
71
77
  end
72
78
  end
@@ -1,4 +1,5 @@
1
1
  module Workflow
2
+ attr_accessor :title, :description
2
3
 
3
4
  def self.doc_parse_first_line(str)
4
5
  if str.match(/^([^\n]*)\n\n(.*)/sm)
@@ -45,33 +46,38 @@ module Workflow
45
46
  end
46
47
  end
47
48
 
48
- def load_documentation
49
- return if @documentation
50
- @documentation ||= Workflow.parse_workflow_doc documentation_markdown
51
- @documentation[:tasks].each do |task, description|
52
- if task.include? "#"
53
- workflow, task = task.split("#")
54
- workflow = begin
55
- Kernel.const_get workflow
56
- rescue
57
- next
58
- end
59
- else
60
- workflow = self
61
- end
62
-
63
- task = task.to_sym
64
- if workflow.tasks.include? task
65
- workflow.tasks[task].description = description
66
- else
67
- Log.low "Documentation for #{ task }, but not a #{ workflow.to_s } task"
68
- end
69
- end
70
- end
71
-
72
49
  attr_accessor :documentation
73
50
  def documentation
74
- load_documentation if @documentation.nil?
75
- @documentation
51
+ @documentation ||= begin
52
+ documentation = Workflow.parse_workflow_doc documentation_markdown
53
+
54
+ if @description && (documentation[:description].nil? || documentation[:description].empty?)
55
+ documentation[:description] = @description
56
+ end
57
+
58
+ if @title && (documentation[:title].nil? || documentation[:title].empty?)
59
+ documentation[:title] = @title
60
+ end
61
+ documentation[:tasks].each do |task, description|
62
+ if task.include? "#"
63
+ workflow, task = task.split("#")
64
+ workflow = begin
65
+ Kernel.const_get workflow
66
+ rescue
67
+ next
68
+ end
69
+ else
70
+ workflow = self
71
+ end
72
+
73
+ task = task.to_sym
74
+ if workflow.tasks.include? task
75
+ workflow.tasks[task].description = description
76
+ else
77
+ Log.low "Documentation for #{ task }, but not a #{ workflow.to_s } task"
78
+ end
79
+ end
80
+ documentation
81
+ end
76
82
  end
77
83
  end
@@ -1,15 +1,21 @@
1
1
  class Step
2
+ SERIALIZER = :json
2
3
  def info_file
3
- @info_file ||= @path + ".info"
4
+ @info_file ||= begin
5
+ info_file = @path + ".info"
6
+ @path.annotate info_file if Path === @path
7
+ info_file
8
+ end
4
9
  end
5
10
 
6
11
  def load_info
7
- @info = Persist.load(info_file, :marshal) || {}
12
+ @info = Persist.load(info_file, SERIALIZER) || {}
13
+ IndiferentHash.setup(@info)
8
14
  @info_load_time = Time.now
9
15
  end
10
16
 
11
- def save_info
12
- Persist.save(@info, info_file, :marshal)
17
+ def save_info(info = nil)
18
+ Persist.save(info, info_file, SERIALIZER)
13
19
  @info_load_time = Time.now
14
20
  end
15
21
 
@@ -40,24 +46,18 @@ class Step
40
46
  info[key] = value
41
47
  end
42
48
  end
43
- save_info
49
+ save_info(info)
44
50
  end
45
51
 
46
52
  def set_info(key, value)
47
53
  merge_info(key => value)
48
54
  end
49
55
 
50
- def init_info
51
- @info = {
52
- :status => :waiting
53
- }
54
- end
55
-
56
56
  def report_status(status, message = nil)
57
57
  if message.nil?
58
- Log.info Log.color(:green, status.to_s) + " " + Log.color(:blue, path)
58
+ Log.info Log.color(status, status.to_s) + " " + Log.color(:path, path)
59
59
  else
60
- Log.info Log.color(:green, status.to_s) + " " + Log.color(:blue, path) + " " + message
60
+ Log.info Log.color(status, status.to_s) + " " + Log.color(:path, path) + " " + message
61
61
  end
62
62
  end
63
63
 
@@ -0,0 +1,18 @@
1
+ class Step
2
+ def self.relocate(path)
3
+ return path if Open.exists?(path)
4
+ Path.setup(path) unless Path === path
5
+ relocated = path.relocate
6
+ return relocated if Open.exists?(relocated)
7
+ subpath = path.split("/")[-3..-1] * "/"
8
+ relocated = Path.setup("var/jobs")[subpath]
9
+ return relocated if Open.exists?(relocated)
10
+ path
11
+ end
12
+
13
+ def self.load(path)
14
+ path = relocate(path) unless Open.exists?(path)
15
+ raise "Could not load #{path}" unless Open.exists?(path)
16
+ s = Step.new path
17
+ end
18
+ end