farce 0.4.1-java → 0.6.0-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -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 +32 -0
- data/docs/variants.md +9 -0
- data/lib/farce/abstract/atom.rb +5 -1
- data/lib/farce/abstract/concurrent_map.rb +122 -0
- data/lib/farce/abstract/counter.rb +36 -1
- data/lib/farce/abstract/executor.rb +30 -7
- data/lib/farce/abstract/flag.rb +12 -0
- data/lib/farce/abstract/molecule.rb +14 -14
- 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 +15 -2
- data/lib/farce/clock.rb +20 -7
- 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 +5 -15
- data/lib/farce/deduper.rb +12 -4
- data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
- data/lib/farce/engine/jruby/map.rb +8 -6
- data/lib/farce/engine/jruby/signal.rb +2 -2
- data/lib/farce/engine/jvm/counter.rb +0 -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_cache_mirror.rb +56 -0
- data/lib/farce/engine/ruby/shared/weak_map.rb +9 -12
- data/lib/farce/engine/ruby.rb +5 -1
- 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/trie/ignore_case.rb +136 -0
- data/lib/farce/engine/shared/trie.rb +14 -1
- data/lib/farce/engine/shared/trie_builder.rb +19 -8
- data/lib/farce/engine/shared/unshared_atom.rb +1 -1
- data/lib/farce/engine/shared/unshared_vector.rb +11 -8
- data/lib/farce/engine/shared/unshared_weak_map.rb +8 -6
- data/lib/farce/engine/shared/vector.rb +2 -0
- 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/shared.rb +2 -0
- data/lib/farce/engine/truffleruby/map.rb +8 -6
- data/lib/farce/engine/truffleruby/native/counter.rb +0 -2
- data/lib/farce/engine/truffleruby/native/priority_queue.rb +18 -4
- data/lib/farce/engine/truffleruby/signal.rb +1 -1
- data/lib/farce/engine/truffleruby.rb +9 -0
- data/lib/farce/extension.rb +5 -3
- data/lib/farce/integrations/concurrent.rb +16 -3
- data/lib/farce/integrations/sorted_set.rb +1 -0
- data/lib/farce/internal/blocking_priority_queue.rb +4 -9
- data/lib/farce/internal/fake_ractor.rb +1 -1
- data/lib/farce/internal/flag_waiting.rb +1 -1
- data/lib/farce/internal/freeze.rb +1 -5
- 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 +17 -9
- 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 +19 -8
- data/lib/farce/lazy.rb +3 -0
- data/lib/farce/lazy_ref.rb +3 -0
- data/lib/farce/local/counter.rb +4 -10
- 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/map.rb +8 -1
- data/lib/farce/memoize.rb +319 -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/shareable.rb +3 -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/strict/weak_cache.rb +199 -0
- 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 +20 -16
- data/lib/farce/version.rb +1 -1
- data/lib/farce/walker/definitions.rb +31 -2
- data/lib/farce/walker/modification.rb +28 -14
- data/lib/farce/walker.rb +9 -1
- data/lib/farce/weak_atom.rb +5 -2
- data/lib/farce/weak_key_map.rb +1 -1
- data/lib/farce/weak_map.rb +1 -1
- data/lib/farce/weak_value_map.rb +1 -1
- data/lib/farce.rb +3 -1
- metadata +22 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# Module to replace some functionality of `concurrent-ruby` with.
|
|
7
|
+
#
|
|
8
|
+
# Nested in a separate module so it can easily be moved into a dedicated gem
|
|
9
|
+
# (this would only happen on a major release).
|
|
10
|
+
#
|
|
11
|
+
# Not all of them are full drop-in replacements, especially when it comes to matching superclasses.
|
|
12
|
+
#
|
|
13
|
+
# If it matches the subset of functionality you need you could replace this:
|
|
14
|
+
#
|
|
15
|
+
# ```ruby
|
|
16
|
+
# require "concurrent"
|
|
17
|
+
# ```
|
|
18
|
+
#
|
|
19
|
+
# With this:
|
|
20
|
+
#
|
|
21
|
+
# ```ruby
|
|
22
|
+
# require "farce"
|
|
23
|
+
# Concurrent = Farce::Concurrent
|
|
24
|
+
# ```
|
|
25
|
+
#
|
|
26
|
+
# If developing a gem or just part of a larger app, you can and should do this under a namespace rather than globally.
|
|
27
|
+
#
|
|
28
|
+
# @see https://ruby-concurrency.github.io/concurrent-ruby/
|
|
29
|
+
module Concurrent
|
|
30
|
+
include Internal::Autoloads
|
|
31
|
+
|
|
32
|
+
Array = Vector
|
|
33
|
+
Atom = Farce::Atom
|
|
34
|
+
AtomicBoolean = Flag
|
|
35
|
+
AtomicFixnum = Counter
|
|
36
|
+
Exchanger = Farce::Exchanger
|
|
37
|
+
Hash = Farce::Map
|
|
38
|
+
Lock = Farce::Lock
|
|
39
|
+
Map = Farce::Map
|
|
40
|
+
ReadWriteLock = Farce::ReadWriteLock
|
|
41
|
+
|
|
42
|
+
autoload :LockLocalVar, "farce/concurrent/fiber_local_var"
|
|
43
|
+
private_constant :LocalVar
|
|
44
|
+
|
|
45
|
+
Farce.on_main do
|
|
46
|
+
Subscription.define(Event) { |_| subscriber.set }
|
|
47
|
+
Subscription.define(CountDownLatch) { |_| subscriber.count_down }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/farce/config.rb
CHANGED
|
@@ -36,6 +36,40 @@ module Farce
|
|
|
36
36
|
# Can also be set via `FARCE_ADDITIONAL_THREAD_POOL_SIZE`. Defaults to 2.
|
|
37
37
|
attr_reader :additional_thread_pool_size
|
|
38
38
|
|
|
39
|
+
# Minimum workers retained by {Farce.in_parallel} on CRuby. Defaults to 0.
|
|
40
|
+
# Configure before the first scheduler is created. Ignored on JRuby and TruffleRuby.
|
|
41
|
+
# @return [Integer]
|
|
42
|
+
attr_reader :parallel_pool_min_size
|
|
43
|
+
|
|
44
|
+
# Maximum workers used by {Farce.in_parallel} on CRuby.
|
|
45
|
+
# Defaults to nil, which uses {System.cpu_count} when the pool is created.
|
|
46
|
+
# @return [Integer, nil]
|
|
47
|
+
attr_reader :parallel_pool_max_size
|
|
48
|
+
|
|
49
|
+
# Soft task limit for each worker in {Farce.in_parallel}'s CRuby pool.
|
|
50
|
+
# Defaults to 64. Set to nil to disable the limit.
|
|
51
|
+
# @return [Integer, nil]
|
|
52
|
+
attr_reader :parallel_pool_max_inflight
|
|
53
|
+
|
|
54
|
+
# Queue wait in seconds before {Farce.in_parallel}'s CRuby pool adds a worker.
|
|
55
|
+
# Defaults to 0.005.
|
|
56
|
+
# @return [Float]
|
|
57
|
+
attr_reader :parallel_pool_grow_after
|
|
58
|
+
|
|
59
|
+
# Idle time in seconds before an extra worker in {Farce.in_parallel}'s CRuby pool retires.
|
|
60
|
+
# Defaults to 30. Set to nil to disable idle retirement.
|
|
61
|
+
# @return [Float, nil]
|
|
62
|
+
attr_reader :parallel_pool_shrink_after
|
|
63
|
+
|
|
64
|
+
# Pending-task capacity of {Farce.in_parallel}'s CRuby pool.
|
|
65
|
+
# Defaults to 1024. Set to nil for an unbounded queue.
|
|
66
|
+
# @return [Integer, nil]
|
|
67
|
+
attr_reader :parallel_pool_capacity
|
|
68
|
+
|
|
69
|
+
PARALLEL_POOL_DEFAULTS = { max_size: nil, min_size: 0, max_inflight: 64,
|
|
70
|
+
grow_after: 0.005, shrink_after: 30, capacity: 1024 }.freeze
|
|
71
|
+
private_constant :PARALLEL_POOL_DEFAULTS
|
|
72
|
+
|
|
39
73
|
# @yield [config] block to modify the configuration
|
|
40
74
|
# @yieldparam config [Config] the configuration object to configure
|
|
41
75
|
def initialize
|
|
@@ -44,21 +78,29 @@ module Farce
|
|
|
44
78
|
self.fiber_scheduler = ENV["FARCE_FIBER_SCHEDULER"] unless instance_variable_defined?(:@fiber_scheduler)
|
|
45
79
|
self.main_thread_pool_size ||= ENV.fetch("FARCE_MAIN_THREAD_POOL_SIZE", 4)
|
|
46
80
|
self.additional_thread_pool_size ||= ENV.fetch("FARCE_ADDITIONAL_THREAD_POOL_SIZE", 2)
|
|
81
|
+
initialize_parallel_pool
|
|
47
82
|
end
|
|
48
83
|
|
|
49
84
|
# @api private
|
|
50
85
|
def marshal_dump
|
|
51
|
-
[1, autoload_integrations, fiber_scheduler, main_thread_pool_size, additional_thread_pool_size,
|
|
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,21 +13,11 @@ module Farce
|
|
|
13
13
|
#
|
|
14
14
|
# On CRuby, subclasses must not add Ruby instance variables.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# @!method increment(by = 1)
|
|
23
|
-
# Increment the counter by the given amount (default is 1).
|
|
24
|
-
# @param [Numeric, String, #to_int] by The amount, converted to an Integer.
|
|
25
|
-
# @return [self] Returns self for chaining.
|
|
26
|
-
#
|
|
27
|
-
# @!method decrement(by = 1)
|
|
28
|
-
# Decrement the counter by the given amount (default is 1).
|
|
29
|
-
# @param [Numeric, String, #to_int] by The amount, converted to an Integer.
|
|
30
|
-
# @return [self] Returns self for chaining.
|
|
16
|
+
# @example Allocate sequence numbers across threads or Ractors
|
|
17
|
+
# counter = Farce::Counter.new
|
|
18
|
+
# ticket = counter.increment # => 1
|
|
19
|
+
# counter.increment # => 2
|
|
20
|
+
# ticket # => 1
|
|
31
21
|
#
|
|
32
22
|
# @example Cross-Ractor counting
|
|
33
23
|
# counter = Farce::Counter.new
|
data/lib/farce/deduper.rb
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
module Farce
|
|
6
6
|
# Reuse equal frozen values throughout object graphs.
|
|
7
|
-
# Plain strings use Ruby's string deduplication. Arrays, hashes,
|
|
8
|
-
# and cached by default. Other objects are
|
|
9
|
-
# their class matches {#store}.
|
|
7
|
+
# Plain strings use Ruby's string deduplication. Arrays, hashes, Ruby Sets, regular
|
|
8
|
+
# expressions, Data records, and ranges are frozen and cached by default. Other objects are
|
|
9
|
+
# traversed but are not cached unless their class matches {#store}.
|
|
10
|
+
# Modules and classes are skipped by default.
|
|
10
11
|
#
|
|
11
12
|
# Cached objects are held weakly. Keep a reference to a returned object if its
|
|
12
13
|
# identity matters. Shareable results can be reused across Ractors. Other
|
|
@@ -28,7 +29,7 @@ module Farce
|
|
|
28
29
|
@shared = Strict::WeakKeyMap.new
|
|
29
30
|
@local = Local::WeakKeyMap.new
|
|
30
31
|
@skip = Strict::Vector[Module]
|
|
31
|
-
@store = Strict::Vector[Hash, Array, ::Set]
|
|
32
|
+
@store = Strict::Vector[Hash, Array, ::Set, Regexp, Data, Range]
|
|
32
33
|
super
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -68,6 +69,13 @@ module Farce
|
|
|
68
69
|
# Use copy: true to preserve the input. Skipped objects are always returned
|
|
69
70
|
# unchanged. Strings are deduplicated before skip rules are checked.
|
|
70
71
|
# Noncopyable coordination objects follow {Walker.modify}'s in-place behavior.
|
|
72
|
+
#
|
|
73
|
+
# @note
|
|
74
|
+
# Deduplication follows Ruby's `hash` and `eql?` semantics. Equal values
|
|
75
|
+
# may differ in observable details. For example, `Time` values representing
|
|
76
|
+
# the same instant can have different time zones. Deduplicating these values
|
|
77
|
+
# or containers holding them may reuse a different timezone representation.
|
|
78
|
+
#
|
|
71
79
|
# @param object [Object] the root object
|
|
72
80
|
# @param copy [Boolean, Symbol] copying policy passed to {Walker.modify}
|
|
73
81
|
# @param skip [Module, Array<Module>, nil] additional exclusions for this call
|
|
Binary file
|
|
@@ -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)
|
|
@@ -68,7 +68,6 @@ module Farce
|
|
|
68
68
|
check_frozen!
|
|
69
69
|
end
|
|
70
70
|
@counter.addAndGet(delta).tap { @signal.broadcast }
|
|
71
|
-
self
|
|
72
71
|
end
|
|
73
72
|
|
|
74
73
|
def decrement(delta = 1)
|
|
@@ -78,7 +77,6 @@ module Farce
|
|
|
78
77
|
check_frozen!
|
|
79
78
|
end
|
|
80
79
|
@counter.addAndGet(-delta).tap { @signal.broadcast }
|
|
81
|
-
self
|
|
82
80
|
end
|
|
83
81
|
|
|
84
82
|
alias value get
|
|
Binary file
|
|
@@ -61,11 +61,11 @@ module Farce
|
|
|
61
61
|
native_signal = signal if RUBY_ENGINE == "jruby" &&
|
|
62
62
|
JVMContainers.identical?(JVMContainers::OBJECT_CLASS_METHOD.bind_call(signal), Signal)
|
|
63
63
|
core = JVMExtension::PriorityQueue.new(COMPARATOR, SNAPSHOT, IDENTITY, EQUAL,
|
|
64
|
-
IDENTICAL, MATCH, committer, SCHEDULED, java_capacity, native_signal,
|
|
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
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Internal # :nodoc: all
|
|
7
|
+
class WeakCacheMirror
|
|
8
|
+
include Shareable::Delegated
|
|
9
|
+
|
|
10
|
+
CACHED_NIL = Object.new.freeze
|
|
11
|
+
private_constant :CACHED_NIL
|
|
12
|
+
|
|
13
|
+
def initialize(shared)
|
|
14
|
+
@shared = shared
|
|
15
|
+
super()
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def store_if_absent(key, &)
|
|
19
|
+
check_frozen!
|
|
20
|
+
return @shared.store_if_absent(key, &) unless Ractor.shareable?(key)
|
|
21
|
+
|
|
22
|
+
local = local_cache
|
|
23
|
+
value = local[key]
|
|
24
|
+
return CACHED_NIL.equal?(value) ? nil : value unless nil.equal?(value)
|
|
25
|
+
|
|
26
|
+
value = @shared.store_if_absent(key, &)
|
|
27
|
+
local[key] = nil.equal?(value) ? CACHED_NIL : value
|
|
28
|
+
value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fetch(key)
|
|
32
|
+
return @shared.fetch(key) { yield(key) } unless Ractor.shareable?(key)
|
|
33
|
+
|
|
34
|
+
local = local_cache
|
|
35
|
+
value = local[key]
|
|
36
|
+
return CACHED_NIL.equal?(value) ? nil : value unless nil.equal?(value)
|
|
37
|
+
|
|
38
|
+
value = @shared.fetch(key) { return yield(key) }
|
|
39
|
+
local[key] = nil.equal?(value) ? CACHED_NIL : value
|
|
40
|
+
value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# CRuby's weak-map operations hold the GVL. Shared misses serialize generation,
|
|
46
|
+
# and a retained value cannot be replaced, so local hits need no reservation.
|
|
47
|
+
# Only shareable keys enter the mirror, since mutable strings can change meaning.
|
|
48
|
+
def local_cache
|
|
49
|
+
caches = Ractor.store_if_absent(:"Farce::Internal::WeakCacheMirror") { ObjectSpace::WeakKeyMap.new }
|
|
50
|
+
caches[self] ||= ObjectSpace::WeakMap.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def freeze_backend = @shared
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -134,17 +134,19 @@ module Farce
|
|
|
134
134
|
# Yield presence and value under the entry claim.
|
|
135
135
|
# MAP_KEEP and MAP_DELETE are control results. Other results replace the value.
|
|
136
136
|
# Return whether a change committed.
|
|
137
|
-
def modify(key)
|
|
137
|
+
def modify(key, timeout: nil)
|
|
138
138
|
raise LocalJumpError, "no block given" unless block_given?
|
|
139
139
|
check_frozen!
|
|
140
140
|
key = canonical_key(key)
|
|
141
|
+
deadline = timeout_deadline(timeout)
|
|
141
142
|
ticket = Object.new.freeze
|
|
142
143
|
sent = finished = false
|
|
143
144
|
begin
|
|
144
|
-
response = await_response(deadline:
|
|
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"
|
data/lib/farce/engine/ruby.rb
CHANGED
|
@@ -40,10 +40,14 @@ module Farce
|
|
|
40
40
|
raise e unless e.path == "#{ENGINE_PATH}/patch"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
if const_defined?(:NATIVE_WEAK_MAPS, false) && NATIVE_WEAK_MAPS
|
|
44
|
+
def native_weak_maps? = true
|
|
45
|
+
else
|
|
46
|
+
def native_weak_maps? = false
|
|
44
47
|
autoload :WeakMap, "farce/engine/ruby/shared/weak_map"
|
|
45
48
|
autoload :WeakKeyMap, "farce/engine/ruby/shared/weak_map"
|
|
46
49
|
autoload :WeakValueMap, "farce/engine/ruby/shared/weak_map"
|
|
50
|
+
autoload :WeakCacheMirror, "farce/engine/ruby/shared/weak_cache_mirror"
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
StrictAtom = Atom
|
|
@@ -77,7 +77,7 @@ module Farce
|
|
|
77
77
|
def wait_with_condition(waiter, deadline)
|
|
78
78
|
@mutex.synchronize do
|
|
79
79
|
until waiter.matched
|
|
80
|
-
remaining =
|
|
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)) }
|