farce 0.4.1-aarch64-linux-gnu → 0.6.0-aarch64-linux-gnu

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 (143) 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/ext/farce/atom.c +23 -7
  16. data/ext/farce/containers.h +3 -0
  17. data/ext/farce/counter.c +4 -6
  18. data/ext/farce/depend +3 -1
  19. data/ext/farce/extconf.rb +19 -14
  20. data/ext/farce/farce.c +110 -3
  21. data/ext/farce/map.c +15 -7
  22. data/ext/farce/priority_queue.c +46 -1
  23. data/ext/farce/queue.c +141 -7
  24. data/ext/farce/trie.c +140 -72
  25. data/ext/farce/trie_search.h +114 -0
  26. data/ext/farce/vector.c +53 -21
  27. data/ext/farce/weak_map.c +16 -11
  28. data/ext/farce/weak_references.h +23 -0
  29. data/ext/java/org/farce/FiberScheduler.java +8 -6
  30. data/ext/java/org/farce/PriorityQueue.java +18 -2
  31. data/lib/farce/abstract/atom.rb +5 -1
  32. data/lib/farce/abstract/concurrent_map.rb +122 -0
  33. data/lib/farce/abstract/counter.rb +36 -1
  34. data/lib/farce/abstract/executor.rb +30 -7
  35. data/lib/farce/abstract/flag.rb +12 -0
  36. data/lib/farce/abstract/molecule.rb +14 -14
  37. data/lib/farce/abstract/pool.rb +158 -47
  38. data/lib/farce/abstract/priority_queue.rb +1 -1
  39. data/lib/farce/abstract/publisher.rb +189 -0
  40. data/lib/farce/abstract/queue.rb +29 -3
  41. data/lib/farce/abstract/scheduler.rb +41 -14
  42. data/lib/farce/abstract/timer_queue.rb +3 -3
  43. data/lib/farce/abstract/vector.rb +3 -1
  44. data/lib/farce/atom.rb +15 -2
  45. data/lib/farce/clock.rb +20 -7
  46. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  47. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  48. data/lib/farce/concurrent/event.rb +60 -0
  49. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  50. data/lib/farce/concurrent/local_var.rb +65 -0
  51. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  52. data/lib/farce/concurrent/tuple.rb +74 -0
  53. data/lib/farce/concurrent.rb +50 -0
  54. data/lib/farce/config.rb +112 -2
  55. data/lib/farce/counter.rb +5 -15
  56. data/lib/farce/deduper.rb +12 -4
  57. data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
  58. data/lib/farce/engine/jruby/map.rb +8 -6
  59. data/lib/farce/engine/jruby/signal.rb +2 -2
  60. data/lib/farce/engine/jvm/counter.rb +0 -2
  61. data/lib/farce/engine/jvm/farce.jar +0 -0
  62. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  63. data/lib/farce/engine/ruby/3.4/farce.so +0 -0
  64. data/lib/farce/engine/ruby/4.0/farce.so +0 -0
  65. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  66. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  67. data/lib/farce/engine/ruby/shared/weak_cache_mirror.rb +56 -0
  68. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  69. data/lib/farce/engine/ruby.rb +5 -1
  70. data/lib/farce/engine/shared/exchanger.rb +2 -2
  71. data/lib/farce/engine/shared/queue.rb +43 -5
  72. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  73. data/lib/farce/engine/shared/strict_map.rb +5 -0
  74. data/lib/farce/engine/shared/trie/ignore_case.rb +136 -0
  75. data/lib/farce/engine/shared/trie.rb +14 -1
  76. data/lib/farce/engine/shared/trie_builder.rb +19 -8
  77. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  78. data/lib/farce/engine/shared/unshared_vector.rb +11 -8
  79. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  80. data/lib/farce/engine/shared/vector.rb +2 -0
  81. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  82. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  83. data/lib/farce/engine/shared.rb +2 -0
  84. data/lib/farce/engine/truffleruby/map.rb +8 -6
  85. data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
  86. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  87. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  88. data/lib/farce/engine/truffleruby.rb +9 -0
  89. data/lib/farce/extension.rb +5 -3
  90. data/lib/farce/integrations/concurrent.rb +16 -3
  91. data/lib/farce/integrations/sorted_set.rb +1 -0
  92. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  93. data/lib/farce/internal/fake_ractor.rb +1 -1
  94. data/lib/farce/internal/flag_waiting.rb +1 -1
  95. data/lib/farce/internal/freeze.rb +1 -5
  96. data/lib/farce/internal/lease_initialization.rb +1 -1
  97. data/lib/farce/internal/lease_pool_state.rb +1 -1
  98. data/lib/farce/internal/lease_state.rb +2 -2
  99. data/lib/farce/internal/local_executor_methods.rb +3 -3
  100. data/lib/farce/internal/map_value_modes.rb +17 -9
  101. data/lib/farce/internal/pool_settings.rb +9 -0
  102. data/lib/farce/internal/pool_worker.rb +34 -21
  103. data/lib/farce/internal/reservation_waiting.rb +1 -1
  104. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  105. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  106. data/lib/farce/internal.rb +19 -8
  107. data/lib/farce/lazy.rb +3 -0
  108. data/lib/farce/lazy_ref.rb +3 -0
  109. data/lib/farce/local/counter.rb +4 -10
  110. data/lib/farce/local/lease.rb +2 -9
  111. data/lib/farce/local/lease_map.rb +2 -9
  112. data/lib/farce/local/pool.rb +38 -5
  113. data/lib/farce/local/publisher.rb +30 -0
  114. data/lib/farce/local/scheduler.rb +18 -5
  115. data/lib/farce/local/vector.rb +3 -2
  116. data/lib/farce/map.rb +8 -1
  117. data/lib/farce/memoize.rb +319 -0
  118. data/lib/farce/pool.rb +5 -0
  119. data/lib/farce/priority_queue.rb +1 -1
  120. data/lib/farce/publisher.rb +57 -0
  121. data/lib/farce/shareable.rb +3 -0
  122. data/lib/farce/signal.rb +1 -1
  123. data/lib/farce/strict/publisher.rb +31 -0
  124. data/lib/farce/strict/vector.rb +3 -2
  125. data/lib/farce/strict/weak_cache.rb +199 -0
  126. data/lib/farce/subscription/definitions.rb +27 -0
  127. data/lib/farce/subscription.rb +121 -0
  128. data/lib/farce/transaction/map.rb +35 -1
  129. data/lib/farce/unshared/pool.rb +1 -1
  130. data/lib/farce/unshared/publisher.rb +25 -0
  131. data/lib/farce/unshared/scheduler.rb +3 -3
  132. data/lib/farce/unshared/vector.rb +3 -2
  133. data/lib/farce/vector.rb +20 -16
  134. data/lib/farce/version.rb +1 -1
  135. data/lib/farce/walker/definitions.rb +31 -2
  136. data/lib/farce/walker/modification.rb +28 -14
  137. data/lib/farce/walker.rb +9 -1
  138. data/lib/farce/weak_atom.rb +5 -2
  139. data/lib/farce/weak_key_map.rb +1 -1
  140. data/lib/farce/weak_map.rb +1 -1
  141. data/lib/farce/weak_value_map.rb +1 -1
  142. data/lib/farce.rb +3 -1
  143. metadata +23 -1
@@ -61,7 +61,7 @@ module Farce
61
61
  end
62
62
 
63
63
  def wait_for_change?(observed, deadline)
64
- timeout = deadline - Clock.now if deadline
64
+ timeout = Clock.remaining(deadline)
65
65
  return false if timeout && !timeout.positive?
66
66
  !!@changes.wait(observed, timeout:) { false }
67
67
  end
@@ -54,6 +54,8 @@ module Farce
54
54
  end
55
55
  end
56
56
 
57
+ def native_weak_maps? = true
58
+
57
59
  # define_method supplies the receiver. Rebinding first is unnecessary without
58
60
  # native Ractor isolation and can change the method context used by super.
59
61
  def prepare_method_definition(&definition) = definition
@@ -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
@@ -214,11 +215,11 @@ module Farce
214
215
  # Yield presence and value under the key reservation.
215
216
  # MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
216
217
  # Return whether a change committed.
217
- def modify(key) # rubocop:disable Naming/PredicateMethod
218
+ def modify(key, timeout: nil)
218
219
  raise LocalJumpError, "no block given" unless block_given?
219
220
  check_frozen!
220
221
  wrapped = wrap_key(key)
221
- _, result = with_mutating_key_operation(wrapped, nil) do |reservation|
222
+ completed, result = with_mutating_key_operation(wrapped, timeout_deadline(timeout)) do |reservation|
222
223
  present, current = native_operation { [concurrent_key?(wrapped), concurrent_get(wrapped)] }
223
224
  value = yield(present, current)
224
225
  next false if MAP_KEEP.equal?(value)
@@ -231,7 +232,7 @@ module Farce
231
232
  changed!
232
233
  end
233
234
  end
234
- !!result
235
+ completed && !!result
235
236
  end
236
237
 
237
238
  def upsert(key, initial_value, timeout: nil)
@@ -557,7 +558,8 @@ module Farce
557
558
  native_operation { concurrent_get(wrapped) }
558
559
  end
559
560
  return fallback&.call unless completed
560
- ready = non_nil ? !current.nil? : !values_equal?(current, expected)
561
+ ready = non_nil ? !current.nil? :
562
+ Internal.value_changed?(current, expected, compare_values_by_identity?)
561
563
  return current if ready
562
564
  return fallback&.call unless wait_for_signal(@change_signal, generation, deadline)
563
565
  end
@@ -566,7 +568,7 @@ module Farce
566
568
  def wait_for_signal(signal, generation, deadline)
567
569
  return signal.wait(generation) unless deadline
568
570
 
569
- remaining = deadline - Clock.now
571
+ remaining = Clock.remaining(deadline)
570
572
  return false unless remaining.positive?
571
573
  !signal.wait(generation, timeout: remaining).nil?
572
574
  end
@@ -60,7 +60,6 @@ module Farce
60
60
  check_frozen!
61
61
  end
62
62
  change(delta)
63
- self
64
63
  end
65
64
 
66
65
  def decrement(delta = 1)
@@ -70,7 +69,6 @@ module Farce
70
69
  check_frozen!
71
70
  end
72
71
  change(-delta)
73
- self
74
72
  end
75
73
 
76
74
  alias value get
@@ -23,8 +23,8 @@ module Farce
23
23
  private_constant :Bucket, :TrackedItem
24
24
 
25
25
  class State
26
- attr_accessor :size, :sealed, :closed, :generation, :operation_owner
27
- attr_reader :buckets, :capacity, :signal, :lock, :tracked_items, :track_age
26
+ attr_accessor :size, :sealed, :closed, :generation, :operation_owner, :capacity
27
+ attr_reader :buckets, :signal, :lock, :tracked_items, :track_age
28
28
 
29
29
  def initialize(capacity:, signal:, lock:, track_age:)
30
30
  @buckets = []
@@ -139,7 +139,18 @@ module Farce
139
139
  with_queue_operation(state) { state.size.zero? } # rubocop:disable Style/ZeroLengthPredicate
140
140
  end
141
141
 
142
- def capacity = initialized_state.capacity
142
+ def capacity
143
+ state = initialized_state
144
+ with_queue_operation(state) { state.capacity }
145
+ end
146
+
147
+ def capacity=(value)
148
+ state = initialized_state
149
+ value = normalize_capacity(value)
150
+ with_queue_operation(state) do
151
+ notify_and_commit(state) { state.capacity = value }
152
+ end
153
+ end
143
154
 
144
155
  def clear
145
156
  state = initialized_state
@@ -262,7 +273,10 @@ module Farce
262
273
 
263
274
  def normalize_capacity(capacity)
264
275
  return nil if capacity.nil?
265
- capacity = capacity.to_int unless primitive_is_a?(capacity, Integer)
276
+ unless primitive_is_a?(capacity, Integer)
277
+ raise TypeError, "capacity must be an Integer or nil" unless capacity.respond_to?(:to_int)
278
+ capacity = capacity.to_int
279
+ end
266
280
  raise TypeError, "capacity must be an Integer or nil" unless primitive_is_a?(capacity, Integer)
267
281
  raise ArgumentError, "capacity must be positive or nil" unless capacity.positive?
268
282
  capacity
@@ -43,7 +43,7 @@ module Farce
43
43
  current = generation
44
44
  break current unless current == observed
45
45
 
46
- remaining = deadline - Clock.now if deadline
46
+ remaining = Clock.remaining(deadline)
47
47
  break TIMED_OUT if remaining && !remaining.positive?
48
48
  @num_waiting.set(@num_waiting.get + 1) unless waiting
49
49
  waiting = true
@@ -39,6 +39,15 @@ module Farce
39
39
 
40
40
  def native_ractors? = false
41
41
 
42
+ # Range#initialize rejects open endpoints on TruffleRuby, while Range.new accepts them.
43
+ def initialize_range(target, first, last, exclude_end)
44
+ if first.nil? || last.nil?
45
+ target.__send__(:initialize_copy, target.class.new(first, last, exclude_end))
46
+ else
47
+ target.__send__(:initialize, first, last, exclude_end)
48
+ end
49
+ end
50
+
42
51
  # @api private
43
52
  # TruffleRuby probes this additional hook before using _dump.
44
53
  def marshal_protocol_method?(name)
@@ -100,13 +100,15 @@ module Farce
100
100
  # @yield the block containing method definitions
101
101
  # @yieldreceiver [Module, Refinement] the module/refinement used for extending the target class.
102
102
  # @return [self] the extension instance
103
- def define(*targets, &)
103
+ # rubocop:disable-next Naming/BlockForwarding, Style/ArgumentsForwarding
104
+ def define(*targets, &block)
105
+ # JRuby rejects repeated anonymous block forwarding inside a block.
104
106
  @lock.synchronize do
105
107
  check_state!
106
108
  targets.each do |target|
107
- refine(target, &)
109
+ refine(target, &block)
108
110
  mixin = @definitions.store_if_absent(target) { Mixin.new(self, target) }
109
- mixin.module_eval(&)
111
+ mixin.module_eval(&block)
110
112
  mixin.install if installed?
111
113
  end
112
114
  end
@@ -3,9 +3,14 @@
3
3
  # warn_indent: true
4
4
 
5
5
  # @!group concurrent-ruby Integration
6
- require "farce" unless defined?(Farce::Abstract)
7
- require "concurrent/map" unless defined?(Concurrent::Map)
8
- require "concurrent/tvar" unless defined?(Concurrent::TVar)
6
+ require "farce" unless defined?(Farce::Abstract)
7
+ require "concurrent/map" unless defined?(Concurrent::Map)
8
+ require "concurrent/tvar" unless defined?(Concurrent::TVar)
9
+ require "concurrent/atomic/atomic_reference" unless defined?(Concurrent::AtomicReference)
10
+ require "concurrent/atomic/atomic_fixnum" unless defined?(Concurrent::AtomicFixnum)
11
+ require "concurrent/atomic/atomic_boolean" unless defined?(Concurrent::AtomicBoolean)
12
+ require "concurrent/atomic/event" unless defined?(Concurrent::Event)
13
+ require "concurrent/atomic/count_down_latch" unless defined?(Concurrent::CountDownLatch)
9
14
 
10
15
  module Farce
11
16
  module Integrations
@@ -109,4 +114,12 @@ module Farce
109
114
  Internal::Converter.define(::Concurrent::Map, :Map) do |instance, value|
110
115
  value.each { instance[_1] = convert(_2) }
111
116
  end
117
+
118
+ Farce.on_main do
119
+ Subscription.define(::Concurrent::AtomicReference) { subscriber.value = it }
120
+ Subscription.define(::Concurrent::AtomicFixnum) { |_| subscriber.increment }
121
+ Subscription.define(::Concurrent::AtomicBoolean) { |_| subscriber.make_true }
122
+ Subscription.define(::Concurrent::Event) { |_| subscriber.set }
123
+ Subscription.define(::Concurrent::CountDownLatch) { |_| subscriber.count_down }
124
+ end
112
125
  end
@@ -7,5 +7,6 @@ require "farce" unless defined?(Farce::Abstract)
7
7
  require "sorted_set"
8
8
 
9
9
  module Farce
10
+ Farce.dedup.store(::SortedSet)
10
11
  Internal::Converter.define(::SortedSet, :SortedSet)
11
12
  end
@@ -32,7 +32,8 @@ module Farce
32
32
 
33
33
  wait(timeout) do
34
34
  raise_unwritable if sealed?
35
- size < capacity
35
+ limit = capacity
36
+ !limit || size < limit
36
37
  end
37
38
  end
38
39
 
@@ -76,7 +77,7 @@ module Farce
76
77
  while true
77
78
  return true if internal_queue.push(priority, value)
78
79
  raise ThreadError, "queue full" if non_block
79
- return false unless wait_push(timeout: remaining_timeout(deadline))
80
+ return false unless wait_push(timeout: Clock.remaining(deadline))
80
81
  end
81
82
  end
82
83
 
@@ -98,7 +99,7 @@ module Farce
98
99
  generation = internal_signal.generation
99
100
  raise ::Farce::ClosedQueueError, "queue is closed" if closed?
100
101
  return true if yield
101
- return false unless internal_signal.wait(generation, timeout: remaining_timeout(deadline))
102
+ return false unless internal_signal.wait(generation, timeout: Clock.remaining(deadline))
102
103
  end
103
104
  end
104
105
 
@@ -111,12 +112,6 @@ module Farce
111
112
  def timeout_at(timeout)
112
113
  Clock.timeout(normalize_timeout(timeout)) unless timeout.nil?
113
114
  end
114
-
115
- def remaining_timeout(timeout_at)
116
- return unless timeout_at
117
- timeout = timeout_at - Clock.now
118
- timeout.positive? ? timeout : 0
119
- end
120
115
  end
121
116
  end
122
117
  end
@@ -61,7 +61,7 @@ module Farce
61
61
  STATES[self] = state
62
62
  GROUPS[group] = self
63
63
 
64
- state.main_thread = Thread.new do # rubocop:disable Lint/UselessSetterCall
64
+ state.main_thread = Thread.new do
65
65
  current = Thread.current
66
66
  group.add(current)
67
67
  current.freeze
@@ -21,7 +21,7 @@ module Farce
21
21
  observed = signal.generation
22
22
  current = value
23
23
  return current unless expected == current
24
- break unless signal.wait(observed, timeout: Internal.remaining_timeout(deadline))
24
+ break unless signal.wait(observed, timeout: Clock.remaining(deadline))
25
25
  end
26
26
  yield if block_given?
27
27
  end
@@ -8,11 +8,7 @@ module Farce
8
8
  module Freeze
9
9
  # Structurally freeze a Ruby facade, then validate and publish its object graph.
10
10
  # This deliberately bypasses an override that controls logical frozen state.
11
- def self.publish(object)
12
- Object.instance_method(:freeze).bind_call(object)
13
- ::Ractor.make_shareable(object) if Internal.native_ractors?
14
- object
15
- end
11
+ def self.publish(object) = Internal.publish_shareable(object)
16
12
 
17
13
  def self.check(object)
18
14
  return object unless object.frozen?
@@ -31,7 +31,7 @@ module Farce
31
31
  reject_recursive_initialization!
32
32
  return unless wait
33
33
 
34
- remaining = deadline - Clock.now if deadline
34
+ remaining = Clock.remaining(deadline)
35
35
  return if remaining && !remaining.positive?
36
36
  reject_unscheduled_fiber_wait!
37
37
  @signal.wait(observed, timeout: wait_interval(remaining))
@@ -108,7 +108,7 @@ module Farce
108
108
  return create_resource(factory) if reserve_creation
109
109
  return unless wait
110
110
 
111
- remaining = deadline - Clock.now if deadline
111
+ remaining = Clock.remaining(deadline)
112
112
  raise TimeoutError, "lease pool checkout timed out" if remaining && !remaining.positive?
113
113
  interval = LeaseWaiting.wait_interval(remaining)
114
114
  changed = Thread.handle_interrupt(Exception => :immediate) do
@@ -156,7 +156,7 @@ module Farce
156
156
  resource = try_acquire(kind)
157
157
  return resource if resource
158
158
 
159
- remaining = deadline - Clock.now if deadline
159
+ remaining = Clock.remaining(deadline)
160
160
  raise TimeoutError, "lease checkout timed out" if remaining && !remaining.positive?
161
161
  reject_unscheduled_fiber_wait!
162
162
  interval = wait_interval(remaining)
@@ -180,7 +180,7 @@ module Farce
180
180
  outcome = try_acquire_block(block)
181
181
  return outcome.first if outcome
182
182
 
183
- remaining = deadline - Clock.now if deadline
183
+ remaining = Clock.remaining(deadline)
184
184
  raise TimeoutError, "lease checkout timed out" if remaining && !remaining.positive?
185
185
  reject_unscheduled_fiber_wait!
186
186
  interval = wait_interval(remaining)
@@ -16,10 +16,10 @@ module Farce
16
16
  # Run a task directly, preserving its block, arguments, and calling fiber.
17
17
  # `mode` and `auto_local` follow the same validation as {#schedule}.
18
18
  # @param (see Abstract::Executor#execute)
19
- # @return [self]
20
- def execute(*, mode: :smart, auto_local: true, &)
19
+ # @return [self, nil]
20
+ def execute(*, mode: :smart, auto_local: true, timeout: nil, &)
21
21
  validate_local_mode(mode, auto_local)
22
- super(*, mode: :local, auto_local: true, &)
22
+ super(*, mode: :local, auto_local: true, timeout:, &)
23
23
  end
24
24
 
25
25
  private
@@ -22,9 +22,7 @@ module Farce
22
22
  compare_keys_by_identity: compare_by_identity, compare_values_by_identity: compare_by_identity)
23
23
  initial_mapping = convert_entries(initial_mapping)
24
24
 
25
- identity = BasicObject.instance_method(:equal?)
26
- valid_comparison = identity.bind_call(compare_values_by_identity, true) ||
27
- identity.bind_call(compare_values_by_identity, false)
25
+ valid_comparison = true.equal?(compare_values_by_identity) || false.equal?(compare_values_by_identity)
28
26
 
29
27
  raise ArgumentError, "compare_values_by_identity must be a boolean" unless valid_comparison
30
28
  @compare_values_by_identity = compare_values_by_identity
@@ -33,9 +31,11 @@ module Farce
33
31
  @map = new_map(compare_by_identity:, compare_keys_by_identity:, compare_values_by_identity: true)
34
32
  KeyNormalizer.prepare_concurrent(@map)
35
33
 
36
- restoring = KeyNormalizer.restoration?(normalize_keys)
37
- normalizer = KeyNormalizer.build(normalize_keys, shareable: true)
38
- KeyNormalizer.install_concurrent(self, normalizer) unless restoring
34
+ unless nil.equal?(normalize_keys)
35
+ restoring = KeyNormalizer.restoration?(normalize_keys)
36
+ normalizer = KeyNormalizer.build(normalize_keys, shareable: true)
37
+ KeyNormalizer.install_concurrent(self, normalizer) unless restoring
38
+ end
39
39
 
40
40
  initial_mapping&.each { |key, value| self[key] = value }
41
41
  KeyNormalizer.install_concurrent(self, normalizer) if restoring
@@ -143,18 +143,26 @@ module Farce
143
143
  end
144
144
  end
145
145
 
146
+ # (see Abstract::ConcurrentMap#wait_until)
147
+ def wait_until(key, timeout: nil)
148
+ raise LocalJumpError, "no block given" unless block_given?
149
+ result = Internal.wait_until(@map, key, timeout:) { |current| yield(unwrap_value(current)) }
150
+ unwrap_value(result)
151
+ end
152
+
146
153
  def wait_until_changed(key, expected, timeout: nil, &)
147
154
  prepared_key = @map.normalize_external_key(key)
148
155
  expected = wrap_comparison(expected)
149
156
  deadline = Internal.timeout_deadline(timeout)
150
- loop do
151
- current = @map.get_prepared(prepared_key, timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
157
+ # JRuby rejects repeated anonymous block forwarding inside a block.
158
+ while true
159
+ current = @map.get_prepared(prepared_key, timeout: Clock.remaining(deadline)) { TIMED_OUT }
152
160
  return unwrap_result(current, &) if TIMED_OUT.equal?(current)
153
161
  return unwrap_value(current) unless values_equal?(nil.equal?(current) ? NIL_VALUE : current, expected)
154
162
  result = @map.wait_until_changed_prepared(
155
163
  prepared_key,
156
164
  current,
157
- timeout: Internal.remaining_timeout(deadline),
165
+ timeout: Clock.remaining(deadline),
158
166
  ) { TIMED_OUT }
159
167
  return unwrap_result(result, &) if TIMED_OUT.equal?(result)
160
168
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ # shareable_constant_value: literal
3
+ # warn_indent: true
4
+
5
+ module Farce
6
+ module Internal # :nodoc: all
7
+ PoolSettings = Data.define(:min_size, :max_size, :max_inflight, :grow_after, :shrink_after, :generation)
8
+ end
9
+ end
@@ -9,46 +9,59 @@ module Farce
9
9
  include Shareable::Unfreezable
10
10
 
11
11
  def initialize(pool)
12
- @pool = pool
13
- @inflight = Counter.new if pool.max_inflight || pool.shrink_after
14
- @pressure = pool.min_size < pool.max_size
12
+ @pool = pool
13
+ @inflight = Counter.new
15
14
  @registered = Atom.new(true)
16
15
  super()
17
16
  end
18
17
 
19
- def idle_timeout = @pool.shrink_after
20
-
21
- def wait_for_admission(scheduler, runnable_count)
22
- return false if @pool.drained?
23
- limit = @pool.max_inflight
24
- return true unless limit
18
+ def settings = @pool.settings
19
+ def idle_timeout = settings.shrink_after
25
20
 
21
+ def wait_for_admission(scheduler, runnable_count, settings = self.settings)
26
22
  loop do
27
23
  return false if @pool.drained?
28
- return true if @pool.closing? || @pool.at_max_size? || @inflight.value < limit
29
- return true if runnable_count && scheduler.public_send(runnable_count).zero?
24
+ return settings if @pool.closing?
25
+ count = @pool.size
26
+ limit = settings.max_inflight
27
+ return false if count > settings.max_size && retire?(excess: true)
28
+ # Busy workers keep queued dependencies runnable until they become idle.
29
+ return settings if !limit || count >= settings.max_size || @inflight.value < limit
30
+ return settings if runnable_count && scheduler.public_send(runnable_count).zero?
30
31
 
32
+ # Read the generation before rechecking state to avoid a lost configuration wakeup.
31
33
  generation = @pool.admission_generation
32
- next if @pool.closing? || @pool.at_max_size? || @inflight.value < limit
34
+ settings = self.settings
35
+ count = @pool.size
36
+ limit = settings.max_inflight
37
+ next if @pool.closing? || !limit || count >= settings.max_size || @inflight.value < limit
33
38
  @pool.wait_for_admission(generation)
39
+ settings = self.settings
34
40
  end
35
41
  end
36
42
 
37
- def task_started
38
- @inflight&.increment
39
- @pool.task_taken if @pressure
43
+ def task_started(settings = self.settings)
44
+ @inflight.increment
45
+ @pool.task_taken(settings)
40
46
  end
41
47
 
42
48
  def task_finished
43
- return unless @inflight
44
49
  @inflight.decrement
45
- @pool.admission_changed if @pool.max_inflight && !@pool.at_max_size?
50
+ settings = self.settings
51
+ count = @pool.size
52
+ if count > settings.max_size
53
+ @inflight.value.zero? ? @pool.wake_retiring_workers : @pool.admission_changed
54
+ elsif settings.max_inflight && count < settings.max_size
55
+ @pool.admission_changed
56
+ end
46
57
  end
47
58
 
48
- def retire?
49
- return false unless @inflight&.value&.zero?
50
- return false unless @pool.retire_worker
51
- @registered.compare_and_set(true, false)
59
+ def retire?(excess: false, expected: nil)
60
+ return false unless @inflight.value.zero?
61
+ return false unless @pool.retire_worker(excess:, expected:)
62
+ retired = @registered.compare_and_set(true, false)
63
+ @pool.worker_retired if retired
64
+ retired
52
65
  end
53
66
 
54
67
  def stopped(error, replace: true) # rubocop:disable Naming/PredicateMethod
@@ -9,7 +9,7 @@ module Farce
9
9
  module ReservationWaiting
10
10
  def self.wait(signal, generation, deadline)
11
11
  while true
12
- remaining = deadline - Clock.now if deadline
12
+ remaining = Clock.remaining(deadline)
13
13
  return false if remaining && !remaining.positive?
14
14
 
15
15
  # LeaseWaiting supplies the full remaining timeout on default engines
@@ -146,8 +146,8 @@ module Farce
146
146
  service_policy
147
147
  break if idle?
148
148
  deadline = @timers.peek_priority
149
- timeout = deadline ? deadline - Clock.now : nil
150
- timeout = 0 if timeout&.negative? || ready? || !@mailbox.empty?
149
+ timeout = Clock.remaining(deadline)
150
+ timeout = 0 if ready? || !@mailbox.empty?
151
151
  dispatch(timeout, @timers.empty? ? @dispatch_budget : 256)
152
152
  end
153
153
 
@@ -24,6 +24,11 @@ module Farce
24
24
  super
25
25
  end
26
26
 
27
+ def replace_if_exists(key, replacement, mode: nil, **)
28
+ @manager.validate_mode!(mode)
29
+ super
30
+ end
31
+
27
32
  def store_if_absent(key, mode: nil, **)
28
33
  raise LocalJumpError, "no block given" unless block_given?
29
34
  @manager.validate_mode!(mode)
@@ -77,18 +77,12 @@ module Farce
77
77
  Clock.now + timeout
78
78
  end
79
79
 
80
- def remaining_timeout(deadline)
81
- return unless deadline
82
- remaining = deadline - Clock.now
83
- remaining.negative? ? 0 : remaining
84
- end
85
-
86
80
  # Repeat with one timeout budget. Even a zero timeout permits the first check.
87
81
  def with_timeout(timeout)
88
82
  raise LocalJumpError, "no block given" unless block_given?
89
83
  deadline = timeout_deadline(timeout)
90
84
  loop do
91
- yield remaining_timeout(deadline), deadline
85
+ yield Clock.remaining(deadline), deadline
92
86
  return if deadline && Clock.now >= deadline
93
87
  end
94
88
  end
@@ -98,10 +92,16 @@ module Farce
98
92
  with_timeout(timeout) do |_, deadline|
99
93
  current = keys.empty? ? receiver.value : receiver[*keys]
100
94
  return current if yield(current)
101
- receiver.wait_until_changed(*keys, current, timeout: remaining_timeout(deadline)) { return }
95
+ receiver.wait_until_changed(*keys, current, timeout: Clock.remaining(deadline)) { return }
102
96
  end
103
97
  end
104
98
 
99
+ # An unchanged observation must permit waiting even when its equality is non-reflexive.
100
+ def value_changed?(current, expected, identity)
101
+ return false if BasicObject.instance_method(:equal?).bind_call(current, expected)
102
+ identity || !(current == expected) # rubocop:disable Style/InverseMethods -- != can be overridden independently.
103
+ end
104
+
105
105
  def garbage_collectable?(object)
106
106
  case object
107
107
  when Integer, Float, Complex, Rational, Symbol, true, false, nil then false
@@ -113,9 +113,20 @@ module Farce
113
113
  def storage_thread(thread) = thread
114
114
  def prepare_mutable_numeric(_) = nil
115
115
  def prepare_map_access(_klass, _kind) = nil
116
+ def prepare_shareable_class(_base) = nil
116
117
  def finalize_engine = nil
117
118
  def with_scheduler_thread_group = yield(ThreadGroup.new)
118
119
 
120
+ def initialize_range(target, first, last, exclude_end)
121
+ target.__send__(:initialize, first, last, exclude_end)
122
+ end
123
+
124
+ def publish_shareable(object)
125
+ Object.instance_method(:freeze).bind_call(object)
126
+ ::Ractor.make_shareable(object) if native_ractors?
127
+ object
128
+ end
129
+
119
130
  def delegate(from, to, *methods)
120
131
  methods.each do |method|
121
132
  if to.is_a?(Module)
data/lib/farce/lazy.rb CHANGED
@@ -59,6 +59,9 @@ module Farce
59
59
  # # Will only compute the expensive attribute once, and return the same value to all Ractors.
60
60
  # 5.times { Ractor.new(object) { it.expensive_attribute } }
61
61
  # ```
62
+ #
63
+ # @see Farce::Memoize
64
+ # @see Farce::LazyRef
62
65
  class Lazy < Farce::Abstract::Lazy
63
66
  include Shareable::Tracked
64
67
 
@@ -22,6 +22,9 @@ module Farce
22
22
  # # initialized.value # => 0
23
23
  # # lazy_ref[:key] = :value
24
24
  # # initialized.value # => 1
25
+ # #
26
+ # # @see Farce::Memoize
27
+ # # @see Farce::Lazy
25
28
  # class LazyRef < Farce::Reference
26
29
  # # @!macro modes
27
30
  # # @overload initialize(factory, mode: :smart)
@@ -50,19 +50,13 @@ module Farce
50
50
 
51
51
  # Increment the counter atomically.
52
52
  # @param by [Numeric, String, #to_int] the amount, converted to an Integer
53
- # @return [self]
54
- def increment(by = 1)
55
- internal_counter.increment(by)
56
- self
57
- end
53
+ # @return [Integer] the value produced by this atomic update
54
+ def increment(by = 1) = internal_counter.increment(by)
58
55
 
59
56
  # Decrement the counter atomically.
60
57
  # @param by [Numeric, String, #to_int] the amount, converted to an Integer
61
- # @return [self]
62
- def decrement(by = 1)
63
- internal_counter.decrement(by)
64
- self
65
- end
58
+ # @return [Integer] the value produced by this atomic update
59
+ def decrement(by = 1) = internal_counter.decrement(by)
66
60
 
67
61
  alias subtract decrement
68
62
  alias remove decrement