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 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