parallel 1.19.2 → 1.24.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5633042712162c3ac1b37b31f546cf70eb45513bc78c2180a5009c8d5a1826a
4
- data.tar.gz: 621b9b2dfa7d8d67494da0ff0ad82c14d67d7e8b11fd73e14fe3b38e7aeaccab
3
+ metadata.gz: f7b4a5662ef68724c75c2d655d9bd09ad109edc02d3199a0744093544608f74b
4
+ data.tar.gz: 247f7f3745c42cdafeb5d403fff250e99b99a56d4dc4948050dcf20ecb60d183
5
5
  SHA512:
6
- metadata.gz: 7de2bb3f72d999e4eacfe84ff8df6ece7d62e60e044ba512fe11d38f2597a53f5c4ccbd0399a9e531184b8e17b25b7671be61d6ef63d739767fcf77bd9808933
7
- data.tar.gz: 76adaacedc029a347231d1690913a2eaccce021e99e80e94a59685a418e617144ad65077d77cc87fd01a3a9cf7a6a4a6250fc2792a3b54f303ef4d99929c4e98
6
+ metadata.gz: 18f62ea9fefb30bda9b89f0189074da99572c5c17fb93ba28e553b0eb9f6d7a61bd70ba423004b138b05c3c86f8e04fd32fce81da40a68c0bdca39e263b8b892
7
+ data.tar.gz: dca61c9e3cab22de0ad064c96c55afc88177214108d6bdbfb8e9a93343f23d6ee321c83aa6c4cfa9ee33da79d01802d7b952f3dd0360f9cca26e5da78a4117fa
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Parallel
2
- VERSION = Version = '1.19.2'
3
+ VERSION = Version = '1.24.0' # rubocop:disable Naming/ConstantName
3
4
  end
data/lib/parallel.rb CHANGED
@@ -1,31 +1,37 @@
1
+ # frozen_string_literal: true
1
2
  require 'rbconfig'
2
3
  require 'parallel/version'
3
- require 'parallel/processor_count'
4
4
 
5
5
  module Parallel
6
- extend Parallel::ProcessorCount
6
+ Stop = Object.new.freeze
7
7
 
8
8
  class DeadWorker < StandardError
9
9
  end
10
10
 
11
11
  class Break < StandardError
12
+ attr_reader :value
13
+
14
+ def initialize(value = nil)
15
+ super()
16
+ @value = value
17
+ end
12
18
  end
13
19
 
14
- class Kill < StandardError
20
+ class Kill < Break
15
21
  end
16
22
 
17
23
  class UndumpableException < StandardError
18
24
  attr_reader :backtrace
25
+
19
26
  def initialize(original)
20
27
  super "#{original.class}: #{original.message}"
21
28
  @backtrace = original.backtrace
22
29
  end
23
30
  end
24
31
 
25
- Stop = Object.new.freeze
26
-
27
32
  class ExceptionWrapper
28
33
  attr_reader :exception
34
+
29
35
  def initialize(exception)
30
36
  # Remove the bindings stack added by the better_errors gem,
31
37
  # because it cannot be marshalled
@@ -36,7 +42,7 @@ module Parallel
36
42
  @exception =
37
43
  begin
38
44
  Marshal.dump(exception) && exception
39
- rescue
45
+ rescue StandardError
40
46
  UndumpableException.new(exception)
41
47
  end
42
48
  end
@@ -45,8 +51,11 @@ module Parallel
45
51
  class Worker
46
52
  attr_reader :pid, :read, :write
47
53
  attr_accessor :thread
54
+
48
55
  def initialize(read, write, pid)
49
- @read, @write, @pid = read, write, pid
56
+ @read = read
57
+ @write = write
58
+ @pid = pid
50
59
  end
51
60
 
52
61
  def stop
@@ -73,7 +82,7 @@ module Parallel
73
82
  rescue EOFError
74
83
  raise DeadWorker
75
84
  end
76
- raise result.exception if ExceptionWrapper === result
85
+ raise result.exception if result.is_a?(ExceptionWrapper)
77
86
  result
78
87
  end
79
88
 
@@ -102,7 +111,7 @@ module Parallel
102
111
  item, index = @mutex.synchronize do
103
112
  return if @stopped
104
113
  item = @lambda.call
105
- @stopped = (item == Parallel::Stop)
114
+ @stopped = (item == Stop)
106
115
  return if @stopped
107
116
  [item, @index += 1]
108
117
  end
@@ -140,7 +149,7 @@ module Parallel
140
149
  end
141
150
 
142
151
  def queue_wrapper(array)
143
- array.respond_to?(:num_waiting) && array.respond_to?(:pop) && lambda { array.pop(false) }
152
+ array.respond_to?(:num_waiting) && array.respond_to?(:pop) && -> { array.pop(false) }
144
153
  end
145
154
  end
146
155
 
@@ -156,7 +165,7 @@ module Parallel
156
165
 
157
166
  if @to_be_killed.empty?
158
167
  old_interrupt = trap_interrupt(signal) do
159
- $stderr.puts 'Parallel execution interrupted, exiting ...'
168
+ warn 'Parallel execution interrupted, exiting ...'
160
169
  @to_be_killed.flatten.each { |pid| kill(pid) }
161
170
  end
162
171
  end
@@ -200,46 +209,44 @@ module Parallel
200
209
  end
201
210
 
202
211
  class << self
203
- def in_threads(options={:count => 2})
212
+ def in_threads(options = { count: 2 })
204
213
  threads = []
205
- count, _ = extract_count_from_options(options)
214
+ count, = extract_count_from_options(options)
206
215
 
207
216
  Thread.handle_interrupt(Exception => :never) do
208
- begin
209
- Thread.handle_interrupt(Exception => :immediate) do
210
- count.times do |i|
211
- threads << Thread.new { yield(i) }
212
- end
213
- threads.map(&:value)
217
+ Thread.handle_interrupt(Exception => :immediate) do
218
+ count.times do |i|
219
+ threads << Thread.new { yield(i) }
214
220
  end
215
- ensure
216
- threads.each(&:kill)
221
+ threads.map(&:value)
217
222
  end
223
+ ensure
224
+ threads.each(&:kill)
218
225
  end
219
226
  end
220
227
 
221
228
  def in_processes(options = {}, &block)
222
229
  count, options = extract_count_from_options(options)
223
230
  count ||= processor_count
224
- map(0...count, options.merge(:in_processes => count), &block)
231
+ map(0...count, options.merge(in_processes: count), &block)
225
232
  end
226
233
 
227
- def each(array, options={}, &block)
228
- map(array, options.merge(:preserve_results => false), &block)
234
+ def each(array, options = {}, &block)
235
+ map(array, options.merge(preserve_results: false), &block)
229
236
  end
230
237
 
231
238
  def any?(*args, &block)
232
239
  raise "You must provide a block when calling #any?" if block.nil?
233
- !each(*args) { |*a| raise Parallel::Kill if block.call(*a) }
240
+ !each(*args) { |*a| raise Kill if block.call(*a) }
234
241
  end
235
242
 
236
243
  def all?(*args, &block)
237
244
  raise "You must provide a block when calling #all?" if block.nil?
238
- !!each(*args) { |*a| raise Parallel::Kill unless block.call(*a) }
245
+ !!each(*args) { |*a| raise Kill unless block.call(*a) }
239
246
  end
240
247
 
241
- def each_with_index(array, options={}, &block)
242
- each(array, options.merge(:with_index => true), &block)
248
+ def each_with_index(array, options = {}, &block)
249
+ each(array, options.merge(with_index: true), &block)
243
250
  end
244
251
 
245
252
  def map(source, options = {}, &block)
@@ -247,13 +254,16 @@ module Parallel
247
254
  options[:mutex] = Mutex.new
248
255
 
249
256
  if options[:in_processes] && options[:in_threads]
250
- raise ArgumentError.new("Please specify only one of `in_processes` or `in_threads`.")
251
- elsif RUBY_PLATFORM =~ /java/ and not options[:in_processes]
257
+ raise ArgumentError, "Please specify only one of `in_processes` or `in_threads`."
258
+ elsif RUBY_PLATFORM =~ (/java/) && !(options[:in_processes])
252
259
  method = :in_threads
253
260
  size = options[method] || processor_count
254
261
  elsif options[:in_threads]
255
262
  method = :in_threads
256
263
  size = options[method]
264
+ elsif options[:in_ractors]
265
+ method = :in_ractors
266
+ size = options[method]
257
267
  else
258
268
  method = :in_processes
259
269
  if Process.respond_to?(:fork)
@@ -270,26 +280,73 @@ module Parallel
270
280
  options[:return_results] = (options[:preserve_results] != false || !!options[:finish])
271
281
  add_progress_bar!(job_factory, options)
272
282
 
273
- results = if size == 0
274
- work_direct(job_factory, options, &block)
275
- elsif method == :in_threads
276
- work_in_threads(job_factory, options.merge(:count => size), &block)
277
- else
278
- work_in_processes(job_factory, options.merge(:count => size), &block)
279
- end
280
- if results
281
- options[:return_results] ? results : source
282
- end
283
+ result =
284
+ if size == 0
285
+ work_direct(job_factory, options, &block)
286
+ elsif method == :in_threads
287
+ work_in_threads(job_factory, options.merge(count: size), &block)
288
+ elsif method == :in_ractors
289
+ work_in_ractors(job_factory, options.merge(count: size), &block)
290
+ else
291
+ work_in_processes(job_factory, options.merge(count: size), &block)
292
+ end
293
+
294
+ return result.value if result.is_a?(Break)
295
+ raise result if result.is_a?(Exception)
296
+ options[:return_results] ? result : source
283
297
  end
284
298
 
285
- def map_with_index(array, options={}, &block)
286
- map(array, options.merge(:with_index => true), &block)
299
+ def map_with_index(array, options = {}, &block)
300
+ map(array, options.merge(with_index: true), &block)
287
301
  end
288
302
 
289
303
  def flat_map(*args, &block)
290
304
  map(*args, &block).flatten(1)
291
305
  end
292
306
 
307
+ def filter_map(*args, &block)
308
+ map(*args, &block).compact
309
+ end
310
+
311
+ # Number of physical processor cores on the current system.
312
+ def physical_processor_count
313
+ @physical_processor_count ||= begin
314
+ ppc =
315
+ case RbConfig::CONFIG["target_os"]
316
+ when /darwin[12]/
317
+ IO.popen("/usr/sbin/sysctl -n hw.physicalcpu").read.to_i
318
+ when /linux/
319
+ cores = {} # unique physical ID / core ID combinations
320
+ phy = 0
321
+ File.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
322
+ if ln.start_with?("physical")
323
+ phy = ln[/\d+/]
324
+ elsif ln.start_with?("core")
325
+ cid = "#{phy}:#{ln[/\d+/]}"
326
+ cores[cid] = true unless cores[cid]
327
+ end
328
+ end
329
+ cores.count
330
+ when /mswin|mingw/
331
+ require 'win32ole'
332
+ result_set = WIN32OLE.connect("winmgmts://").ExecQuery(
333
+ "select NumberOfCores from Win32_Processor"
334
+ )
335
+ result_set.to_enum.collect(&:NumberOfCores).reduce(:+)
336
+ else
337
+ processor_count
338
+ end
339
+ # fall back to logical count if physical info is invalid
340
+ ppc > 0 ? ppc : processor_count
341
+ end
342
+ end
343
+
344
+ # Number of processors seen by the OS, used for process scheduling
345
+ def processor_count
346
+ require 'etc'
347
+ @processor_count ||= Integer(ENV['PARALLEL_PROCESSOR_COUNT'] || Etc.nprocessors)
348
+ end
349
+
293
350
  def worker_number
294
351
  Thread.current[:parallel_worker_number]
295
352
  end
@@ -337,10 +394,10 @@ module Parallel
337
394
  call_with_index(item, index, options, &block)
338
395
  end
339
396
  end
340
- rescue
397
+ rescue StandardError
341
398
  exception = $!
342
399
  end
343
- handle_exception(exception, results)
400
+ exception || results
344
401
  ensure
345
402
  self.worker_number = nil
346
403
  end
@@ -361,13 +418,79 @@ module Parallel
361
418
  call_with_index(item, index, options, &block)
362
419
  end
363
420
  results_mutex.synchronize { results[index] = result }
364
- rescue
421
+ rescue StandardError
365
422
  exception = $!
366
423
  end
367
424
  end
368
425
  end
369
426
 
370
- handle_exception(exception, results)
427
+ exception || results
428
+ end
429
+
430
+ def work_in_ractors(job_factory, options)
431
+ exception = nil
432
+ results = []
433
+ results_mutex = Mutex.new # arrays are not thread-safe on jRuby
434
+
435
+ callback = options[:ractor]
436
+ if block_given? || !callback
437
+ raise ArgumentError, "pass the code you want to execute as `ractor: [ClassName, :method_name]`"
438
+ end
439
+
440
+ # build
441
+ ractors = Array.new(options.fetch(:count)) do
442
+ Ractor.new do
443
+ loop do
444
+ got = receive
445
+ (klass, method_name), item, index = got
446
+ break if index == :break
447
+ begin
448
+ Ractor.yield [nil, klass.send(method_name, item), item, index]
449
+ rescue StandardError => e
450
+ Ractor.yield [e, nil, item, index]
451
+ end
452
+ end
453
+ end
454
+ end
455
+
456
+ # start
457
+ ractors.dup.each do |ractor|
458
+ if set = job_factory.next
459
+ item, index = set
460
+ instrument_start item, index, options
461
+ ractor.send [callback, item, index]
462
+ else
463
+ ractor.send([[nil, nil], nil, :break]) # stop the ractor
464
+ ractors.delete ractor
465
+ end
466
+ end
467
+
468
+ # replace with new items
469
+ while set = job_factory.next
470
+ item_next, index_next = set
471
+ done, (exception, result, item, index) = Ractor.select(*ractors)
472
+ if exception
473
+ ractors.delete done
474
+ break
475
+ end
476
+ instrument_finish item, index, result, options
477
+ results_mutex.synchronize { results[index] = (options[:preserve_results] == false ? nil : result) }
478
+
479
+ instrument_start item_next, index_next, options
480
+ done.send([callback, item_next, index_next])
481
+ end
482
+
483
+ # finish
484
+ ractors.each do |ractor|
485
+ (new_exception, result, item, index) = ractor.take
486
+ exception ||= new_exception
487
+ next if new_exception
488
+ instrument_finish item, index, result, options
489
+ results_mutex.synchronize { results[index] = (options[:preserve_results] == false ? nil : result) }
490
+ ractor.send([[nil, nil], nil, :break]) # stop the ractor
491
+ end
492
+
493
+ exception || results
371
494
  end
372
495
 
373
496
  def work_in_processes(job_factory, options, &blk)
@@ -401,9 +524,9 @@ module Parallel
401
524
  results_mutex.synchronize { results[index] = result } # arrays are not threads safe on jRuby
402
525
  rescue StandardError => e
403
526
  exception = e
404
- if Parallel::Kill === exception
527
+ if exception.is_a?(Kill)
405
528
  (workers - [worker]).each do |w|
406
- w.thread.kill if w.thread
529
+ w.thread&.kill
407
530
  UserInterruptHandler.kill(w.pid)
408
531
  end
409
532
  end
@@ -415,18 +538,18 @@ module Parallel
415
538
  end
416
539
  end
417
540
 
418
- handle_exception(exception, results)
541
+ exception || results
419
542
  end
420
543
 
421
- def replace_worker(job_factory, workers, i, options, blk)
544
+ def replace_worker(job_factory, workers, index, options, blk)
422
545
  options[:mutex].synchronize do
423
546
  # old worker is no longer used ... stop it
424
- worker = workers[i]
547
+ worker = workers[index]
425
548
  worker.stop
426
549
 
427
550
  # create a new replacement worker
428
551
  running = workers - [worker]
429
- workers[i] = worker(job_factory, options.merge(started_workers: running, worker_number: i), &blk)
552
+ workers[index] = worker(job_factory, options.merge(started_workers: running, worker_number: index), &blk)
430
553
  end
431
554
  end
432
555
 
@@ -468,14 +591,17 @@ module Parallel
468
591
  until read.eof?
469
592
  data = Marshal.load(read)
470
593
  item, index = job_factory.unpack(data)
471
- result = begin
472
- call_with_index(item, index, options, &block)
473
- # https://github.com/rspec/rspec-support/blob/673133cdd13b17077b3d88ece8d7380821f8d7dc/lib/rspec/support.rb#L132-L140
474
- rescue NoMemoryError, SignalException, Interrupt, SystemExit
475
- raise $!
476
- rescue Exception
477
- ExceptionWrapper.new($!)
478
- end
594
+
595
+ result =
596
+ begin
597
+ call_with_index(item, index, options, &block)
598
+ # https://github.com/rspec/rspec-support/blob/673133cdd13b17077b3d88ece8d7380821f8d7dc/lib/rspec/support.rb#L132-L140
599
+ rescue NoMemoryError, SignalException, Interrupt, SystemExit # rubocop:disable Lint/ShadowedException
600
+ raise $!
601
+ rescue Exception # # rubocop:disable Lint/RescueException
602
+ ExceptionWrapper.new($!)
603
+ end
604
+
479
605
  begin
480
606
  Marshal.dump(result, write)
481
607
  rescue Errno::EPIPE
@@ -484,12 +610,6 @@ module Parallel
484
610
  end
485
611
  end
486
612
 
487
- def handle_exception(exception, results)
488
- return nil if [Parallel::Break, Parallel::Kill].include? exception.class
489
- raise exception if exception
490
- results
491
- end
492
-
493
613
  # options is either a Integer or a Hash with :count
494
614
  def extract_count_from_options(options)
495
615
  if options.is_a?(Hash)
@@ -504,21 +624,53 @@ module Parallel
504
624
  def call_with_index(item, index, options, &block)
505
625
  args = [item]
506
626
  args << index if options[:with_index]
627
+ results = block.call(*args)
507
628
  if options[:return_results]
508
- block.call(*args)
629
+ results
509
630
  else
510
- block.call(*args)
511
631
  nil # avoid GC overhead of passing large results around
512
632
  end
513
633
  end
514
634
 
515
635
  def with_instrumentation(item, index, options)
516
- on_start = options[:start]
517
- on_finish = options[:finish]
518
- options[:mutex].synchronize { on_start.call(item, index) } if on_start
636
+ instrument_start(item, index, options)
519
637
  result = yield
520
- options[:mutex].synchronize { on_finish.call(item, index, result) } if on_finish
638
+ instrument_finish(item, index, result, options)
521
639
  result unless options[:preserve_results] == false
522
640
  end
641
+
642
+ def instrument_finish(item, index, result, options)
643
+ return unless (on_finish = options[:finish])
644
+ return instrument_finish_in_order(item, index, result, options) if options[:finish_in_order]
645
+ options[:mutex].synchronize { on_finish.call(item, index, result) }
646
+ end
647
+
648
+ # yield results in the order of the input items
649
+ # needs to use `options` to store state between executions
650
+ # needs to use `done` index since a nil result would also be valid
651
+ def instrument_finish_in_order(item, index, result, options)
652
+ options[:mutex].synchronize do
653
+ # initialize our state
654
+ options[:finish_done] ||= []
655
+ options[:finish_expecting] ||= 0 # we wait for item at index 0
656
+
657
+ # store current result
658
+ options[:finish_done][index] = [item, result]
659
+
660
+ # yield all results that are now in order
661
+ break unless index == options[:finish_expecting]
662
+ index.upto(options[:finish_done].size).each do |i|
663
+ break unless (done = options[:finish_done][i])
664
+ options[:finish_done][i] = nil # allow GC to free this item and result
665
+ options[:finish].call(done[0], i, done[1])
666
+ options[:finish_expecting] += 1
667
+ end
668
+ end
669
+ end
670
+
671
+ def instrument_start(item, index, options)
672
+ return unless on_start = options[:start]
673
+ options[:mutex].synchronize { on_start.call(item, index) }
674
+ end
523
675
  end
524
676
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.2
4
+ version: 1.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-17 00:00:00.000000000 Z
11
+ date: 2023-12-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: michael@grosser.it
@@ -18,15 +18,14 @@ extra_rdoc_files: []
18
18
  files:
19
19
  - MIT-LICENSE.txt
20
20
  - lib/parallel.rb
21
- - lib/parallel/processor_count.rb
22
21
  - lib/parallel/version.rb
23
22
  homepage: https://github.com/grosser/parallel
24
23
  licenses:
25
24
  - MIT
26
25
  metadata:
27
26
  bug_tracker_uri: https://github.com/grosser/parallel/issues
28
- documentation_uri: https://github.com/grosser/parallel/blob/v1.19.2/Readme.md
29
- source_code_uri: https://github.com/grosser/parallel/tree/v1.19.2
27
+ documentation_uri: https://github.com/grosser/parallel/blob/v1.24.0/Readme.md
28
+ source_code_uri: https://github.com/grosser/parallel/tree/v1.24.0
30
29
  wiki_uri: https://github.com/grosser/parallel/wiki
31
30
  post_install_message:
32
31
  rdoc_options: []
@@ -36,14 +35,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
36
35
  requirements:
37
36
  - - ">="
38
37
  - !ruby/object:Gem::Version
39
- version: '2.2'
38
+ version: '2.5'
40
39
  required_rubygems_version: !ruby/object:Gem::Requirement
41
40
  requirements:
42
41
  - - ">="
43
42
  - !ruby/object:Gem::Version
44
43
  version: '0'
45
44
  requirements: []
46
- rubygems_version: 3.1.3
45
+ rubygems_version: 3.1.6
47
46
  signing_key:
48
47
  specification_version: 4
49
48
  summary: Run any kind of code in parallel processes
@@ -1,42 +0,0 @@
1
- require 'etc'
2
-
3
- module Parallel
4
- # TODO: inline this method into parallel.rb and kill physical_processor_count in next major release
5
- module ProcessorCount
6
- # Number of processors seen by the OS, used for process scheduling
7
- def processor_count
8
- @processor_count ||= Integer(ENV['PARALLEL_PROCESSOR_COUNT'] || Etc.nprocessors)
9
- end
10
-
11
- # Number of physical processor cores on the current system.
12
- def physical_processor_count
13
- @physical_processor_count ||= begin
14
- ppc = case RbConfig::CONFIG["target_os"]
15
- when /darwin1/
16
- IO.popen("/usr/sbin/sysctl -n hw.physicalcpu").read.to_i
17
- when /linux/
18
- cores = {} # unique physical ID / core ID combinations
19
- phy = 0
20
- IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
21
- if ln.start_with?("physical")
22
- phy = ln[/\d+/]
23
- elsif ln.start_with?("core")
24
- cid = phy + ":" + ln[/\d+/]
25
- cores[cid] = true if not cores[cid]
26
- end
27
- end
28
- cores.count
29
- when /mswin|mingw/
30
- require 'win32ole'
31
- result_set = WIN32OLE.connect("winmgmts://").ExecQuery(
32
- "select NumberOfCores from Win32_Processor")
33
- result_set.to_enum.collect(&:NumberOfCores).reduce(:+)
34
- else
35
- processor_count
36
- end
37
- # fall back to logical count if physical info is invalid
38
- ppc > 0 ? ppc : processor_count
39
- end
40
- end
41
- end
42
- end