parallel 1.19.2 → 1.24.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: 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