farce 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -2
- data/README.md +3 -1
- data/docs/benchmarks.md +37 -21
- data/docs/compatibility.md +13 -0
- data/docs/data-structures.md +37 -0
- data/docs/integrations.md +1 -0
- data/docs/introduction.md +19 -0
- data/docs/modes.md +2 -0
- data/docs/scheduling.md +35 -3
- data/docs/scopes.md +3 -1
- data/docs/shims.md +53 -0
- data/docs/utilities.md +31 -0
- data/docs/variants.md +9 -0
- data/ext/farce/farce.c +9 -0
- data/ext/farce/map.c +14 -3
- data/ext/farce/priority_queue.c +46 -1
- data/ext/farce/queue.c +141 -7
- data/ext/farce/vector.c +3 -3
- data/ext/farce/weak_map.c +12 -3
- data/ext/java/org/farce/PriorityQueue.java +18 -2
- data/lib/farce/abstract/concurrent_map.rb +122 -0
- data/lib/farce/abstract/counter.rb +35 -1
- data/lib/farce/abstract/executor.rb +30 -7
- data/lib/farce/abstract/flag.rb +12 -0
- data/lib/farce/abstract/pool.rb +158 -47
- data/lib/farce/abstract/priority_queue.rb +1 -1
- data/lib/farce/abstract/publisher.rb +189 -0
- data/lib/farce/abstract/queue.rb +29 -3
- data/lib/farce/abstract/scheduler.rb +41 -14
- data/lib/farce/abstract/timer_queue.rb +3 -3
- data/lib/farce/abstract/vector.rb +3 -1
- data/lib/farce/atom.rb +9 -2
- data/lib/farce/clock.rb +12 -0
- data/lib/farce/concurrent/count_down_latch.rb +37 -0
- data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
- data/lib/farce/concurrent/event.rb +60 -0
- data/lib/farce/concurrent/fiber_local_var.rb +31 -0
- data/lib/farce/concurrent/local_var.rb +65 -0
- data/lib/farce/concurrent/thread_local_var.rb +28 -0
- data/lib/farce/concurrent/tuple.rb +74 -0
- data/lib/farce/concurrent.rb +50 -0
- data/lib/farce/config.rb +112 -2
- data/lib/farce/counter.rb +0 -16
- data/lib/farce/engine/jruby/map.rb +8 -6
- data/lib/farce/engine/jruby/signal.rb +2 -2
- data/lib/farce/engine/jvm/farce.jar +0 -0
- data/lib/farce/engine/jvm/priority_queue.rb +14 -4
- data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
- data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
- data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
- data/lib/farce/engine/shared/exchanger.rb +2 -2
- data/lib/farce/engine/shared/queue.rb +43 -5
- data/lib/farce/engine/shared/ractor_selector.rb +1 -1
- data/lib/farce/engine/shared/strict_map.rb +5 -0
- data/lib/farce/engine/shared/unshared_atom.rb +1 -1
- data/lib/farce/engine/shared/unshared_vector.rb +8 -7
- data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
- data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
- data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
- data/lib/farce/engine/truffleruby/map.rb +8 -6
- data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
- data/lib/farce/engine/truffleruby/signal.rb +1 -1
- data/lib/farce/integrations/concurrent.rb +16 -3
- data/lib/farce/internal/blocking_priority_queue.rb +4 -9
- data/lib/farce/internal/flag_waiting.rb +1 -1
- data/lib/farce/internal/lease_initialization.rb +1 -1
- data/lib/farce/internal/lease_pool_state.rb +1 -1
- data/lib/farce/internal/lease_state.rb +2 -2
- data/lib/farce/internal/local_executor_methods.rb +3 -3
- data/lib/farce/internal/map_value_modes.rb +9 -2
- data/lib/farce/internal/pool_settings.rb +9 -0
- data/lib/farce/internal/pool_worker.rb +34 -21
- data/lib/farce/internal/reservation_waiting.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
- data/lib/farce/internal/weak_map_value_modes.rb +5 -0
- data/lib/farce/internal.rb +8 -8
- data/lib/farce/lazy.rb +3 -0
- data/lib/farce/lazy_ref.rb +3 -0
- data/lib/farce/local/lease.rb +2 -9
- data/lib/farce/local/lease_map.rb +2 -9
- data/lib/farce/local/pool.rb +38 -5
- data/lib/farce/local/publisher.rb +30 -0
- data/lib/farce/local/scheduler.rb +18 -5
- data/lib/farce/local/vector.rb +3 -2
- data/lib/farce/memoize.rb +314 -0
- data/lib/farce/pool.rb +5 -0
- data/lib/farce/priority_queue.rb +1 -1
- data/lib/farce/publisher.rb +57 -0
- data/lib/farce/signal.rb +1 -1
- data/lib/farce/strict/publisher.rb +31 -0
- data/lib/farce/strict/vector.rb +3 -2
- data/lib/farce/subscription/definitions.rb +27 -0
- data/lib/farce/subscription.rb +121 -0
- data/lib/farce/transaction/map.rb +35 -1
- data/lib/farce/unshared/pool.rb +1 -1
- data/lib/farce/unshared/publisher.rb +25 -0
- data/lib/farce/unshared/scheduler.rb +3 -3
- data/lib/farce/unshared/vector.rb +3 -2
- data/lib/farce/vector.rb +12 -9
- data/lib/farce/version.rb +1 -1
- data/lib/farce/walker/definitions.rb +13 -2
- data/lib/farce/weak_atom.rb +5 -2
- data/lib/farce.rb +3 -1
- metadata +18 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Concurrent
|
|
7
|
+
# A replacement for `concurrent-ruby`'s `Concurrent::FiberLocalVar`
|
|
8
|
+
# @see https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/FiberLocalVar.html
|
|
9
|
+
#
|
|
10
|
+
# May or may not be Ractor-shareable depending on provided default values.
|
|
11
|
+
#
|
|
12
|
+
# @attribute [rw] value
|
|
13
|
+
# @return [Object] the current value of the fiber-local variable
|
|
14
|
+
#
|
|
15
|
+
# @!method bind(value)
|
|
16
|
+
# @param value [Object] the value to bind to the fiber-local variable
|
|
17
|
+
# @yield the block with the value bound to the fiber-local variable
|
|
18
|
+
# @return [BasicObject] the result of the block
|
|
19
|
+
class FiberLocalVar < LocalVar
|
|
20
|
+
# @overload initialize(default = nil)
|
|
21
|
+
# @param default [Object] the default value for the fiber-local variable
|
|
22
|
+
#
|
|
23
|
+
# @overload initialize
|
|
24
|
+
# @yield the block to compute the default value for the fiber-local variable
|
|
25
|
+
def initialize(...) = super(:fiber, ...)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Alias for `FiberLocalVar` to maintain compatibility with `LockLocalVar`
|
|
29
|
+
LockLocalVar = FiberLocalVar
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Concurrent
|
|
7
|
+
# @!visibility private
|
|
8
|
+
class LocalVar # :nodoc: all
|
|
9
|
+
include Abstract::Value
|
|
10
|
+
include Internal::Freeze::Tracked
|
|
11
|
+
include Internal::MarshalSupport::Reject
|
|
12
|
+
|
|
13
|
+
def initialize(scope, default = nil, &block)
|
|
14
|
+
@scope = scope
|
|
15
|
+
|
|
16
|
+
if block
|
|
17
|
+
@default_block = try_shareable(block)
|
|
18
|
+
@default = nil
|
|
19
|
+
else
|
|
20
|
+
@default_block = nil
|
|
21
|
+
@default = default
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
super()
|
|
25
|
+
__freeze__
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def bind(value)
|
|
29
|
+
check_frozen!
|
|
30
|
+
return unless block_given?
|
|
31
|
+
current_storage = storage
|
|
32
|
+
old_value = self.value
|
|
33
|
+
current_storage[self] = value
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
yield
|
|
37
|
+
ensure
|
|
38
|
+
current_storage[self] = old_value
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def value = storage.store_if_absent(self) { @default_block ? @default_block.call : @default }
|
|
43
|
+
|
|
44
|
+
def value=(new_value)
|
|
45
|
+
check_frozen!
|
|
46
|
+
storage[self] = new_value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def storage = Internal::Storage.scope(@scope)
|
|
52
|
+
|
|
53
|
+
def try_shareable(block)
|
|
54
|
+
return block if Ractor.shareable?(block)
|
|
55
|
+
Ractor.shareable_proc(&block)
|
|
56
|
+
rescue StandardError
|
|
57
|
+
block
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
define_method(:__freeze__, Kernel.instance_method(:freeze))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private_constant :LocalVar
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Concurrent
|
|
7
|
+
# A replacement for `concurrent-ruby`'s `Concurrent::ThreadLocalVar`
|
|
8
|
+
# @see https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadLocalVar.html
|
|
9
|
+
#
|
|
10
|
+
# May or may not be Ractor-shareable depending on provided default values.
|
|
11
|
+
#
|
|
12
|
+
# @attribute [rw] value
|
|
13
|
+
# @return [Object] the current value of the thread-local variable
|
|
14
|
+
#
|
|
15
|
+
# @!method bind(value)
|
|
16
|
+
# @param value [Object] the value to bind to the thread-local variable
|
|
17
|
+
# @yield the block with the value bound to the thread-local variable
|
|
18
|
+
# @return [BasicObject] the result of the block
|
|
19
|
+
class ThreadLocalVar < LocalVar
|
|
20
|
+
# @overload initialize(default = nil)
|
|
21
|
+
# @param default [Object] the default value for the thread-local variable
|
|
22
|
+
#
|
|
23
|
+
# @overload initialize
|
|
24
|
+
# @yield the block to compute the default value for the thread-local variable
|
|
25
|
+
def initialize(...) = super(:thread, ...)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Concurrent
|
|
7
|
+
# A replacement for `Concurrent::Tuple` from `concurrent-ruby`.
|
|
8
|
+
# @see https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Tuple.html
|
|
9
|
+
class Tuple
|
|
10
|
+
include Enumerable
|
|
11
|
+
include Shareable::Delegated
|
|
12
|
+
|
|
13
|
+
# @return [Integer] the size of the tuple
|
|
14
|
+
attr_reader :size
|
|
15
|
+
|
|
16
|
+
# @param size [Integer] the size of the tuple
|
|
17
|
+
def initialize(size)
|
|
18
|
+
@size = Integer(size)
|
|
19
|
+
@tuple = Vector.new(@size)
|
|
20
|
+
super()
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Retrieves the value at the given index, or `nil` if the index is out of bounds.
|
|
24
|
+
# @param index [Integer] the index of the value to retrieve
|
|
25
|
+
# @return [BasicObject, nil] the value at the given index, or `nil` if out of bounds
|
|
26
|
+
def get(index) = index.between?(0, @size - 1) ? @tuple[index] : nil
|
|
27
|
+
alias volatile_get get
|
|
28
|
+
|
|
29
|
+
# Sets the value at the given index, or does nothing if the index is out of bounds.
|
|
30
|
+
# @param index [Integer] the index of the value to set
|
|
31
|
+
# @param value [BasicObject] the value to set
|
|
32
|
+
# @return [BasicObject, nil] the value set, or `nil` if out of bounds
|
|
33
|
+
def set(index, value) = index.between?(0, @size - 1) ? @tuple[index] = value : nil
|
|
34
|
+
alias volatile_set set
|
|
35
|
+
|
|
36
|
+
# Atomically sets the value at the given index to `new_value` if the current value is equal to `old_value`.
|
|
37
|
+
# @param index [Integer] the index of the value to set
|
|
38
|
+
# @param old_value [BasicObject] the expected current value
|
|
39
|
+
# @param new_value [BasicObject] the new value to set
|
|
40
|
+
# @return [Boolean] `true` if the value was set, `false` otherwise
|
|
41
|
+
def compare_and_set(index, old_value, new_value)
|
|
42
|
+
return false unless index.between?(0, @size - 1)
|
|
43
|
+
@tuple.compare_and_set(index, old_value, new_value)
|
|
44
|
+
end
|
|
45
|
+
alias cas compare_and_set
|
|
46
|
+
|
|
47
|
+
# Iterates over each element in the tuple.
|
|
48
|
+
# @yieldparam value [BasicObject] the value of each element
|
|
49
|
+
# @return [Enumerator, self] if no block is given, self otherwise
|
|
50
|
+
def each(&)
|
|
51
|
+
return enum_for(:each) unless block_given?
|
|
52
|
+
@tuple.each(&)
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @api private
|
|
57
|
+
def marshal_dump
|
|
58
|
+
[1, @tuple.to_a.map { Internal::MarshalSupport.value(it) }, frozen?]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @api private
|
|
62
|
+
def marshal_load(data)
|
|
63
|
+
entries, frozen = Internal::MarshalSupport.payload(data, 2)
|
|
64
|
+
initialize(entries.length)
|
|
65
|
+
entries.each_with_index do |entry, index|
|
|
66
|
+
@tuple[index] = Internal::MarshalSupport.restore_value(entry)
|
|
67
|
+
end
|
|
68
|
+
Internal::MarshalSupport.freeze(self, frozen)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private def freeze_backend = @tuple
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# Module to replace some functionality of `concurrent-ruby` with.
|
|
7
|
+
#
|
|
8
|
+
# Nested in a separate module so it can easily be moved into a dedicated gem
|
|
9
|
+
# (this would only happen on a major release).
|
|
10
|
+
#
|
|
11
|
+
# Not all of them are full drop-in replacements, especially when it comes to matching superclasses.
|
|
12
|
+
#
|
|
13
|
+
# If it matches the subset of functionality you need you could replace this:
|
|
14
|
+
#
|
|
15
|
+
# ```ruby
|
|
16
|
+
# require "concurrent"
|
|
17
|
+
# ```
|
|
18
|
+
#
|
|
19
|
+
# With this:
|
|
20
|
+
#
|
|
21
|
+
# ```ruby
|
|
22
|
+
# require "farce"
|
|
23
|
+
# Concurrent = Farce::Concurrent
|
|
24
|
+
# ```
|
|
25
|
+
#
|
|
26
|
+
# If developing a gem or just part of a larger app, you can and should do this under a namespace rather than globally.
|
|
27
|
+
#
|
|
28
|
+
# @see https://ruby-concurrency.github.io/concurrent-ruby/
|
|
29
|
+
module Concurrent
|
|
30
|
+
include Internal::Autoloads
|
|
31
|
+
|
|
32
|
+
Array = Vector
|
|
33
|
+
Atom = Farce::Atom
|
|
34
|
+
AtomicBoolean = Flag
|
|
35
|
+
AtomicFixnum = Counter
|
|
36
|
+
Exchanger = Farce::Exchanger
|
|
37
|
+
Hash = Farce::Map
|
|
38
|
+
Lock = Farce::Lock
|
|
39
|
+
Map = Farce::Map
|
|
40
|
+
ReadWriteLock = Farce::ReadWriteLock
|
|
41
|
+
|
|
42
|
+
autoload :LockLocalVar, "farce/concurrent/fiber_local_var"
|
|
43
|
+
private_constant :LocalVar
|
|
44
|
+
|
|
45
|
+
Farce.on_main do
|
|
46
|
+
Subscription.define(Event) { |_| subscriber.set }
|
|
47
|
+
Subscription.define(CountDownLatch) { |_| subscriber.count_down }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/farce/config.rb
CHANGED
|
@@ -36,6 +36,40 @@ module Farce
|
|
|
36
36
|
# Can also be set via `FARCE_ADDITIONAL_THREAD_POOL_SIZE`. Defaults to 2.
|
|
37
37
|
attr_reader :additional_thread_pool_size
|
|
38
38
|
|
|
39
|
+
# Minimum workers retained by {Farce.in_parallel} on CRuby. Defaults to 0.
|
|
40
|
+
# Configure before the first scheduler is created. Ignored on JRuby and TruffleRuby.
|
|
41
|
+
# @return [Integer]
|
|
42
|
+
attr_reader :parallel_pool_min_size
|
|
43
|
+
|
|
44
|
+
# Maximum workers used by {Farce.in_parallel} on CRuby.
|
|
45
|
+
# Defaults to nil, which uses {System.cpu_count} when the pool is created.
|
|
46
|
+
# @return [Integer, nil]
|
|
47
|
+
attr_reader :parallel_pool_max_size
|
|
48
|
+
|
|
49
|
+
# Soft task limit for each worker in {Farce.in_parallel}'s CRuby pool.
|
|
50
|
+
# Defaults to 64. Set to nil to disable the limit.
|
|
51
|
+
# @return [Integer, nil]
|
|
52
|
+
attr_reader :parallel_pool_max_inflight
|
|
53
|
+
|
|
54
|
+
# Queue wait in seconds before {Farce.in_parallel}'s CRuby pool adds a worker.
|
|
55
|
+
# Defaults to 0.005.
|
|
56
|
+
# @return [Float]
|
|
57
|
+
attr_reader :parallel_pool_grow_after
|
|
58
|
+
|
|
59
|
+
# Idle time in seconds before an extra worker in {Farce.in_parallel}'s CRuby pool retires.
|
|
60
|
+
# Defaults to 30. Set to nil to disable idle retirement.
|
|
61
|
+
# @return [Float, nil]
|
|
62
|
+
attr_reader :parallel_pool_shrink_after
|
|
63
|
+
|
|
64
|
+
# Pending-task capacity of {Farce.in_parallel}'s CRuby pool.
|
|
65
|
+
# Defaults to 1024. Set to nil for an unbounded queue.
|
|
66
|
+
# @return [Integer, nil]
|
|
67
|
+
attr_reader :parallel_pool_capacity
|
|
68
|
+
|
|
69
|
+
PARALLEL_POOL_DEFAULTS = { max_size: nil, min_size: 0, max_inflight: 64,
|
|
70
|
+
grow_after: 0.005, shrink_after: 30, capacity: 1024 }.freeze
|
|
71
|
+
private_constant :PARALLEL_POOL_DEFAULTS
|
|
72
|
+
|
|
39
73
|
# @yield [config] block to modify the configuration
|
|
40
74
|
# @yieldparam config [Config] the configuration object to configure
|
|
41
75
|
def initialize
|
|
@@ -44,21 +78,29 @@ module Farce
|
|
|
44
78
|
self.fiber_scheduler = ENV["FARCE_FIBER_SCHEDULER"] unless instance_variable_defined?(:@fiber_scheduler)
|
|
45
79
|
self.main_thread_pool_size ||= ENV.fetch("FARCE_MAIN_THREAD_POOL_SIZE", 4)
|
|
46
80
|
self.additional_thread_pool_size ||= ENV.fetch("FARCE_ADDITIONAL_THREAD_POOL_SIZE", 2)
|
|
81
|
+
initialize_parallel_pool
|
|
47
82
|
end
|
|
48
83
|
|
|
49
84
|
# @api private
|
|
50
85
|
def marshal_dump
|
|
51
|
-
[1, autoload_integrations, fiber_scheduler, main_thread_pool_size, additional_thread_pool_size,
|
|
86
|
+
[1, autoload_integrations, fiber_scheduler, main_thread_pool_size, additional_thread_pool_size,
|
|
87
|
+
parallel_pool_options, frozen?]
|
|
52
88
|
end
|
|
53
89
|
|
|
54
90
|
# @api private
|
|
55
91
|
def marshal_load(data)
|
|
56
|
-
|
|
92
|
+
if data.is_a?(Array) && data.length == 6
|
|
93
|
+
autoload, scheduler, main_size, additional_size, frozen = Internal::MarshalSupport.payload(data, 5)
|
|
94
|
+
options = {}
|
|
95
|
+
else
|
|
96
|
+
autoload, scheduler, main_size, additional_size, options, frozen = Internal::MarshalSupport.payload(data, 6)
|
|
97
|
+
end
|
|
57
98
|
|
|
58
99
|
self.autoload_integrations = autoload
|
|
59
100
|
self.fiber_scheduler = scheduler
|
|
60
101
|
self.main_thread_pool_size = main_size
|
|
61
102
|
self.additional_thread_pool_size = additional_size
|
|
103
|
+
initialize_parallel_pool(options)
|
|
62
104
|
freeze if frozen
|
|
63
105
|
end
|
|
64
106
|
|
|
@@ -149,6 +191,45 @@ module Farce
|
|
|
149
191
|
@additional_thread_pool_size = normalize_thread_pool_size(value)
|
|
150
192
|
end
|
|
151
193
|
|
|
194
|
+
# Sets a non-negative minimum worker count for the shared parallel pool.
|
|
195
|
+
# It must not exceed an explicitly configured maximum.
|
|
196
|
+
def parallel_pool_min_size=(value)
|
|
197
|
+
value = normalize_parallel_pool_size(value, :min_size, minimum: 0)
|
|
198
|
+
if parallel_pool_max_size && value > parallel_pool_max_size
|
|
199
|
+
raise ArgumentError, "parallel pool min_size must not exceed max_size"
|
|
200
|
+
end
|
|
201
|
+
@parallel_pool_min_size = value
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Sets a positive maximum worker count, or nil to use the CPU count.
|
|
205
|
+
def parallel_pool_max_size=(value)
|
|
206
|
+
value = normalize_parallel_pool_size(value, :max_size, nil_allowed: true)
|
|
207
|
+
if value && parallel_pool_min_size && value < parallel_pool_min_size
|
|
208
|
+
raise ArgumentError, "parallel pool min_size must not exceed max_size"
|
|
209
|
+
end
|
|
210
|
+
@parallel_pool_max_size = value
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Sets a positive soft task limit per worker, or nil to disable it.
|
|
214
|
+
def parallel_pool_max_inflight=(value)
|
|
215
|
+
@parallel_pool_max_inflight = normalize_parallel_pool_size(value, :max_inflight, nil_allowed: true)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Sets a finite, non-negative queue wait before growing the pool.
|
|
219
|
+
def parallel_pool_grow_after=(value)
|
|
220
|
+
@parallel_pool_grow_after = normalize_parallel_pool_duration(value, :grow_after)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Sets a finite, non-negative idle timeout, or nil to disable retirement.
|
|
224
|
+
def parallel_pool_shrink_after=(value)
|
|
225
|
+
@parallel_pool_shrink_after = normalize_parallel_pool_duration(value, :shrink_after, nil_allowed: true)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Sets a positive pending-task capacity, or nil for an unbounded queue.
|
|
229
|
+
def parallel_pool_capacity=(value)
|
|
230
|
+
@parallel_pool_capacity = normalize_parallel_pool_size(value, :capacity, nil_allowed: true)
|
|
231
|
+
end
|
|
232
|
+
|
|
152
233
|
# Called internally once the configuration is applied.
|
|
153
234
|
def freeze
|
|
154
235
|
return self if frozen?
|
|
@@ -159,6 +240,35 @@ module Farce
|
|
|
159
240
|
|
|
160
241
|
private
|
|
161
242
|
|
|
243
|
+
def initialize_parallel_pool(options = {})
|
|
244
|
+
PARALLEL_POOL_DEFAULTS.each do |name, default|
|
|
245
|
+
next if instance_variable_defined?(:"@parallel_pool_#{name}")
|
|
246
|
+
public_send(:"parallel_pool_#{name}=", options.fetch(name, default))
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def parallel_pool_options
|
|
251
|
+
PARALLEL_POOL_DEFAULTS.to_h { |name, _| [name, public_send(:"parallel_pool_#{name}")] }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def normalize_parallel_pool_size(value, name, minimum: 1, nil_allowed: false)
|
|
255
|
+
return if value.nil? && nil_allowed
|
|
256
|
+
value = Integer(value, 10) if value.is_a?(String)
|
|
257
|
+
unless value.is_a?(Integer) && value >= minimum
|
|
258
|
+
raise ArgumentError, "parallel pool #{name} must be an integer >= #{minimum}"
|
|
259
|
+
end
|
|
260
|
+
value
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def normalize_parallel_pool_duration(value, name, nil_allowed: false)
|
|
264
|
+
return if value.nil? && nil_allowed
|
|
265
|
+
value = Float(value)
|
|
266
|
+
raise ArgumentError unless value.finite? && value >= 0
|
|
267
|
+
value
|
|
268
|
+
rescue ArgumentError, TypeError
|
|
269
|
+
raise ArgumentError, "parallel pool #{name} must be finite and non-negative"
|
|
270
|
+
end
|
|
271
|
+
|
|
162
272
|
def normalize_thread_pool_size(value)
|
|
163
273
|
size = value.is_a?(String) ? Integer(value, 10) : value
|
|
164
274
|
raise ArgumentError, "thread pool size must be a positive integer" unless size.is_a?(Integer) && size.positive?
|
data/lib/farce/counter.rb
CHANGED
|
@@ -13,22 +13,6 @@ module Farce
|
|
|
13
13
|
#
|
|
14
14
|
# On CRuby, subclasses must not add Ruby instance variables.
|
|
15
15
|
#
|
|
16
|
-
# @!method initial
|
|
17
|
-
# @return [Integer] The initial value retained by reset and copies.
|
|
18
|
-
#
|
|
19
|
-
# @!method value
|
|
20
|
-
# @return [Integer] The current value of the counter.
|
|
21
|
-
#
|
|
22
|
-
# @!method increment(by = 1)
|
|
23
|
-
# Increment the counter by the given amount (default is 1).
|
|
24
|
-
# @param [Numeric, String, #to_int] by The amount, converted to an Integer.
|
|
25
|
-
# @return [self] Returns self for chaining.
|
|
26
|
-
#
|
|
27
|
-
# @!method decrement(by = 1)
|
|
28
|
-
# Decrement the counter by the given amount (default is 1).
|
|
29
|
-
# @param [Numeric, String, #to_int] by The amount, converted to an Integer.
|
|
30
|
-
# @return [self] Returns self for chaining.
|
|
31
|
-
#
|
|
32
16
|
# @example Cross-Ractor counting
|
|
33
17
|
# counter = Farce::Counter.new
|
|
34
18
|
# counter.value # => 0
|
|
@@ -28,7 +28,8 @@ module Farce
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def eql?(other)
|
|
31
|
-
result = other.is_a?(Key) &&
|
|
31
|
+
result = other.is_a?(Key) &&
|
|
32
|
+
(BASIC_OBJECT_EQUAL_METHOD.bind_call(value, other.value) || value.eql?(other.value))
|
|
32
33
|
owner = mutation_owner || (other.mutation_owner if other.is_a?(Key))
|
|
33
34
|
Internal::Freeze.check(owner) if owner
|
|
34
35
|
result
|
|
@@ -218,11 +219,11 @@ module Farce
|
|
|
218
219
|
# Yield presence and value under the key reservation.
|
|
219
220
|
# MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
|
|
220
221
|
# Return whether a change committed.
|
|
221
|
-
def modify(key
|
|
222
|
+
def modify(key, timeout: nil)
|
|
222
223
|
raise LocalJumpError, "no block given" unless block_given?
|
|
223
224
|
check_frozen!
|
|
224
225
|
wrapped = wrap_key(key)
|
|
225
|
-
|
|
226
|
+
completed, result = with_mutating_key_operation(wrapped, timeout_deadline(timeout)) do |reservation|
|
|
226
227
|
present, current = native_operation { [concurrent_key?(wrapped), concurrent_get(wrapped)] }
|
|
227
228
|
value = yield(present, current)
|
|
228
229
|
next false if MAP_KEEP.equal?(value)
|
|
@@ -235,7 +236,7 @@ module Farce
|
|
|
235
236
|
changed!
|
|
236
237
|
end
|
|
237
238
|
end
|
|
238
|
-
!!result
|
|
239
|
+
completed && !!result
|
|
239
240
|
end
|
|
240
241
|
|
|
241
242
|
def upsert(key, initial_value, timeout: nil)
|
|
@@ -429,7 +430,8 @@ module Farce
|
|
|
429
430
|
native_operation { concurrent_get(wrapped) }
|
|
430
431
|
end
|
|
431
432
|
return fallback&.call unless completed
|
|
432
|
-
ready = non_nil ? !current.nil? :
|
|
433
|
+
ready = non_nil ? !current.nil? :
|
|
434
|
+
Internal.value_changed?(current, expected, compare_values_by_identity?)
|
|
433
435
|
return current if ready
|
|
434
436
|
return fallback&.call unless wait_for_signal(@change_signal, generation, deadline)
|
|
435
437
|
end
|
|
@@ -438,7 +440,7 @@ module Farce
|
|
|
438
440
|
def wait_for_signal(signal, generation, deadline)
|
|
439
441
|
return signal.wait(generation) unless deadline
|
|
440
442
|
|
|
441
|
-
remaining =
|
|
443
|
+
remaining = Clock.remaining(deadline)
|
|
442
444
|
return false unless remaining.positive?
|
|
443
445
|
!signal.wait(generation, timeout: remaining).nil?
|
|
444
446
|
end
|
|
@@ -90,7 +90,7 @@ module Farce
|
|
|
90
90
|
current = generation
|
|
91
91
|
return current unless current == observed
|
|
92
92
|
|
|
93
|
-
remaining =
|
|
93
|
+
remaining = Clock.remaining(deadline)
|
|
94
94
|
return yield if remaining && !remaining.positive?
|
|
95
95
|
|
|
96
96
|
# Java Phaser waits do not deliver Ruby cancellation until they return.
|
|
@@ -131,7 +131,7 @@ module Farce
|
|
|
131
131
|
current = generation
|
|
132
132
|
return current unless current == observed
|
|
133
133
|
|
|
134
|
-
remaining =
|
|
134
|
+
remaining = Clock.remaining(deadline)
|
|
135
135
|
return yield if remaining && !remaining.positive?
|
|
136
136
|
|
|
137
137
|
scheduler.io_wait(reader, READABLE, remaining)
|
|
Binary file
|
|
@@ -61,11 +61,11 @@ module Farce
|
|
|
61
61
|
native_signal = signal if RUBY_ENGINE == "jruby" &&
|
|
62
62
|
JVMContainers.identical?(JVMContainers::OBJECT_CLASS_METHOD.bind_call(signal), Signal)
|
|
63
63
|
core = JVMExtension::PriorityQueue.new(COMPARATOR, SNAPSHOT, IDENTITY, EQUAL,
|
|
64
|
-
IDENTICAL, MATCH, committer, SCHEDULED, java_capacity, native_signal,
|
|
64
|
+
IDENTICAL, MATCH, committer, SCHEDULED, java_capacity, Box.new(parsed_capacity), native_signal,
|
|
65
|
+
track_age ? true : false)
|
|
65
66
|
INITIALIZATION_LOCK.synchronize do
|
|
66
67
|
Thread.handle_interrupt(INTERRUPT_MASK) do
|
|
67
68
|
check_initialization
|
|
68
|
-
@capacity = parsed_capacity
|
|
69
69
|
@core = core
|
|
70
70
|
JVMContainers.freeze_object(self)
|
|
71
71
|
end
|
|
@@ -112,8 +112,18 @@ module Farce
|
|
|
112
112
|
def empty? = size.zero?
|
|
113
113
|
|
|
114
114
|
def capacity
|
|
115
|
-
storage
|
|
116
|
-
|
|
115
|
+
storage.getCapacity.value
|
|
116
|
+
rescue JVMExtension::QueueFailure => e
|
|
117
|
+
raise_failure(e)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def capacity=(value)
|
|
121
|
+
value = normalize_capacity(value)
|
|
122
|
+
java_capacity = value ? [value, JAVA_CAPACITY_MAX].min : 0
|
|
123
|
+
storage.setCapacity(java_capacity, Box.new(value))
|
|
124
|
+
value
|
|
125
|
+
rescue JVMExtension::QueueFailure => e
|
|
126
|
+
raise_failure(e)
|
|
117
127
|
end
|
|
118
128
|
|
|
119
129
|
def closed?
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
module Farce
|
|
6
6
|
module Internal # :nodoc: all
|
|
7
|
-
|
|
7
|
+
config = FROZEN_CONFIG
|
|
8
|
+
ParallelScheduler = Pool.new(
|
|
9
|
+
min_size: config.parallel_pool_min_size,
|
|
10
|
+
max_size: config.parallel_pool_max_size || System.cpu_count,
|
|
11
|
+
max_inflight: config.parallel_pool_max_inflight,
|
|
12
|
+
grow_after: config.parallel_pool_grow_after,
|
|
13
|
+
shrink_after: config.parallel_pool_shrink_after,
|
|
14
|
+
capacity: config.parallel_pool_capacity,
|
|
15
|
+
)
|
|
8
16
|
end
|
|
9
17
|
end
|
|
@@ -125,10 +125,7 @@ module Farce
|
|
|
125
125
|
command(:add, request)
|
|
126
126
|
request.ready&.pop
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
remaining = deadline - Clock.now
|
|
130
|
-
remaining = 0 if remaining.negative?
|
|
131
|
-
end
|
|
128
|
+
remaining = Clock.remaining(deadline)
|
|
132
129
|
|
|
133
130
|
delivery = request.reply.pop(timeout: remaining)
|
|
134
131
|
if delivery
|
|
@@ -134,17 +134,19 @@ module Farce
|
|
|
134
134
|
# Yield presence and value under the entry claim.
|
|
135
135
|
# MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
|
|
136
136
|
# Return whether a change committed.
|
|
137
|
-
def modify(key)
|
|
137
|
+
def modify(key, timeout: nil)
|
|
138
138
|
raise LocalJumpError, "no block given" unless block_given?
|
|
139
139
|
check_frozen!
|
|
140
140
|
key = canonical_key(key)
|
|
141
|
+
deadline = timeout_deadline(timeout)
|
|
141
142
|
ticket = Object.new.freeze
|
|
142
143
|
sent = finished = false
|
|
143
144
|
begin
|
|
144
|
-
response = await_response(deadline:
|
|
145
|
+
response = await_response(deadline:) do
|
|
145
146
|
sent = true
|
|
146
147
|
request(:claim, key, ticket, true)
|
|
147
148
|
end
|
|
149
|
+
return false if response.equal?(TIMED_OUT)
|
|
148
150
|
present, current, signal = response[1], response[2], response[3]
|
|
149
151
|
owners = claim_owners
|
|
150
152
|
owners[signal] = Fiber.current
|
|
@@ -306,7 +308,7 @@ module Farce
|
|
|
306
308
|
|
|
307
309
|
signal, generation = response[1], response[2]
|
|
308
310
|
reject_recursive_wait!(signal)
|
|
309
|
-
timeout =
|
|
311
|
+
timeout = Clock.remaining(deadline)
|
|
310
312
|
if timeout&.zero?
|
|
311
313
|
return TIMED_OUT if checked_after_timeout
|
|
312
314
|
checked_after_timeout = true
|
|
@@ -332,11 +334,13 @@ module Farce
|
|
|
332
334
|
key = response[5] if response.first == :ok
|
|
333
335
|
present = response.first == :ok && response[1]
|
|
334
336
|
current = present ? response[2] : nil
|
|
335
|
-
|
|
337
|
+
|
|
338
|
+
return current if non_nil ? !current.nil? :
|
|
339
|
+
Internal.value_changed?(current, expected, compare_values_by_identity?)
|
|
336
340
|
|
|
337
341
|
signal = response.first == :ok ? response[3] : response[1]
|
|
338
342
|
generation = response.first == :ok ? response[4] : response[2]
|
|
339
|
-
changed = signal.wait_until_changed(generation, timeout:
|
|
343
|
+
changed = signal.wait_until_changed(generation, timeout: Clock.remaining(deadline)) { TIMED_OUT }
|
|
340
344
|
if changed.equal?(TIMED_OUT)
|
|
341
345
|
return fallback&.call if checked_after_timeout
|
|
342
346
|
checked_after_timeout = true
|
|
@@ -381,13 +385,6 @@ module Farce
|
|
|
381
385
|
Clock.now + timeout
|
|
382
386
|
end
|
|
383
387
|
|
|
384
|
-
def remaining_timeout(deadline)
|
|
385
|
-
return unless deadline
|
|
386
|
-
|
|
387
|
-
remaining = deadline - Clock.now
|
|
388
|
-
remaining.positive? ? remaining : 0
|
|
389
|
-
end
|
|
390
|
-
|
|
391
388
|
def validate_boolean(value, name)
|
|
392
389
|
return if value.equal?(true) || value.equal?(false)
|
|
393
390
|
raise ArgumentError, "#{name} must be true or false"
|
|
@@ -77,7 +77,7 @@ module Farce
|
|
|
77
77
|
def wait_with_condition(waiter, deadline)
|
|
78
78
|
@mutex.synchronize do
|
|
79
79
|
until waiter.matched
|
|
80
|
-
remaining =
|
|
80
|
+
remaining = Clock.remaining(deadline)
|
|
81
81
|
return cancel(waiter) if remaining && !remaining.positive?
|
|
82
82
|
waiter.condition.wait(@mutex, remaining)
|
|
83
83
|
end
|
|
@@ -91,7 +91,7 @@ module Farce
|
|
|
91
91
|
if waiter.matched
|
|
92
92
|
[nil, waiter.received]
|
|
93
93
|
else
|
|
94
|
-
remaining =
|
|
94
|
+
remaining = Clock.remaining(deadline)
|
|
95
95
|
remaining && !remaining.positive? ? [nil, cancel(waiter)] : [WAITING, remaining]
|
|
96
96
|
end
|
|
97
97
|
end
|