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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -2
  3. data/README.md +3 -1
  4. data/docs/benchmarks.md +37 -21
  5. data/docs/compatibility.md +13 -0
  6. data/docs/data-structures.md +37 -0
  7. data/docs/integrations.md +1 -0
  8. data/docs/introduction.md +19 -0
  9. data/docs/modes.md +2 -0
  10. data/docs/scheduling.md +35 -3
  11. data/docs/scopes.md +3 -1
  12. data/docs/shims.md +53 -0
  13. data/docs/utilities.md +31 -0
  14. data/docs/variants.md +9 -0
  15. data/ext/farce/farce.c +9 -0
  16. data/ext/farce/map.c +14 -3
  17. data/ext/farce/priority_queue.c +46 -1
  18. data/ext/farce/queue.c +141 -7
  19. data/ext/farce/vector.c +3 -3
  20. data/ext/farce/weak_map.c +12 -3
  21. data/ext/java/org/farce/PriorityQueue.java +18 -2
  22. data/lib/farce/abstract/concurrent_map.rb +122 -0
  23. data/lib/farce/abstract/counter.rb +35 -1
  24. data/lib/farce/abstract/executor.rb +30 -7
  25. data/lib/farce/abstract/flag.rb +12 -0
  26. data/lib/farce/abstract/pool.rb +158 -47
  27. data/lib/farce/abstract/priority_queue.rb +1 -1
  28. data/lib/farce/abstract/publisher.rb +189 -0
  29. data/lib/farce/abstract/queue.rb +29 -3
  30. data/lib/farce/abstract/scheduler.rb +41 -14
  31. data/lib/farce/abstract/timer_queue.rb +3 -3
  32. data/lib/farce/abstract/vector.rb +3 -1
  33. data/lib/farce/atom.rb +9 -2
  34. data/lib/farce/clock.rb +12 -0
  35. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  36. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  37. data/lib/farce/concurrent/event.rb +60 -0
  38. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  39. data/lib/farce/concurrent/local_var.rb +65 -0
  40. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  41. data/lib/farce/concurrent/tuple.rb +74 -0
  42. data/lib/farce/concurrent.rb +50 -0
  43. data/lib/farce/config.rb +112 -2
  44. data/lib/farce/counter.rb +0 -16
  45. data/lib/farce/engine/jruby/map.rb +8 -6
  46. data/lib/farce/engine/jruby/signal.rb +2 -2
  47. data/lib/farce/engine/jvm/farce.jar +0 -0
  48. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  49. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  50. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  51. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  52. data/lib/farce/engine/shared/exchanger.rb +2 -2
  53. data/lib/farce/engine/shared/queue.rb +43 -5
  54. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  55. data/lib/farce/engine/shared/strict_map.rb +5 -0
  56. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  57. data/lib/farce/engine/shared/unshared_vector.rb +8 -7
  58. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  59. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  60. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  61. data/lib/farce/engine/truffleruby/map.rb +8 -6
  62. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  63. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  64. data/lib/farce/integrations/concurrent.rb +16 -3
  65. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  66. data/lib/farce/internal/flag_waiting.rb +1 -1
  67. data/lib/farce/internal/lease_initialization.rb +1 -1
  68. data/lib/farce/internal/lease_pool_state.rb +1 -1
  69. data/lib/farce/internal/lease_state.rb +2 -2
  70. data/lib/farce/internal/local_executor_methods.rb +3 -3
  71. data/lib/farce/internal/map_value_modes.rb +9 -2
  72. data/lib/farce/internal/pool_settings.rb +9 -0
  73. data/lib/farce/internal/pool_worker.rb +34 -21
  74. data/lib/farce/internal/reservation_waiting.rb +1 -1
  75. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  76. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  77. data/lib/farce/internal.rb +8 -8
  78. data/lib/farce/lazy.rb +3 -0
  79. data/lib/farce/lazy_ref.rb +3 -0
  80. data/lib/farce/local/lease.rb +2 -9
  81. data/lib/farce/local/lease_map.rb +2 -9
  82. data/lib/farce/local/pool.rb +38 -5
  83. data/lib/farce/local/publisher.rb +30 -0
  84. data/lib/farce/local/scheduler.rb +18 -5
  85. data/lib/farce/local/vector.rb +3 -2
  86. data/lib/farce/memoize.rb +314 -0
  87. data/lib/farce/pool.rb +5 -0
  88. data/lib/farce/priority_queue.rb +1 -1
  89. data/lib/farce/publisher.rb +57 -0
  90. data/lib/farce/signal.rb +1 -1
  91. data/lib/farce/strict/publisher.rb +31 -0
  92. data/lib/farce/strict/vector.rb +3 -2
  93. data/lib/farce/subscription/definitions.rb +27 -0
  94. data/lib/farce/subscription.rb +121 -0
  95. data/lib/farce/transaction/map.rb +35 -1
  96. data/lib/farce/unshared/pool.rb +1 -1
  97. data/lib/farce/unshared/publisher.rb +25 -0
  98. data/lib/farce/unshared/scheduler.rb +3 -3
  99. data/lib/farce/unshared/vector.rb +3 -2
  100. data/lib/farce/vector.rb +12 -9
  101. data/lib/farce/version.rb +1 -1
  102. data/lib/farce/walker/definitions.rb +13 -2
  103. data/lib/farce/weak_atom.rb +5 -2
  104. data/lib/farce.rb +3 -1
  105. 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, frozen?]
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
- autoload, scheduler, main_size, additional_size, frozen = Internal::MarshalSupport.payload(data, 5)
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) && value.eql?(other.value)
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) # rubocop:disable Naming/PredicateMethod
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
- _, result = with_mutating_key_operation(wrapped, nil) do |reservation|
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? : !values_equal?(current, expected)
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 = deadline - Clock.now
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 = deadline - Clock.now if deadline
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 = deadline - Clock.now if deadline
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, track_age ? true : false)
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
- @capacity
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
- ParallelScheduler = Pool.new(max_size: System.cpu_count)
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
- if deadline
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: nil) do
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 = remaining_timeout(deadline)
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
- return current if non_nil ? !current.nil? : !values_equal?(current, expected)
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: remaining_timeout(deadline)) { TIMED_OUT }
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 = deadline - Clock.now if deadline
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 = deadline - Clock.now if deadline
94
+ remaining = Clock.remaining(deadline)
95
95
  remaining && !remaining.positive? ? [nil, cancel(waiter)] : [WAITING, remaining]
96
96
  end
97
97
  end