farce 0.4.1-java → 0.6.0-java

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -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 +32 -0
  14. data/docs/variants.md +9 -0
  15. data/lib/farce/abstract/atom.rb +5 -1
  16. data/lib/farce/abstract/concurrent_map.rb +122 -0
  17. data/lib/farce/abstract/counter.rb +36 -1
  18. data/lib/farce/abstract/executor.rb +30 -7
  19. data/lib/farce/abstract/flag.rb +12 -0
  20. data/lib/farce/abstract/molecule.rb +14 -14
  21. data/lib/farce/abstract/pool.rb +158 -47
  22. data/lib/farce/abstract/priority_queue.rb +1 -1
  23. data/lib/farce/abstract/publisher.rb +189 -0
  24. data/lib/farce/abstract/queue.rb +29 -3
  25. data/lib/farce/abstract/scheduler.rb +41 -14
  26. data/lib/farce/abstract/timer_queue.rb +3 -3
  27. data/lib/farce/abstract/vector.rb +3 -1
  28. data/lib/farce/atom.rb +15 -2
  29. data/lib/farce/clock.rb +20 -7
  30. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  31. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  32. data/lib/farce/concurrent/event.rb +60 -0
  33. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  34. data/lib/farce/concurrent/local_var.rb +65 -0
  35. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  36. data/lib/farce/concurrent/tuple.rb +74 -0
  37. data/lib/farce/concurrent.rb +50 -0
  38. data/lib/farce/config.rb +112 -2
  39. data/lib/farce/counter.rb +5 -15
  40. data/lib/farce/deduper.rb +12 -4
  41. data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
  42. data/lib/farce/engine/jruby/map.rb +8 -6
  43. data/lib/farce/engine/jruby/signal.rb +2 -2
  44. data/lib/farce/engine/jvm/counter.rb +0 -2
  45. data/lib/farce/engine/jvm/farce.jar +0 -0
  46. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  47. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  48. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  49. data/lib/farce/engine/ruby/shared/weak_cache_mirror.rb +56 -0
  50. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  51. data/lib/farce/engine/ruby.rb +5 -1
  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/trie/ignore_case.rb +136 -0
  57. data/lib/farce/engine/shared/trie.rb +14 -1
  58. data/lib/farce/engine/shared/trie_builder.rb +19 -8
  59. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  60. data/lib/farce/engine/shared/unshared_vector.rb +11 -8
  61. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  62. data/lib/farce/engine/shared/vector.rb +2 -0
  63. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  64. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  65. data/lib/farce/engine/shared.rb +2 -0
  66. data/lib/farce/engine/truffleruby/map.rb +8 -6
  67. data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
  68. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  69. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  70. data/lib/farce/engine/truffleruby.rb +9 -0
  71. data/lib/farce/extension.rb +5 -3
  72. data/lib/farce/integrations/concurrent.rb +16 -3
  73. data/lib/farce/integrations/sorted_set.rb +1 -0
  74. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  75. data/lib/farce/internal/fake_ractor.rb +1 -1
  76. data/lib/farce/internal/flag_waiting.rb +1 -1
  77. data/lib/farce/internal/freeze.rb +1 -5
  78. data/lib/farce/internal/lease_initialization.rb +1 -1
  79. data/lib/farce/internal/lease_pool_state.rb +1 -1
  80. data/lib/farce/internal/lease_state.rb +2 -2
  81. data/lib/farce/internal/local_executor_methods.rb +3 -3
  82. data/lib/farce/internal/map_value_modes.rb +17 -9
  83. data/lib/farce/internal/pool_settings.rb +9 -0
  84. data/lib/farce/internal/pool_worker.rb +34 -21
  85. data/lib/farce/internal/reservation_waiting.rb +1 -1
  86. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  87. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  88. data/lib/farce/internal.rb +19 -8
  89. data/lib/farce/lazy.rb +3 -0
  90. data/lib/farce/lazy_ref.rb +3 -0
  91. data/lib/farce/local/counter.rb +4 -10
  92. data/lib/farce/local/lease.rb +2 -9
  93. data/lib/farce/local/lease_map.rb +2 -9
  94. data/lib/farce/local/pool.rb +38 -5
  95. data/lib/farce/local/publisher.rb +30 -0
  96. data/lib/farce/local/scheduler.rb +18 -5
  97. data/lib/farce/local/vector.rb +3 -2
  98. data/lib/farce/map.rb +8 -1
  99. data/lib/farce/memoize.rb +319 -0
  100. data/lib/farce/pool.rb +5 -0
  101. data/lib/farce/priority_queue.rb +1 -1
  102. data/lib/farce/publisher.rb +57 -0
  103. data/lib/farce/shareable.rb +3 -0
  104. data/lib/farce/signal.rb +1 -1
  105. data/lib/farce/strict/publisher.rb +31 -0
  106. data/lib/farce/strict/vector.rb +3 -2
  107. data/lib/farce/strict/weak_cache.rb +199 -0
  108. data/lib/farce/subscription/definitions.rb +27 -0
  109. data/lib/farce/subscription.rb +121 -0
  110. data/lib/farce/transaction/map.rb +35 -1
  111. data/lib/farce/unshared/pool.rb +1 -1
  112. data/lib/farce/unshared/publisher.rb +25 -0
  113. data/lib/farce/unshared/scheduler.rb +3 -3
  114. data/lib/farce/unshared/vector.rb +3 -2
  115. data/lib/farce/vector.rb +20 -16
  116. data/lib/farce/version.rb +1 -1
  117. data/lib/farce/walker/definitions.rb +31 -2
  118. data/lib/farce/walker/modification.rb +28 -14
  119. data/lib/farce/walker.rb +9 -1
  120. data/lib/farce/weak_atom.rb +5 -2
  121. data/lib/farce/weak_key_map.rb +1 -1
  122. data/lib/farce/weak_map.rb +1 -1
  123. data/lib/farce/weak_value_map.rb +1 -1
  124. data/lib/farce.rb +3 -1
  125. metadata +22 -2
@@ -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,21 +13,11 @@ 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.
16
+ # @example Allocate sequence numbers across threads or Ractors
17
+ # counter = Farce::Counter.new
18
+ # ticket = counter.increment # => 1
19
+ # counter.increment # => 2
20
+ # ticket # => 1
31
21
  #
32
22
  # @example Cross-Ractor counting
33
23
  # counter = Farce::Counter.new
data/lib/farce/deduper.rb CHANGED
@@ -4,9 +4,10 @@
4
4
 
5
5
  module Farce
6
6
  # Reuse equal frozen values throughout object graphs.
7
- # Plain strings use Ruby's string deduplication. Arrays, hashes, and Ruby Sets are frozen
8
- # and cached by default. Other objects are traversed but are not cached unless
9
- # their class matches {#store}. Modules and classes are skipped by default.
7
+ # Plain strings use Ruby's string deduplication. Arrays, hashes, Ruby Sets, regular
8
+ # expressions, Data records, and ranges are frozen and cached by default. Other objects are
9
+ # traversed but are not cached unless their class matches {#store}.
10
+ # Modules and classes are skipped by default.
10
11
  #
11
12
  # Cached objects are held weakly. Keep a reference to a returned object if its
12
13
  # identity matters. Shareable results can be reused across Ractors. Other
@@ -28,7 +29,7 @@ module Farce
28
29
  @shared = Strict::WeakKeyMap.new
29
30
  @local = Local::WeakKeyMap.new
30
31
  @skip = Strict::Vector[Module]
31
- @store = Strict::Vector[Hash, Array, ::Set]
32
+ @store = Strict::Vector[Hash, Array, ::Set, Regexp, Data, Range]
32
33
  super
33
34
  end
34
35
 
@@ -68,6 +69,13 @@ module Farce
68
69
  # Use copy: true to preserve the input. Skipped objects are always returned
69
70
  # unchanged. Strings are deduplicated before skip rules are checked.
70
71
  # Noncopyable coordination objects follow {Walker.modify}'s in-place behavior.
72
+ #
73
+ # @note
74
+ # Deduplication follows Ruby's `hash` and `eql?` semantics. Equal values
75
+ # may differ in observable details. For example, `Time` values representing
76
+ # the same instant can have different time zones. Deduplicating these values
77
+ # or containers holding them may reuse a different timezone representation.
78
+ #
71
79
  # @param object [Object] the root object
72
80
  # @param copy [Boolean, Symbol] copying policy passed to {Walker.modify}
73
81
  # @param skip [Module, Array<Module>, nil] additional exclusions for this call
@@ -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)
@@ -68,7 +68,6 @@ module Farce
68
68
  check_frozen!
69
69
  end
70
70
  @counter.addAndGet(delta).tap { @signal.broadcast }
71
- self
72
71
  end
73
72
 
74
73
  def decrement(delta = 1)
@@ -78,7 +77,6 @@ module Farce
78
77
  check_frozen!
79
78
  end
80
79
  @counter.addAndGet(-delta).tap { @signal.broadcast }
81
- self
82
80
  end
83
81
 
84
82
  alias value get
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
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Internal # :nodoc: all
7
+ class WeakCacheMirror
8
+ include Shareable::Delegated
9
+
10
+ CACHED_NIL = Object.new.freeze
11
+ private_constant :CACHED_NIL
12
+
13
+ def initialize(shared)
14
+ @shared = shared
15
+ super()
16
+ end
17
+
18
+ def store_if_absent(key, &)
19
+ check_frozen!
20
+ return @shared.store_if_absent(key, &) unless Ractor.shareable?(key)
21
+
22
+ local = local_cache
23
+ value = local[key]
24
+ return CACHED_NIL.equal?(value) ? nil : value unless nil.equal?(value)
25
+
26
+ value = @shared.store_if_absent(key, &)
27
+ local[key] = nil.equal?(value) ? CACHED_NIL : value
28
+ value
29
+ end
30
+
31
+ def fetch(key)
32
+ return @shared.fetch(key) { yield(key) } unless Ractor.shareable?(key)
33
+
34
+ local = local_cache
35
+ value = local[key]
36
+ return CACHED_NIL.equal?(value) ? nil : value unless nil.equal?(value)
37
+
38
+ value = @shared.fetch(key) { return yield(key) }
39
+ local[key] = nil.equal?(value) ? CACHED_NIL : value
40
+ value
41
+ end
42
+
43
+ private
44
+
45
+ # CRuby's weak-map operations hold the GVL. Shared misses serialize generation,
46
+ # and a retained value cannot be replaced, so local hits need no reservation.
47
+ # Only shareable keys enter the mirror, since mutable strings can change meaning.
48
+ def local_cache
49
+ caches = Ractor.store_if_absent(:"Farce::Internal::WeakCacheMirror") { ObjectSpace::WeakKeyMap.new }
50
+ caches[self] ||= ObjectSpace::WeakMap.new
51
+ end
52
+
53
+ def freeze_backend = @shared
54
+ end
55
+ end
56
+ end
@@ -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"
@@ -40,10 +40,14 @@ module Farce
40
40
  raise e unless e.path == "#{ENGINE_PATH}/patch"
41
41
  end
42
42
 
43
- unless const_defined?(:NATIVE_WEAK_MAPS, false) && NATIVE_WEAK_MAPS
43
+ if const_defined?(:NATIVE_WEAK_MAPS, false) && NATIVE_WEAK_MAPS
44
+ def native_weak_maps? = true
45
+ else
46
+ def native_weak_maps? = false
44
47
  autoload :WeakMap, "farce/engine/ruby/shared/weak_map"
45
48
  autoload :WeakKeyMap, "farce/engine/ruby/shared/weak_map"
46
49
  autoload :WeakValueMap, "farce/engine/ruby/shared/weak_map"
50
+ autoload :WeakCacheMirror, "farce/engine/ruby/shared/weak_cache_mirror"
47
51
  end
48
52
 
49
53
  StrictAtom = Atom
@@ -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
@@ -9,17 +9,56 @@ module Farce
9
9
  include Freeze::Unfreezable
10
10
 
11
11
  NIL_VALUE = Object.new.freeze
12
- attr_reader :capacity
13
12
 
14
13
  def initialize(capacity: 1024, track_age: false)
15
14
  @queue = capacity ? Thread::SizedQueue.new(capacity) : Thread::Queue.new
16
- @capacity = capacity
17
15
  @signal = Signal.new
18
16
  @tracking = track_age ? { lock: Mutex.new, timestamps: [], generation: 0 } : nil
19
- @lifecycle = { lock: Mutex.new, hard_closed: false }
17
+ @lifecycle = { lock: Mutex.new, hard_closed: false, token: nil, generation: -1 }
20
18
  Freeze.publish(self)
21
19
  end
22
20
 
21
+ def capacity = @queue.is_a?(Thread::SizedQueue) ? @queue.max : nil
22
+
23
+ def capacity=(value)
24
+ value = Integer(value) unless value.nil?
25
+ raise ArgumentError, "capacity must be positive or nil" if value && !value.positive?
26
+ if value.nil? != capacity.nil?
27
+ raise ArgumentError, "cannot switch between bounded and unbounded queues on this Ruby engine"
28
+ end
29
+ @queue.max = value if value
30
+ broadcast
31
+ value
32
+ end
33
+
34
+ def interrupt(token, generation)
35
+ @lifecycle[:lock].synchronize do
36
+ return self if generation <= @lifecycle[:generation]
37
+ @lifecycle[:token] = token
38
+ @lifecycle[:generation] = generation
39
+ end
40
+ broadcast
41
+ self
42
+ end
43
+
44
+ def pop_interruptible(token, timeout)
45
+ timeout_at = timeout_at(timeout)
46
+ loop do
47
+ return false unless @lifecycle[:lock].synchronize { @lifecycle[:token].equal?(token) }
48
+ begin
49
+ result = tracked_pop
50
+ raise ThreadError if nil.equal?(result) && @queue.closed?
51
+ broadcast
52
+ return NIL_VALUE.equal?(result) ? nil : result
53
+ rescue ThreadError
54
+ ready = wait(remaining_timeout(timeout_at)) do
55
+ !@lifecycle[:lock].synchronize { @lifecycle[:token].equal?(token) } || size.positive?
56
+ end
57
+ return block_given? ? yield : nil unless ready
58
+ end
59
+ end
60
+ end
61
+
23
62
  def pop(timeout: nil)
24
63
  timeout_at = timeout_at(timeout)
25
64
  # Observing an empty queue is sufficient for a nonblocking miss. Avoid
@@ -223,8 +262,7 @@ module Farce
223
262
  def remaining_timeout(timeout_at)
224
263
  return unless timeout_at
225
264
  return 0 if timeout_at.zero?
226
- timeout = timeout_at - Clock.now
227
- timeout.positive? ? timeout : 0
265
+ Clock.remaining(timeout_at)
228
266
  end
229
267
 
230
268
  def broadcast = @signal.broadcast
@@ -61,7 +61,7 @@ module Farce
61
61
  result = port.select_receive
62
62
  return result if result
63
63
  end
64
- remaining = deadline - Clock.now if deadline
64
+ remaining = Clock.remaining(deadline)
65
65
  return if remaining && !remaining.positive?
66
66
  return unless ReservationWaiting.wait(wakeup, generation, deadline)
67
67
  end
@@ -67,6 +67,11 @@ module Farce
67
67
  super(check_key(key), **) { |value| check_value(yield(value)) }
68
68
  end
69
69
 
70
+ def modify(key, **)
71
+ raise LocalJumpError, "no block given" unless block_given?
72
+ super(check_key(key), **) { |present, value| check_value(yield(present, value)) }
73
+ end
74
+
70
75
  def upsert(key, initial, **)
71
76
  raise LocalJumpError, "no block given" unless block_given?
72
77
  super(check_key(key), check_value(initial), **) { |value| check_value(yield(value)) }