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 +5 -5
- data/lib/parallel/serializer.rb +52 -0
- data/lib/parallel/version.rb +2 -1
- data/lib/parallel.rb +413 -84
- metadata +12 -11
- data/lib/parallel/processor_count.rb +0 -85
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
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,66 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'rbconfig'
|
|
2
3
|
require 'parallel/version'
|
|
3
|
-
require 'parallel/
|
|
4
|
+
require 'parallel/serializer'
|
|
4
5
|
|
|
5
6
|
module Parallel
|
|
6
|
-
|
|
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 <
|
|
32
|
+
class Kill < Break
|
|
15
33
|
end
|
|
16
34
|
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 =
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
88
|
+
@serializer.dump(data, write)
|
|
52
89
|
rescue Errno::EPIPE
|
|
53
90
|
raise DeadWorker
|
|
54
91
|
end
|
|
55
92
|
|
|
56
93
|
result = begin
|
|
57
|
-
|
|
94
|
+
@serializer.load(read)
|
|
58
95
|
rescue EOFError
|
|
59
96
|
raise DeadWorker
|
|
60
97
|
end
|
|
61
|
-
raise result.exception if ExceptionWrapper
|
|
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 ==
|
|
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) &&
|
|
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
|
-
|
|
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={:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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(:
|
|
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(:
|
|
192
|
-
array
|
|
247
|
+
def each(array, options = {}, &block)
|
|
248
|
+
map(array, options.merge(preserve_results: false), &block)
|
|
193
249
|
end
|
|
194
250
|
|
|
195
|
-
def
|
|
196
|
-
|
|
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
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
587
|
+
if exception.is_a?(Kill)
|
|
318
588
|
(workers - [worker]).each do |w|
|
|
319
|
-
w.thread
|
|
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.
|
|
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
|
-
|
|
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]).
|
|
338
|
-
workers << worker(job_factory, options.merge(:
|
|
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 =
|
|
654
|
+
data = serializer.load(read)
|
|
370
655
|
item, index = job_factory.unpack(data)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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.
|
|
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:
|
|
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/
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|