parallel 1.27.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
2
  SHA256:
3
- metadata.gz: a657608c1d396b6c563f8905778d64d24c4ae6bb010e716e60a45ccc4a71295e
4
- data.tar.gz: eb26d89f92521fed38f1bdf99f6374a2cca2e3f4c1557a7f3f92ed81d396ffb7
3
+ metadata.gz: 3331bc6634cd376e4f3f8511049e49a0d4501110aaeac395e2f0c0b638f4a402
4
+ data.tar.gz: 357e1424cb8297b6472c2c5b9486e26da3969edd928be7d6ca7f21670211a8e0
5
5
  SHA512:
6
- metadata.gz: d7accf7f9b3d74e1e76f7b89db8f9040d23876e8373a6d6fa9b31e927b9ca4ce8b13330ffd888871795c79d6e3bbe7b17aa787789146ffed67f46c21fa9af3ad
7
- data.tar.gz: ead77e379ad1c18641ca8d0457214af393beeee60fe4e5558c69d601f6f269628ce2830b6a35a7fe66e461b88fbebad33dd9bb8b767324030294e2a8f7164d8d
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,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Parallel
3
- VERSION = Version = '1.27.0' # rubocop:disable Naming/ConstantName
3
+ VERSION = Version = '2.1.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
@@ -15,6 +16,17 @@ module Parallel
15
16
  super()
16
17
  @value = value
17
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
18
30
  end
19
31
 
20
32
  class Kill < Break
@@ -52,10 +64,11 @@ module Parallel
52
64
  attr_reader :pid, :read, :write
53
65
  attr_accessor :thread
54
66
 
55
- def initialize(read, write, pid)
67
+ def initialize(read, write, pid, serializer)
56
68
  @read = read
57
69
  @write = write
58
70
  @pid = pid
71
+ @serializer = serializer
59
72
  end
60
73
 
61
74
  def stop
@@ -72,13 +85,13 @@ module Parallel
72
85
 
73
86
  def work(data)
74
87
  begin
75
- Marshal.dump(data, write)
88
+ @serializer.dump(data, write)
76
89
  rescue Errno::EPIPE
77
90
  raise DeadWorker
78
91
  end
79
92
 
80
93
  result = begin
81
- Marshal.load(read)
94
+ @serializer.load(read)
82
95
  rescue EOFError
83
96
  raise DeadWorker
84
97
  end
@@ -255,7 +268,7 @@ module Parallel
255
268
 
256
269
  if options[:in_processes] && options[:in_threads]
257
270
  raise ArgumentError, "Please specify only one of `in_processes` or `in_threads`."
258
- elsif RUBY_PLATFORM =~ (/java/) && !options[:in_processes]
271
+ elsif RUBY_PLATFORM.include?('java') && !options[:in_processes]
259
272
  method = :in_threads
260
273
  size = options[method] || processor_count
261
274
  elsif options[:in_threads]
@@ -460,62 +473,86 @@ module Parallel
460
473
  raise ArgumentError, "pass the code you want to execute as `ractor: [ClassName, :method_name]`"
461
474
  end
462
475
 
476
+ use_port = defined?(Ractor::Port)
477
+
463
478
  # build
464
- ractors = Array.new(options.fetch(:count)) do
465
- Ractor.new do
466
- loop do
467
- got = receive
468
- (klass, method_name), item, index = got
469
- break if index == :break
470
- begin
471
- Ractor.yield [nil, klass.send(method_name, item), item, index]
472
- rescue StandardError => e
473
- Ractor.yield [e, nil, item, index]
474
- end
475
- end
476
- end
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
477
483
  end
478
484
 
479
485
  # start
480
- ractors.dup.each do |ractor|
481
- if (set = job_factory.next)
482
- item, index = set
486
+ ports.dup.each do |port, ractor|
487
+ if (job = job_factory.next)
488
+ item, index = job
483
489
  instrument_start item, index, options
484
490
  ractor.send [callback, item, index]
485
- else
486
- ractor.send([[nil, nil], nil, :break]) # stop the ractor
487
- ractors.delete ractor
491
+ else # not enough work, `receive` would hang
492
+ ractor_stop ractor
493
+ ports.delete port
488
494
  end
489
495
  end
490
496
 
491
- # replace with new items
492
- while (set = job_factory.next)
493
- item_next, index_next = set
494
- done, (exception, result, item, index) = Ractor.select(*ractors)
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]
495
502
  if exception
496
- ractors.delete done
503
+ ports.delete done_port
497
504
  break
498
505
  end
499
- instrument_finish item, index, result, options
500
- results_mutex.synchronize { results[index] = (options[:preserve_results] == false ? nil : result) }
506
+ ractor_result item_prev, index_prev, result, results, results_mutex, options
501
507
 
508
+ # send new
509
+ item_next, index_next = job
502
510
  instrument_start item_next, index_next, options
503
- done.send([callback, item_next, index_next])
511
+ done_ractor.send([callback, item_next, index_next])
504
512
  end
505
513
 
506
514
  # finish
507
- ractors.each do |ractor|
508
- (new_exception, result, item, index) = ractor.take
515
+ ports.each do |port, ractor|
516
+ (new_exception, result, item, index) = use_port ? port.receive : ractor.take
509
517
  exception ||= new_exception
510
518
  next if new_exception
511
- instrument_finish item, index, result, options
512
- results_mutex.synchronize { results[index] = (options[:preserve_results] == false ? nil : result) }
513
- ractor.send([[nil, nil], nil, :break]) # stop the ractor
519
+ ractor_result item, index, result, results, results_mutex, options
520
+ ractor_stop ractor
514
521
  end
515
522
 
516
523
  exception || results
517
524
  end
518
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]
534
+ rescue StandardError => e
535
+ result = [e, nil, item, index]
536
+ end
537
+ if port
538
+ port.send result
539
+ else
540
+ Ractor.yield result
541
+ end
542
+ end
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
551
+
552
+ def ractor_stop(ractor)
553
+ ractor.send([[nil, nil], nil, :break])
554
+ end
555
+
519
556
  def work_in_processes(job_factory, options, &blk)
520
557
  workers = create_workers(job_factory, options, &blk)
521
558
  results = []
@@ -587,6 +624,7 @@ module Parallel
587
624
  def worker(job_factory, options, &block)
588
625
  child_read, parent_write = IO.pipe
589
626
  parent_read, child_write = IO.pipe
627
+ options[:serializer] ||= Serializer::Marshal
590
628
 
591
629
  pid = Process.fork do
592
630
  self.worker_number = options[:worker_number]
@@ -607,12 +645,13 @@ module Parallel
607
645
  child_read.close
608
646
  child_write.close
609
647
 
610
- Worker.new(parent_read, parent_write, pid)
648
+ Worker.new(parent_read, parent_write, pid, options[:serializer])
611
649
  end
612
650
 
613
651
  def process_incoming_jobs(read, write, job_factory, options, &block)
652
+ serializer = options.fetch(:serializer)
614
653
  until read.eof?
615
- data = Marshal.load(read)
654
+ data = serializer.load(read)
616
655
  item, index = job_factory.unpack(data)
617
656
 
618
657
  result =
@@ -621,12 +660,12 @@ module Parallel
621
660
  # https://github.com/rspec/rspec-support/blob/673133cdd13b17077b3d88ece8d7380821f8d7dc/lib/rspec/support.rb#L132-L140
622
661
  rescue NoMemoryError, SignalException, Interrupt, SystemExit # rubocop:disable Lint/ShadowedException
623
662
  raise $!
624
- rescue Exception # # rubocop:disable Lint/RescueException
663
+ rescue Exception # rubocop:disable Lint/RescueException
625
664
  ExceptionWrapper.new($!)
626
665
  end
627
666
 
628
667
  begin
629
- Marshal.dump(result, write)
668
+ serializer.dump(result, write)
630
669
  rescue Errno::EPIPE
631
670
  return # parent thread already dead
632
671
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.27.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-14 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  email: michael@grosser.it
13
13
  executables: []
@@ -16,15 +16,18 @@ 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/v1.27.0/Readme.md
26
- source_code_uri: https://github.com/grosser/parallel/tree/v1.27.0
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
27
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,14 +35,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
32
35
  requirements:
33
36
  - - ">="
34
37
  - !ruby/object:Gem::Version
35
- version: '2.7'
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
- rubygems_version: 3.6.2
45
+ rubygems_version: 4.0.3
43
46
  specification_version: 4
44
47
  summary: Run any kind of code in parallel processes
45
48
  test_files: []