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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -2
- data/README.md +3 -1
- data/docs/benchmarks.md +37 -21
- data/docs/compatibility.md +13 -0
- data/docs/data-structures.md +37 -0
- data/docs/integrations.md +1 -0
- data/docs/introduction.md +19 -0
- data/docs/modes.md +2 -0
- data/docs/scheduling.md +35 -3
- data/docs/scopes.md +3 -1
- data/docs/shims.md +53 -0
- data/docs/utilities.md +31 -0
- data/docs/variants.md +9 -0
- data/lib/farce/abstract/concurrent_map.rb +122 -0
- data/lib/farce/abstract/counter.rb +35 -1
- data/lib/farce/abstract/executor.rb +30 -7
- data/lib/farce/abstract/flag.rb +12 -0
- data/lib/farce/abstract/pool.rb +158 -47
- data/lib/farce/abstract/priority_queue.rb +1 -1
- data/lib/farce/abstract/publisher.rb +189 -0
- data/lib/farce/abstract/queue.rb +29 -3
- data/lib/farce/abstract/scheduler.rb +41 -14
- data/lib/farce/abstract/timer_queue.rb +3 -3
- data/lib/farce/abstract/vector.rb +3 -1
- data/lib/farce/atom.rb +9 -2
- data/lib/farce/clock.rb +12 -0
- data/lib/farce/concurrent/count_down_latch.rb +37 -0
- data/lib/farce/concurrent/cyclic_barrier.rb +109 -0
- data/lib/farce/concurrent/event.rb +60 -0
- data/lib/farce/concurrent/fiber_local_var.rb +31 -0
- data/lib/farce/concurrent/local_var.rb +65 -0
- data/lib/farce/concurrent/thread_local_var.rb +28 -0
- data/lib/farce/concurrent/tuple.rb +74 -0
- data/lib/farce/concurrent.rb +50 -0
- data/lib/farce/config.rb +112 -2
- data/lib/farce/counter.rb +0 -16
- data/lib/farce/engine/jruby/map.rb +8 -6
- data/lib/farce/engine/jruby/signal.rb +2 -2
- data/lib/farce/engine/jvm/farce.jar +0 -0
- data/lib/farce/engine/jvm/priority_queue.rb +14 -4
- data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -1
- data/lib/farce/engine/ruby/shared/ractor_selector.rb +1 -4
- data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
- data/lib/farce/engine/shared/exchanger.rb +2 -2
- data/lib/farce/engine/shared/queue.rb +43 -5
- data/lib/farce/engine/shared/ractor_selector.rb +1 -1
- data/lib/farce/engine/shared/strict_map.rb +5 -0
- data/lib/farce/engine/shared/unshared_atom.rb +1 -1
- data/lib/farce/engine/shared/unshared_vector.rb +8 -7
- data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
- data/lib/farce/engine/shared/weak_atom/base.rb +5 -10
- data/lib/farce/engine/shared/weak_map/cell.rb +1 -1
- data/lib/farce/engine/truffleruby/map.rb +8 -6
- data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
- data/lib/farce/engine/truffleruby/signal.rb +1 -1
- data/lib/farce/integrations/concurrent.rb +16 -3
- data/lib/farce/internal/blocking_priority_queue.rb +4 -9
- data/lib/farce/internal/flag_waiting.rb +1 -1
- data/lib/farce/internal/lease_initialization.rb +1 -1
- data/lib/farce/internal/lease_pool_state.rb +1 -1
- data/lib/farce/internal/lease_state.rb +2 -2
- data/lib/farce/internal/local_executor_methods.rb +3 -3
- data/lib/farce/internal/map_value_modes.rb +9 -2
- data/lib/farce/internal/pool_settings.rb +9 -0
- data/lib/farce/internal/pool_worker.rb +34 -21
- data/lib/farce/internal/reservation_waiting.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +2 -2
- data/lib/farce/internal/weak_map_value_modes.rb +5 -0
- data/lib/farce/internal.rb +8 -8
- data/lib/farce/lazy.rb +3 -0
- data/lib/farce/lazy_ref.rb +3 -0
- data/lib/farce/local/lease.rb +2 -9
- data/lib/farce/local/lease_map.rb +2 -9
- data/lib/farce/local/pool.rb +38 -5
- data/lib/farce/local/publisher.rb +30 -0
- data/lib/farce/local/scheduler.rb +18 -5
- data/lib/farce/local/vector.rb +3 -2
- data/lib/farce/memoize.rb +314 -0
- data/lib/farce/pool.rb +5 -0
- data/lib/farce/priority_queue.rb +1 -1
- data/lib/farce/publisher.rb +57 -0
- data/lib/farce/signal.rb +1 -1
- data/lib/farce/strict/publisher.rb +31 -0
- data/lib/farce/strict/vector.rb +3 -2
- data/lib/farce/subscription/definitions.rb +27 -0
- data/lib/farce/subscription.rb +121 -0
- data/lib/farce/transaction/map.rb +35 -1
- data/lib/farce/unshared/pool.rb +1 -1
- data/lib/farce/unshared/publisher.rb +25 -0
- data/lib/farce/unshared/scheduler.rb +3 -3
- data/lib/farce/unshared/vector.rb +3 -2
- data/lib/farce/vector.rb +12 -9
- data/lib/farce/version.rb +1 -1
- data/lib/farce/walker/definitions.rb +13 -2
- data/lib/farce/weak_atom.rb +5 -2
- data/lib/farce.rb +3 -1
- 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,
|
|
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
|
-
|
|
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) &&
|
|
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
|
|
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
|
-
|
|
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? :
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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? :
|
|
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 =
|
|
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
|
|
148
|
+
def modify(key, timeout: nil)
|
|
149
149
|
raise LocalJumpError, "no block given" unless block_given?
|
|
150
150
|
check_frozen!
|
|
151
|
-
|
|
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
|
|
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? :
|
|
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 =
|
|
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:
|
|
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? :
|
|
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
|
|
165
|
-
@changes.wait_until_changed(generation, timeout:
|
|
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
|