scout-gear 5.2.0 → 7.1.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.
- checksums.yaml +4 -4
- data/.vimproject +450 -440
- data/VERSION +1 -1
- data/lib/scout/exceptions.rb +22 -2
- data/lib/scout/log/color.rb +61 -10
- data/lib/scout/log/progress/report.rb +5 -4
- data/lib/scout/log/progress/util.rb +2 -0
- data/lib/scout/log/progress.rb +2 -0
- data/lib/scout/log.rb +5 -1
- data/lib/scout/misc/digest.rb +1 -3
- data/lib/scout/misc/monitor.rb +18 -0
- data/lib/scout/open/stream.rb +50 -19
- data/lib/scout/semaphore.rb +148 -0
- data/lib/scout/tsv/parser.rb +144 -0
- data/lib/scout/tsv.rb +14 -0
- data/lib/scout/work_queue/socket.rb +119 -0
- data/lib/scout/work_queue/worker.rb +59 -0
- data/lib/scout/work_queue.rb +113 -0
- data/lib/scout/workflow/step/info.rb +2 -2
- data/lib/scout/workflow/step.rb +2 -1
- data/lib/scout/workflow/task.rb +2 -2
- data/scout-gear.gemspec +18 -3
- data/share/color/color_names +507 -0
- data/share/color/diverging_colors.hex +12 -0
- data/test/scout/log/test_color.rb +0 -0
- data/test/scout/open/test_stream.rb +1 -1
- data/test/scout/test_semaphore.rb +17 -0
- data/test/scout/test_tsv.rb +34 -0
- data/test/scout/test_work_queue.rb +121 -0
- data/test/scout/tsv/test_parser.rb +87 -0
- data/test/scout/work_queue/test_socket.rb +46 -0
- data/test/scout/work_queue/test_worker.rb +147 -0
- metadata +17 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
7.1.0
|
data/lib/scout/exceptions.rb
CHANGED
@@ -71,8 +71,28 @@ end
|
|
71
71
|
|
72
72
|
class LockInterrupted < TryAgain; end
|
73
73
|
|
74
|
-
|
75
|
-
|
74
|
+
class ClosedStream < StandardError; end
|
75
|
+
|
76
|
+
class DoneProcessing < Exception
|
77
|
+
attr_accessor :pid
|
78
|
+
def initialize(pid = Process.pid)
|
79
|
+
@pid = pid
|
80
|
+
end
|
81
|
+
|
82
|
+
def message
|
83
|
+
"Done processing pid #{pid}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class WorkerException < ScoutException
|
88
|
+
attr_accessor :exception, :pid
|
89
|
+
def initialize(exception, pid)
|
90
|
+
@exception = exception
|
91
|
+
@pid = pid
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
76
96
|
#class OpenGzipError < StandardError; end
|
77
97
|
#
|
78
98
|
#
|
data/lib/scout/log/color.rb
CHANGED
@@ -2,6 +2,7 @@ require_relative 'color_class'
|
|
2
2
|
require_relative '../indiferent_hash'
|
3
3
|
|
4
4
|
require 'term/ansicolor'
|
5
|
+
require 'colorist'
|
5
6
|
|
6
7
|
module Colorize
|
7
8
|
def self.colors=(colors)
|
@@ -9,7 +10,13 @@ module Colorize
|
|
9
10
|
end
|
10
11
|
|
11
12
|
def self.colors
|
12
|
-
@colors ||= IndiferentHash.setup(
|
13
|
+
@colors ||= IndiferentHash.setup(Hash[<<-EOF.split("\n").collect{|l| l.split(" ")}])
|
14
|
+
green #00cd00
|
15
|
+
red #cd0000
|
16
|
+
yellow #ffd700
|
17
|
+
blue #0000cd
|
18
|
+
path blue
|
19
|
+
EOF
|
13
20
|
end
|
14
21
|
|
15
22
|
def self.diverging_colors=(colors)
|
@@ -33,7 +40,6 @@ module Colorize
|
|
33
40
|
EOF
|
34
41
|
end
|
35
42
|
|
36
|
-
|
37
43
|
def self.from_name(color)
|
38
44
|
return color if color =~ /^#?[0-9A-F]+$/i
|
39
45
|
return colors[color.to_s] if colors.include?(color.to_s)
|
@@ -52,7 +58,7 @@ module Colorize
|
|
52
58
|
when 'blue'
|
53
59
|
colors["RoyalBlue"]
|
54
60
|
else
|
55
|
-
colors[color.to_s] || color
|
61
|
+
colors[color.to_s] || color.to_s
|
56
62
|
end
|
57
63
|
end
|
58
64
|
|
@@ -134,8 +140,22 @@ module Log
|
|
134
140
|
WHITE, DARK, GREEN, YELLOW, RED = Color::SOLARIZED.values_at :base0, :base00, :green, :yellow, :magenta
|
135
141
|
|
136
142
|
SEVERITY_COLOR = [reset, cyan, green, magenta, blue, yellow, red] #.collect{|e| "\033[#{e}"}
|
143
|
+
CONCEPT_COLORS = IndiferentHash.setup({
|
144
|
+
:title => magenta,
|
145
|
+
:path => blue,
|
146
|
+
:input => cyan,
|
147
|
+
:value => green,
|
148
|
+
:integer => green,
|
149
|
+
:negative => red,
|
150
|
+
:float => green,
|
151
|
+
:waiting => yellow,
|
152
|
+
:started => cyan,
|
153
|
+
:start => cyan,
|
154
|
+
:done => green,
|
155
|
+
:error => red,
|
156
|
+
})
|
137
157
|
HIGHLIGHT = "\033[1m"
|
138
|
-
|
158
|
+
|
139
159
|
def self.uncolor(str)
|
140
160
|
"" << Term::ANSIColor.uncolor(str)
|
141
161
|
end
|
@@ -144,15 +164,46 @@ module Log
|
|
144
164
|
reset
|
145
165
|
end
|
146
166
|
|
147
|
-
def self.color(
|
167
|
+
def self.color(color, str = nil, reset = false)
|
148
168
|
return str.dup || "" if nocolor
|
149
|
-
|
150
|
-
color
|
151
|
-
|
169
|
+
|
170
|
+
if (color == :integer || color == :float) && Numeric === str
|
171
|
+
color = if str < 0
|
172
|
+
:red
|
173
|
+
elsif str > 1
|
174
|
+
:cyan
|
175
|
+
else
|
176
|
+
:green
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
if color == :status
|
181
|
+
color = case str.to_sym
|
182
|
+
when :done
|
183
|
+
:green
|
184
|
+
when :error, :aborted
|
185
|
+
:red
|
186
|
+
when :waiting, :queued
|
187
|
+
:yellow
|
188
|
+
when :started, :start, :streamming
|
189
|
+
:cyan
|
190
|
+
else
|
191
|
+
:cyan
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
color = SEVERITY_COLOR[color] if Integer === color
|
196
|
+
color = CONCEPT_COLORS[color] if CONCEPT_COLORS.include?(color)
|
197
|
+
color = Term::ANSIColor.send(color) if Symbol === color and Term::ANSIColor.respond_to?(color)
|
198
|
+
|
199
|
+
str = str.to_s unless str.nil?
|
200
|
+
return str if Symbol === color
|
201
|
+
color_str = reset ? Term::ANSIColor.reset : ""
|
202
|
+
color_str << color
|
152
203
|
if str.nil?
|
153
|
-
|
204
|
+
color_str
|
154
205
|
else
|
155
|
-
|
206
|
+
color_str + str.to_s + Term::ANSIColor.reset
|
156
207
|
end
|
157
208
|
end
|
158
209
|
|
@@ -195,15 +195,16 @@ module Log
|
|
195
195
|
def done(io = STDERR)
|
196
196
|
done_msg = Log.color(:magenta, "· ") << Log.color(:green, "done")
|
197
197
|
if @start
|
198
|
-
ellapsed = (Time.now - @start)
|
198
|
+
ellapsed = (Time.now - @start)
|
199
199
|
else
|
200
200
|
ellapsed = 0
|
201
201
|
end
|
202
|
-
|
203
|
-
done_msg << " " << Log.color(:blue, (@ticks).to_s) << " #{bytes ? 'bytes' : 'items'} in " << Log.color(:green,
|
202
|
+
ellapsed_str = [ellapsed/3600, ellapsed/60 % 60, ellapsed % 60].map{|t| "%02i" % t }.join(':')
|
203
|
+
done_msg << " " << Log.color(:blue, (@ticks).to_s) << " #{bytes ? 'bytes' : 'items'} in " << Log.color(:green, ellapsed_str)
|
204
204
|
@last_count = 0
|
205
205
|
@last_time = @start
|
206
|
-
|
206
|
+
thr = ellapsed > 0 ? (@ticks / ellapsed).to_i.to_s : 0
|
207
|
+
done_msg << " - " << Log.color(:blue, thr) << " per second"
|
207
208
|
done_msg << Log.color(:magenta, " · " << desc)
|
208
209
|
print(io, Log.up_lines(@depth) << done_msg << Log.down_lines(@depth))
|
209
210
|
|
data/lib/scout/log/progress.rb
CHANGED
data/lib/scout/log.rb
CHANGED
@@ -206,7 +206,11 @@ module Log
|
|
206
206
|
line = line.sub('`',"'")
|
207
207
|
color = :green if line =~ /workflow/
|
208
208
|
color = :blue if line =~ /scout-/
|
209
|
-
|
209
|
+
if color
|
210
|
+
Log.color color, line
|
211
|
+
else
|
212
|
+
line
|
213
|
+
end
|
210
214
|
end unless stack.nil?
|
211
215
|
end
|
212
216
|
|
data/lib/scout/misc/digest.rb
CHANGED
data/lib/scout/misc/monitor.rb
CHANGED
@@ -20,4 +20,22 @@ module Misc
|
|
20
20
|
end
|
21
21
|
res
|
22
22
|
end
|
23
|
+
|
24
|
+
def self.profile(options = {})
|
25
|
+
require 'ruby-prof'
|
26
|
+
profiler = RubyProf::Profile.new
|
27
|
+
profiler.start
|
28
|
+
begin
|
29
|
+
res = yield
|
30
|
+
rescue Exception
|
31
|
+
puts "Profiling aborted"
|
32
|
+
raise $!
|
33
|
+
ensure
|
34
|
+
result = profiler.stop
|
35
|
+
printer = RubyProf::FlatPrinter.new(result)
|
36
|
+
printer.print(STDOUT, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
res
|
40
|
+
end
|
23
41
|
end
|
data/lib/scout/open/stream.rb
CHANGED
@@ -31,13 +31,16 @@ module Open
|
|
31
31
|
Thread.current.report_on_exception = false
|
32
32
|
consume_stream(io, false, into, into_close)
|
33
33
|
end
|
34
|
+
|
34
35
|
io.threads.push(consumer_thread) if io.respond_to?(:threads)
|
36
|
+
Thread.pass until consumer_thread["name"]
|
37
|
+
|
35
38
|
consumer_thread
|
36
39
|
else
|
37
40
|
if into
|
38
|
-
Log.
|
41
|
+
Log.low "Consuming stream #{Log.fingerprint io} -> #{Log.fingerprint into}"
|
39
42
|
else
|
40
|
-
Log.
|
43
|
+
Log.low "Consuming stream #{Log.fingerprint io}"
|
41
44
|
end
|
42
45
|
|
43
46
|
begin
|
@@ -53,7 +56,6 @@ module Open
|
|
53
56
|
into_close = false unless into.respond_to? :close
|
54
57
|
io.sync = true
|
55
58
|
|
56
|
-
Log.high "started consuming stream #{Log.fingerprint io}"
|
57
59
|
begin
|
58
60
|
while c = io.readpartial(BLOCK_SIZE)
|
59
61
|
into << c if into
|
@@ -67,15 +69,14 @@ module Open
|
|
67
69
|
into.close if into and into_close and not into.closed?
|
68
70
|
block.call if block_given?
|
69
71
|
|
70
|
-
Log.high "Done consuming stream #{Log.fingerprint io} into #{into_path || into}"
|
71
72
|
c
|
72
73
|
rescue Aborted
|
73
|
-
Log.
|
74
|
+
Log.low "Consume stream Aborted #{Log.fingerprint io} into #{into_path || into}"
|
74
75
|
io.abort $! if io.respond_to? :abort
|
75
76
|
into.close if into.respond_to?(:closed?) && ! into.closed?
|
76
77
|
FileUtils.rm into_path if into_path and File.exist?(into_path)
|
77
78
|
rescue Exception
|
78
|
-
Log.
|
79
|
+
Log.low "Consume stream Exception reading #{Log.fingerprint io} into #{into_path || into} - #{$!.message}"
|
79
80
|
exception = io.stream_exception || $!
|
80
81
|
io.abort exception if io.respond_to? :abort
|
81
82
|
into.close if into.respond_to?(:closed?) && ! into.closed?
|
@@ -145,12 +146,12 @@ module Open
|
|
145
146
|
|
146
147
|
Open.notify_write(path)
|
147
148
|
rescue Aborted
|
148
|
-
Log.
|
149
|
+
Log.low "Aborted sensible_write -- #{ Log.reset << Log.color(:blue, path) }"
|
149
150
|
content.abort if content.respond_to? :abort
|
150
151
|
Open.rm path if File.exist? path
|
151
152
|
rescue Exception
|
152
153
|
exception = (AbortedStream === content and content.exception) ? content.exception : $!
|
153
|
-
Log.
|
154
|
+
Log.low "Exception in sensible_write: [#{Process.pid}] #{exception.message} -- #{ Log.color :blue, path }"
|
154
155
|
content.abort if content.respond_to? :abort
|
155
156
|
Open.rm path if File.exist? path
|
156
157
|
raise exception
|
@@ -219,16 +220,15 @@ module Open
|
|
219
220
|
|
220
221
|
#parent_pid = Process.pid
|
221
222
|
pid = Process.fork {
|
222
|
-
purge_pipes(sin)
|
223
|
-
sout.close
|
224
223
|
begin
|
224
|
+
purge_pipes(sin)
|
225
|
+
sout.close
|
225
226
|
|
226
227
|
yield sin
|
227
228
|
sin.close if close and not sin.closed?
|
228
229
|
|
229
230
|
rescue Exception
|
230
231
|
Log.exception $!
|
231
|
-
#Process.kill :INT, parent_pid
|
232
232
|
Kernel.exit!(-1)
|
233
233
|
end
|
234
234
|
Kernel.exit! 0
|
@@ -242,18 +242,18 @@ module Open
|
|
242
242
|
ConcurrentStream.setup sout, :pair => sin
|
243
243
|
|
244
244
|
thread = Thread.new do
|
245
|
-
Thread.current["name"] = "Pipe input #{Log.fingerprint sin} => #{Log.fingerprint sout}"
|
246
|
-
Thread.current.report_on_exception = false
|
247
245
|
begin
|
246
|
+
Thread.current.report_on_exception = false
|
247
|
+
Thread.current["name"] = "Pipe input #{Log.fingerprint sin} => #{Log.fingerprint sout}"
|
248
248
|
|
249
249
|
yield sin
|
250
250
|
|
251
251
|
sin.close if close and not sin.closed? and not sin.aborted?
|
252
252
|
rescue Aborted
|
253
|
-
Log.
|
253
|
+
Log.low "Aborted open_pipe: #{$!.message}"
|
254
254
|
raise $!
|
255
255
|
rescue Exception
|
256
|
-
Log.
|
256
|
+
Log.low "Exception in open_pipe: #{$!.message}"
|
257
257
|
begin
|
258
258
|
sout.threads.delete(Thread.current)
|
259
259
|
sout.pair = []
|
@@ -269,6 +269,7 @@ module Open
|
|
269
269
|
|
270
270
|
sin.threads = [thread]
|
271
271
|
sout.threads = [thread]
|
272
|
+
Thread.pass until thread["name"]
|
272
273
|
end
|
273
274
|
|
274
275
|
sout
|
@@ -287,8 +288,8 @@ module Open
|
|
287
288
|
|
288
289
|
splitter_thread = Thread.new(Thread.current) do |parent|
|
289
290
|
begin
|
290
|
-
Thread.current["name"] = "Splitter #{Log.fingerprint stream}"
|
291
291
|
Thread.current.report_on_exception = false
|
292
|
+
Thread.current["name"] = "Splitter #{Log.fingerprint stream}"
|
292
293
|
|
293
294
|
skip = [false] * num
|
294
295
|
begin
|
@@ -317,7 +318,7 @@ module Open
|
|
317
318
|
out_pipes.each do |sout|
|
318
319
|
sout.abort if sout.respond_to? :abort
|
319
320
|
end
|
320
|
-
Log.
|
321
|
+
Log.low "Tee aborting #{Log.fingerprint stream}"
|
321
322
|
raise $!
|
322
323
|
rescue Exception
|
323
324
|
begin
|
@@ -332,7 +333,7 @@ module Open
|
|
332
333
|
in_pipes.each do |sin|
|
333
334
|
sin.close unless sin.closed?
|
334
335
|
end
|
335
|
-
Log.
|
336
|
+
Log.low "Tee exception #{Log.fingerprint stream}"
|
336
337
|
rescue
|
337
338
|
Log.exception $!
|
338
339
|
ensure
|
@@ -348,7 +349,7 @@ module Open
|
|
348
349
|
out_pipes.each do |sout|
|
349
350
|
ConcurrentStream.setup sout, :threads => splitter_thread, :filename => filename, :pair => stream
|
350
351
|
end
|
351
|
-
|
352
|
+
Thread.pass until splitter_thread["name"]
|
352
353
|
|
353
354
|
main_pipe = out_pipes.first
|
354
355
|
main_pipe.autojoin = true
|
@@ -370,4 +371,34 @@ module Open
|
|
370
371
|
def self.tee_stream(stream)
|
371
372
|
tee_stream_thread(stream)
|
372
373
|
end
|
374
|
+
|
375
|
+
def self.read_stream(stream, size)
|
376
|
+
str = nil
|
377
|
+
Thread.pass while IO.select([stream],nil,nil,1).nil?
|
378
|
+
while not str = stream.read(size)
|
379
|
+
IO.select([stream],nil,nil,1)
|
380
|
+
Thread.pass
|
381
|
+
raise ClosedStream if stream.eof?
|
382
|
+
end
|
383
|
+
|
384
|
+
while str.length < size
|
385
|
+
raise ClosedStream if stream.eof?
|
386
|
+
IO.select([stream],nil,nil,1)
|
387
|
+
if new = stream.read(size-str.length)
|
388
|
+
str << new
|
389
|
+
end
|
390
|
+
end
|
391
|
+
str
|
392
|
+
end
|
393
|
+
|
394
|
+
def self.read_stream(stream, size)
|
395
|
+
str = ""
|
396
|
+
while str.length < size
|
397
|
+
missing = size - str.length
|
398
|
+
more = stream.read(missing)
|
399
|
+
str << more
|
400
|
+
end
|
401
|
+
str
|
402
|
+
end
|
403
|
+
|
373
404
|
end
|
@@ -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
|
+
|