rbbt-util 5.12.3 → 5.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rbbt/persist.rb +127 -109
- data/lib/rbbt/tsv/dumper.rb +0 -1
- data/lib/rbbt/tsv/parallel/traverse.rb +101 -36
- data/lib/rbbt/util/concurrency/processes.rb +5 -2
- data/lib/rbbt/util/concurrency/threads.rb +6 -4
- data/lib/rbbt/util/log.rb +1 -0
- data/lib/rbbt/util/log/progress.rb +163 -0
- data/lib/rbbt/util/misc/options.rb +1 -0
- data/lib/rbbt/util/misc/pipes.rb +63 -50
- data/lib/rbbt/util/misc/progress.rb +0 -0
- data/lib/rbbt/util/open.rb +45 -11
- data/lib/rbbt/util/simpleopt/get.rb +1 -1
- data/lib/rbbt/workflow/accessor.rb +59 -38
- data/lib/rbbt/workflow/step/run.rb +2 -3
- data/share/rbbt_commands/workflow/task +14 -4
- data/test/rbbt/tsv/parallel/test_traverse.rb +35 -1
- data/test/rbbt/util/log/test_progress.rb +49 -0
- data/test/rbbt/util/misc/test_pipes.rb +4 -4
- data/test/rbbt/util/test_open.rb +0 -3
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50b93d44cf66b527dc5cb3171ea799ad7618024f
|
4
|
+
data.tar.gz: f8647589d8b4c281074309db7509bd4b26e50aea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b04572a90e96cc1ac50f476df87025f459a0b36258de071179ae7f702da7799e17680a5ceb9d36ecf6e243a2008cd93f45ba7523756568cd99948580cc254356
|
7
|
+
data.tar.gz: ab57d3affae47af95efebbe2d2368418f4b9eae2b7dd8ab980d5d3dfaecd414dbdc0b5ce4615a6770b3d665877c4584ae13c73d7f308f878beb0979e0364d40d
|
data/lib/rbbt/persist.rb
CHANGED
@@ -111,7 +111,17 @@ module Persist
|
|
111
111
|
res
|
112
112
|
when :marshal
|
113
113
|
Open.open(path) do |stream|
|
114
|
-
|
114
|
+
case stream
|
115
|
+
when StringIO
|
116
|
+
begin
|
117
|
+
Marshal.load(stream)
|
118
|
+
rescue
|
119
|
+
Log.exception $!
|
120
|
+
raise $!
|
121
|
+
end
|
122
|
+
else
|
123
|
+
Marshal.load(stream)
|
124
|
+
end
|
115
125
|
end
|
116
126
|
when :yaml
|
117
127
|
Open.open(path) do |stream|
|
@@ -208,7 +218,7 @@ module Persist
|
|
208
218
|
def self.tee_stream_thread(stream, path, type, callback = nil)
|
209
219
|
file, out = Misc.tee_stream(stream)
|
210
220
|
|
211
|
-
saver_thread = Thread.new(Thread.current
|
221
|
+
saver_thread = Thread.new(Thread.current) do |parent|
|
212
222
|
begin
|
213
223
|
Thread.current["name"] = "file saver: " + path
|
214
224
|
Misc.lock(path) do
|
@@ -216,11 +226,11 @@ module Persist
|
|
216
226
|
end
|
217
227
|
rescue Aborted
|
218
228
|
Log.error "Persist stream thread aborted: #{ Log.color :blue, path }"
|
219
|
-
|
229
|
+
file.abort if file.respond_to? :abort
|
220
230
|
rescue Exception
|
221
231
|
Log.error "Persist stream thread exception: #{ Log.color :blue, path }"
|
222
232
|
Log.exception $!
|
223
|
-
|
233
|
+
file.abort if file.respond_to? :abort
|
224
234
|
parent.raise $!
|
225
235
|
end
|
226
236
|
end
|
@@ -290,8 +300,119 @@ module Persist
|
|
290
300
|
alias tee_stream tee_stream_thread
|
291
301
|
end
|
292
302
|
|
303
|
+
def self.get_result(path, type, persist_options, lockfile, &block)
|
304
|
+
res = yield
|
305
|
+
|
306
|
+
if persist_options[:no_load] == :stream
|
307
|
+
case res
|
308
|
+
when IO
|
309
|
+
res = tee_stream(res, path, type, res.respond_to?(:callback)? res.callback : nil)
|
310
|
+
ConcurrentStream.setup res do
|
311
|
+
begin
|
312
|
+
lockfile.unlock if lockfile.locked?
|
313
|
+
rescue
|
314
|
+
Log.exception $!
|
315
|
+
Log.warn "Lockfile exception: " << $!.message
|
316
|
+
end
|
317
|
+
end
|
318
|
+
res.abort_callback = Proc.new do
|
319
|
+
begin
|
320
|
+
lockfile.unlock if lockfile.locked?
|
321
|
+
rescue
|
322
|
+
Log.exception $!
|
323
|
+
Log.warn "Lockfile exception: " << $!.message
|
324
|
+
end
|
325
|
+
end
|
326
|
+
raise KeepLocked.new res
|
327
|
+
when TSV::Dumper
|
328
|
+
res = tee_stream(res.stream, path, type, res.respond_to?(:callback)? res.callback : nil)
|
329
|
+
ConcurrentStream.setup res do
|
330
|
+
begin
|
331
|
+
lockfile.unlock
|
332
|
+
rescue
|
333
|
+
Log.exception $!
|
334
|
+
Log.warn "Lockfile exception: " << $!.message
|
335
|
+
end
|
336
|
+
end
|
337
|
+
res.abort_callback = Proc.new do
|
338
|
+
begin
|
339
|
+
lockfile.unlock
|
340
|
+
rescue
|
341
|
+
Log.exception $!
|
342
|
+
Log.warn "Lockfile exception: " << $!.message
|
343
|
+
end
|
344
|
+
end
|
345
|
+
raise KeepLocked.new res
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
case res
|
350
|
+
when IO
|
351
|
+
begin
|
352
|
+
res = case
|
353
|
+
when :array
|
354
|
+
res.read.split "\n"
|
355
|
+
when :tsv
|
356
|
+
TSV.open(res)
|
357
|
+
else
|
358
|
+
res.read
|
359
|
+
end
|
360
|
+
res.join if res.respond_to? :join
|
361
|
+
rescue
|
362
|
+
res.abort if res.respond_to? :abort
|
363
|
+
raise $!
|
364
|
+
end
|
365
|
+
when (defined? TSV and TSV::Dumper)
|
366
|
+
begin
|
367
|
+
io = res.stream
|
368
|
+
res = TSV.open(io)
|
369
|
+
io.join if io.respond_to? :join
|
370
|
+
rescue
|
371
|
+
io.abort if io.respond_to? :abort
|
372
|
+
raise $!
|
373
|
+
end
|
374
|
+
end
|
375
|
+
res
|
376
|
+
end
|
377
|
+
|
378
|
+
def self.persist_file(path, type, persist_options, &block)
|
379
|
+
|
380
|
+
if is_persisted?(path, persist_options)
|
381
|
+
Log.low "Persist up-to-date: #{ path } - #{Misc.fingerprint persist_options}"
|
382
|
+
return path if persist_options[:no_load]
|
383
|
+
return load_file(path, type)
|
384
|
+
end
|
385
|
+
|
386
|
+
begin
|
387
|
+
|
388
|
+
lock_filename = Persist.persistence_path(path + '.persist', {:dir => Persist.lock_dir})
|
389
|
+
Misc.lock lock_filename do |lockfile|
|
390
|
+
|
391
|
+
if is_persisted?(path, persist_options)
|
392
|
+
Log.low "Persist up-to-date (suddenly): #{ path } - #{Misc.fingerprint persist_options}"
|
393
|
+
return path if persist_options[:no_load]
|
394
|
+
return load_file(path, type)
|
395
|
+
end
|
396
|
+
|
397
|
+
Log.medium "Persist create: #{ path } - #{type} #{Misc.fingerprint persist_options}"
|
398
|
+
|
399
|
+
res = get_result(path, type, persist_options, lockfile, &block)
|
400
|
+
|
401
|
+
Misc.lock(path) do
|
402
|
+
save_file(path, type, res)
|
403
|
+
end
|
404
|
+
|
405
|
+
persist_options[:no_load] ? path : res
|
406
|
+
end
|
293
407
|
|
294
|
-
|
408
|
+
rescue
|
409
|
+
Log.error "Error in persist: #{path}#{Open.exists?(path) ? Log.color(:red, " Erasing") : ""}"
|
410
|
+
FileUtils.rm path if Open.exists? path
|
411
|
+
raise $!
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def self.persist(name, type = nil, persist_options = {}, &block)
|
295
416
|
type ||= :marshal
|
296
417
|
|
297
418
|
return (persist_options[:repo] || Persist::MEMORY)[persist_options[:file]] ||= yield if type ==:memory and persist_options[:file] and persist_options[:persist] and persist_options[:persist] != :update
|
@@ -388,110 +509,7 @@ module Persist
|
|
388
509
|
end
|
389
510
|
|
390
511
|
else
|
391
|
-
|
392
|
-
if is_persisted?(path, persist_options)
|
393
|
-
Log.low "Persist up-to-date: #{ path } - #{Misc.fingerprint persist_options}"
|
394
|
-
return path if persist_options[:no_load]
|
395
|
-
return load_file(path, type)
|
396
|
-
end
|
397
|
-
|
398
|
-
begin
|
399
|
-
|
400
|
-
lock_filename = Persist.persistence_path(path + '.persist', {:dir => Persist.lock_dir})
|
401
|
-
Misc.lock lock_filename do |lockfile|
|
402
|
-
|
403
|
-
if is_persisted?(path, persist_options)
|
404
|
-
Log.low "Persist up-to-date (suddenly): #{ path } - #{Misc.fingerprint persist_options}"
|
405
|
-
return path if persist_options[:no_load]
|
406
|
-
return load_file(path, type)
|
407
|
-
end
|
408
|
-
|
409
|
-
Log.medium "Persist create: #{ path } - #{Misc.fingerprint persist_options}"
|
410
|
-
|
411
|
-
res = yield
|
412
|
-
|
413
|
-
if persist_options[:no_load] == :stream
|
414
|
-
case res
|
415
|
-
when IO
|
416
|
-
res = tee_stream(res, path, type, res.respond_to?(:callback)? res.callback : nil)
|
417
|
-
ConcurrentStream.setup res do
|
418
|
-
begin
|
419
|
-
lockfile.unlock if lockfile.locked?
|
420
|
-
rescue
|
421
|
-
Log.exception $!
|
422
|
-
Log.warn "Lockfile exception: " << $!.message
|
423
|
-
end
|
424
|
-
end
|
425
|
-
res.abort_callback = Proc.new do
|
426
|
-
begin
|
427
|
-
lockfile.unlock if lockfile.locked?
|
428
|
-
rescue
|
429
|
-
Log.exception $!
|
430
|
-
Log.warn "Lockfile exception: " << $!.message
|
431
|
-
end
|
432
|
-
end
|
433
|
-
raise KeepLocked.new res
|
434
|
-
when TSV::Dumper
|
435
|
-
res = tee_stream(res.stream, path, type, res.respond_to?(:callback)? res.callback : nil)
|
436
|
-
ConcurrentStream.setup res do
|
437
|
-
begin
|
438
|
-
lockfile.unlock
|
439
|
-
rescue
|
440
|
-
Log.exception $!
|
441
|
-
Log.warn "Lockfile exception: " << $!.message
|
442
|
-
end
|
443
|
-
end
|
444
|
-
res.abort_callback = Proc.new do
|
445
|
-
begin
|
446
|
-
lockfile.unlock
|
447
|
-
rescue
|
448
|
-
Log.exception $!
|
449
|
-
Log.warn "Lockfile exception: " << $!.message
|
450
|
-
end
|
451
|
-
end
|
452
|
-
raise KeepLocked.new res
|
453
|
-
end
|
454
|
-
end
|
455
|
-
|
456
|
-
case res
|
457
|
-
when IO
|
458
|
-
begin
|
459
|
-
res = case
|
460
|
-
when :array
|
461
|
-
res.read.split "\n"
|
462
|
-
when :tsv
|
463
|
-
TSV.open(res)
|
464
|
-
else
|
465
|
-
res.read
|
466
|
-
end
|
467
|
-
res.join if res.respond_to? :join
|
468
|
-
rescue
|
469
|
-
res.abort if res.respond_to? :abort
|
470
|
-
raise $!
|
471
|
-
end
|
472
|
-
when (defined? TSV and TSV::Dumper)
|
473
|
-
begin
|
474
|
-
io = res.stream
|
475
|
-
res = TSV.open(io)
|
476
|
-
io.join if io.respond_to? :join
|
477
|
-
rescue
|
478
|
-
io.abort if io.respond_to? :abort
|
479
|
-
raise $!
|
480
|
-
end
|
481
|
-
end
|
482
|
-
|
483
|
-
Misc.lock(path) do
|
484
|
-
save_file(path, type, res)
|
485
|
-
end
|
486
|
-
|
487
|
-
persist_options[:no_load] ? path : res
|
488
|
-
end
|
489
|
-
|
490
|
-
rescue
|
491
|
-
Log.error "Error in persist: #{path}#{Open.exists?(path) ? Log.color(:red, " Erasing") : ""}"
|
492
|
-
FileUtils.rm path if Open.exists? path
|
493
|
-
raise $!
|
494
|
-
end
|
512
|
+
persist_file(path, type, persist_options, &block)
|
495
513
|
end
|
496
514
|
|
497
515
|
end
|
data/lib/rbbt/tsv/dumper.rb
CHANGED
@@ -13,8 +13,8 @@ module TSV
|
|
13
13
|
def self.stream_name(obj)
|
14
14
|
filename_obj = obj.respond_to?(:filename) ? obj.filename : nil
|
15
15
|
filename_obj ||= obj.respond_to?(:path) ? obj.path : nil
|
16
|
-
stream_obj = obj_stream(obj)
|
17
|
-
filename_obj.nil? ? stream_obj
|
16
|
+
stream_obj = obj_stream(obj) || obj
|
17
|
+
filename_obj.nil? ? Misc.fingerprint(stream_obj) : filename_obj + "(#{Misc.fingerprint(stream_obj)})"
|
18
18
|
end
|
19
19
|
|
20
20
|
def self.report(msg, obj, into)
|
@@ -23,71 +23,101 @@ module TSV
|
|
23
23
|
Log.error "#{ msg } #{stream_name(obj)} -> #{stream_name(into)}"
|
24
24
|
end
|
25
25
|
|
26
|
+
#{{{ TRAVERSE OBJECTS
|
27
|
+
|
26
28
|
def self.traverse_tsv(tsv, options = {}, &block)
|
27
|
-
callback = Misc.process_options options, :callback
|
29
|
+
callback, bar, join = Misc.process_options options, :callback, :bar, :join
|
28
30
|
|
29
31
|
if callback
|
30
32
|
tsv.through options[:key_field], options[:fields] do |k,v|
|
31
|
-
|
33
|
+
begin
|
34
|
+
callback.call yield(k,v)
|
35
|
+
ensure
|
36
|
+
bar.tick if bar
|
37
|
+
end
|
32
38
|
end
|
33
39
|
else
|
34
40
|
tsv.through options[:key_field], options[:fields] do |k,v|
|
35
|
-
|
41
|
+
begin
|
42
|
+
yield k,v
|
43
|
+
ensure
|
44
|
+
bar.tick if bar
|
45
|
+
end
|
36
46
|
end
|
37
47
|
end
|
48
|
+
join.call if join
|
38
49
|
end
|
39
50
|
|
40
51
|
def self.traverse_hash(hash, options = {}, &block)
|
41
|
-
callback = Misc.process_options options, :callback
|
52
|
+
callback, bar, join = Misc.process_options options, :callback, :bar, :join
|
42
53
|
|
43
54
|
if callback
|
44
55
|
hash.each do |k,v|
|
45
|
-
|
56
|
+
begin
|
57
|
+
callback.call yield(k,v)
|
58
|
+
ensure
|
59
|
+
bar.tick if bar
|
60
|
+
end
|
46
61
|
end
|
47
62
|
else
|
48
63
|
hash.each do |k,v|
|
49
|
-
|
64
|
+
begin
|
65
|
+
yield k,v
|
66
|
+
ensure
|
67
|
+
bar.tick if bar
|
68
|
+
end
|
50
69
|
end
|
51
70
|
end
|
71
|
+
join.call if join
|
52
72
|
end
|
53
73
|
|
54
74
|
def self.traverse_array(array, options = {}, &block)
|
55
|
-
callback = Misc.process_options options, :callback
|
75
|
+
callback, bar, join = Misc.process_options options, :callback, :bar, :join
|
56
76
|
|
57
77
|
if callback
|
58
78
|
array.each do |e|
|
59
|
-
|
60
|
-
|
79
|
+
begin
|
80
|
+
callback.call yield(e)
|
81
|
+
ensure
|
82
|
+
bar.tick if bar
|
83
|
+
end
|
61
84
|
end
|
62
85
|
else
|
63
86
|
array.each do |e|
|
64
|
-
|
87
|
+
begin
|
88
|
+
yield e
|
89
|
+
ensure
|
90
|
+
bar.tick if bar
|
91
|
+
end
|
65
92
|
end
|
66
93
|
end
|
94
|
+
join.call if join
|
67
95
|
end
|
68
96
|
|
69
97
|
def self.traverse_io_array(io, options = {}, &block)
|
70
|
-
callback = Misc.process_options options, :callback
|
98
|
+
callback, bar, join = Misc.process_options options, :callback, :bar, :join
|
71
99
|
if callback
|
72
100
|
while line = io.gets
|
73
|
-
|
74
|
-
|
101
|
+
begin
|
102
|
+
callback.call yield line.strip
|
103
|
+
ensure
|
104
|
+
bar.tick if bar
|
105
|
+
end
|
75
106
|
end
|
76
107
|
else
|
77
108
|
while line = io.gets
|
78
109
|
yield line.strip
|
79
110
|
end
|
80
111
|
end
|
112
|
+
join.call if join
|
81
113
|
end
|
82
114
|
|
83
115
|
def self.traverse_io(io, options = {}, &block)
|
84
|
-
|
85
|
-
callback = Misc.process_options options, :callback
|
116
|
+
callback, bar, join = Misc.process_options options, :callback, :bar, :join
|
86
117
|
begin
|
87
118
|
if callback
|
88
119
|
TSV::Parser.traverse(io, options) do |k,v|
|
89
|
-
|
90
|
-
callback.call res
|
120
|
+
callback.call yield k, v
|
91
121
|
end
|
92
122
|
else
|
93
123
|
TSV::Parser.traverse(io, options, &block)
|
@@ -96,10 +126,10 @@ module TSV
|
|
96
126
|
Log.error "Traverse IO error"
|
97
127
|
raise $!
|
98
128
|
end
|
129
|
+
join.call if join
|
99
130
|
end
|
100
131
|
|
101
132
|
def self.traverse_obj(obj, options = {}, &block)
|
102
|
-
filename = obj.filename if obj.respond_to? :filename
|
103
133
|
if options[:type] == :keys
|
104
134
|
options[:fields] = []
|
105
135
|
options[:type] = :single
|
@@ -114,8 +144,7 @@ module TSV
|
|
114
144
|
callback = Misc.process_options options, :callback
|
115
145
|
if callback
|
116
146
|
obj.traverse(options) do |k,v|
|
117
|
-
|
118
|
-
callback.call res
|
147
|
+
callback.call yield k, v
|
119
148
|
end
|
120
149
|
else
|
121
150
|
obj.traverse(options, &block)
|
@@ -195,9 +224,8 @@ module TSV
|
|
195
224
|
|
196
225
|
def self.traverse_cpus(num, obj, options, &block)
|
197
226
|
begin
|
198
|
-
|
199
|
-
|
200
|
-
q = RbbtProcessQueue.new num, cleanup
|
227
|
+
callback, cleanup, join = Misc.process_options options, :callback, :cleanup, :join
|
228
|
+
q = RbbtProcessQueue.new num, cleanup, join
|
201
229
|
|
202
230
|
q.callback &callback
|
203
231
|
q.init &block
|
@@ -209,8 +237,16 @@ module TSV
|
|
209
237
|
end
|
210
238
|
|
211
239
|
thread.join
|
240
|
+
rescue Interrupt, Aborted
|
241
|
+
Log.error "Aborted traversal in CPUs for #{stream_name(obj) || Misc.fingerprint(obj)}"
|
242
|
+
stream = obj_stream(obj)
|
243
|
+
stream.abort if stream.respond_to? :abort
|
244
|
+
stream = obj_stream(options[:into])
|
245
|
+
stream.abort if stream.respond_to? :abort
|
246
|
+
q.abort
|
247
|
+
raise $!
|
212
248
|
rescue Exception
|
213
|
-
Log.error "Exception
|
249
|
+
Log.error "Exception during traversal in CPUs for #{stream_name(obj) || Misc.fingerprint(obj)}"
|
214
250
|
Log.exception $!
|
215
251
|
|
216
252
|
stream = obj_stream(obj)
|
@@ -295,11 +331,13 @@ module TSV
|
|
295
331
|
close_streams.concat(get_streams_to_close(obj))
|
296
332
|
options[:close_streams] = close_streams
|
297
333
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
334
|
+
if close_streams and close_streams.any?
|
335
|
+
options[:cleanup] = Proc.new do
|
336
|
+
close_streams.uniq.each do |s|
|
337
|
+
s.close unless s.closed?
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
303
341
|
|
304
342
|
traverse_cpus cpus, obj, options, &block
|
305
343
|
end
|
@@ -323,13 +361,8 @@ module TSV
|
|
323
361
|
end
|
324
362
|
|
325
363
|
def self.traverse(obj, options = {}, &block)
|
326
|
-
threads = Misc.process_options options, :threads
|
327
|
-
cpus = Misc.process_options options, :cpus
|
328
364
|
into = options[:into]
|
329
365
|
|
330
|
-
threads = nil if threads and threads.to_i <= 1
|
331
|
-
cpus = nil if cpus and cpus.to_i <= 1
|
332
|
-
|
333
366
|
if into == :stream
|
334
367
|
sout = Misc.open_pipe false, false do |sin|
|
335
368
|
begin
|
@@ -342,13 +375,45 @@ module TSV
|
|
342
375
|
return sout
|
343
376
|
end
|
344
377
|
|
378
|
+
threads = Misc.process_options options, :threads
|
379
|
+
cpus = Misc.process_options options, :cpus
|
380
|
+
threads = nil if threads and threads.to_i <= 1
|
381
|
+
cpus = nil if cpus and cpus.to_i <= 1
|
382
|
+
|
383
|
+
bar = Misc.process_options options, :bar
|
384
|
+
bar ||= Misc.process_options options, :progress
|
385
|
+
options[:bar] = case bar
|
386
|
+
when String
|
387
|
+
Log::ProgressBar.new_bar(nil, {:desc => bar})
|
388
|
+
when TrueClass
|
389
|
+
Log::ProgressBar.new_bar(nil, nil)
|
390
|
+
when Fixnum
|
391
|
+
Log::ProgressBar.new_bar(bar)
|
392
|
+
when Hash
|
393
|
+
max = Misc.process_options bar, :max
|
394
|
+
Log::ProgressBar.new_bar(max, bar)
|
395
|
+
else
|
396
|
+
bar
|
397
|
+
end
|
398
|
+
|
345
399
|
if into
|
400
|
+
bar = Misc.process_options options, :bar
|
401
|
+
|
402
|
+
options[:join] = Proc.new do
|
403
|
+
Log::ProgressBar.remove_bar(bar)
|
404
|
+
end if bar
|
405
|
+
|
346
406
|
options[:callback] = Proc.new do |e|
|
347
407
|
begin
|
348
408
|
store_into into, e
|
409
|
+
rescue Aborted
|
410
|
+
Log.error "Traversal info #{stream_name into} aborted"
|
411
|
+
raise $!
|
349
412
|
rescue Exception
|
350
413
|
Log.exception $!
|
351
414
|
raise $!
|
415
|
+
ensure
|
416
|
+
bar.tick if bar
|
352
417
|
end
|
353
418
|
end
|
354
419
|
|