parallel 0.5.21 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/lib/parallel.rb +253 -232
- data/lib/parallel/version.rb +1 -1
- data/spec/cases/map_with_killed_worker_before_read.rb +9 -0
- data/spec/cases/map_with_killed_worker_before_write.rb +18 -0
- data/spec/parallel_spec.rb +8 -0
- metadata +8 -6
data/Gemfile.lock
CHANGED
data/lib/parallel.rb
CHANGED
@@ -3,308 +3,329 @@ require 'rbconfig'
|
|
3
3
|
require 'parallel/version'
|
4
4
|
|
5
5
|
module Parallel
|
6
|
-
|
7
|
-
count, options = extract_count_from_options(options)
|
8
|
-
|
9
|
-
out = []
|
10
|
-
threads = []
|
11
|
-
|
12
|
-
count.times do |i|
|
13
|
-
threads[i] = Thread.new do
|
14
|
-
out[i] = yield(i)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
wait_for_threads(threads)
|
19
|
-
|
20
|
-
out
|
6
|
+
class DeadWorker < EOFError
|
21
7
|
end
|
22
8
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
9
|
+
class ExceptionWrapper
|
10
|
+
attr_reader :exception
|
11
|
+
def initialize(exception)
|
12
|
+
dumpable = Marshal.dump(exception) rescue nil
|
13
|
+
unless dumpable
|
14
|
+
exception = RuntimeError.new("Undumpable Exception -- #{exception.inspect}")
|
15
|
+
end
|
28
16
|
|
29
|
-
|
30
|
-
|
31
|
-
array
|
17
|
+
@exception = exception
|
18
|
+
end
|
32
19
|
end
|
33
20
|
|
34
|
-
|
35
|
-
|
36
|
-
|
21
|
+
class Worker
|
22
|
+
attr_reader :pid, :read, :write
|
23
|
+
def initialize(read, write, pid)
|
24
|
+
@read, @write, @pid = read, write, pid
|
25
|
+
end
|
37
26
|
|
38
|
-
|
39
|
-
|
27
|
+
def close_pipes
|
28
|
+
read.close
|
29
|
+
write.close
|
30
|
+
end
|
40
31
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
method = :in_processes
|
46
|
-
size = options[method] || processor_count
|
32
|
+
def wait
|
33
|
+
Process.wait(pid)
|
34
|
+
rescue Interrupt
|
35
|
+
# process died
|
47
36
|
end
|
48
|
-
size = [array.size, size].min
|
49
37
|
|
50
|
-
|
38
|
+
def work(index)
|
39
|
+
begin
|
40
|
+
Marshal.dump(index, write)
|
41
|
+
rescue Errno::EPIPE
|
42
|
+
raise DeadWorker
|
43
|
+
end
|
51
44
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
45
|
+
begin
|
46
|
+
Marshal.load(read)
|
47
|
+
rescue EOFError
|
48
|
+
raise Parallel::DeadWorker
|
49
|
+
end
|
56
50
|
end
|
57
51
|
end
|
58
52
|
|
59
|
-
|
60
|
-
|
61
|
-
|
53
|
+
class << self
|
54
|
+
def in_threads(options={:count => 2})
|
55
|
+
count, options = extract_count_from_options(options)
|
62
56
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
wmi = WIN32OLE.connect("winmgmts://")
|
76
|
-
cpu = wmi.ExecQuery("select NumberOfLogicalProcessors from Win32_Processor")
|
77
|
-
cpu.to_enum.first.NumberOfLogicalProcessors
|
78
|
-
when /solaris2/
|
79
|
-
`psrinfo -p`.to_i # this is physical cpus afaik
|
80
|
-
else
|
81
|
-
$stderr.puts "Unknown architecture ( #{RbConfig::CONFIG["host_os"]} ) assuming one processor."
|
82
|
-
1
|
57
|
+
out = []
|
58
|
+
threads = []
|
59
|
+
|
60
|
+
count.times do |i|
|
61
|
+
threads[i] = Thread.new do
|
62
|
+
out[i] = yield(i)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
wait_for_threads(threads)
|
67
|
+
|
68
|
+
out
|
83
69
|
end
|
84
|
-
end
|
85
70
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
when /linux/
|
91
|
-
`grep cores /proc/cpuinfo`[/\d+/].to_i
|
92
|
-
when /mswin|mingw/
|
93
|
-
require 'win32ole'
|
94
|
-
wmi = WIN32OLE.connect("winmgmts://")
|
95
|
-
cpu = wmi.ExecQuery("select NumberOfProcessors from Win32_Processor")
|
96
|
-
cpu.to_enum.first.NumberOfLogicalProcessors
|
97
|
-
else
|
98
|
-
processor_count
|
71
|
+
def in_processes(options = {}, &block)
|
72
|
+
count, options = extract_count_from_options(options)
|
73
|
+
count ||= processor_count
|
74
|
+
map(0...count, options.merge(:in_processes => count), &block)
|
99
75
|
end
|
100
|
-
end
|
101
76
|
|
102
|
-
|
77
|
+
def each(array, options={}, &block)
|
78
|
+
map(array, options.merge(:preserve_results => false), &block)
|
79
|
+
array
|
80
|
+
end
|
103
81
|
|
104
|
-
|
105
|
-
|
106
|
-
array.each_with_index do |e,i|
|
107
|
-
results << (options[:with_index] ? yield(e,i) : yield(e))
|
82
|
+
def each_with_index(array, options={}, &block)
|
83
|
+
each(array, options.merge(:with_index => true), &block)
|
108
84
|
end
|
109
|
-
results
|
110
|
-
end
|
111
85
|
|
112
|
-
|
113
|
-
|
114
|
-
end
|
86
|
+
def map(array, options = {}, &block)
|
87
|
+
array = array.to_a # turn Range and other Enumerable-s into an Array
|
115
88
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
89
|
+
if options[:in_threads]
|
90
|
+
method = :in_threads
|
91
|
+
size = options[method]
|
92
|
+
else
|
93
|
+
method = :in_processes
|
94
|
+
size = options[method] || processor_count
|
95
|
+
end
|
96
|
+
size = [array.size, size].min
|
122
97
|
|
123
|
-
|
124
|
-
# as long as there are more items, work on one of them
|
125
|
-
loop do
|
126
|
-
break if exception
|
98
|
+
return work_direct(array, options, &block) if size == 0
|
127
99
|
|
128
|
-
|
129
|
-
|
100
|
+
if method == :in_threads
|
101
|
+
work_in_threads(array, options.merge(:count => size), &block)
|
102
|
+
else
|
103
|
+
work_in_processes(array, options.merge(:count => size), &block)
|
104
|
+
end
|
105
|
+
end
|
130
106
|
|
131
|
-
|
132
|
-
|
107
|
+
def map_with_index(array, options={}, &block)
|
108
|
+
map(array, options.merge(:with_index => true), &block)
|
109
|
+
end
|
133
110
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
111
|
+
def processor_count
|
112
|
+
@processor_count ||= case RbConfig::CONFIG['host_os']
|
113
|
+
when /darwin9/
|
114
|
+
`hwprefs cpu_count`.to_i
|
115
|
+
when /darwin/
|
116
|
+
(hwprefs_available? ? `hwprefs thread_count` : `sysctl -n hw.ncpu`).to_i
|
117
|
+
when /linux|cygwin/
|
118
|
+
`grep -c processor /proc/cpuinfo`.to_i
|
119
|
+
when /(open|free)bsd/
|
120
|
+
`sysctl -n hw.ncpu`.to_i
|
121
|
+
when /mswin|mingw/
|
122
|
+
require 'win32ole'
|
123
|
+
wmi = WIN32OLE.connect("winmgmts://")
|
124
|
+
cpu = wmi.ExecQuery("select NumberOfLogicalProcessors from Win32_Processor")
|
125
|
+
cpu.to_enum.first.NumberOfLogicalProcessors
|
126
|
+
when /solaris2/
|
127
|
+
`psrinfo -p`.to_i # this is physical cpus afaik
|
128
|
+
else
|
129
|
+
$stderr.puts "Unknown architecture ( #{RbConfig::CONFIG["host_os"]} ) assuming one processor."
|
130
|
+
1
|
142
131
|
end
|
143
132
|
end
|
144
133
|
|
145
|
-
|
134
|
+
def physical_processor_count
|
135
|
+
@physical_processor_count ||= case RbConfig::CONFIG['host_os']
|
136
|
+
when /darwin1/, /freebsd/
|
137
|
+
`sysctl -n hw.physicalcpu`.to_i
|
138
|
+
when /linux/
|
139
|
+
`grep cores /proc/cpuinfo`[/\d+/].to_i
|
140
|
+
when /mswin|mingw/
|
141
|
+
require 'win32ole'
|
142
|
+
wmi = WIN32OLE.connect("winmgmts://")
|
143
|
+
cpu = wmi.ExecQuery("select NumberOfProcessors from Win32_Processor")
|
144
|
+
cpu.to_enum.first.NumberOfLogicalProcessors
|
145
|
+
else
|
146
|
+
processor_count
|
147
|
+
end
|
148
|
+
end
|
146
149
|
|
147
|
-
|
148
|
-
end
|
150
|
+
private
|
149
151
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
152
|
+
def work_direct(array, options)
|
153
|
+
results = []
|
154
|
+
array.each_with_index do |e,i|
|
155
|
+
results << (options[:with_index] ? yield(e,i) : yield(e))
|
156
|
+
end
|
157
|
+
results
|
158
|
+
end
|
159
|
+
|
160
|
+
def hwprefs_available?
|
161
|
+
`which hwprefs` != ''
|
162
|
+
end
|
157
163
|
|
158
|
-
|
159
|
-
|
164
|
+
def work_in_threads(items, options, &block)
|
165
|
+
results = []
|
166
|
+
current = -1
|
167
|
+
exception = nil
|
160
168
|
|
161
|
-
|
169
|
+
in_threads(options[:count]) do
|
170
|
+
# as long as there are more items, work on one of them
|
162
171
|
loop do
|
163
172
|
break if exception
|
164
|
-
index = Thread.exclusive{ current_index += 1 }
|
165
|
-
break if index >= items.size
|
166
173
|
|
167
|
-
|
168
|
-
|
169
|
-
Marshal.dump(index, worker[:write])
|
170
|
-
on_start.call(item, index) if on_start
|
171
|
-
|
172
|
-
output = Marshal.load(worker[:read])
|
173
|
-
on_finish.call(item, index) if on_finish
|
174
|
+
index = Thread.exclusive{ current+=1 }
|
175
|
+
break if index >= items.size
|
174
176
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
177
|
+
with_instrumentation items[index], index, options do
|
178
|
+
begin
|
179
|
+
results[index] = call_with_index(items, index, options, &block)
|
180
|
+
rescue Exception => e
|
181
|
+
exception = e
|
182
|
+
break
|
183
|
+
end
|
179
184
|
end
|
180
185
|
end
|
181
|
-
ensure
|
182
|
-
close_pipes(worker)
|
183
|
-
wait_for_process worker[:pid] # if it goes zombie, rather wait here to be able to debug
|
184
186
|
end
|
185
|
-
end
|
186
187
|
|
187
|
-
|
188
|
+
raise exception if exception
|
188
189
|
|
189
|
-
|
190
|
-
end
|
191
|
-
|
192
|
-
def self.create_workers(items, options, &block)
|
193
|
-
workers = []
|
194
|
-
Array.new(options[:count]).each do
|
195
|
-
workers << worker(items, options.merge(:started_workers => workers), &block)
|
190
|
+
results
|
196
191
|
end
|
197
192
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
193
|
+
def work_in_processes(items, options, &blk)
|
194
|
+
workers = create_workers(items, options, &blk)
|
195
|
+
current_index = -1
|
196
|
+
results = []
|
197
|
+
exception = nil
|
202
198
|
|
203
|
-
|
204
|
-
|
205
|
-
GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
|
199
|
+
in_threads(options[:count]) do |i|
|
200
|
+
worker = workers[i]
|
206
201
|
|
207
|
-
|
208
|
-
|
202
|
+
begin
|
203
|
+
loop do
|
204
|
+
break if exception
|
205
|
+
index = Thread.exclusive{ current_index += 1 }
|
206
|
+
break if index >= items.size
|
207
|
+
|
208
|
+
output = with_instrumentation items[index], index, options do
|
209
|
+
worker.work(index)
|
210
|
+
end
|
211
|
+
|
212
|
+
if ExceptionWrapper === output
|
213
|
+
exception = output.exception
|
214
|
+
else
|
215
|
+
results[index] = output
|
216
|
+
end
|
217
|
+
end
|
218
|
+
ensure
|
219
|
+
worker.close_pipes
|
220
|
+
worker.wait # if it goes zombie, rather wait here to be able to debug
|
221
|
+
end
|
222
|
+
end
|
209
223
|
|
210
|
-
|
211
|
-
begin
|
212
|
-
options.delete(:started_workers).each{|w| close_pipes w }
|
224
|
+
raise exception if exception
|
213
225
|
|
214
|
-
|
215
|
-
|
226
|
+
results
|
227
|
+
end
|
216
228
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
229
|
+
def create_workers(items, options, &block)
|
230
|
+
workers = []
|
231
|
+
Array.new(options[:count]).each do
|
232
|
+
workers << worker(items, options.merge(:started_workers => workers), &block)
|
221
233
|
end
|
234
|
+
|
235
|
+
kill_on_ctrl_c(workers.map(&:pid))
|
236
|
+
workers
|
222
237
|
end
|
223
238
|
|
224
|
-
|
225
|
-
|
239
|
+
def worker(items, options, &block)
|
240
|
+
# use less memory on REE
|
241
|
+
GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
|
226
242
|
|
227
|
-
|
228
|
-
|
243
|
+
child_read, parent_write = IO.pipe
|
244
|
+
parent_read, child_write = IO.pipe
|
229
245
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
end
|
246
|
+
pid = Process.fork do
|
247
|
+
begin
|
248
|
+
options.delete(:started_workers).each(&:close_pipes)
|
234
249
|
|
235
|
-
|
236
|
-
|
237
|
-
index = Marshal.load(read)
|
238
|
-
begin
|
239
|
-
result = call_with_index(items, index, options, &block)
|
240
|
-
result = nil if options[:preserve_results] == false
|
241
|
-
rescue Exception => e
|
242
|
-
result = ExceptionWrapper.new(e)
|
243
|
-
end
|
244
|
-
Marshal.dump(result, write)
|
245
|
-
end
|
246
|
-
end
|
250
|
+
parent_write.close
|
251
|
+
parent_read.close
|
247
252
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
# thread died, do not stop other threads
|
253
|
+
process_incoming_jobs(child_read, child_write, items, options, &block)
|
254
|
+
ensure
|
255
|
+
child_read.close
|
256
|
+
child_write.close
|
257
|
+
end
|
254
258
|
end
|
255
|
-
end
|
256
|
-
end
|
257
259
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
# process died
|
260
|
+
child_read.close
|
261
|
+
child_write.close
|
262
|
+
|
263
|
+
Worker.new(parent_read, parent_write, pid)
|
263
264
|
end
|
264
|
-
end
|
265
265
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
266
|
+
def process_incoming_jobs(read, write, items, options, &block)
|
267
|
+
while !read.eof?
|
268
|
+
index = Marshal.load(read)
|
269
|
+
begin
|
270
|
+
result = call_with_index(items, index, options, &block)
|
271
|
+
result = nil if options[:preserve_results] == false
|
272
|
+
rescue Exception => e
|
273
|
+
result = ExceptionWrapper.new(e)
|
274
|
+
end
|
275
|
+
Marshal.dump(result, write)
|
276
|
+
end
|
273
277
|
end
|
274
|
-
[count, options]
|
275
|
-
end
|
276
278
|
|
277
|
-
|
278
|
-
|
279
|
-
Signal.trap :SIGINT do
|
280
|
-
$stderr.puts 'Parallel execution interrupted, exiting ...'
|
281
|
-
pids.compact.each do |pid|
|
279
|
+
def wait_for_threads(threads)
|
280
|
+
threads.compact.each do |t|
|
282
281
|
begin
|
283
|
-
|
284
|
-
rescue
|
285
|
-
#
|
286
|
-
# so we just ignore them not being there
|
282
|
+
t.join
|
283
|
+
rescue Interrupt
|
284
|
+
# thread died, do not stop other threads
|
287
285
|
end
|
288
286
|
end
|
289
|
-
exit 1 # Quit with 'failed' signal
|
290
287
|
end
|
291
|
-
end
|
292
288
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
289
|
+
# options is either a Integer or a Hash with :count
|
290
|
+
def extract_count_from_options(options)
|
291
|
+
if options.is_a?(Hash)
|
292
|
+
count = options[:count]
|
293
|
+
else
|
294
|
+
count = options
|
295
|
+
options = {}
|
296
|
+
end
|
297
|
+
[count, options]
|
298
|
+
end
|
298
299
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
300
|
+
# kill all these processes (children) if user presses Ctrl+c
|
301
|
+
def kill_on_ctrl_c(pids)
|
302
|
+
Signal.trap :SIGINT do
|
303
|
+
$stderr.puts 'Parallel execution interrupted, exiting ...'
|
304
|
+
pids.compact.each do |pid|
|
305
|
+
begin
|
306
|
+
Process.kill(:KILL, pid)
|
307
|
+
rescue Errno::ESRCH
|
308
|
+
# some linux systems already automatically killed the children at this point
|
309
|
+
# so we just ignore them not being there
|
310
|
+
end
|
311
|
+
end
|
312
|
+
exit 1 # Quit with 'failed' signal
|
305
313
|
end
|
314
|
+
end
|
306
315
|
|
307
|
-
|
316
|
+
def call_with_index(array, index, options, &block)
|
317
|
+
args = [array[index]]
|
318
|
+
args << index if options[:with_index]
|
319
|
+
block.call(*args)
|
320
|
+
end
|
321
|
+
|
322
|
+
def with_instrumentation(item, index, options)
|
323
|
+
on_start = options[:start]
|
324
|
+
on_finish = options[:finish]
|
325
|
+
on_start.call(item, index) if on_start
|
326
|
+
yield
|
327
|
+
ensure
|
328
|
+
on_finish.call(item, index) if on_finish
|
308
329
|
end
|
309
330
|
end
|
310
331
|
end
|
data/lib/parallel/version.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.expand_path('spec/spec_helper')
|
2
|
+
|
3
|
+
Parallel::Worker.class_eval do
|
4
|
+
alias_method :work_without_kill, :work
|
5
|
+
def work(*args)
|
6
|
+
Process.kill("SIGKILL", pid)
|
7
|
+
sleep 0.5
|
8
|
+
work_without_kill(*args)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
begin
|
13
|
+
Parallel.map([1,2,3]) do |x, i|
|
14
|
+
Process.kill("SIGKILL", Process.pid)
|
15
|
+
end
|
16
|
+
rescue Parallel::DeadWorker
|
17
|
+
puts "DEAD"
|
18
|
+
end
|
data/spec/parallel_spec.rb
CHANGED
@@ -206,6 +206,14 @@ describe Parallel do
|
|
206
206
|
monitor.should_receive(:call).once.with(:third, 2)
|
207
207
|
Parallel.map([:first, :second, :third], :finish => monitor, :in_threads => 3) {}
|
208
208
|
end
|
209
|
+
|
210
|
+
it "spits out a useful error when a worker dies before read" do
|
211
|
+
`ruby spec/cases/map_with_killed_worker_before_read.rb 2>&1`.should include "DEAD"
|
212
|
+
end
|
213
|
+
|
214
|
+
it "spits out a useful error when a worker dies before write" do
|
215
|
+
`ruby spec/cases/map_with_killed_worker_before_write.rb 2>&1`.should include "DEAD"
|
216
|
+
end
|
209
217
|
end
|
210
218
|
|
211
219
|
describe ".map_with_index" do
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: parallel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.21
|
5
4
|
prerelease:
|
5
|
+
version: 0.6.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Michael Grosser
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
12
|
+
date: 2012-12-15 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description:
|
15
15
|
email: michael@grosser.it
|
@@ -31,6 +31,8 @@ files:
|
|
31
31
|
- spec/cases/host_os_override_processor_count.rb
|
32
32
|
- spec/cases/map_with_index.rb
|
33
33
|
- spec/cases/map_with_index_empty.rb
|
34
|
+
- spec/cases/map_with_killed_worker_before_read.rb
|
35
|
+
- spec/cases/map_with_killed_worker_before_write.rb
|
34
36
|
- spec/cases/map_with_nested_arrays_and_nil.rb
|
35
37
|
- spec/cases/map_with_processes_and_exceptions.rb
|
36
38
|
- spec/cases/map_with_threads_and_exceptions.rb
|
@@ -59,23 +61,23 @@ rdoc_options: []
|
|
59
61
|
require_paths:
|
60
62
|
- lib
|
61
63
|
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
-
none: false
|
63
64
|
requirements:
|
64
65
|
- - ! '>='
|
65
66
|
- !ruby/object:Gem::Version
|
66
67
|
version: '0'
|
67
68
|
segments:
|
68
69
|
- 0
|
69
|
-
hash: -
|
70
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
hash: -1821301885117501782
|
71
71
|
none: false
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
73
|
requirements:
|
73
74
|
- - ! '>='
|
74
75
|
- !ruby/object:Gem::Version
|
75
76
|
version: '0'
|
76
77
|
segments:
|
77
78
|
- 0
|
78
|
-
hash: -
|
79
|
+
hash: -1821301885117501782
|
80
|
+
none: false
|
79
81
|
requirements: []
|
80
82
|
rubyforge_project:
|
81
83
|
rubygems_version: 1.8.24
|