parallel 1.20.1 → 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: fa334cb0d83b8049f260a5e6724d2fd0e65768f85afd0968a2af05d55dda4e0e
4
- data.tar.gz: 7ba3ade3a7af4bcfe5944b8d7313b5ec6e4074d27755a83f95e4f8a01d77261e
3
+ metadata.gz: f7b4a5662ef68724c75c2d655d9bd09ad109edc02d3199a0744093544608f74b
4
+ data.tar.gz: 247f7f3745c42cdafeb5d403fff250e99b99a56d4dc4948050dcf20ecb60d183
5
5
  SHA512:
6
- metadata.gz: d470562bd90ab80c66f69b9c1ab016f737bf52e091ac3027c51ea7482eaecd3bd3c186daab756c5d4276b9fb54f0e80f149a8071dc17c31a321ff4fc4d369600
7
- data.tar.gz: f01bf1c3a052579c11c3097b02b02a4a30c8e881dbb4916c3ceba6f122aab3a40c276a1de82f7ea7d3790eeff2f994bd20283361151439a3d57b025bdd24dfc8
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.20.1'
3
+ VERSION = Version = '1.24.0' # rubocop:disable Naming/ConstantName
3
4
  end
data/lib/parallel.rb CHANGED
@@ -1,10 +1,8 @@
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 ProcessorCount
7
-
8
6
  Stop = Object.new.freeze
9
7
 
10
8
  class DeadWorker < StandardError
@@ -12,7 +10,9 @@ module Parallel
12
10
 
13
11
  class Break < StandardError
14
12
  attr_reader :value
13
+
15
14
  def initialize(value = nil)
15
+ super()
16
16
  @value = value
17
17
  end
18
18
  end
@@ -22,6 +22,7 @@ module Parallel
22
22
 
23
23
  class UndumpableException < StandardError
24
24
  attr_reader :backtrace
25
+
25
26
  def initialize(original)
26
27
  super "#{original.class}: #{original.message}"
27
28
  @backtrace = original.backtrace
@@ -30,6 +31,7 @@ module Parallel
30
31
 
31
32
  class ExceptionWrapper
32
33
  attr_reader :exception
34
+
33
35
  def initialize(exception)
34
36
  # Remove the bindings stack added by the better_errors gem,
35
37
  # because it cannot be marshalled
@@ -40,7 +42,7 @@ module Parallel
40
42
  @exception =
41
43
  begin
42
44
  Marshal.dump(exception) && exception
43
- rescue
45
+ rescue StandardError
44
46
  UndumpableException.new(exception)
45
47
  end
46
48
  end
@@ -49,8 +51,11 @@ module Parallel
49
51
  class Worker
50
52
  attr_reader :pid, :read, :write
51
53
  attr_accessor :thread
54
+
52
55
  def initialize(read, write, pid)
53
- @read, @write, @pid = read, write, pid
56
+ @read = read
57
+ @write = write
58
+ @pid = pid
54
59
  end
55
60
 
56
61
  def stop
@@ -77,7 +82,7 @@ module Parallel
77
82
  rescue EOFError
78
83
  raise DeadWorker
79
84
  end
80
- raise result.exception if ExceptionWrapper === result
85
+ raise result.exception if result.is_a?(ExceptionWrapper)
81
86
  result
82
87
  end
83
88
 
@@ -144,7 +149,7 @@ module Parallel
144
149
  end
145
150
 
146
151
  def queue_wrapper(array)
147
- 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) }
148
153
  end
149
154
  end
150
155
 
@@ -160,7 +165,7 @@ module Parallel
160
165
 
161
166
  if @to_be_killed.empty?
162
167
  old_interrupt = trap_interrupt(signal) do
163
- $stderr.puts 'Parallel execution interrupted, exiting ...'
168
+ warn 'Parallel execution interrupted, exiting ...'
164
169
  @to_be_killed.flatten.each { |pid| kill(pid) }
165
170
  end
166
171
  end
@@ -204,32 +209,30 @@ module Parallel
204
209
  end
205
210
 
206
211
  class << self
207
- def in_threads(options={:count => 2})
212
+ def in_threads(options = { count: 2 })
208
213
  threads = []
209
- count, _ = extract_count_from_options(options)
214
+ count, = extract_count_from_options(options)
210
215
 
211
216
  Thread.handle_interrupt(Exception => :never) do
212
- begin
213
- Thread.handle_interrupt(Exception => :immediate) do
214
- count.times do |i|
215
- threads << Thread.new { yield(i) }
216
- end
217
- threads.map(&:value)
217
+ Thread.handle_interrupt(Exception => :immediate) do
218
+ count.times do |i|
219
+ threads << Thread.new { yield(i) }
218
220
  end
219
- ensure
220
- threads.each(&:kill)
221
+ threads.map(&:value)
221
222
  end
223
+ ensure
224
+ threads.each(&:kill)
222
225
  end
223
226
  end
224
227
 
225
228
  def in_processes(options = {}, &block)
226
229
  count, options = extract_count_from_options(options)
227
230
  count ||= processor_count
228
- map(0...count, options.merge(:in_processes => count), &block)
231
+ map(0...count, options.merge(in_processes: count), &block)
229
232
  end
230
233
 
231
- def each(array, options={}, &block)
232
- map(array, options.merge(:preserve_results => false), &block)
234
+ def each(array, options = {}, &block)
235
+ map(array, options.merge(preserve_results: false), &block)
233
236
  end
234
237
 
235
238
  def any?(*args, &block)
@@ -242,8 +245,8 @@ module Parallel
242
245
  !!each(*args) { |*a| raise Kill unless block.call(*a) }
243
246
  end
244
247
 
245
- def each_with_index(array, options={}, &block)
246
- 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)
247
250
  end
248
251
 
249
252
  def map(source, options = {}, &block)
@@ -251,13 +254,16 @@ module Parallel
251
254
  options[:mutex] = Mutex.new
252
255
 
253
256
  if options[:in_processes] && options[:in_threads]
254
- raise ArgumentError.new("Please specify only one of `in_processes` or `in_threads`.")
255
- 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])
256
259
  method = :in_threads
257
260
  size = options[method] || processor_count
258
261
  elsif options[:in_threads]
259
262
  method = :in_threads
260
263
  size = options[method]
264
+ elsif options[:in_ractors]
265
+ method = :in_ractors
266
+ size = options[method]
261
267
  else
262
268
  method = :in_processes
263
269
  if Process.respond_to?(:fork)
@@ -278,9 +284,11 @@ module Parallel
278
284
  if size == 0
279
285
  work_direct(job_factory, options, &block)
280
286
  elsif method == :in_threads
281
- work_in_threads(job_factory, options.merge(:count => size), &block)
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)
282
290
  else
283
- work_in_processes(job_factory, options.merge(:count => size), &block)
291
+ work_in_processes(job_factory, options.merge(count: size), &block)
284
292
  end
285
293
 
286
294
  return result.value if result.is_a?(Break)
@@ -288,14 +296,57 @@ module Parallel
288
296
  options[:return_results] ? result : source
289
297
  end
290
298
 
291
- def map_with_index(array, options={}, &block)
292
- 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)
293
301
  end
294
302
 
295
303
  def flat_map(*args, &block)
296
304
  map(*args, &block).flatten(1)
297
305
  end
298
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
+
299
350
  def worker_number
300
351
  Thread.current[:parallel_worker_number]
301
352
  end
@@ -343,7 +394,7 @@ module Parallel
343
394
  call_with_index(item, index, options, &block)
344
395
  end
345
396
  end
346
- rescue
397
+ rescue StandardError
347
398
  exception = $!
348
399
  end
349
400
  exception || results
@@ -367,7 +418,7 @@ module Parallel
367
418
  call_with_index(item, index, options, &block)
368
419
  end
369
420
  results_mutex.synchronize { results[index] = result }
370
- rescue
421
+ rescue StandardError
371
422
  exception = $!
372
423
  end
373
424
  end
@@ -376,6 +427,72 @@ module Parallel
376
427
  exception || results
377
428
  end
378
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
494
+ end
495
+
379
496
  def work_in_processes(job_factory, options, &blk)
380
497
  workers = create_workers(job_factory, options, &blk)
381
498
  results = []
@@ -407,9 +524,9 @@ module Parallel
407
524
  results_mutex.synchronize { results[index] = result } # arrays are not threads safe on jRuby
408
525
  rescue StandardError => e
409
526
  exception = e
410
- if Kill === exception
527
+ if exception.is_a?(Kill)
411
528
  (workers - [worker]).each do |w|
412
- w.thread.kill if w.thread
529
+ w.thread&.kill
413
530
  UserInterruptHandler.kill(w.pid)
414
531
  end
415
532
  end
@@ -420,18 +537,19 @@ module Parallel
420
537
  end
421
538
  end
422
539
  end
540
+
423
541
  exception || results
424
542
  end
425
543
 
426
- def replace_worker(job_factory, workers, i, options, blk)
544
+ def replace_worker(job_factory, workers, index, options, blk)
427
545
  options[:mutex].synchronize do
428
546
  # old worker is no longer used ... stop it
429
- worker = workers[i]
547
+ worker = workers[index]
430
548
  worker.stop
431
549
 
432
550
  # create a new replacement worker
433
551
  running = workers - [worker]
434
- 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)
435
553
  end
436
554
  end
437
555
 
@@ -473,14 +591,17 @@ module Parallel
473
591
  until read.eof?
474
592
  data = Marshal.load(read)
475
593
  item, index = job_factory.unpack(data)
476
- result = begin
477
- call_with_index(item, index, options, &block)
478
- # https://github.com/rspec/rspec-support/blob/673133cdd13b17077b3d88ece8d7380821f8d7dc/lib/rspec/support.rb#L132-L140
479
- rescue NoMemoryError, SignalException, Interrupt, SystemExit
480
- raise $!
481
- rescue Exception
482
- ExceptionWrapper.new($!)
483
- 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
+
484
605
  begin
485
606
  Marshal.dump(result, write)
486
607
  rescue Errno::EPIPE
@@ -503,21 +624,53 @@ module Parallel
503
624
  def call_with_index(item, index, options, &block)
504
625
  args = [item]
505
626
  args << index if options[:with_index]
627
+ results = block.call(*args)
506
628
  if options[:return_results]
507
- block.call(*args)
629
+ results
508
630
  else
509
- block.call(*args)
510
631
  nil # avoid GC overhead of passing large results around
511
632
  end
512
633
  end
513
634
 
514
635
  def with_instrumentation(item, index, options)
515
- on_start = options[:start]
516
- on_finish = options[:finish]
517
- options[:mutex].synchronize { on_start.call(item, index) } if on_start
636
+ instrument_start(item, index, options)
518
637
  result = yield
519
- options[:mutex].synchronize { on_finish.call(item, index, result) } if on_finish
638
+ instrument_finish(item, index, result, options)
520
639
  result unless options[:preserve_results] == false
521
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
522
675
  end
523
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.20.1
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-11-22 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.20.1/Readme.md
29
- source_code_uri: https://github.com/grosser/parallel/tree/v1.20.1
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.4'
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