parallel 1.6.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0f363b126a1d42e09e74864a71c940edaf139d79
4
- data.tar.gz: a8976de6367828f3ef06b8eb81822515817b1afc
2
+ SHA256:
3
+ metadata.gz: 3331bc6634cd376e4f3f8511049e49a0d4501110aaeac395e2f0c0b638f4a402
4
+ data.tar.gz: 357e1424cb8297b6472c2c5b9486e26da3969edd928be7d6ca7f21670211a8e0
5
5
  SHA512:
6
- metadata.gz: 0a78ebb2abf7b12749b4ffb318eb70b8b04f00af516dcf438423080a26136bd2a87a097e9b593130294eaf4e9a1e97a754da7c8f7a7a46c7d7b84f5446bdd224
7
- data.tar.gz: 3eb8871382f2669e89fecca80bc2dfe198c05df133795e55fd12bd283d0760d7a9c5035c2abc337431b57048647b343ea9022a3ca83bf4da875c53d8a47c3b0c
6
+ metadata.gz: 1885d4f814023905f76105f0edce9155024478408be851a4f6869e537400e924e03d34196e03b428803c274b5fbf7e9bf2d93f4548782aa6d3015063c7ed7883
7
+ data.tar.gz: 97d22f6b0320a089a3584e76c0baa245811861776e7337e913961cd64cfe0770131493d7de7e1355a62d6339d47d0b1f3ad32dd89084f31c69a61fcfab9f3148
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ require 'openssl'
3
+ require 'securerandom'
4
+
5
+ module Parallel
6
+ # Pluggable wire serializers. Each must respond to `dump(data, io)` /
7
+ # `load(io)` (used directly by Worker) and `dump(data)` / `load(string)`
8
+ # (used by wrappers like Hmac).
9
+ module Serializer
10
+ # Raw Marshal. Fast but trusts anything written to the pipe — a same-UID
11
+ # attacker that reopens /proc/<pid>/fd/<n> can inject Marshal gadgets (RCE).
12
+ Marshal = ::Marshal
13
+
14
+ # Wraps any inner serializer with a length-prefixed HMAC-SHA256 frame keyed
15
+ # on a per-worker secret generated before fork. Forged frames from a
16
+ # pipe-injector fail verification.
17
+ class Hmac
18
+ LENGTH_FORMAT = 'N' # 32-bit big-endian unsigned int
19
+ LENGTH_BYTES = 4
20
+ MAC_BYTES = 32 # SHA256
21
+
22
+ def initialize(inner: Marshal, secret: SecureRandom.bytes(32))
23
+ @inner = inner
24
+ @secret = secret
25
+ end
26
+
27
+ def dump(data, io)
28
+ payload = @inner.dump(data)
29
+ mac = OpenSSL::HMAC.digest('SHA256', @secret, payload)
30
+ io.write([payload.bytesize].pack(LENGTH_FORMAT), mac, payload)
31
+ end
32
+
33
+ def load(io)
34
+ # nil at frame boundary = clean EOF (worker died / pipe closed between messages)
35
+ header = io.read(LENGTH_BYTES) || raise(EOFError) # eof stops worker
36
+ raise SecurityError, "truncated frame header" if header.bytesize != LENGTH_BYTES
37
+
38
+ length = header.unpack1(LENGTH_FORMAT)
39
+ mac = io.read(MAC_BYTES)
40
+ raise SecurityError, "truncated frame mac" if mac.nil? || mac.bytesize != MAC_BYTES
41
+
42
+ payload = io.read(length)
43
+ raise SecurityError, "truncated frame payload" if payload.nil? || payload.bytesize != length
44
+
45
+ expected = OpenSSL::HMAC.digest('SHA256', @secret, payload)
46
+ raise SecurityError, "HMAC mismatch on worker pipe" unless OpenSSL.fixed_length_secure_compare(mac, expected)
47
+
48
+ @inner.load(payload)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Parallel
2
- VERSION = Version = '1.6.0'
3
+ VERSION = Version = '2.1.0' # rubocop:disable Naming/ConstantName
3
4
  end
data/lib/parallel.rb CHANGED
@@ -1,66 +1,111 @@
1
+ # frozen_string_literal: true
1
2
  require 'rbconfig'
2
3
  require 'parallel/version'
3
- require 'parallel/processor_count'
4
+ require 'parallel/serializer'
4
5
 
5
6
  module Parallel
6
- extend Parallel::ProcessorCount
7
+ Stop = Object.new.freeze
7
8
 
8
9
  class DeadWorker < StandardError
9
10
  end
10
11
 
11
12
  class Break < StandardError
13
+ attr_reader :value
14
+
15
+ def initialize(value = nil)
16
+ super()
17
+ @value = value
18
+ end
19
+
20
+ # marshal_dump that is used for ruby exceptions
21
+ # avoid dumping the cause since nobody needs that and it can include undumpable exceptions
22
+ def _dump(_depth)
23
+ Marshal.dump(@value)
24
+ end
25
+
26
+ # marshal_load that is used for ruby exceptions
27
+ def self._load(data)
28
+ new(Marshal.load(data))
29
+ end
12
30
  end
13
31
 
14
- class Kill < StandardError
32
+ class Kill < Break
15
33
  end
16
34
 
17
- Stop = Object.new
35
+ class UndumpableException < StandardError
36
+ attr_reader :backtrace
37
+
38
+ def initialize(original)
39
+ super("#{original.class}: #{original.message}")
40
+ @backtrace = original.backtrace
41
+ end
42
+ end
18
43
 
19
44
  class ExceptionWrapper
20
45
  attr_reader :exception
46
+
21
47
  def initialize(exception)
22
- dumpable = Marshal.dump(exception) rescue nil
23
- unless dumpable
24
- exception = RuntimeError.new("Undumpable Exception -- #{exception.inspect}")
48
+ # Remove the bindings stack added by the better_errors gem,
49
+ # because it cannot be marshalled
50
+ if exception.instance_variable_defined? :@__better_errors_bindings_stack
51
+ exception.send :remove_instance_variable, :@__better_errors_bindings_stack
25
52
  end
26
53
 
27
- @exception = exception
54
+ @exception =
55
+ begin
56
+ Marshal.dump(exception) && exception
57
+ rescue StandardError
58
+ UndumpableException.new(exception)
59
+ end
28
60
  end
29
61
  end
30
62
 
31
63
  class Worker
32
64
  attr_reader :pid, :read, :write
33
65
  attr_accessor :thread
34
- def initialize(read, write, pid)
35
- @read, @write, @pid = read, write, pid
66
+
67
+ def initialize(read, write, pid, serializer)
68
+ @read = read
69
+ @write = write
70
+ @pid = pid
71
+ @serializer = serializer
36
72
  end
37
73
 
38
- def close_pipes
39
- read.close
40
- write.close
74
+ def stop
75
+ close_pipes
76
+ wait # if it goes zombie, rather wait here to be able to debug
41
77
  end
42
78
 
43
- def wait
44
- Process.wait(pid)
45
- rescue Interrupt
46
- # process died
79
+ # might be passed to started_processes and simultaneously closed by another thread
80
+ # when running in isolation mode, so we have to check if it is closed before closing
81
+ def close_pipes
82
+ read.close unless read.closed?
83
+ write.close unless write.closed?
47
84
  end
48
85
 
49
86
  def work(data)
50
87
  begin
51
- Marshal.dump(data, write)
88
+ @serializer.dump(data, write)
52
89
  rescue Errno::EPIPE
53
90
  raise DeadWorker
54
91
  end
55
92
 
56
93
  result = begin
57
- Marshal.load(read)
94
+ @serializer.load(read)
58
95
  rescue EOFError
59
96
  raise DeadWorker
60
97
  end
61
- raise result.exception if ExceptionWrapper === result
98
+ raise result.exception if result.is_a?(ExceptionWrapper)
62
99
  result
63
100
  end
101
+
102
+ private
103
+
104
+ def wait
105
+ Process.wait(pid)
106
+ rescue Interrupt
107
+ # process died
108
+ end
64
109
  end
65
110
 
66
111
  class JobFactory
@@ -79,7 +124,7 @@ module Parallel
79
124
  item, index = @mutex.synchronize do
80
125
  return if @stopped
81
126
  item = @lambda.call
82
- @stopped = (item == Parallel::Stop)
127
+ @stopped = (item == Stop)
83
128
  return if @stopped
84
129
  [item, @index += 1]
85
130
  end
@@ -99,10 +144,13 @@ module Parallel
99
144
  end
100
145
  end
101
146
 
147
+ # generate item that is sent to workers
148
+ # just index is faster + less likely to blow up with unserializable errors
102
149
  def pack(item, index)
103
150
  producer? ? [item, index] : index
104
151
  end
105
152
 
153
+ # unpack item that is sent to workers
106
154
  def unpack(data)
107
155
  producer? ? data : [@source[data], data]
108
156
  end
@@ -114,7 +162,7 @@ module Parallel
114
162
  end
115
163
 
116
164
  def queue_wrapper(array)
117
- array.respond_to?(:num_waiting) && array.respond_to?(:pop) && lambda { array.pop(false) }
165
+ array.respond_to?(:num_waiting) && array.respond_to?(:pop) && -> { array.pop(false) }
118
166
  end
119
167
  end
120
168
 
@@ -130,7 +178,7 @@ module Parallel
130
178
 
131
179
  if @to_be_killed.empty?
132
180
  old_interrupt = trap_interrupt(signal) do
133
- $stderr.puts 'Parallel execution interrupted, exiting ...'
181
+ warn 'Parallel execution interrupted, exiting ...'
134
182
  @to_be_killed.flatten.each { |pid| kill(pid) }
135
183
  end
136
184
  end
@@ -157,7 +205,7 @@ module Parallel
157
205
 
158
206
  Signal.trap signal do
159
207
  yield
160
- if old == "DEFAULT"
208
+ if !old || old == "DEFAULT"
161
209
  raise Interrupt
162
210
  else
163
211
  old.call
@@ -174,37 +222,61 @@ module Parallel
174
222
  end
175
223
 
176
224
  class << self
177
- def in_threads(options={:count => 2})
178
- count, _ = extract_count_from_options(options)
179
- Array.new(count).each_with_index.map do |_, i|
180
- Thread.new { yield(i) }
181
- end.map!(&:value)
225
+ def in_threads(options = { count: 2 })
226
+ threads = []
227
+ count, = extract_count_from_options(options)
228
+
229
+ Thread.handle_interrupt(Exception => :never) do
230
+ Thread.handle_interrupt(Exception => :immediate) do
231
+ count.times do |i|
232
+ threads << Thread.new { yield(i) }
233
+ end
234
+ threads.map(&:value)
235
+ end
236
+ ensure
237
+ threads.each(&:kill)
238
+ end
182
239
  end
183
240
 
184
241
  def in_processes(options = {}, &block)
185
242
  count, options = extract_count_from_options(options)
186
243
  count ||= processor_count
187
- map(0...count, options.merge(:in_processes => count), &block)
244
+ map(0...count, options.merge(in_processes: count), &block)
188
245
  end
189
246
 
190
- def each(array, options={}, &block)
191
- map(array, options.merge(:preserve_results => false), &block)
192
- array
247
+ def each(array, options = {}, &block)
248
+ map(array, options.merge(preserve_results: false), &block)
193
249
  end
194
250
 
195
- def each_with_index(array, options={}, &block)
196
- each(array, options.merge(:with_index => true), &block)
251
+ def any?(*args, &block)
252
+ raise "You must provide a block when calling #any?" if block.nil?
253
+ !each(*args) { |*a| raise Kill if block.call(*a) }
254
+ end
255
+
256
+ def all?(*args, &block)
257
+ raise "You must provide a block when calling #all?" if block.nil?
258
+ !!each(*args) { |*a| raise Kill unless block.call(*a) }
259
+ end
260
+
261
+ def each_with_index(array, options = {}, &block)
262
+ each(array, options.merge(with_index: true), &block)
197
263
  end
198
264
 
199
265
  def map(source, options = {}, &block)
266
+ options = options.dup
200
267
  options[:mutex] = Mutex.new
201
268
 
202
- if RUBY_PLATFORM =~ /java/ and not options[:in_processes]
269
+ if options[:in_processes] && options[:in_threads]
270
+ raise ArgumentError, "Please specify only one of `in_processes` or `in_threads`."
271
+ elsif RUBY_PLATFORM.include?('java') && !options[:in_processes]
203
272
  method = :in_threads
204
273
  size = options[method] || processor_count
205
274
  elsif options[:in_threads]
206
275
  method = :in_threads
207
276
  size = options[method]
277
+ elsif options[:in_ractors]
278
+ method = :in_ractors
279
+ size = options[method]
208
280
  else
209
281
  method = :in_processes
210
282
  if Process.respond_to?(:fork)
@@ -221,27 +293,115 @@ module Parallel
221
293
  options[:return_results] = (options[:preserve_results] != false || !!options[:finish])
222
294
  add_progress_bar!(job_factory, options)
223
295
 
224
- if size == 0
225
- work_direct(job_factory, options, &block)
226
- elsif method == :in_threads
227
- work_in_threads(job_factory, options.merge(:count => size), &block)
228
- else
229
- work_in_processes(job_factory, options.merge(:count => size), &block)
296
+ result =
297
+ if size == 0
298
+ work_direct(job_factory, options, &block)
299
+ elsif method == :in_threads
300
+ work_in_threads(job_factory, options.merge(count: size), &block)
301
+ elsif method == :in_ractors
302
+ work_in_ractors(job_factory, options.merge(count: size), &block)
303
+ else
304
+ work_in_processes(job_factory, options.merge(count: size), &block)
305
+ end
306
+
307
+ return result.value if result.is_a?(Break)
308
+ raise result if result.is_a?(Exception)
309
+ options[:return_results] ? result : source
310
+ end
311
+
312
+ def map_with_index(array, options = {}, &block)
313
+ map(array, options.merge(with_index: true), &block)
314
+ end
315
+
316
+ def flat_map(...)
317
+ map(...).flatten(1)
318
+ end
319
+
320
+ def filter_map(...)
321
+ map(...).compact
322
+ end
323
+
324
+ # Number of physical processor cores on the current system.
325
+ def physical_processor_count
326
+ @physical_processor_count ||= begin
327
+ ppc =
328
+ case RbConfig::CONFIG["target_os"]
329
+ when /darwin[12]/
330
+ IO.popen("/usr/sbin/sysctl -n hw.physicalcpu").read.to_i
331
+ when /linux/
332
+ cores = {} # unique physical ID / core ID combinations
333
+ phy = 0
334
+ File.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
335
+ if ln.start_with?("physical")
336
+ phy = ln[/\d+/]
337
+ elsif ln.start_with?("core")
338
+ cid = "#{phy}:#{ln[/\d+/]}"
339
+ cores[cid] = true unless cores[cid]
340
+ end
341
+ end
342
+ cores.count
343
+ when /mswin|mingw/
344
+ physical_processor_count_windows
345
+ else
346
+ processor_count
347
+ end
348
+ # fall back to logical count if physical info is invalid
349
+ ppc > 0 ? ppc : processor_count
230
350
  end
231
351
  end
232
352
 
233
- def map_with_index(array, options={}, &block)
234
- map(array, options.merge(:with_index => true), &block)
353
+ # Number of processors seen by the OS or value considering CPU quota if the process is inside a cgroup,
354
+ # used for process scheduling
355
+ def processor_count
356
+ @processor_count ||= Integer(ENV['PARALLEL_PROCESSOR_COUNT'] || available_processor_count)
357
+ end
358
+
359
+ def worker_number
360
+ Thread.current[:parallel_worker_number]
361
+ end
362
+
363
+ # TODO: this does not work when doing threads in forks, so should remove and yield the number instead if needed
364
+ def worker_number=(worker_num)
365
+ Thread.current[:parallel_worker_number] = worker_num
235
366
  end
236
367
 
237
368
  private
238
369
 
370
+ def physical_processor_count_windows
371
+ # Get-CimInstance introduced in PowerShell 3 or earlier: https://learn.microsoft.com/en-us/previous-versions/powershell/module/cimcmdlets/get-ciminstance?view=powershell-3.0
372
+ result = run(
373
+ 'powershell -command "Get-CimInstance -ClassName Win32_Processor -Property NumberOfCores ' \
374
+ '| Select-Object -Property NumberOfCores"'
375
+ )
376
+ if !result || $?.exitstatus != 0
377
+ # fallback to deprecated wmic for older systems
378
+ result = run("wmic cpu get NumberOfCores")
379
+ end
380
+ if !result || $?.exitstatus != 0
381
+ # Bail out if both commands returned something unexpected
382
+ warn "guessing pyhsical processor count"
383
+ processor_count
384
+ else
385
+ # powershell: "\nNumberOfCores\n-------------\n 4\n\n\n"
386
+ # wmic: "NumberOfCores \n\n4 \n\n\n\n"
387
+ result.scan(/\d+/).map(&:to_i).reduce(:+)
388
+ end
389
+ end
390
+
391
+ def run(command)
392
+ IO.popen(command, &:read)
393
+ rescue Errno::ENOENT
394
+ # Ignore
395
+ end
396
+
239
397
  def add_progress_bar!(job_factory, options)
240
- if progress_options = options[:progress]
398
+ if (progress_options = options[:progress])
241
399
  raise "Progressbar can only be used with array like items" if job_factory.size == Float::INFINITY
242
400
  require 'ruby-progressbar'
243
401
 
244
- if progress_options.respond_to? :to_str
402
+ if progress_options == true
403
+ progress_options = { title: "Progress" }
404
+ elsif progress_options.respond_to? :to_str
245
405
  progress_options = { title: progress_options.to_str }
246
406
  end
247
407
 
@@ -260,47 +420,150 @@ module Parallel
260
420
  end
261
421
 
262
422
  def work_direct(job_factory, options, &block)
423
+ self.worker_number = 0
263
424
  results = []
264
- while set = job_factory.next
265
- item, index = set
266
- results << with_instrumentation(item, index, options) do
267
- call_with_index(item, index, options, &block)
425
+ exception = nil
426
+ begin
427
+ while (set = job_factory.next)
428
+ item, index = set
429
+ results << with_instrumentation(item, index, options) do
430
+ call_with_index(item, index, options, &block)
431
+ end
268
432
  end
433
+ rescue StandardError
434
+ exception = $!
269
435
  end
270
- results
436
+ exception || results
437
+ ensure
438
+ self.worker_number = nil
271
439
  end
272
440
 
273
441
  def work_in_threads(job_factory, options, &block)
274
442
  raise "interrupt_signal is no longer supported for threads" if options[:interrupt_signal]
275
443
  results = []
444
+ results_mutex = Mutex.new # arrays are not thread-safe on jRuby
276
445
  exception = nil
277
446
 
278
- in_threads(options) do
447
+ in_threads(options) do |worker_num|
448
+ self.worker_number = worker_num
279
449
  # as long as there are more jobs, work on one of them
280
- while !exception && set = job_factory.next
450
+ while !exception && (set = job_factory.next)
281
451
  begin
282
452
  item, index = set
283
- results[index] = with_instrumentation item, index, options do
453
+ result = with_instrumentation item, index, options do
284
454
  call_with_index(item, index, options, &block)
285
455
  end
456
+ results_mutex.synchronize { results[index] = result }
457
+ rescue StandardError
458
+ exception = $!
459
+ end
460
+ end
461
+ end
462
+
463
+ exception || results
464
+ end
465
+
466
+ def work_in_ractors(job_factory, options)
467
+ exception = nil
468
+ results = []
469
+ results_mutex = Mutex.new # arrays are not thread-safe on jRuby
470
+
471
+ callback = options[:ractor]
472
+ if block_given? || !callback
473
+ raise ArgumentError, "pass the code you want to execute as `ractor: [ClassName, :method_name]`"
474
+ end
475
+
476
+ use_port = defined?(Ractor::Port)
477
+
478
+ # build
479
+ ports = {} # port (ruby 4+) or ractor (ruby 3) => ractor
480
+ options.fetch(:count).times do
481
+ port, ractor = ractor_build(use_port)
482
+ ports[port] = ractor
483
+ end
484
+
485
+ # start
486
+ ports.dup.each do |port, ractor|
487
+ if (job = job_factory.next)
488
+ item, index = job
489
+ instrument_start item, index, options
490
+ ractor.send [callback, item, index]
491
+ else # not enough work, `receive` would hang
492
+ ractor_stop ractor
493
+ ports.delete port
494
+ end
495
+ end
496
+
497
+ # receive result and send new items to done ractors
498
+ while (job = job_factory.next)
499
+ # receive result
500
+ done_port, (exception, result, item_prev, index_prev) = Ractor.select(*ports.keys)
501
+ done_ractor = ports[done_port]
502
+ if exception
503
+ ports.delete done_port
504
+ break
505
+ end
506
+ ractor_result item_prev, index_prev, result, results, results_mutex, options
507
+
508
+ # send new
509
+ item_next, index_next = job
510
+ instrument_start item_next, index_next, options
511
+ done_ractor.send([callback, item_next, index_next])
512
+ end
513
+
514
+ # finish
515
+ ports.each do |port, ractor|
516
+ (new_exception, result, item, index) = use_port ? port.receive : ractor.take
517
+ exception ||= new_exception
518
+ next if new_exception
519
+ ractor_result item, index, result, results, results_mutex, options
520
+ ractor_stop ractor
521
+ end
522
+
523
+ exception || results
524
+ end
525
+
526
+ def ractor_build(use_port)
527
+ args = use_port ? [Ractor::Port.new] : []
528
+ ractor = Ractor.new(*args) do |port|
529
+ loop do
530
+ (klass, method_name), item, index = receive
531
+ break if index == :break
532
+ begin
533
+ result = [nil, klass.send(method_name, item), item, index]
286
534
  rescue StandardError => e
287
- exception = e
535
+ result = [e, nil, item, index]
536
+ end
537
+ if port
538
+ port.send result
539
+ else
540
+ Ractor.yield result
288
541
  end
289
542
  end
290
543
  end
544
+ [use_port ? args.first : ractor, ractor]
545
+ end
546
+
547
+ def ractor_result(item, index, result, results, results_mutex, options)
548
+ instrument_finish item, index, result, options
549
+ results_mutex.synchronize { results[index] = (options[:preserve_results] == false ? nil : result) }
550
+ end
291
551
 
292
- handle_exception(exception, results)
552
+ def ractor_stop(ractor)
553
+ ractor.send([[nil, nil], nil, :break])
293
554
  end
294
555
 
295
556
  def work_in_processes(job_factory, options, &blk)
296
557
  workers = create_workers(job_factory, options, &blk)
297
558
  results = []
559
+ results_mutex = Mutex.new # arrays are not thread-safe
298
560
  exception = nil
299
561
 
300
562
  UserInterruptHandler.kill_on_ctrl_c(workers.map(&:pid), options) do
301
563
  in_threads(options) do |i|
302
564
  worker = workers[i]
303
565
  worker.thread = Thread.current
566
+ worked = false
304
567
 
305
568
  begin
306
569
  loop do
@@ -308,34 +571,52 @@ module Parallel
308
571
  item, index = job_factory.next
309
572
  break unless index
310
573
 
574
+ if options[:isolation]
575
+ worker = replace_worker(job_factory, workers, i, options, blk) if worked
576
+ worked = true
577
+ worker.thread = Thread.current
578
+ end
579
+
311
580
  begin
312
- results[index] = with_instrumentation item, index, options do
581
+ result = with_instrumentation item, index, options do
313
582
  worker.work(job_factory.pack(item, index))
314
583
  end
584
+ results_mutex.synchronize { results[index] = result } # arrays are not threads safe on jRuby
315
585
  rescue StandardError => e
316
586
  exception = e
317
- if Parallel::Kill === exception
587
+ if exception.is_a?(Kill)
318
588
  (workers - [worker]).each do |w|
319
- w.thread.kill
589
+ w.thread&.kill
320
590
  UserInterruptHandler.kill(w.pid)
321
591
  end
322
592
  end
323
593
  end
324
594
  end
325
595
  ensure
326
- worker.close_pipes
327
- worker.wait # if it goes zombie, rather wait here to be able to debug
596
+ worker.stop
328
597
  end
329
598
  end
330
599
  end
331
600
 
332
- handle_exception(exception, results)
601
+ exception || results
602
+ end
603
+
604
+ def replace_worker(job_factory, workers, index, options, blk)
605
+ options[:mutex].synchronize do
606
+ # old worker is no longer used ... stop it
607
+ worker = workers[index]
608
+ worker.stop
609
+
610
+ # create a new replacement worker
611
+ running = workers - [worker]
612
+ workers[index] = worker(job_factory, options.merge(started_workers: running, worker_number: index), &blk)
613
+ end
333
614
  end
334
615
 
335
616
  def create_workers(job_factory, options, &block)
336
617
  workers = []
337
- Array.new(options[:count]).each do
338
- workers << worker(job_factory, options.merge(:started_workers => workers), &block)
618
+ Array.new(options[:count]).each_with_index do |_, i|
619
+ workers << worker(job_factory, options.merge(started_workers: workers, worker_number: i), &block)
339
620
  end
340
621
  workers
341
622
  end
@@ -343,8 +624,11 @@ module Parallel
343
624
  def worker(job_factory, options, &block)
344
625
  child_read, parent_write = IO.pipe
345
626
  parent_read, child_write = IO.pipe
627
+ options[:serializer] ||= Serializer::Marshal
346
628
 
347
629
  pid = Process.fork do
630
+ self.worker_number = options[:worker_number]
631
+
348
632
  begin
349
633
  options.delete(:started_workers).each(&:close_pipes)
350
634
 
@@ -361,28 +645,33 @@ module Parallel
361
645
  child_read.close
362
646
  child_write.close
363
647
 
364
- Worker.new(parent_read, parent_write, pid)
648
+ Worker.new(parent_read, parent_write, pid, options[:serializer])
365
649
  end
366
650
 
367
651
  def process_incoming_jobs(read, write, job_factory, options, &block)
652
+ serializer = options.fetch(:serializer)
368
653
  until read.eof?
369
- data = Marshal.load(read)
654
+ data = serializer.load(read)
370
655
  item, index = job_factory.unpack(data)
371
- result = begin
372
- call_with_index(item, index, options, &block)
373
- rescue StandardError => e
374
- ExceptionWrapper.new(e)
656
+
657
+ result =
658
+ begin
659
+ call_with_index(item, index, options, &block)
660
+ # https://github.com/rspec/rspec-support/blob/673133cdd13b17077b3d88ece8d7380821f8d7dc/lib/rspec/support.rb#L132-L140
661
+ rescue NoMemoryError, SignalException, Interrupt, SystemExit # rubocop:disable Lint/ShadowedException
662
+ raise $!
663
+ rescue Exception # rubocop:disable Lint/RescueException
664
+ ExceptionWrapper.new($!)
665
+ end
666
+
667
+ begin
668
+ serializer.dump(result, write)
669
+ rescue Errno::EPIPE
670
+ return # parent thread already dead
375
671
  end
376
- Marshal.dump(result, write)
377
672
  end
378
673
  end
379
674
 
380
- def handle_exception(exception, results)
381
- return nil if [Parallel::Break, Parallel::Kill].include? exception.class
382
- raise exception if exception
383
- results
384
- end
385
-
386
675
  # options is either a Integer or a Hash with :count
387
676
  def extract_count_from_options(options)
388
677
  if options.is_a?(Hash)
@@ -397,22 +686,62 @@ module Parallel
397
686
  def call_with_index(item, index, options, &block)
398
687
  args = [item]
399
688
  args << index if options[:with_index]
689
+ results = block.call(*args)
400
690
  if options[:return_results]
401
- block.call(*args)
691
+ results
402
692
  else
403
- block.call(*args)
404
693
  nil # avoid GC overhead of passing large results around
405
694
  end
406
695
  end
407
696
 
408
697
  def with_instrumentation(item, index, options)
409
- on_start = options[:start]
410
- on_finish = options[:finish]
411
- options[:mutex].synchronize { on_start.call(item, index) } if on_start
698
+ instrument_start(item, index, options)
412
699
  result = yield
700
+ instrument_finish(item, index, result, options)
413
701
  result unless options[:preserve_results] == false
414
- ensure
415
- options[:mutex].synchronize { on_finish.call(item, index, result) } if on_finish
702
+ end
703
+
704
+ def instrument_finish(item, index, result, options)
705
+ return unless (on_finish = options[:finish])
706
+ return instrument_finish_in_order(item, index, result, options) if options[:finish_in_order]
707
+ options[:mutex].synchronize { on_finish.call(item, index, result) }
708
+ end
709
+
710
+ # yield results in the order of the input items
711
+ # needs to use `options` to store state between executions
712
+ # needs to use `done` index since a nil result would also be valid
713
+ def instrument_finish_in_order(item, index, result, options)
714
+ options[:mutex].synchronize do
715
+ # initialize our state
716
+ options[:finish_done] ||= []
717
+ options[:finish_expecting] ||= 0 # we wait for item at index 0
718
+
719
+ # store current result
720
+ options[:finish_done][index] = [item, result]
721
+
722
+ # yield all results that are now in order
723
+ break unless index == options[:finish_expecting]
724
+ index.upto(options[:finish_done].size).each do |i|
725
+ break unless (done = options[:finish_done][i])
726
+ options[:finish_done][i] = nil # allow GC to free this item and result
727
+ options[:finish].call(done[0], i, done[1])
728
+ options[:finish_expecting] += 1
729
+ end
730
+ end
731
+ end
732
+
733
+ def instrument_start(item, index, options)
734
+ return unless (on_start = options[:start])
735
+ options[:mutex].synchronize { on_start.call(item, index) }
736
+ end
737
+
738
+ def available_processor_count
739
+ gem 'concurrent-ruby', '>= 1.3.4'
740
+ require 'concurrent-ruby'
741
+ Concurrent.available_processor_count.floor
742
+ rescue LoadError
743
+ require 'etc'
744
+ Etc.nprocessors
416
745
  end
417
746
  end
418
747
  end
metadata CHANGED
@@ -1,16 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2015-05-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description:
14
12
  email: michael@grosser.it
15
13
  executables: []
16
14
  extensions: []
@@ -18,13 +16,18 @@ extra_rdoc_files: []
18
16
  files:
19
17
  - MIT-LICENSE.txt
20
18
  - lib/parallel.rb
21
- - lib/parallel/processor_count.rb
19
+ - lib/parallel/serializer.rb
22
20
  - lib/parallel/version.rb
23
21
  homepage: https://github.com/grosser/parallel
24
22
  licenses:
25
23
  - MIT
26
- metadata: {}
27
- post_install_message:
24
+ metadata:
25
+ bug_tracker_uri: https://github.com/grosser/parallel/issues
26
+ documentation_uri: https://github.com/grosser/parallel/blob/v2.1.0/Readme.md
27
+ source_code_uri: https://github.com/grosser/parallel/tree/v2.1.0
28
+ wiki_uri: https://github.com/grosser/parallel/wiki
29
+ changelog_uri: https://github.com/grosser/parallel/blob/v2.1.0/CHANGELOG.md
30
+ rubygems_mfa_required: 'true'
28
31
  rdoc_options: []
29
32
  require_paths:
30
33
  - lib
@@ -32,16 +35,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
32
35
  requirements:
33
36
  - - ">="
34
37
  - !ruby/object:Gem::Version
35
- version: 1.9.3
38
+ version: '3.3'
36
39
  required_rubygems_version: !ruby/object:Gem::Requirement
37
40
  requirements:
38
41
  - - ">="
39
42
  - !ruby/object:Gem::Version
40
43
  version: '0'
41
44
  requirements: []
42
- rubyforge_project:
43
- rubygems_version: 2.2.2
44
- signing_key:
45
+ rubygems_version: 4.0.3
45
46
  specification_version: 4
46
47
  summary: Run any kind of code in parallel processes
47
48
  test_files: []
@@ -1,85 +0,0 @@
1
- module Parallel
2
- module ProcessorCount
3
- # Number of processors seen by the OS and used for process scheduling.
4
- #
5
- # * AIX: /usr/sbin/pmcycles (AIX 5+), /usr/sbin/lsdev
6
- # * BSD: /sbin/sysctl
7
- # * Cygwin: /proc/cpuinfo
8
- # * Darwin: /usr/bin/hwprefs, /usr/sbin/sysctl
9
- # * HP-UX: /usr/sbin/ioscan
10
- # * IRIX: /usr/sbin/sysconf
11
- # * Linux: /proc/cpuinfo
12
- # * Minix 3+: /proc/cpuinfo
13
- # * Solaris: /usr/sbin/psrinfo
14
- # * Tru64 UNIX: /usr/sbin/psrinfo
15
- # * UnixWare: /usr/sbin/psrinfo
16
- #
17
- def processor_count
18
- @processor_count ||= begin
19
- os_name = RbConfig::CONFIG["target_os"]
20
- if os_name =~ /mingw|mswin/
21
- require 'win32ole'
22
- result = WIN32OLE.connect("winmgmts://").ExecQuery(
23
- "select NumberOfLogicalProcessors from Win32_Processor")
24
- result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+)
25
- elsif File.readable?("/proc/cpuinfo")
26
- IO.read("/proc/cpuinfo").scan(/^processor/).size
27
- elsif File.executable?("/usr/bin/hwprefs")
28
- IO.popen("/usr/bin/hwprefs thread_count").read.to_i
29
- elsif File.executable?("/usr/sbin/psrinfo")
30
- IO.popen("/usr/sbin/psrinfo").read.scan(/^.*on-*line/).size
31
- elsif File.executable?("/usr/sbin/ioscan")
32
- IO.popen("/usr/sbin/ioscan -kC processor") do |out|
33
- out.read.scan(/^.*processor/).size
34
- end
35
- elsif File.executable?("/usr/sbin/pmcycles")
36
- IO.popen("/usr/sbin/pmcycles -m").read.count("\n")
37
- elsif File.executable?("/usr/sbin/lsdev")
38
- IO.popen("/usr/sbin/lsdev -Cc processor -S 1").read.count("\n")
39
- elsif File.executable?("/usr/sbin/sysconf") and os_name =~ /irix/i
40
- IO.popen("/usr/sbin/sysconf NPROC_ONLN").read.to_i
41
- elsif File.executable?("/usr/sbin/sysctl")
42
- IO.popen("/usr/sbin/sysctl -n hw.ncpu").read.to_i
43
- elsif File.executable?("/sbin/sysctl")
44
- IO.popen("/sbin/sysctl -n hw.ncpu").read.to_i
45
- else
46
- $stderr.puts "Unknown platform: " + RbConfig::CONFIG["target_os"]
47
- $stderr.puts "Assuming 1 processor."
48
- 1
49
- end
50
- end
51
- end
52
-
53
- # Number of physical processor cores on the current system.
54
- #
55
- def physical_processor_count
56
- @physical_processor_count ||= begin
57
- ppc = case RbConfig::CONFIG["target_os"]
58
- when /darwin1/
59
- IO.popen("/usr/sbin/sysctl -n hw.physicalcpu").read.to_i
60
- when /linux/
61
- cores = {} # unique physical ID / core ID combinations
62
- phy = 0
63
- IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
64
- if ln.start_with?("physical")
65
- phy = ln[/\d+/]
66
- elsif ln.start_with?("core")
67
- cid = phy + ":" + ln[/\d+/]
68
- cores[cid] = true if not cores[cid]
69
- end
70
- end
71
- cores.count
72
- when /mswin|mingw/
73
- require 'win32ole'
74
- result_set = WIN32OLE.connect("winmgmts://").ExecQuery(
75
- "select NumberOfCores from Win32_Processor")
76
- result_set.to_enum.collect(&:NumberOfCores).reduce(:+)
77
- else
78
- processor_count
79
- end
80
- # fall back to logical count if physical info is invalid
81
- ppc > 0 ? ppc : processor_count
82
- end
83
- end
84
- end
85
- end