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
@@ -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)) }
@@ -129,7 +129,7 @@ module Farce
129
129
 
130
130
  loop do
131
131
  current, version = @mutex.synchronize { [@value, @version] }
132
- return current unless values_equal?(current, expected)
132
+ return current if Internal.value_changed?(current, expected, compare_by_identity?)
133
133
 
134
134
  result = @mutex.synchronize do
135
135
  next unless @version == version
@@ -274,12 +274,12 @@ module Farce
274
274
  finish_update if completed
275
275
  end
276
276
 
277
- def wait_until_changed(index, expected, timeout: nil)
278
- wait_for_value(convert_index(index), expected, timeout_deadline(timeout), non_nil: false)
277
+ def wait_until_changed(index, expected, timeout: nil, &fallback)
278
+ wait_for_value(convert_index(index), expected, timeout_deadline(timeout), fallback, non_nil: false)
279
279
  end
280
280
 
281
281
  def wait_until_non_nil(index, timeout: nil)
282
- wait_for_value(convert_index(index), nil, timeout_deadline(timeout), non_nil: true)
282
+ wait_for_value(convert_index(index), nil, timeout_deadline(timeout), nil, non_nil: true)
283
283
  end
284
284
 
285
285
  def compare_by_identity? = @compare_by_identity
@@ -341,20 +341,21 @@ module Farce
341
341
  raise FrozenError.new("can't modify frozen #{self.class}", receiver: self)
342
342
  end
343
343
 
344
- def wait_for_value(index, expected, deadline, non_nil:)
344
+ def wait_for_value(index, expected, deadline, fallback, non_nil:)
345
345
  while true
346
346
  generation = @signal.generation
347
347
  current = @mutex.synchronize { @values[index] }
348
- ready = non_nil ? !current.nil? : !values_equal?(current, expected)
348
+ ready = non_nil ? !current.nil? :
349
+ Internal.value_changed?(current, expected, compare_by_identity?)
349
350
  return current if ready
350
- return unless wait_for_signal(generation, deadline)
351
+ return fallback&.call unless wait_for_signal(generation, deadline)
351
352
  end
352
353
  end
353
354
 
354
355
  def wait_for_signal(generation, deadline)
355
356
  return @signal.wait(generation) unless deadline
356
357
 
357
- remaining = deadline - Clock.now
358
+ remaining = Clock.remaining(deadline)
358
359
  return false unless remaining.positive?
359
360
  !@signal.wait(generation, timeout: remaining).nil?
360
361
  end
@@ -145,10 +145,11 @@ module Farce
145
145
  # Yield presence and value under the entry reservation.
146
146
  # MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
147
147
  # Return whether a change committed.
148
- def modify(key) # rubocop:disable Naming/PredicateMethod
148
+ def modify(key, timeout: nil)
149
149
  raise LocalJumpError, "no block given" unless block_given?
150
150
  check_frozen!
151
- _, result = with_entry(key, nil, create: true, mutating: true) do |present, current, entry, index|
151
+ deadline = timeout_deadline(timeout)
152
+ completed, result = with_entry(key, deadline, create: true, mutating: true) do |present, current, entry, index|
152
153
  value = yield(present, current)
153
154
  check_frozen!
154
155
  if MAP_KEEP.equal?(value)
@@ -161,7 +162,7 @@ module Farce
161
162
  entry.store(value)
162
163
  end
163
164
  end
164
- !!result
165
+ completed && !!result
165
166
  end
166
167
 
167
168
  def wait_until_changed(key, expected, timeout: nil, &fallback)
@@ -352,7 +353,7 @@ module Farce
352
353
  return fallback&.call if timed_out == :timed_out
353
354
 
354
355
  unless entry
355
- return nil unless values_equal?(nil, expected)
356
+ return nil if Internal.value_changed?(nil, expected, compare_values_by_identity?)
356
357
  return fallback&.call unless signal_changed?(@index.change_signal, observed, deadline)
357
358
  next
358
359
  end
@@ -373,13 +374,14 @@ module Farce
373
374
  # lookup object. Otherwise GC could orphan the entry's change signal.
374
375
  key = stored_key
375
376
  current = nil unless present
376
- return current if non_nil ? !current.nil? : !values_equal?(current, expected)
377
+ return current if non_nil ? !current.nil? :
378
+ Internal.value_changed?(current, expected, compare_values_by_identity?)
377
379
  return fallback&.call unless entry.wait_for_change?(changed, deadline)
378
380
  end
379
381
  end
380
382
 
381
383
  def signal_changed?(signal, observed, deadline)
382
- timeout = deadline - Clock.now if deadline
384
+ timeout = Clock.remaining(deadline)
383
385
  return false if timeout && !timeout.positive?
384
386
 
385
387
  timed_out = Object.new
@@ -146,7 +146,7 @@ module Farce
146
146
  def with_value(timeout)
147
147
  deadline = timeout_deadline(timeout)
148
148
  result = TIMED_OUT
149
- @lock.update(timeout: remaining_timeout(deadline)) do
149
+ @lock.update(timeout: Clock.remaining(deadline)) do
150
150
  result = yield
151
151
  nil
152
152
  end
@@ -158,11 +158,12 @@ module Farce
158
158
  loop do
159
159
  generation = @changes.value
160
160
  current = read_value
161
- return current if non_nil ? !current.nil? : !values_equal?(current, expected)
161
+ return current if non_nil ? !current.nil? :
162
+ Internal.value_changed?(current, expected, compare_by_identity?)
162
163
  # Detect recursive waits without waiting for another owner's update.
163
164
  @lock.get(timeout: 0)
164
- return fallback&.call if remaining_timeout(deadline)&.zero?
165
- @changes.wait_until_changed(generation, timeout: remaining_timeout(deadline))
165
+ return fallback&.call if Clock.remaining(deadline)&.zero?
166
+ @changes.wait_until_changed(generation, timeout: Clock.remaining(deadline))
166
167
  end
167
168
  end
168
169
 
@@ -177,12 +178,6 @@ module Farce
177
178
  raise ArgumentError, "timeout must be finite and non-negative" unless timeout.finite? && !timeout.negative?
178
179
  Clock.now + timeout
179
180
  end
180
-
181
- def remaining_timeout(deadline)
182
- return unless deadline
183
- remaining = deadline - Clock.now
184
- remaining.negative? ? 0 : remaining
185
- end
186
181
  end
187
182
  private_constant :WeakAtomBase
188
183
  end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -143,18 +143,25 @@ 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
157
  loop do
151
- current = @map.get_prepared(prepared_key, timeout: Internal.remaining_timeout(deadline)) { TIMED_OUT }
158
+ current = @map.get_prepared(prepared_key, timeout: Clock.remaining(deadline)) { TIMED_OUT }
152
159
  return unwrap_result(current, &) if TIMED_OUT.equal?(current)
153
160
  return unwrap_value(current) unless values_equal?(nil.equal?(current) ? NIL_VALUE : current, expected)
154
161
  result = @map.wait_until_changed_prepared(
155
162
  prepared_key,
156
163
  current,
157
- timeout: Internal.remaining_timeout(deadline),
164
+ timeout: Clock.remaining(deadline),
158
165
  ) { TIMED_OUT }
159
166
  return unwrap_result(result, &) if TIMED_OUT.equal?(result)
160
167
  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)