farce 0.4.1-java → 0.5.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 (98) 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/lib/farce/abstract/concurrent_map.rb +122 -0
  16. data/lib/farce/abstract/counter.rb +35 -1
  17. data/lib/farce/abstract/executor.rb +30 -7
  18. data/lib/farce/abstract/flag.rb +12 -0
  19. data/lib/farce/abstract/pool.rb +158 -47
  20. data/lib/farce/abstract/priority_queue.rb +1 -1
  21. data/lib/farce/abstract/publisher.rb +189 -0
  22. data/lib/farce/abstract/queue.rb +29 -3
  23. data/lib/farce/abstract/scheduler.rb +41 -14
  24. data/lib/farce/abstract/timer_queue.rb +3 -3
  25. data/lib/farce/abstract/vector.rb +3 -1
  26. data/lib/farce/atom.rb +9 -2
  27. data/lib/farce/clock.rb +12 -0
  28. data/lib/farce/concurrent/count_down_latch.rb +37 -0
  29. data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
  30. data/lib/farce/concurrent/event.rb +60 -0
  31. data/lib/farce/concurrent/fiber_local_var.rb +31 -0
  32. data/lib/farce/concurrent/local_var.rb +65 -0
  33. data/lib/farce/concurrent/thread_local_var.rb +28 -0
  34. data/lib/farce/concurrent/tuple.rb +74 -0
  35. data/lib/farce/concurrent.rb +50 -0
  36. data/lib/farce/config.rb +112 -2
  37. data/lib/farce/counter.rb +0 -16
  38. data/lib/farce/engine/jruby/map.rb +8 -6
  39. data/lib/farce/engine/jruby/signal.rb +2 -2
  40. data/lib/farce/engine/jvm/farce.jar +0 -0
  41. data/lib/farce/engine/jvm/priority_queue.rb +14 -4
  42. data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
  43. data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
  44. data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
  45. data/lib/farce/engine/shared/exchanger.rb +2 -2
  46. data/lib/farce/engine/shared/queue.rb +43 -5
  47. data/lib/farce/engine/shared/ractor_selector.rb +1 -1
  48. data/lib/farce/engine/shared/strict_map.rb +5 -0
  49. data/lib/farce/engine/shared/unshared_atom.rb +1 -1
  50. data/lib/farce/engine/shared/unshared_vector.rb +8 -7
  51. data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
  52. data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
  53. data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
  54. data/lib/farce/engine/truffleruby/map.rb +8 -6
  55. data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
  56. data/lib/farce/engine/truffleruby/signal.rb +1 -1
  57. data/lib/farce/integrations/concurrent.rb +16 -3
  58. data/lib/farce/internal/blocking_priority_queue.rb +4 -9
  59. data/lib/farce/internal/flag_waiting.rb +1 -1
  60. data/lib/farce/internal/lease_initialization.rb +1 -1
  61. data/lib/farce/internal/lease_pool_state.rb +1 -1
  62. data/lib/farce/internal/lease_state.rb +2 -2
  63. data/lib/farce/internal/local_executor_methods.rb +3 -3
  64. data/lib/farce/internal/map_value_modes.rb +9 -2
  65. data/lib/farce/internal/pool_settings.rb +9 -0
  66. data/lib/farce/internal/pool_worker.rb +34 -21
  67. data/lib/farce/internal/reservation_waiting.rb +1 -1
  68. data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
  69. data/lib/farce/internal/weak_map_value_modes.rb +5 -0
  70. data/lib/farce/internal.rb +8 -8
  71. data/lib/farce/lazy.rb +3 -0
  72. data/lib/farce/lazy_ref.rb +3 -0
  73. data/lib/farce/local/lease.rb +2 -9
  74. data/lib/farce/local/lease_map.rb +2 -9
  75. data/lib/farce/local/pool.rb +38 -5
  76. data/lib/farce/local/publisher.rb +30 -0
  77. data/lib/farce/local/scheduler.rb +18 -5
  78. data/lib/farce/local/vector.rb +3 -2
  79. data/lib/farce/memoize.rb +314 -0
  80. data/lib/farce/pool.rb +5 -0
  81. data/lib/farce/priority_queue.rb +1 -1
  82. data/lib/farce/publisher.rb +57 -0
  83. data/lib/farce/signal.rb +1 -1
  84. data/lib/farce/strict/publisher.rb +31 -0
  85. data/lib/farce/strict/vector.rb +3 -2
  86. data/lib/farce/subscription/definitions.rb +27 -0
  87. data/lib/farce/subscription.rb +121 -0
  88. data/lib/farce/transaction/map.rb +35 -1
  89. data/lib/farce/unshared/pool.rb +1 -1
  90. data/lib/farce/unshared/publisher.rb +25 -0
  91. data/lib/farce/unshared/scheduler.rb +3 -3
  92. data/lib/farce/unshared/vector.rb +3 -2
  93. data/lib/farce/vector.rb +12 -9
  94. data/lib/farce/version.rb +1 -1
  95. data/lib/farce/walker/definitions.rb +13 -2
  96. data/lib/farce/weak_atom.rb +5 -2
  97. data/lib/farce.rb +3 -1
  98. metadata +19 -2
data/lib/farce/config.rb CHANGED
@@ -36,6 +36,40 @@ module Farce
36
36
  # Can also be set via `FARCE_ADDITIONAL_THREAD_POOL_SIZE`. Defaults to 2.
37
37
  attr_reader :additional_thread_pool_size
38
38
 
39
+ # Minimum workers retained by {Farce.in_parallel} on CRuby. Defaults to 0.
40
+ # Configure before the first scheduler is created. Ignored on JRuby and TruffleRuby.
41
+ # @return [Integer]
42
+ attr_reader :parallel_pool_min_size
43
+
44
+ # Maximum workers used by {Farce.in_parallel} on CRuby.
45
+ # Defaults to nil, which uses {System.cpu_count} when the pool is created.
46
+ # @return [Integer, nil]
47
+ attr_reader :parallel_pool_max_size
48
+
49
+ # Soft task limit for each worker in {Farce.in_parallel}'s CRuby pool.
50
+ # Defaults to 64. Set to nil to disable the limit.
51
+ # @return [Integer, nil]
52
+ attr_reader :parallel_pool_max_inflight
53
+
54
+ # Queue wait in seconds before {Farce.in_parallel}'s CRuby pool adds a worker.
55
+ # Defaults to 0.005.
56
+ # @return [Float]
57
+ attr_reader :parallel_pool_grow_after
58
+
59
+ # Idle time in seconds before an extra worker in {Farce.in_parallel}'s CRuby pool retires.
60
+ # Defaults to 30. Set to nil to disable idle retirement.
61
+ # @return [Float, nil]
62
+ attr_reader :parallel_pool_shrink_after
63
+
64
+ # Pending-task capacity of {Farce.in_parallel}'s CRuby pool.
65
+ # Defaults to 1024. Set to nil for an unbounded queue.
66
+ # @return [Integer, nil]
67
+ attr_reader :parallel_pool_capacity
68
+
69
+ PARALLEL_POOL_DEFAULTS = { max_size: nil, min_size: 0, max_inflight: 64,
70
+ grow_after: 0.005, shrink_after: 30, capacity: 1024 }.freeze
71
+ private_constant :PARALLEL_POOL_DEFAULTS
72
+
39
73
  # @yield [config] block to modify the configuration
40
74
  # @yieldparam config [Config] the configuration object to configure
41
75
  def initialize
@@ -44,21 +78,29 @@ module Farce
44
78
  self.fiber_scheduler = ENV["FARCE_FIBER_SCHEDULER"] unless instance_variable_defined?(:@fiber_scheduler)
45
79
  self.main_thread_pool_size ||= ENV.fetch("FARCE_MAIN_THREAD_POOL_SIZE", 4)
46
80
  self.additional_thread_pool_size ||= ENV.fetch("FARCE_ADDITIONAL_THREAD_POOL_SIZE", 2)
81
+ initialize_parallel_pool
47
82
  end
48
83
 
49
84
  # @api private
50
85
  def marshal_dump
51
- [1, autoload_integrations, fiber_scheduler, main_thread_pool_size, additional_thread_pool_size, frozen?]
86
+ [1, autoload_integrations, fiber_scheduler, main_thread_pool_size, additional_thread_pool_size,
87
+ parallel_pool_options, frozen?]
52
88
  end
53
89
 
54
90
  # @api private
55
91
  def marshal_load(data)
56
- autoload, scheduler, main_size, additional_size, frozen = Internal::MarshalSupport.payload(data, 5)
92
+ if data.is_a?(Array) && data.length == 6
93
+ autoload, scheduler, main_size, additional_size, frozen = Internal::MarshalSupport.payload(data, 5)
94
+ options = {}
95
+ else
96
+ autoload, scheduler, main_size, additional_size, options, frozen = Internal::MarshalSupport.payload(data, 6)
97
+ end
57
98
 
58
99
  self.autoload_integrations = autoload
59
100
  self.fiber_scheduler = scheduler
60
101
  self.main_thread_pool_size = main_size
61
102
  self.additional_thread_pool_size = additional_size
103
+ initialize_parallel_pool(options)
62
104
  freeze if frozen
63
105
  end
64
106
 
@@ -149,6 +191,45 @@ module Farce
149
191
  @additional_thread_pool_size = normalize_thread_pool_size(value)
150
192
  end
151
193
 
194
+ # Sets a non-negative minimum worker count for the shared parallel pool.
195
+ # It must not exceed an explicitly configured maximum.
196
+ def parallel_pool_min_size=(value)
197
+ value = normalize_parallel_pool_size(value, :min_size, minimum: 0)
198
+ if parallel_pool_max_size && value > parallel_pool_max_size
199
+ raise ArgumentError, "parallel pool min_size must not exceed max_size"
200
+ end
201
+ @parallel_pool_min_size = value
202
+ end
203
+
204
+ # Sets a positive maximum worker count, or nil to use the CPU count.
205
+ def parallel_pool_max_size=(value)
206
+ value = normalize_parallel_pool_size(value, :max_size, nil_allowed: true)
207
+ if value && parallel_pool_min_size && value < parallel_pool_min_size
208
+ raise ArgumentError, "parallel pool min_size must not exceed max_size"
209
+ end
210
+ @parallel_pool_max_size = value
211
+ end
212
+
213
+ # Sets a positive soft task limit per worker, or nil to disable it.
214
+ def parallel_pool_max_inflight=(value)
215
+ @parallel_pool_max_inflight = normalize_parallel_pool_size(value, :max_inflight, nil_allowed: true)
216
+ end
217
+
218
+ # Sets a finite, non-negative queue wait before growing the pool.
219
+ def parallel_pool_grow_after=(value)
220
+ @parallel_pool_grow_after = normalize_parallel_pool_duration(value, :grow_after)
221
+ end
222
+
223
+ # Sets a finite, non-negative idle timeout, or nil to disable retirement.
224
+ def parallel_pool_shrink_after=(value)
225
+ @parallel_pool_shrink_after = normalize_parallel_pool_duration(value, :shrink_after, nil_allowed: true)
226
+ end
227
+
228
+ # Sets a positive pending-task capacity, or nil for an unbounded queue.
229
+ def parallel_pool_capacity=(value)
230
+ @parallel_pool_capacity = normalize_parallel_pool_size(value, :capacity, nil_allowed: true)
231
+ end
232
+
152
233
  # Called internally once the configuration is applied.
153
234
  def freeze
154
235
  return self if frozen?
@@ -159,6 +240,35 @@ module Farce
159
240
 
160
241
  private
161
242
 
243
+ def initialize_parallel_pool(options = {})
244
+ PARALLEL_POOL_DEFAULTS.each do |name, default|
245
+ next if instance_variable_defined?(:"@parallel_pool_#{name}")
246
+ public_send(:"parallel_pool_#{name}=", options.fetch(name, default))
247
+ end
248
+ end
249
+
250
+ def parallel_pool_options
251
+ PARALLEL_POOL_DEFAULTS.to_h { |name, _| [name, public_send(:"parallel_pool_#{name}")] }
252
+ end
253
+
254
+ def normalize_parallel_pool_size(value, name, minimum: 1, nil_allowed: false)
255
+ return if value.nil? && nil_allowed
256
+ value = Integer(value, 10) if value.is_a?(String)
257
+ unless value.is_a?(Integer) && value >= minimum
258
+ raise ArgumentError, "parallel pool #{name} must be an integer >= #{minimum}"
259
+ end
260
+ value
261
+ end
262
+
263
+ def normalize_parallel_pool_duration(value, name, nil_allowed: false)
264
+ return if value.nil? && nil_allowed
265
+ value = Float(value)
266
+ raise ArgumentError unless value.finite? && value >= 0
267
+ value
268
+ rescue ArgumentError, TypeError
269
+ raise ArgumentError, "parallel pool #{name} must be finite and non-negative"
270
+ end
271
+
162
272
  def normalize_thread_pool_size(value)
163
273
  size = value.is_a?(String) ? Integer(value, 10) : value
164
274
  raise ArgumentError, "thread pool size must be a positive integer" unless size.is_a?(Integer) && size.positive?
data/lib/farce/counter.rb CHANGED
@@ -13,22 +13,6 @@ module Farce
13
13
  #
14
14
  # On CRuby, subclasses must not add Ruby instance variables.
15
15
  #
16
- # @!method initial
17
- # @return [Integer] The initial value retained by reset and copies.
18
- #
19
- # @!method value
20
- # @return [Integer] The current value of the counter.
21
- #
22
- # @!method increment(by = 1)
23
- # Increment the counter by the given amount (default is 1).
24
- # @param [Numeric, String, #to_int] by The amount, converted to an Integer.
25
- # @return [self] Returns self for chaining.
26
- #
27
- # @!method decrement(by = 1)
28
- # Decrement the counter by the given amount (default is 1).
29
- # @param [Numeric, String, #to_int] by The amount, converted to an Integer.
30
- # @return [self] Returns self for chaining.
31
- #
32
16
  # @example Cross-Ractor counting
33
17
  # counter = Farce::Counter.new
34
18
  # counter.value # => 0
@@ -28,7 +28,8 @@ module Farce
28
28
  end
29
29
 
30
30
  def eql?(other)
31
- result = other.is_a?(Key) && value.eql?(other.value)
31
+ result = other.is_a?(Key) &&
32
+ (BASIC_OBJECT_EQUAL_METHOD.bind_call(value, other.value) || value.eql?(other.value))
32
33
  owner = mutation_owner || (other.mutation_owner if other.is_a?(Key))
33
34
  Internal::Freeze.check(owner) if owner
34
35
  result
@@ -218,11 +219,11 @@ module Farce
218
219
  # Yield presence and value under the key reservation.
219
220
  # MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
220
221
  # Return whether a change committed.
221
- def modify(key) # rubocop:disable Naming/PredicateMethod
222
+ def modify(key, timeout: nil)
222
223
  raise LocalJumpError, "no block given" unless block_given?
223
224
  check_frozen!
224
225
  wrapped = wrap_key(key)
225
- _, result = with_mutating_key_operation(wrapped, nil) do |reservation|
226
+ completed, result = with_mutating_key_operation(wrapped, timeout_deadline(timeout)) do |reservation|
226
227
  present, current = native_operation { [concurrent_key?(wrapped), concurrent_get(wrapped)] }
227
228
  value = yield(present, current)
228
229
  next false if MAP_KEEP.equal?(value)
@@ -235,7 +236,7 @@ module Farce
235
236
  changed!
236
237
  end
237
238
  end
238
- !!result
239
+ completed && !!result
239
240
  end
240
241
 
241
242
  def upsert(key, initial_value, timeout: nil)
@@ -429,7 +430,8 @@ module Farce
429
430
  native_operation { concurrent_get(wrapped) }
430
431
  end
431
432
  return fallback&.call unless completed
432
- ready = non_nil ? !current.nil? : !values_equal?(current, expected)
433
+ ready = non_nil ? !current.nil? :
434
+ Internal.value_changed?(current, expected, compare_values_by_identity?)
433
435
  return current if ready
434
436
  return fallback&.call unless wait_for_signal(@change_signal, generation, deadline)
435
437
  end
@@ -438,7 +440,7 @@ module Farce
438
440
  def wait_for_signal(signal, generation, deadline)
439
441
  return signal.wait(generation) unless deadline
440
442
 
441
- remaining = deadline - Clock.now
443
+ remaining = Clock.remaining(deadline)
442
444
  return false unless remaining.positive?
443
445
  !signal.wait(generation, timeout: remaining).nil?
444
446
  end
@@ -90,7 +90,7 @@ module Farce
90
90
  current = generation
91
91
  return current unless current == observed
92
92
 
93
- remaining = deadline - Clock.now if deadline
93
+ remaining = Clock.remaining(deadline)
94
94
  return yield if remaining && !remaining.positive?
95
95
 
96
96
  # Java Phaser waits do not deliver Ruby cancellation until they return.
@@ -131,7 +131,7 @@ module Farce
131
131
  current = generation
132
132
  return current unless current == observed
133
133
 
134
- remaining = deadline - Clock.now if deadline
134
+ remaining = Clock.remaining(deadline)
135
135
  return yield if remaining && !remaining.positive?
136
136
 
137
137
  scheduler.io_wait(reader, READABLE, remaining)
Binary file
@@ -61,11 +61,11 @@ module Farce
61
61
  native_signal = signal if RUBY_ENGINE == "jruby" &&
62
62
  JVMContainers.identical?(JVMContainers::OBJECT_CLASS_METHOD.bind_call(signal), Signal)
63
63
  core = JVMExtension::PriorityQueue.new(COMPARATOR, SNAPSHOT, IDENTITY, EQUAL,
64
- IDENTICAL, MATCH, committer, SCHEDULED, java_capacity, native_signal, track_age ? true : false)
64
+ IDENTICAL, MATCH, committer, SCHEDULED, java_capacity, Box.new(parsed_capacity), native_signal,
65
+ track_age ? true : false)
65
66
  INITIALIZATION_LOCK.synchronize do
66
67
  Thread.handle_interrupt(INTERRUPT_MASK) do
67
68
  check_initialization
68
- @capacity = parsed_capacity
69
69
  @core = core
70
70
  JVMContainers.freeze_object(self)
71
71
  end
@@ -112,8 +112,18 @@ module Farce
112
112
  def empty? = size.zero?
113
113
 
114
114
  def capacity
115
- storage
116
- @capacity
115
+ storage.getCapacity.value
116
+ rescue JVMExtension::QueueFailure => e
117
+ raise_failure(e)
118
+ end
119
+
120
+ def capacity=(value)
121
+ value = normalize_capacity(value)
122
+ java_capacity = value ? [value, JAVA_CAPACITY_MAX].min : 0
123
+ storage.setCapacity(java_capacity, Box.new(value))
124
+ value
125
+ rescue JVMExtension::QueueFailure => e
126
+ raise_failure(e)
117
127
  end
118
128
 
119
129
  def closed?
@@ -4,6 +4,14 @@
4
4
 
5
5
  module Farce
6
6
  module Internal # :nodoc: all
7
- ParallelScheduler = Pool.new(max_size: System.cpu_count)
7
+ config = FROZEN_CONFIG
8
+ ParallelScheduler = Pool.new(
9
+ min_size: config.parallel_pool_min_size,
10
+ max_size: config.parallel_pool_max_size || System.cpu_count,
11
+ max_inflight: config.parallel_pool_max_inflight,
12
+ grow_after: config.parallel_pool_grow_after,
13
+ shrink_after: config.parallel_pool_shrink_after,
14
+ capacity: config.parallel_pool_capacity,
15
+ )
8
16
  end
9
17
  end
@@ -125,10 +125,7 @@ module Farce
125
125
  command(:add, request)
126
126
  request.ready&.pop
127
127
 
128
- if deadline
129
- remaining = deadline - Clock.now
130
- remaining = 0 if remaining.negative?
131
- end
128
+ remaining = Clock.remaining(deadline)
132
129
 
133
130
  delivery = request.reply.pop(timeout: remaining)
134
131
  if delivery
@@ -134,17 +134,19 @@ module Farce
134
134
  # Yield presence and value under the entry claim.
135
135
  # MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
136
136
  # Return whether a change committed.
137
- def modify(key)
137
+ def modify(key, timeout: nil)
138
138
  raise LocalJumpError, "no block given" unless block_given?
139
139
  check_frozen!
140
140
  key = canonical_key(key)
141
+ deadline = timeout_deadline(timeout)
141
142
  ticket = Object.new.freeze
142
143
  sent = finished = false
143
144
  begin
144
- response = await_response(deadline: nil) do
145
+ response = await_response(deadline:) do
145
146
  sent = true
146
147
  request(:claim, key, ticket, true)
147
148
  end
149
+ return false if response.equal?(TIMED_OUT)
148
150
  present, current, signal = response[1], response[2], response[3]
149
151
  owners = claim_owners
150
152
  owners[signal] = Fiber.current
@@ -306,7 +308,7 @@ module Farce
306
308
 
307
309
  signal, generation = response[1], response[2]
308
310
  reject_recursive_wait!(signal)
309
- timeout = remaining_timeout(deadline)
311
+ timeout = Clock.remaining(deadline)
310
312
  if timeout&.zero?
311
313
  return TIMED_OUT if checked_after_timeout
312
314
  checked_after_timeout = true
@@ -332,11 +334,13 @@ module Farce
332
334
  key = response[5] if response.first == :ok
333
335
  present = response.first == :ok && response[1]
334
336
  current = present ? response[2] : nil
335
- return current if non_nil ? !current.nil? : !values_equal?(current, expected)
337
+
338
+ return current if non_nil ? !current.nil? :
339
+ Internal.value_changed?(current, expected, compare_values_by_identity?)
336
340
 
337
341
  signal = response.first == :ok ? response[3] : response[1]
338
342
  generation = response.first == :ok ? response[4] : response[2]
339
- changed = signal.wait_until_changed(generation, timeout: remaining_timeout(deadline)) { TIMED_OUT }
343
+ changed = signal.wait_until_changed(generation, timeout: Clock.remaining(deadline)) { TIMED_OUT }
340
344
  if changed.equal?(TIMED_OUT)
341
345
  return fallback&.call if checked_after_timeout
342
346
  checked_after_timeout = true
@@ -381,13 +385,6 @@ module Farce
381
385
  Clock.now + timeout
382
386
  end
383
387
 
384
- def remaining_timeout(deadline)
385
- return unless deadline
386
-
387
- remaining = deadline - Clock.now
388
- remaining.positive? ? remaining : 0
389
- end
390
-
391
388
  def validate_boolean(value, name)
392
389
  return if value.equal?(true) || value.equal?(false)
393
390
  raise ArgumentError, "#{name} must be true or false"
@@ -77,7 +77,7 @@ module Farce
77
77
  def wait_with_condition(waiter, deadline)
78
78
  @mutex.synchronize do
79
79
  until waiter.matched
80
- remaining = deadline - Clock.now if deadline
80
+ remaining = Clock.remaining(deadline)
81
81
  return cancel(waiter) if remaining && !remaining.positive?
82
82
  waiter.condition.wait(@mutex, remaining)
83
83
  end
@@ -91,7 +91,7 @@ module Farce
91
91
  if waiter.matched
92
92
  [nil, waiter.received]
93
93
  else
94
- remaining = deadline - Clock.now if deadline
94
+ remaining = Clock.remaining(deadline)
95
95
  remaining && !remaining.positive? ? [nil, cancel(waiter)] : [WAITING, remaining]
96
96
  end
97
97
  end
@@ -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