parallel 0.5.21 → 0.6.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.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- parallel (0.5.21)
4
+ parallel (0.6.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/lib/parallel.rb CHANGED
@@ -3,308 +3,329 @@ require 'rbconfig'
3
3
  require 'parallel/version'
4
4
 
5
5
  module Parallel
6
- def self.in_threads(options={:count => 2})
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
- def self.in_processes(options = {}, &block)
24
- count, options = extract_count_from_options(options)
25
- count ||= processor_count
26
- map(0...count, options.merge(:in_processes => count), &block)
27
- end
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
- def self.each(array, options={}, &block)
30
- map(array, options.merge(:preserve_results => false), &block)
31
- array
17
+ @exception = exception
18
+ end
32
19
  end
33
20
 
34
- def self.each_with_index(array, options={}, &block)
35
- each(array, options.merge(:with_index => true), &block)
36
- end
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
- def self.map(array, options = {}, &block)
39
- array = array.to_a # turn Range and other Enumerable-s into an Array
27
+ def close_pipes
28
+ read.close
29
+ write.close
30
+ end
40
31
 
41
- if options[:in_threads]
42
- method = :in_threads
43
- size = options[method]
44
- else
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
- return work_direct(array, options, &block) if size == 0
38
+ def work(index)
39
+ begin
40
+ Marshal.dump(index, write)
41
+ rescue Errno::EPIPE
42
+ raise DeadWorker
43
+ end
51
44
 
52
- if method == :in_threads
53
- work_in_threads(array, options.merge(:count => size), &block)
54
- else
55
- work_in_processes(array, options.merge(:count => size), &block)
45
+ begin
46
+ Marshal.load(read)
47
+ rescue EOFError
48
+ raise Parallel::DeadWorker
49
+ end
56
50
  end
57
51
  end
58
52
 
59
- def self.map_with_index(array, options={}, &block)
60
- map(array, options.merge(:with_index => true), &block)
61
- end
53
+ class << self
54
+ def in_threads(options={:count => 2})
55
+ count, options = extract_count_from_options(options)
62
56
 
63
- def self.processor_count
64
- @processor_count ||= case RbConfig::CONFIG['host_os']
65
- when /darwin9/
66
- `hwprefs cpu_count`.to_i
67
- when /darwin/
68
- (hwprefs_available? ? `hwprefs thread_count` : `sysctl -n hw.ncpu`).to_i
69
- when /linux|cygwin/
70
- `grep -c processor /proc/cpuinfo`.to_i
71
- when /(open|free)bsd/
72
- `sysctl -n hw.ncpu`.to_i
73
- when /mswin|mingw/
74
- require 'win32ole'
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
- def self.physical_processor_count
87
- @physical_processor_count ||= case RbConfig::CONFIG['host_os']
88
- when /darwin1/, /freebsd/
89
- `sysctl -n hw.physicalcpu`.to_i
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
- private
77
+ def each(array, options={}, &block)
78
+ map(array, options.merge(:preserve_results => false), &block)
79
+ array
80
+ end
103
81
 
104
- def self.work_direct(array, options)
105
- results = []
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
- def self.hwprefs_available?
113
- `which hwprefs` != ''
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
- def self.work_in_threads(items, options, &block)
117
- results = []
118
- current = -1
119
- exception = nil
120
- on_start = options[:start]
121
- on_finish = options[:finish]
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
- in_threads(options[:count]) do
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
- index = Thread.exclusive{ current+=1 }
129
- break if index >= items.size
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
- item = items[index]
132
- on_start.call(item, index) if on_start
107
+ def map_with_index(array, options={}, &block)
108
+ map(array, options.merge(:with_index => true), &block)
109
+ end
133
110
 
134
- begin
135
- results[index] = call_with_index(items, index, options, &block)
136
- rescue Exception => e
137
- exception = e
138
- break
139
- ensure
140
- on_finish.call(item, index) if on_finish
141
- end
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
- raise exception if exception
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
- results
148
- end
150
+ private
149
151
 
150
- def self.work_in_processes(items, options, &blk)
151
- workers = create_workers(items, options, &blk)
152
- current_index = -1
153
- results = []
154
- exception = nil
155
- on_start = options[:start]
156
- on_finish = options[:finish]
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
- in_threads(options[:count]) do |i|
159
- worker = workers[i]
164
+ def work_in_threads(items, options, &block)
165
+ results = []
166
+ current = -1
167
+ exception = nil
160
168
 
161
- begin
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
- item = items[index]
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
- if ExceptionWrapper === output
176
- exception = output.exception
177
- else
178
- results[index] = output
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
- raise exception if exception
188
+ raise exception if exception
188
189
 
189
- results
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
- pids = workers.map{|worker| worker[:pid] }
199
- kill_on_ctrl_c(pids)
200
- workers
201
- end
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
- def self.worker(items, options, &block)
204
- # use less memory on REE
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
- child_read, parent_write = IO.pipe
208
- parent_read, child_write = IO.pipe
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
- pid = Process.fork do
211
- begin
212
- options.delete(:started_workers).each{|w| close_pipes w }
224
+ raise exception if exception
213
225
 
214
- parent_write.close
215
- parent_read.close
226
+ results
227
+ end
216
228
 
217
- process_incoming_jobs(child_read, child_write, items, options, &block)
218
- ensure
219
- child_read.close
220
- child_write.close
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
- child_read.close
225
- child_write.close
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
- {:read => parent_read, :write => parent_write, :pid => pid}
228
- end
243
+ child_read, parent_write = IO.pipe
244
+ parent_read, child_write = IO.pipe
229
245
 
230
- def self.close_pipes(worker)
231
- worker[:read].close
232
- worker[:write].close
233
- end
246
+ pid = Process.fork do
247
+ begin
248
+ options.delete(:started_workers).each(&:close_pipes)
234
249
 
235
- def self.process_incoming_jobs(read, write, items, options, &block)
236
- while !read.eof?
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
- def self.wait_for_threads(threads)
249
- threads.compact.each do |t|
250
- begin
251
- t.join
252
- rescue Interrupt
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
- def self.wait_for_process(pid)
259
- begin
260
- Process.wait(pid)
261
- rescue Interrupt
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
- # options is either a Integer or a Hash with :count
267
- def self.extract_count_from_options(options)
268
- if options.is_a?(Hash)
269
- count = options[:count]
270
- else
271
- count = options
272
- options = {}
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
- # kill all these processes (children) if user presses Ctrl+c
278
- def self.kill_on_ctrl_c(pids)
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
- Process.kill(:KILL, pid)
284
- rescue Errno::ESRCH
285
- # some linux systems already automatically killed the children at this point
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
- def self.call_with_index(array, index, options, &block)
294
- args = [array[index]]
295
- args << index if options[:with_index]
296
- block.call(*args)
297
- end
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
- class ExceptionWrapper
300
- attr_reader :exception
301
- def initialize(exception)
302
- dumpable = Marshal.dump(exception) rescue nil
303
- unless dumpable
304
- exception = RuntimeError.new("Undumpable Exception -- #{exception.inspect}")
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
- @exception = exception
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
@@ -1,3 +1,3 @@
1
1
  module Parallel
2
- VERSION = Version = '0.5.21'
2
+ VERSION = Version = '0.6.0'
3
3
  end
@@ -0,0 +1,9 @@
1
+ require File.expand_path('spec/spec_helper')
2
+
3
+ begin
4
+ Parallel.map([1,2,3]) do |x, i|
5
+ Process.kill("SIGKILL", Process.pid)
6
+ end
7
+ rescue Parallel::DeadWorker
8
+ puts "DEAD"
9
+ end
@@ -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
@@ -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-01 00:00:00.000000000 Z
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: -4018217091904548173
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: -4018217091904548173
79
+ hash: -1821301885117501782
80
+ none: false
79
81
  requirements: []
80
82
  rubyforge_project:
81
83
  rubygems_version: 1.8.24