parallel 1.28.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: 4b172785b4c554ff694b90a15c5314d32dceac1cb7caf8abf313cf8b75621b54
4
- data.tar.gz: efd61bfc5273f2f105e64391294a636c4d2fceaab650fc6e0346f0764f9dcfa6
3
+ metadata.gz: 3331bc6634cd376e4f3f8511049e49a0d4501110aaeac395e2f0c0b638f4a402
4
+ data.tar.gz: 357e1424cb8297b6472c2c5b9486e26da3969edd928be7d6ca7f21670211a8e0
5
5
  SHA512:
6
- metadata.gz: dc0fca1c5760881f31e2c4b229c0bc27d173f070d278f79e6fa9afb3dabae7e6b899fe919409a431506ee45e27a486e2b812250f74ce14ee1e08a62f0a948297
7
- data.tar.gz: 1ef1b735cfa30ee8b3f92e6d9075752742ae72ee704acbe509e218dd9957715d8776b17b96e6cca2b4d56182e27dd913a8f2f65e67c0a30c7bf50c69c566332c
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.28.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
@@ -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
@@ -266,7 +268,7 @@ module Parallel
266
268
 
267
269
  if options[:in_processes] && options[:in_threads]
268
270
  raise ArgumentError, "Please specify only one of `in_processes` or `in_threads`."
269
- elsif RUBY_PLATFORM =~ (/java/) && !options[:in_processes]
271
+ elsif RUBY_PLATFORM.include?('java') && !options[:in_processes]
270
272
  method = :in_threads
271
273
  size = options[method] || processor_count
272
274
  elsif options[:in_threads]
@@ -471,62 +473,86 @@ module Parallel
471
473
  raise ArgumentError, "pass the code you want to execute as `ractor: [ClassName, :method_name]`"
472
474
  end
473
475
 
476
+ use_port = defined?(Ractor::Port)
477
+
474
478
  # build
475
- ractors = Array.new(options.fetch(:count)) do
476
- Ractor.new do
477
- loop do
478
- got = receive
479
- (klass, method_name), item, index = got
480
- break if index == :break
481
- begin
482
- Ractor.yield [nil, klass.send(method_name, item), item, index]
483
- rescue StandardError => e
484
- Ractor.yield [e, nil, item, index]
485
- end
486
- end
487
- 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
488
483
  end
489
484
 
490
485
  # start
491
- ractors.dup.each do |ractor|
492
- if (set = job_factory.next)
493
- item, index = set
486
+ ports.dup.each do |port, ractor|
487
+ if (job = job_factory.next)
488
+ item, index = job
494
489
  instrument_start item, index, options
495
490
  ractor.send [callback, item, index]
496
- else
497
- ractor.send([[nil, nil], nil, :break]) # stop the ractor
498
- ractors.delete ractor
491
+ else # not enough work, `receive` would hang
492
+ ractor_stop ractor
493
+ ports.delete port
499
494
  end
500
495
  end
501
496
 
502
- # replace with new items
503
- while (set = job_factory.next)
504
- item_next, index_next = set
505
- 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]
506
502
  if exception
507
- ractors.delete done
503
+ ports.delete done_port
508
504
  break
509
505
  end
510
- instrument_finish item, index, result, options
511
- results_mutex.synchronize { results[index] = (options[:preserve_results] == false ? nil : result) }
506
+ ractor_result item_prev, index_prev, result, results, results_mutex, options
512
507
 
508
+ # send new
509
+ item_next, index_next = job
513
510
  instrument_start item_next, index_next, options
514
- done.send([callback, item_next, index_next])
511
+ done_ractor.send([callback, item_next, index_next])
515
512
  end
516
513
 
517
514
  # finish
518
- ractors.each do |ractor|
519
- (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
520
517
  exception ||= new_exception
521
518
  next if new_exception
522
- instrument_finish item, index, result, options
523
- results_mutex.synchronize { results[index] = (options[:preserve_results] == false ? nil : result) }
524
- ractor.send([[nil, nil], nil, :break]) # stop the ractor
519
+ ractor_result item, index, result, results, results_mutex, options
520
+ ractor_stop ractor
525
521
  end
526
522
 
527
523
  exception || results
528
524
  end
529
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
+
530
556
  def work_in_processes(job_factory, options, &blk)
531
557
  workers = create_workers(job_factory, options, &blk)
532
558
  results = []
@@ -598,6 +624,7 @@ module Parallel
598
624
  def worker(job_factory, options, &block)
599
625
  child_read, parent_write = IO.pipe
600
626
  parent_read, child_write = IO.pipe
627
+ options[:serializer] ||= Serializer::Marshal
601
628
 
602
629
  pid = Process.fork do
603
630
  self.worker_number = options[:worker_number]
@@ -618,12 +645,13 @@ module Parallel
618
645
  child_read.close
619
646
  child_write.close
620
647
 
621
- Worker.new(parent_read, parent_write, pid)
648
+ Worker.new(parent_read, parent_write, pid, options[:serializer])
622
649
  end
623
650
 
624
651
  def process_incoming_jobs(read, write, job_factory, options, &block)
652
+ serializer = options.fetch(:serializer)
625
653
  until read.eof?
626
- data = Marshal.load(read)
654
+ data = serializer.load(read)
627
655
  item, index = job_factory.unpack(data)
628
656
 
629
657
  result =
@@ -632,12 +660,12 @@ module Parallel
632
660
  # https://github.com/rspec/rspec-support/blob/673133cdd13b17077b3d88ece8d7380821f8d7dc/lib/rspec/support.rb#L132-L140
633
661
  rescue NoMemoryError, SignalException, Interrupt, SystemExit # rubocop:disable Lint/ShadowedException
634
662
  raise $!
635
- rescue Exception # # rubocop:disable Lint/RescueException
663
+ rescue Exception # rubocop:disable Lint/RescueException
636
664
  ExceptionWrapper.new($!)
637
665
  end
638
666
 
639
667
  begin
640
- Marshal.dump(result, write)
668
+ serializer.dump(result, write)
641
669
  rescue Errno::EPIPE
642
670
  return # parent thread already dead
643
671
  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.28.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: 2026-04-02 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,16 +16,18 @@ extra_rdoc_files: []
18
16
  files:
19
17
  - MIT-LICENSE.txt
20
18
  - lib/parallel.rb
19
+ - lib/parallel/serializer.rb
21
20
  - lib/parallel/version.rb
22
21
  homepage: https://github.com/grosser/parallel
23
22
  licenses:
24
23
  - MIT
25
24
  metadata:
26
25
  bug_tracker_uri: https://github.com/grosser/parallel/issues
27
- documentation_uri: https://github.com/grosser/parallel/blob/v1.28.0/Readme.md
28
- source_code_uri: https://github.com/grosser/parallel/tree/v1.28.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
29
28
  wiki_uri: https://github.com/grosser/parallel/wiki
30
- post_install_message:
29
+ changelog_uri: https://github.com/grosser/parallel/blob/v2.1.0/CHANGELOG.md
30
+ rubygems_mfa_required: 'true'
31
31
  rdoc_options: []
32
32
  require_paths:
33
33
  - lib
@@ -35,15 +35,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
35
35
  requirements:
36
36
  - - ">="
37
37
  - !ruby/object:Gem::Version
38
- version: '2.7'
38
+ version: '3.3'
39
39
  required_rubygems_version: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
43
  version: '0'
44
44
  requirements: []
45
- rubygems_version: 3.4.10
46
- signing_key:
45
+ rubygems_version: 4.0.3
47
46
  specification_version: 4
48
47
  summary: Run any kind of code in parallel processes
49
48
  test_files: []