parallel 2.0.1 → 2.2.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
2
  SHA256:
3
- metadata.gz: 0556b829a420b259f2c4610988421e0b21516101042373b14cb9acfe870e9c8d
4
- data.tar.gz: 6c0a7aa158fb4a905902c1ef94e3901b75c73518754faa57a421cea4e82df6c7
3
+ metadata.gz: bcd30f5bfe7076b4a0872b389f00057efaae80917298e3d227c160cc8c1be410
4
+ data.tar.gz: d6dd66ee7f75d2e14123c292ce13ac2b448134622c712650c07482986dbd48a3
5
5
  SHA512:
6
- metadata.gz: 76173acf1c6a08e53bf12cfe4c039b02ca5d891b7a0bfed7024a2ba0093d7cbe42790f6e2ac1ee8e195c99e3582c24fb2abb27c8882541361bdc794492df86f3
7
- data.tar.gz: a032ace70ba955047cd3d1bb73c85af8bf67664f41e2d61ccdc0d1298277754a8ce190e1f33d654507236a1208d532e7fe973cfe5abee55abd46f7b13b70be36
6
+ metadata.gz: 11ed4f49a53e9e20d052725911593266b808dfaa7b2392f503d39494b318a16df15ad628b586f93b700f5e22627116c465afff818d61ac2f0ca3aed4dcd1b8c0
7
+ data.tar.gz: af37003c9dc58ae81dac2a80355f5b16db9f54734f06a9c0dc33e73730818f7e81306237f152cd2976399a6453300915b19593e2e6928b480330fa736c516bc6
@@ -0,0 +1,56 @@
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 inspect
28
+ "#<#{self.class} @inner=#{@inner.inspect}, @secret=[REDACTED]>"
29
+ end
30
+
31
+ def dump(data, io)
32
+ payload = @inner.dump(data)
33
+ mac = OpenSSL::HMAC.digest('SHA256', @secret, payload)
34
+ io.write([payload.bytesize].pack(LENGTH_FORMAT), mac, payload)
35
+ end
36
+
37
+ def load(io)
38
+ # nil at frame boundary = clean EOF (worker died / pipe closed between messages)
39
+ header = io.read(LENGTH_BYTES) || raise(EOFError) # eof stops worker
40
+ raise SecurityError, "truncated frame header" if header.bytesize != LENGTH_BYTES
41
+
42
+ length = header.unpack1(LENGTH_FORMAT)
43
+ mac = io.read(MAC_BYTES)
44
+ raise SecurityError, "truncated frame mac" if mac.nil? || mac.bytesize != MAC_BYTES
45
+
46
+ payload = io.read(length)
47
+ raise SecurityError, "truncated frame payload" if payload.nil? || payload.bytesize != length
48
+
49
+ expected = OpenSSL::HMAC.digest('SHA256', @secret, payload)
50
+ raise SecurityError, "HMAC mismatch on worker pipe" unless OpenSSL.fixed_length_secure_compare(mac, expected)
51
+
52
+ @inner.load(payload)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Parallel
3
- VERSION = Version = '2.0.1' # rubocop:disable Naming/ConstantName
3
+ VERSION = Version = '2.2.0' # rubocop:disable Naming/ConstantName
4
4
  end
data/lib/parallel.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require 'rbconfig'
3
3
  require 'parallel/version'
4
+ require 'parallel/serializer'
4
5
 
5
6
  module Parallel
6
7
  Stop = Object.new.freeze
@@ -63,10 +64,11 @@ module Parallel
63
64
  attr_reader :pid, :read, :write
64
65
  attr_accessor :thread
65
66
 
66
- def initialize(read, write, pid)
67
+ def initialize(read, write, pid, serializer)
67
68
  @read = read
68
69
  @write = write
69
70
  @pid = pid
71
+ @serializer = serializer
70
72
  end
71
73
 
72
74
  def stop
@@ -83,13 +85,13 @@ module Parallel
83
85
 
84
86
  def work(data)
85
87
  begin
86
- Marshal.dump(data, write)
88
+ @serializer.dump(data, write)
87
89
  rescue Errno::EPIPE
88
90
  raise DeadWorker
89
91
  end
90
92
 
91
93
  result = begin
92
- Marshal.load(read)
94
+ @serializer.load(read)
93
95
  rescue EOFError
94
96
  raise DeadWorker
95
97
  end
@@ -122,7 +124,7 @@ module Parallel
122
124
  item, index = @mutex.synchronize do
123
125
  return if @stopped
124
126
  item = @lambda.call
125
- @stopped = (item == Stop)
127
+ @stopped = Stop.equal?(item)
126
128
  return if @stopped
127
129
  [item, @index += 1]
128
130
  end
@@ -176,7 +178,7 @@ module Parallel
176
178
 
177
179
  if @to_be_killed.empty?
178
180
  old_interrupt = trap_interrupt(signal) do
179
- warn 'Parallel execution interrupted, exiting ...'
181
+ warn 'parallel: execution interrupted, exiting ...'
180
182
  @to_be_killed.flatten.each { |pid| kill(pid) }
181
183
  end
182
184
  end
@@ -243,7 +245,7 @@ module Parallel
243
245
  end
244
246
 
245
247
  def each(array, options = {}, &block)
246
- map(array, options.merge(preserve_results: false), &block)
248
+ map(array, options.merge(discard_results: true), &block)
247
249
  end
248
250
 
249
251
  def any?(*args, &block)
@@ -264,9 +266,15 @@ module Parallel
264
266
  options = options.dup
265
267
  options[:mutex] = Mutex.new
266
268
 
267
- if options[:in_processes] && options[:in_threads]
268
- raise ArgumentError, "Please specify only one of `in_processes` or `in_threads`."
269
- elsif RUBY_PLATFORM =~ /java/ && !options[:in_processes]
269
+ if options.slice(:in_processes, :in_threads, :in_ractors).size > 1
270
+ raise ArgumentError, "Use only one of `in_processes`, `in_threads`, or `in_ractors`."
271
+ end
272
+
273
+ if options[:in_ractors] ? block : options[:ractor]
274
+ raise ArgumentError, "use either in_ractors with :ractor or a not in_ractors and block"
275
+ end
276
+
277
+ if RUBY_PLATFORM.include?('java') && !options[:in_processes]
270
278
  method = :in_threads
271
279
  size = options[method] || processor_count
272
280
  elsif options[:in_threads]
@@ -280,31 +288,38 @@ module Parallel
280
288
  if Process.respond_to?(:fork)
281
289
  size = options[method] || processor_count
282
290
  else
283
- warn "Process.fork is not supported by this Ruby"
291
+ warn "parallel: Process.fork is not supported by this Ruby"
284
292
  size = 0
285
293
  end
286
294
  end
287
295
 
296
+ raise ArgumentError, "worker count must be a non-negative Integer" unless size.is_a?(Integer) && size >= 0
297
+
288
298
  job_factory = JobFactory.new(source, options[:mutex])
289
299
  size = [job_factory.size, size].min
290
300
 
291
- options[:return_results] = (options[:preserve_results] != false || !!options[:finish])
301
+ discard_results = options[:discard_results]
302
+
303
+ # finish callback needs the results, careful to do that before add_progress_bar which adds finish
304
+ options[:discard_results] = discard_results && !options[:finish]
305
+
292
306
  add_progress_bar!(job_factory, options)
293
307
 
294
308
  result =
295
309
  if size == 0
310
+ block = ractor_block(options) if method == :in_ractors
296
311
  work_direct(job_factory, options, &block)
297
312
  elsif method == :in_threads
298
313
  work_in_threads(job_factory, options.merge(count: size), &block)
299
314
  elsif method == :in_ractors
300
- work_in_ractors(job_factory, options.merge(count: size), &block)
315
+ work_in_ractors(job_factory, options.merge(count: size))
301
316
  else
302
317
  work_in_processes(job_factory, options.merge(count: size), &block)
303
318
  end
304
319
 
305
320
  return result.value if result.is_a?(Break)
306
321
  raise result if result.is_a?(Exception)
307
- options[:return_results] ? result : source
322
+ discard_results ? source : result
308
323
  end
309
324
 
310
325
  def map_with_index(array, options = {}, &block)
@@ -316,7 +331,7 @@ module Parallel
316
331
  end
317
332
 
318
333
  def filter_map(...)
319
- map(...).compact
334
+ map(...).select { |value| value }
320
335
  end
321
336
 
322
337
  # Number of physical processor cores on the current system.
@@ -377,7 +392,7 @@ module Parallel
377
392
  end
378
393
  if !result || $?.exitstatus != 0
379
394
  # Bail out if both commands returned something unexpected
380
- warn "guessing pyhsical processor count"
395
+ warn "parallel: guessing physical processor count"
381
396
  processor_count
382
397
  else
383
398
  # powershell: "\nNumberOfCores\n-------------\n 4\n\n\n"
@@ -418,22 +433,24 @@ module Parallel
418
433
  end
419
434
 
420
435
  def work_direct(job_factory, options, &block)
436
+ previous_worker_number = worker_number
421
437
  self.worker_number = 0
422
438
  results = []
423
439
  exception = nil
424
440
  begin
425
441
  while (set = job_factory.next)
426
442
  item, index = set
427
- results << with_instrumentation(item, index, options) do
443
+ result = with_instrumentation(item, index, options) do
428
444
  call_with_index(item, index, options, &block)
429
445
  end
446
+ results << result unless options[:discard_results]
430
447
  end
431
448
  rescue StandardError
432
449
  exception = $!
433
450
  end
434
451
  exception || results
435
452
  ensure
436
- self.worker_number = nil
453
+ self.worker_number = previous_worker_number
437
454
  end
438
455
 
439
456
  def work_in_threads(job_factory, options, &block)
@@ -451,7 +468,7 @@ module Parallel
451
468
  result = with_instrumentation item, index, options do
452
469
  call_with_index(item, index, options, &block)
453
470
  end
454
- results_mutex.synchronize { results[index] = result }
471
+ results_mutex.synchronize { results[index] = result } unless options[:discard_results]
455
472
  rescue StandardError
456
473
  exception = $!
457
474
  end
@@ -480,55 +497,70 @@ module Parallel
480
497
  ports[port] = ractor
481
498
  end
482
499
 
483
- # start
484
- ports.dup.each do |port, ractor|
485
- if (job = job_factory.next)
486
- item, index = job
487
- instrument_start item, index, options
488
- ractor.send [callback, item, index]
489
- else # not enough work, `receive` would hang
490
- ractor_stop ractor
491
- ports.delete port
500
+ begin
501
+ # start by sending 1 item each
502
+ ports.dup.each do |port, ractor|
503
+ if (job = job_factory.next)
504
+ ractor_send ractor, callback, job, options
505
+ else # not enough work, `receive` would hang
506
+ ractor_stop ractor
507
+ ports.delete port
508
+ end
492
509
  end
493
- end
494
510
 
495
- # receive result and send new items to done ractors
496
- while (job = job_factory.next)
497
- # receive result
498
- done_port, (exception, result, item_prev, index_prev) = Ractor.select(*ports.keys)
499
- done_ractor = ports[done_port]
500
- if exception
501
- ports.delete done_port
502
- break
503
- end
504
- ractor_result item_prev, index_prev, result, results, results_mutex, options
511
+ # receive result and send new items
512
+ while (job = job_factory.next)
513
+ # receive result
514
+ done_port, (exception, result, item_prev, index_prev) = Ractor.select(*ports.keys)
515
+ done_ractor = ports[done_port]
516
+ if exception
517
+ ractor_stop done_ractor
518
+ ports.delete done_port
519
+ break
520
+ end
521
+ ractor_result item_prev, index_prev, result, results, results_mutex, options
505
522
 
506
- # send new
507
- item_next, index_next = job
508
- instrument_start item_next, index_next, options
509
- done_ractor.send([callback, item_next, index_next])
510
- end
523
+ # send new
524
+ ractor_send done_ractor, callback, job, options
525
+ end
511
526
 
512
- # finish
513
- ports.each do |port, ractor|
514
- (new_exception, result, item, index) = use_port ? port.receive : ractor.take
515
- exception ||= new_exception
516
- next if new_exception
517
- ractor_result item, index, result, results, results_mutex, options
518
- ractor_stop ractor
527
+ # finish by receiving the last results
528
+ ports.dup.each do |port, ractor|
529
+ (new_exception, result, item, index) = use_port ? port.receive : ractor.take
530
+ exception ||= new_exception
531
+ ractor_result item, index, result, results, results_mutex, options unless new_exception
532
+ ractor_stop ractor
533
+ ports.delete port
534
+ end
535
+ ensure # close any ports that remained open in case something blew up
536
+ ports.each_value do |ractor|
537
+ ractor_stop(ractor)
538
+ ractor.take unless use_port
539
+ rescue Ractor::ClosedError, Ractor::RemoteError
540
+ nil
541
+ end
519
542
  end
520
543
 
521
544
  exception || results
522
545
  end
523
546
 
547
+ # ractors cannot execute blocks, so run the callback directly when not using ractors
548
+ def ractor_block(options)
549
+ klass, method_name = options[:ractor] ||
550
+ raise(ArgumentError, "pass the code you want to execute as `ractor: [ClassName, :method_name]`")
551
+ ->(*args) { klass.send(method_name, *args) }
552
+ end
553
+
524
554
  def ractor_build(use_port)
525
555
  args = use_port ? [Ractor::Port.new] : []
526
556
  ractor = Ractor.new(*args) do |port|
527
557
  loop do
528
- (klass, method_name), item, index = receive
558
+ (klass, method_name), item, index, with_index, discard_results = receive
529
559
  break if index == :break
530
560
  begin
531
- result = [nil, klass.send(method_name, item), item, index]
561
+ value = with_index ? klass.send(method_name, item, index) : klass.send(method_name, item)
562
+ value = nil if discard_results
563
+ result = [nil, value, item, index]
532
564
  rescue StandardError => e
533
565
  result = [e, nil, item, index]
534
566
  end
@@ -544,11 +576,19 @@ module Parallel
544
576
 
545
577
  def ractor_result(item, index, result, results, results_mutex, options)
546
578
  instrument_finish item, index, result, options
547
- results_mutex.synchronize { results[index] = (options[:preserve_results] == false ? nil : result) }
579
+ results_mutex.synchronize { results[index] = result } unless options[:discard_results]
580
+ end
581
+
582
+ def ractor_send(ractor, callback, job, options)
583
+ item, index = job
584
+ instrument_start item, index, options
585
+ ractor.send [callback, item, index, options[:with_index], options[:discard_results]]
548
586
  end
549
587
 
550
588
  def ractor_stop(ractor)
551
589
  ractor.send([[nil, nil], nil, :break])
590
+ rescue Ractor::ClosedError
591
+ nil
552
592
  end
553
593
 
554
594
  def work_in_processes(job_factory, options, &blk)
@@ -579,7 +619,8 @@ module Parallel
579
619
  result = with_instrumentation item, index, options do
580
620
  worker.work(job_factory.pack(item, index))
581
621
  end
582
- results_mutex.synchronize { results[index] = result } # arrays are not threads safe on jRuby
622
+ # arrays are not threads safe on jRuby
623
+ results_mutex.synchronize { results[index] = result } unless options[:discard_results]
583
624
  rescue StandardError => e
584
625
  exception = e
585
626
  if exception.is_a?(Kill)
@@ -617,11 +658,19 @@ module Parallel
617
658
  workers << worker(job_factory, options.merge(started_workers: workers, worker_number: i), &block)
618
659
  end
619
660
  workers
661
+ rescue Exception # rubocop:disable Lint/RescueException
662
+ workers.each do |worker|
663
+ worker.stop
664
+ rescue StandardError
665
+ nil
666
+ end
667
+ raise
620
668
  end
621
669
 
622
670
  def worker(job_factory, options, &block)
623
671
  child_read, parent_write = IO.pipe
624
672
  parent_read, child_write = IO.pipe
673
+ options[:serializer] ||= Serializer::Marshal
625
674
 
626
675
  pid = Process.fork do
627
676
  self.worker_number = options[:worker_number]
@@ -642,12 +691,26 @@ module Parallel
642
691
  child_read.close
643
692
  child_write.close
644
693
 
645
- Worker.new(parent_read, parent_write, pid)
694
+ Worker.new(parent_read, parent_write, pid, options[:serializer])
695
+ rescue Exception # rubocop:disable Lint/RescueException
696
+ [child_read, parent_write, parent_read, child_write].compact.each do |io|
697
+ io.close unless io.closed?
698
+ end
699
+ if pid
700
+ UserInterruptHandler.kill(pid)
701
+ begin
702
+ Process.wait(pid)
703
+ rescue Errno::ECHILD
704
+ nil
705
+ end
706
+ end
707
+ raise
646
708
  end
647
709
 
648
710
  def process_incoming_jobs(read, write, job_factory, options, &block)
711
+ serializer = options.fetch(:serializer)
649
712
  until read.eof?
650
- data = Marshal.load(read)
713
+ data = serializer.load(read)
651
714
  item, index = job_factory.unpack(data)
652
715
 
653
716
  result =
@@ -661,7 +724,7 @@ module Parallel
661
724
  end
662
725
 
663
726
  begin
664
- Marshal.dump(result, write)
727
+ serializer.dump(result, write)
665
728
  rescue Errno::EPIPE
666
729
  return # parent thread already dead
667
730
  end
@@ -683,10 +746,10 @@ module Parallel
683
746
  args = [item]
684
747
  args << index if options[:with_index]
685
748
  results = block.call(*args)
686
- if options[:return_results]
687
- results
688
- else
749
+ if options[:discard_results]
689
750
  nil # avoid GC overhead of passing large results around
751
+ else
752
+ results
690
753
  end
691
754
  end
692
755
 
@@ -694,7 +757,7 @@ module Parallel
694
757
  instrument_start(item, index, options)
695
758
  result = yield
696
759
  instrument_finish(item, index, result, options)
697
- result unless options[:preserve_results] == false
760
+ result unless options[:discard_results]
698
761
  end
699
762
 
700
763
  def instrument_finish(item, index, result, options)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
@@ -16,16 +16,17 @@ extra_rdoc_files: []
16
16
  files:
17
17
  - MIT-LICENSE.txt
18
18
  - lib/parallel.rb
19
+ - lib/parallel/serializer.rb
19
20
  - lib/parallel/version.rb
20
21
  homepage: https://github.com/grosser/parallel
21
22
  licenses:
22
23
  - MIT
23
24
  metadata:
24
25
  bug_tracker_uri: https://github.com/grosser/parallel/issues
25
- documentation_uri: https://github.com/grosser/parallel/blob/v2.0.1/Readme.md
26
- source_code_uri: https://github.com/grosser/parallel/tree/v2.0.1
26
+ documentation_uri: https://github.com/grosser/parallel/blob/v2.2.0/Readme.md
27
+ source_code_uri: https://github.com/grosser/parallel/tree/v2.2.0
27
28
  wiki_uri: https://github.com/grosser/parallel/wiki
28
- changelog_uri: https://github.com/grosser/parallel/blob/v2.0.1/CHANGELOG.md
29
+ changelog_uri: https://github.com/grosser/parallel/blob/v2.2.0/CHANGELOG.md
29
30
  rubygems_mfa_required: 'true'
30
31
  rdoc_options: []
31
32
  require_paths: