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 +4 -4
- data/lib/parallel/serializer.rb +52 -0
- data/lib/parallel/version.rb +1 -1
- data/lib/parallel.rb +79 -40
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3331bc6634cd376e4f3f8511049e49a0d4501110aaeac395e2f0c0b638f4a402
|
|
4
|
+
data.tar.gz: 357e1424cb8297b6472c2c5b9486e26da3969edd928be7d6ca7f21670211a8e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/parallel/version.rb
CHANGED
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
|
-
|
|
88
|
+
@serializer.dump(data, write)
|
|
76
89
|
rescue Errno::EPIPE
|
|
77
90
|
raise DeadWorker
|
|
78
91
|
end
|
|
79
92
|
|
|
80
93
|
result = begin
|
|
81
|
-
|
|
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
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
481
|
-
if (
|
|
482
|
-
item, index =
|
|
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
|
-
|
|
487
|
-
|
|
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
|
-
#
|
|
492
|
-
while (
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
503
|
+
ports.delete done_port
|
|
497
504
|
break
|
|
498
505
|
end
|
|
499
|
-
|
|
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
|
-
|
|
511
|
+
done_ractor.send([callback, item_next, index_next])
|
|
504
512
|
end
|
|
505
513
|
|
|
506
514
|
# finish
|
|
507
|
-
|
|
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
|
-
|
|
512
|
-
|
|
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 =
|
|
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 #
|
|
663
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
625
664
|
ExceptionWrapper.new($!)
|
|
626
665
|
end
|
|
627
666
|
|
|
628
667
|
begin
|
|
629
|
-
|
|
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.
|
|
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:
|
|
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/
|
|
26
|
-
source_code_uri: https://github.com/grosser/parallel/tree/
|
|
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: '
|
|
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:
|
|
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: []
|