scout-gear 5.1.1 → 6.0.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.
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