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
|
@@ -32,7 +32,8 @@ module Farce
|
|
|
32
32
|
|
|
33
33
|
wait(timeout) do
|
|
34
34
|
raise_unwritable if sealed?
|
|
35
|
-
|
|
35
|
+
limit = capacity
|
|
36
|
+
!limit || size < limit
|
|
36
37
|
end
|
|
37
38
|
end
|
|
38
39
|
|
|
@@ -76,7 +77,7 @@ module Farce
|
|
|
76
77
|
while true
|
|
77
78
|
return true if internal_queue.push(priority, value)
|
|
78
79
|
raise ThreadError, "queue full" if non_block
|
|
79
|
-
return false unless wait_push(timeout:
|
|
80
|
+
return false unless wait_push(timeout: Clock.remaining(deadline))
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
83
|
|
|
@@ -98,7 +99,7 @@ module Farce
|
|
|
98
99
|
generation = internal_signal.generation
|
|
99
100
|
raise ::Farce::ClosedQueueError, "queue is closed" if closed?
|
|
100
101
|
return true if yield
|
|
101
|
-
return false unless internal_signal.wait(generation, timeout:
|
|
102
|
+
return false unless internal_signal.wait(generation, timeout: Clock.remaining(deadline))
|
|
102
103
|
end
|
|
103
104
|
end
|
|
104
105
|
|
|
@@ -111,12 +112,6 @@ module Farce
|
|
|
111
112
|
def timeout_at(timeout)
|
|
112
113
|
Clock.timeout(normalize_timeout(timeout)) unless timeout.nil?
|
|
113
114
|
end
|
|
114
|
-
|
|
115
|
-
def remaining_timeout(timeout_at)
|
|
116
|
-
return unless timeout_at
|
|
117
|
-
timeout = timeout_at - Clock.now
|
|
118
|
-
timeout.positive? ? timeout : 0
|
|
119
|
-
end
|
|
120
115
|
end
|
|
121
116
|
end
|
|
122
117
|
end
|
|
@@ -21,7 +21,7 @@ module Farce
|
|
|
21
21
|
observed = signal.generation
|
|
22
22
|
current = value
|
|
23
23
|
return current unless expected == current
|
|
24
|
-
break unless signal.wait(observed, timeout:
|
|
24
|
+
break unless signal.wait(observed, timeout: Clock.remaining(deadline))
|
|
25
25
|
end
|
|
26
26
|
yield if block_given?
|
|
27
27
|
end
|
|
@@ -8,11 +8,7 @@ module Farce
|
|
|
8
8
|
module Freeze
|
|
9
9
|
# Structurally freeze a Ruby facade, then validate and publish its object graph.
|
|
10
10
|
# This deliberately bypasses an override that controls logical frozen state.
|
|
11
|
-
def self.publish(object)
|
|
12
|
-
Object.instance_method(:freeze).bind_call(object)
|
|
13
|
-
::Ractor.make_shareable(object) if Internal.native_ractors?
|
|
14
|
-
object
|
|
15
|
-
end
|
|
11
|
+
def self.publish(object) = Internal.publish_shareable(object)
|
|
16
12
|
|
|
17
13
|
def self.check(object)
|
|
18
14
|
return object unless object.frozen?
|
|
@@ -31,7 +31,7 @@ module Farce
|
|
|
31
31
|
reject_recursive_initialization!
|
|
32
32
|
return unless wait
|
|
33
33
|
|
|
34
|
-
remaining =
|
|
34
|
+
remaining = Clock.remaining(deadline)
|
|
35
35
|
return if remaining && !remaining.positive?
|
|
36
36
|
reject_unscheduled_fiber_wait!
|
|
37
37
|
@signal.wait(observed, timeout: wait_interval(remaining))
|
|
@@ -108,7 +108,7 @@ module Farce
|
|
|
108
108
|
return create_resource(factory) if reserve_creation
|
|
109
109
|
return unless wait
|
|
110
110
|
|
|
111
|
-
remaining =
|
|
111
|
+
remaining = Clock.remaining(deadline)
|
|
112
112
|
raise TimeoutError, "lease pool checkout timed out" if remaining && !remaining.positive?
|
|
113
113
|
interval = LeaseWaiting.wait_interval(remaining)
|
|
114
114
|
changed = Thread.handle_interrupt(Exception => :immediate) do
|
|
@@ -156,7 +156,7 @@ module Farce
|
|
|
156
156
|
resource = try_acquire(kind)
|
|
157
157
|
return resource if resource
|
|
158
158
|
|
|
159
|
-
remaining =
|
|
159
|
+
remaining = Clock.remaining(deadline)
|
|
160
160
|
raise TimeoutError, "lease checkout timed out" if remaining && !remaining.positive?
|
|
161
161
|
reject_unscheduled_fiber_wait!
|
|
162
162
|
interval = wait_interval(remaining)
|
|
@@ -180,7 +180,7 @@ module Farce
|
|
|
180
180
|
outcome = try_acquire_block(block)
|
|
181
181
|
return outcome.first if outcome
|
|
182
182
|
|
|
183
|
-
remaining =
|
|
183
|
+
remaining = Clock.remaining(deadline)
|
|
184
184
|
raise TimeoutError, "lease checkout timed out" if remaining && !remaining.positive?
|
|
185
185
|
reject_unscheduled_fiber_wait!
|
|
186
186
|
interval = wait_interval(remaining)
|
|
@@ -16,10 +16,10 @@ module Farce
|
|
|
16
16
|
# Run a task directly, preserving its block, arguments, and calling fiber.
|
|
17
17
|
# `mode` and `auto_local` follow the same validation as {#schedule}.
|
|
18
18
|
# @param (see Abstract::Executor#execute)
|
|
19
|
-
# @return [self]
|
|
20
|
-
def execute(*, mode: :smart, auto_local: true, &)
|
|
19
|
+
# @return [self, nil]
|
|
20
|
+
def execute(*, mode: :smart, auto_local: true, timeout: nil, &)
|
|
21
21
|
validate_local_mode(mode, auto_local)
|
|
22
|
-
super(*, mode: :local, auto_local: true, &)
|
|
22
|
+
super(*, mode: :local, auto_local: true, timeout:, &)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
private
|
|
@@ -22,9 +22,7 @@ module Farce
|
|
|
22
22
|
compare_keys_by_identity: compare_by_identity, compare_values_by_identity: compare_by_identity)
|
|
23
23
|
initial_mapping = convert_entries(initial_mapping)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
valid_comparison = identity.bind_call(compare_values_by_identity, true) ||
|
|
27
|
-
identity.bind_call(compare_values_by_identity, false)
|
|
25
|
+
valid_comparison = true.equal?(compare_values_by_identity) || false.equal?(compare_values_by_identity)
|
|
28
26
|
|
|
29
27
|
raise ArgumentError, "compare_values_by_identity must be a boolean" unless valid_comparison
|
|
30
28
|
@compare_values_by_identity = compare_values_by_identity
|
|
@@ -33,9 +31,11 @@ module Farce
|
|
|
33
31
|
@map = new_map(compare_by_identity:, compare_keys_by_identity:, compare_values_by_identity: true)
|
|
34
32
|
KeyNormalizer.prepare_concurrent(@map)
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
unless nil.equal?(normalize_keys)
|
|
35
|
+
restoring = KeyNormalizer.restoration?(normalize_keys)
|
|
36
|
+
normalizer = KeyNormalizer.build(normalize_keys, shareable: true)
|
|
37
|
+
KeyNormalizer.install_concurrent(self, normalizer) unless restoring
|
|
38
|
+
end
|
|
39
39
|
|
|
40
40
|
initial_mapping&.each { |key, value| self[key] = value }
|
|
41
41
|
KeyNormalizer.install_concurrent(self, normalizer) if restoring
|
|
@@ -143,18 +143,26 @@ module Farce
|
|
|
143
143
|
end
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
+
# (see Abstract::ConcurrentMap#wait_until)
|
|
147
|
+
def wait_until(key, timeout: nil)
|
|
148
|
+
raise LocalJumpError, "no block given" unless block_given?
|
|
149
|
+
result = Internal.wait_until(@map, key, timeout:) { |current| yield(unwrap_value(current)) }
|
|
150
|
+
unwrap_value(result)
|
|
151
|
+
end
|
|
152
|
+
|
|
146
153
|
def wait_until_changed(key, expected, timeout: nil, &)
|
|
147
154
|
prepared_key = @map.normalize_external_key(key)
|
|
148
155
|
expected = wrap_comparison(expected)
|
|
149
156
|
deadline = Internal.timeout_deadline(timeout)
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
# JRuby rejects repeated anonymous block forwarding inside a block.
|
|
158
|
+
while true
|
|
159
|
+
current = @map.get_prepared(prepared_key, timeout: Clock.remaining(deadline)) { TIMED_OUT }
|
|
152
160
|
return unwrap_result(current, &) if TIMED_OUT.equal?(current)
|
|
153
161
|
return unwrap_value(current) unless values_equal?(nil.equal?(current) ? NIL_VALUE : current, expected)
|
|
154
162
|
result = @map.wait_until_changed_prepared(
|
|
155
163
|
prepared_key,
|
|
156
164
|
current,
|
|
157
|
-
timeout:
|
|
165
|
+
timeout: Clock.remaining(deadline),
|
|
158
166
|
) { TIMED_OUT }
|
|
159
167
|
return unwrap_result(result, &) if TIMED_OUT.equal?(result)
|
|
160
168
|
end
|
|
@@ -9,46 +9,59 @@ module Farce
|
|
|
9
9
|
include Shareable::Unfreezable
|
|
10
10
|
|
|
11
11
|
def initialize(pool)
|
|
12
|
-
@pool
|
|
13
|
-
@inflight
|
|
14
|
-
@pressure = pool.min_size < pool.max_size
|
|
12
|
+
@pool = pool
|
|
13
|
+
@inflight = Counter.new
|
|
15
14
|
@registered = Atom.new(true)
|
|
16
15
|
super()
|
|
17
16
|
end
|
|
18
17
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
def wait_for_admission(scheduler, runnable_count)
|
|
22
|
-
return false if @pool.drained?
|
|
23
|
-
limit = @pool.max_inflight
|
|
24
|
-
return true unless limit
|
|
18
|
+
def settings = @pool.settings
|
|
19
|
+
def idle_timeout = settings.shrink_after
|
|
25
20
|
|
|
21
|
+
def wait_for_admission(scheduler, runnable_count, settings = self.settings)
|
|
26
22
|
loop do
|
|
27
23
|
return false if @pool.drained?
|
|
28
|
-
return
|
|
29
|
-
|
|
24
|
+
return settings if @pool.closing?
|
|
25
|
+
count = @pool.size
|
|
26
|
+
limit = settings.max_inflight
|
|
27
|
+
return false if count > settings.max_size && retire?(excess: true)
|
|
28
|
+
# Busy workers keep queued dependencies runnable until they become idle.
|
|
29
|
+
return settings if !limit || count >= settings.max_size || @inflight.value < limit
|
|
30
|
+
return settings if runnable_count && scheduler.public_send(runnable_count).zero?
|
|
30
31
|
|
|
32
|
+
# Read the generation before rechecking state to avoid a lost configuration wakeup.
|
|
31
33
|
generation = @pool.admission_generation
|
|
32
|
-
|
|
34
|
+
settings = self.settings
|
|
35
|
+
count = @pool.size
|
|
36
|
+
limit = settings.max_inflight
|
|
37
|
+
next if @pool.closing? || !limit || count >= settings.max_size || @inflight.value < limit
|
|
33
38
|
@pool.wait_for_admission(generation)
|
|
39
|
+
settings = self.settings
|
|
34
40
|
end
|
|
35
41
|
end
|
|
36
42
|
|
|
37
|
-
def task_started
|
|
38
|
-
@inflight
|
|
39
|
-
@pool.task_taken
|
|
43
|
+
def task_started(settings = self.settings)
|
|
44
|
+
@inflight.increment
|
|
45
|
+
@pool.task_taken(settings)
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def task_finished
|
|
43
|
-
return unless @inflight
|
|
44
49
|
@inflight.decrement
|
|
45
|
-
|
|
50
|
+
settings = self.settings
|
|
51
|
+
count = @pool.size
|
|
52
|
+
if count > settings.max_size
|
|
53
|
+
@inflight.value.zero? ? @pool.wake_retiring_workers : @pool.admission_changed
|
|
54
|
+
elsif settings.max_inflight && count < settings.max_size
|
|
55
|
+
@pool.admission_changed
|
|
56
|
+
end
|
|
46
57
|
end
|
|
47
58
|
|
|
48
|
-
def retire?
|
|
49
|
-
return false unless @inflight
|
|
50
|
-
return false unless @pool.retire_worker
|
|
51
|
-
@registered.compare_and_set(true, false)
|
|
59
|
+
def retire?(excess: false, expected: nil)
|
|
60
|
+
return false unless @inflight.value.zero?
|
|
61
|
+
return false unless @pool.retire_worker(excess:, expected:)
|
|
62
|
+
retired = @registered.compare_and_set(true, false)
|
|
63
|
+
@pool.worker_retired if retired
|
|
64
|
+
retired
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
def stopped(error, replace: true) # rubocop:disable Naming/PredicateMethod
|
|
@@ -9,7 +9,7 @@ module Farce
|
|
|
9
9
|
module ReservationWaiting
|
|
10
10
|
def self.wait(signal, generation, deadline)
|
|
11
11
|
while true
|
|
12
|
-
remaining =
|
|
12
|
+
remaining = Clock.remaining(deadline)
|
|
13
13
|
return false if remaining && !remaining.positive?
|
|
14
14
|
|
|
15
15
|
# LeaseWaiting supplies the full remaining timeout on default engines
|
|
@@ -146,8 +146,8 @@ module Farce
|
|
|
146
146
|
service_policy
|
|
147
147
|
break if idle?
|
|
148
148
|
deadline = @timers.peek_priority
|
|
149
|
-
timeout =
|
|
150
|
-
timeout = 0 if
|
|
149
|
+
timeout = Clock.remaining(deadline)
|
|
150
|
+
timeout = 0 if ready? || !@mailbox.empty?
|
|
151
151
|
dispatch(timeout, @timers.empty? ? @dispatch_budget : 256)
|
|
152
152
|
end
|
|
153
153
|
|
|
@@ -24,6 +24,11 @@ module Farce
|
|
|
24
24
|
super
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def replace_if_exists(key, replacement, mode: nil, **)
|
|
28
|
+
@manager.validate_mode!(mode)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
def store_if_absent(key, mode: nil, **)
|
|
28
33
|
raise LocalJumpError, "no block given" unless block_given?
|
|
29
34
|
@manager.validate_mode!(mode)
|
data/lib/farce/internal.rb
CHANGED
|
@@ -77,18 +77,12 @@ module Farce
|
|
|
77
77
|
Clock.now + timeout
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
def remaining_timeout(deadline)
|
|
81
|
-
return unless deadline
|
|
82
|
-
remaining = deadline - Clock.now
|
|
83
|
-
remaining.negative? ? 0 : remaining
|
|
84
|
-
end
|
|
85
|
-
|
|
86
80
|
# Repeat with one timeout budget. Even a zero timeout permits the first check.
|
|
87
81
|
def with_timeout(timeout)
|
|
88
82
|
raise LocalJumpError, "no block given" unless block_given?
|
|
89
83
|
deadline = timeout_deadline(timeout)
|
|
90
84
|
loop do
|
|
91
|
-
yield
|
|
85
|
+
yield Clock.remaining(deadline), deadline
|
|
92
86
|
return if deadline && Clock.now >= deadline
|
|
93
87
|
end
|
|
94
88
|
end
|
|
@@ -98,10 +92,16 @@ module Farce
|
|
|
98
92
|
with_timeout(timeout) do |_, deadline|
|
|
99
93
|
current = keys.empty? ? receiver.value : receiver[*keys]
|
|
100
94
|
return current if yield(current)
|
|
101
|
-
receiver.wait_until_changed(*keys, current, timeout:
|
|
95
|
+
receiver.wait_until_changed(*keys, current, timeout: Clock.remaining(deadline)) { return }
|
|
102
96
|
end
|
|
103
97
|
end
|
|
104
98
|
|
|
99
|
+
# An unchanged observation must permit waiting even when its equality is non-reflexive.
|
|
100
|
+
def value_changed?(current, expected, identity)
|
|
101
|
+
return false if BasicObject.instance_method(:equal?).bind_call(current, expected)
|
|
102
|
+
identity || !(current == expected) # rubocop:disable Style/InverseMethods -- != can be overridden independently.
|
|
103
|
+
end
|
|
104
|
+
|
|
105
105
|
def garbage_collectable?(object)
|
|
106
106
|
case object
|
|
107
107
|
when Integer, Float, Complex, Rational, Symbol, true, false, nil then false
|
|
@@ -113,9 +113,20 @@ module Farce
|
|
|
113
113
|
def storage_thread(thread) = thread
|
|
114
114
|
def prepare_mutable_numeric(_) = nil
|
|
115
115
|
def prepare_map_access(_klass, _kind) = nil
|
|
116
|
+
def prepare_shareable_class(_base) = nil
|
|
116
117
|
def finalize_engine = nil
|
|
117
118
|
def with_scheduler_thread_group = yield(ThreadGroup.new)
|
|
118
119
|
|
|
120
|
+
def initialize_range(target, first, last, exclude_end)
|
|
121
|
+
target.__send__(:initialize, first, last, exclude_end)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def publish_shareable(object)
|
|
125
|
+
Object.instance_method(:freeze).bind_call(object)
|
|
126
|
+
::Ractor.make_shareable(object) if native_ractors?
|
|
127
|
+
object
|
|
128
|
+
end
|
|
129
|
+
|
|
119
130
|
def delegate(from, to, *methods)
|
|
120
131
|
methods.each do |method|
|
|
121
132
|
if to.is_a?(Module)
|
data/lib/farce/lazy.rb
CHANGED
|
@@ -59,6 +59,9 @@ module Farce
|
|
|
59
59
|
# # Will only compute the expensive attribute once, and return the same value to all Ractors.
|
|
60
60
|
# 5.times { Ractor.new(object) { it.expensive_attribute } }
|
|
61
61
|
# ```
|
|
62
|
+
#
|
|
63
|
+
# @see Farce::Memoize
|
|
64
|
+
# @see Farce::LazyRef
|
|
62
65
|
class Lazy < Farce::Abstract::Lazy
|
|
63
66
|
include Shareable::Tracked
|
|
64
67
|
|
data/lib/farce/lazy_ref.rb
CHANGED
|
@@ -22,6 +22,9 @@ module Farce
|
|
|
22
22
|
# # initialized.value # => 0
|
|
23
23
|
# # lazy_ref[:key] = :value
|
|
24
24
|
# # initialized.value # => 1
|
|
25
|
+
# #
|
|
26
|
+
# # @see Farce::Memoize
|
|
27
|
+
# # @see Farce::Lazy
|
|
25
28
|
# class LazyRef < Farce::Reference
|
|
26
29
|
# # @!macro modes
|
|
27
30
|
# # @overload initialize(factory, mode: :smart)
|
data/lib/farce/local/counter.rb
CHANGED
|
@@ -50,19 +50,13 @@ module Farce
|
|
|
50
50
|
|
|
51
51
|
# Increment the counter atomically.
|
|
52
52
|
# @param by [Numeric, String, #to_int] the amount, converted to an Integer
|
|
53
|
-
# @return [
|
|
54
|
-
def increment(by = 1)
|
|
55
|
-
internal_counter.increment(by)
|
|
56
|
-
self
|
|
57
|
-
end
|
|
53
|
+
# @return [Integer] the value produced by this atomic update
|
|
54
|
+
def increment(by = 1) = internal_counter.increment(by)
|
|
58
55
|
|
|
59
56
|
# Decrement the counter atomically.
|
|
60
57
|
# @param by [Numeric, String, #to_int] the amount, converted to an Integer
|
|
61
|
-
# @return [
|
|
62
|
-
def decrement(by = 1)
|
|
63
|
-
internal_counter.decrement(by)
|
|
64
|
-
self
|
|
65
|
-
end
|
|
58
|
+
# @return [Integer] the value produced by this atomic update
|
|
59
|
+
def decrement(by = 1) = internal_counter.decrement(by)
|
|
66
60
|
|
|
67
61
|
alias subtract decrement
|
|
68
62
|
alias remove decrement
|
data/lib/farce/local/lease.rb
CHANGED
|
@@ -34,10 +34,10 @@ module Farce
|
|
|
34
34
|
# (see Farce::Abstract::Lease#checkout)
|
|
35
35
|
def checkout(timeout: nil, &block)
|
|
36
36
|
deadline = timeout_deadline(timeout)
|
|
37
|
-
lease = scoped_lease(timeout:
|
|
37
|
+
lease = scoped_lease(timeout: Clock.remaining(deadline))
|
|
38
38
|
raise TimeoutError, "lease checkout timed out" unless lease
|
|
39
39
|
|
|
40
|
-
timeout =
|
|
40
|
+
timeout = Clock.remaining(deadline)
|
|
41
41
|
block ? lease.checkout(timeout:, &block) : lease.checkout(timeout:)
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -65,13 +65,6 @@ module Farce
|
|
|
65
65
|
end
|
|
66
66
|
Clock.now + timeout
|
|
67
67
|
end
|
|
68
|
-
|
|
69
|
-
def remaining_timeout(deadline)
|
|
70
|
-
return unless deadline
|
|
71
|
-
|
|
72
|
-
remaining = deadline - Clock.now
|
|
73
|
-
remaining.positive? ? remaining : 0
|
|
74
|
-
end
|
|
75
68
|
end
|
|
76
69
|
end
|
|
77
70
|
end
|
|
@@ -35,10 +35,10 @@ module Farce
|
|
|
35
35
|
|
|
36
36
|
def checkout_canonical(key, missing_key:, timeout: nil, &)
|
|
37
37
|
deadline = timeout_deadline(timeout)
|
|
38
|
-
map = scoped_lease_map(timeout:
|
|
38
|
+
map = scoped_lease_map(timeout: Clock.remaining(deadline))
|
|
39
39
|
raise TimeoutError, "lease checkout timed out" unless map
|
|
40
40
|
|
|
41
|
-
map.checkout(key, timeout:
|
|
41
|
+
map.checkout(key, timeout: Clock.remaining(deadline), receiver: self, missing_key:, &)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def try_checkout_canonical(key, missing_key:, &)
|
|
@@ -68,13 +68,6 @@ module Farce
|
|
|
68
68
|
end
|
|
69
69
|
Clock.now + timeout
|
|
70
70
|
end
|
|
71
|
-
|
|
72
|
-
def remaining_timeout(deadline)
|
|
73
|
-
return unless deadline
|
|
74
|
-
|
|
75
|
-
remaining = deadline - Clock.now
|
|
76
|
-
remaining.positive? ? remaining : 0
|
|
77
|
-
end
|
|
78
71
|
end
|
|
79
72
|
end
|
|
80
73
|
end
|
data/lib/farce/local/pool.rb
CHANGED
|
@@ -7,7 +7,8 @@ module Farce
|
|
|
7
7
|
# A shareable handle with an independent thread pool in each scope.
|
|
8
8
|
#
|
|
9
9
|
# Each pool uses the scaling settings and fiber scheduler configured at
|
|
10
|
-
# construction.
|
|
10
|
+
# construction. Changing settings tunes the current scope's pool. New scopes
|
|
11
|
+
# use the constructor settings. Tasks always keep their block and argument references. Only
|
|
11
12
|
# local and smart modes are accepted when `auto_local` is false. A scheduler
|
|
12
13
|
# constructor must be convertible to a shareable proc for use in each Ractor.
|
|
13
14
|
# {#close} drains only the current scope's pool. {#close_all} drains every pool
|
|
@@ -31,14 +32,45 @@ module Farce
|
|
|
31
32
|
grow_after: 0.005, shrink_after: 30, capacity: 1024, scope: :ractor,
|
|
32
33
|
backend: Internal::FROZEN_CONFIG.io_backend, &constructor)
|
|
33
34
|
initialize_configuration(min_size:, max_size:, max_inflight:, grow_after:, shrink_after:, backend:)
|
|
34
|
-
@
|
|
35
|
-
@
|
|
36
|
-
@
|
|
35
|
+
@capacity = capacity && Integer(capacity)
|
|
36
|
+
@constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
|
|
37
|
+
@pools = Strict::Map.new
|
|
38
|
+
@closed_all = Internal::Flag.new(false)
|
|
37
39
|
@initialization_key = Object.new.freeze
|
|
38
40
|
super(scope:, min_size: @min_size, max_size: @max_size, max_inflight: @max_inflight,
|
|
39
41
|
grow_after: @grow_after, shrink_after: @shrink_after, capacity:, backend:)
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
# Settings changes affect only the current scope. New scopes use constructor defaults.
|
|
45
|
+
# @api private
|
|
46
|
+
def settings = (pool = current_pool) ? pool.settings : @settings.value
|
|
47
|
+
|
|
48
|
+
def capacity = (pool = current_pool) ? pool.capacity : @capacity
|
|
49
|
+
|
|
50
|
+
def min_size=(value)
|
|
51
|
+
scoped_value.min_size = value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def max_size=(value)
|
|
55
|
+
scoped_value.max_size = value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def max_inflight=(value)
|
|
59
|
+
scoped_value.max_inflight = value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def grow_after=(value)
|
|
63
|
+
scoped_value.grow_after = value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def shrink_after=(value)
|
|
67
|
+
scoped_value.shrink_after = value
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def capacity=(value)
|
|
71
|
+
scoped_value.capacity = value
|
|
72
|
+
end
|
|
73
|
+
|
|
42
74
|
# Stop accepting work in this scope and drain its queued tasks.
|
|
43
75
|
# @return [self]
|
|
44
76
|
def close
|
|
@@ -74,7 +106,8 @@ module Farce
|
|
|
74
106
|
def closed? = @closed_all.value || super
|
|
75
107
|
|
|
76
108
|
Internal.delegate(self, :scoped_value, :drained?, :admission_generation, :wait_for_admission,
|
|
77
|
-
:admission_changed, :task_taken, :retire_worker, :
|
|
109
|
+
:admission_changed, :task_taken, :retire_worker, :wake_retiring_workers,
|
|
110
|
+
:worker_retired, :worker_stopped, :scale_if_needed)
|
|
78
111
|
|
|
79
112
|
private
|
|
80
113
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Local
|
|
7
|
+
# Local variant of {Farce::Publisher}.
|
|
8
|
+
class Publisher < Abstract::Publisher
|
|
9
|
+
include Shareable::Unfreezable
|
|
10
|
+
|
|
11
|
+
# The scope with independent contents.
|
|
12
|
+
# @return [Symbol] The configured scope name.
|
|
13
|
+
attr_reader :scope
|
|
14
|
+
|
|
15
|
+
# @overload initialize(*subscribers)
|
|
16
|
+
# (see Farce::Abstract#initialize)
|
|
17
|
+
def initialize(*, scope: :ractor)
|
|
18
|
+
raise ArgumentError, "Invalid scope: #{scope.inspect}" unless Farce::SCOPES.include?(scope)
|
|
19
|
+
@scope = scope
|
|
20
|
+
super(*)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def map = Internal::Storage.store_if_absent(self, scope:) { Unshared::Map.new }
|
|
26
|
+
|
|
27
|
+
def default_mode = :local
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -56,6 +56,7 @@ module Farce
|
|
|
56
56
|
def initialize(capacity: 1024, backend: Internal::FROZEN_CONFIG.io_backend, scope: :ractor,
|
|
57
57
|
worker: nil, &constructor)
|
|
58
58
|
@constructor = prepare_constructor(constructor || Internal::FROZEN_CONFIG.fiber_scheduler_constructor)
|
|
59
|
+
@capacity = capacity && Integer(capacity)
|
|
59
60
|
@backend = backend
|
|
60
61
|
@worker = worker
|
|
61
62
|
@schedulers = Strict::Map.new
|
|
@@ -65,15 +66,26 @@ module Farce
|
|
|
65
66
|
super(scope:, capacity: capacity && Integer(capacity), backend:)
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
# Pending-task capacity in this scope. New scopes retain the constructor value.
|
|
70
|
+
def capacity = (scheduler = current_scheduler) ? scheduler.capacity : @capacity
|
|
71
|
+
|
|
72
|
+
# Resize the queue in this scope without starting a worker.
|
|
73
|
+
def capacity=(value)
|
|
74
|
+
raise SchedulerClosedError, "cannot configure a closed scheduler" if @closed_all.value
|
|
75
|
+
scoped_value.capacity = value
|
|
76
|
+
end
|
|
77
|
+
|
|
68
78
|
# Enqueue a task in the current scope, preserving its block and arguments.
|
|
69
79
|
# `mode` is ignored when `auto_local` is true. Otherwise, only local and smart modes are accepted.
|
|
70
|
-
# @overload schedule(*args, mode: :smart, auto_local: true)
|
|
80
|
+
# @overload schedule(*args, mode: :smart, auto_local: true, timeout: nil)
|
|
71
81
|
# @param (see Farce::Abstract::Executor#schedule)
|
|
72
|
-
# @return [self]
|
|
82
|
+
# @return [self, nil] self when submitted, or nil when the scheduling timeout expires.
|
|
73
83
|
def schedule(...) = enqueue_task(false, ...)
|
|
74
84
|
|
|
75
85
|
# (see Abstract::Scheduler#try_schedule)
|
|
76
|
-
def try_schedule(
|
|
86
|
+
def try_schedule(*, mode: :smart, auto_local: true, &)
|
|
87
|
+
enqueue_task(true, *, mode:, auto_local:, &)
|
|
88
|
+
end
|
|
77
89
|
|
|
78
90
|
# Starts a worker for the current scope. Other scopes retain their creation behavior.
|
|
79
91
|
# @param executor [Class, nil] Thread or a custom executor that stays in this Ractor.
|
|
@@ -147,12 +159,13 @@ module Farce
|
|
|
147
159
|
|
|
148
160
|
private
|
|
149
161
|
|
|
150
|
-
def enqueue_task(non_block, *, mode: :smart, auto_local: true, &block)
|
|
162
|
+
def enqueue_task(non_block, *, mode: :smart, auto_local: true, timeout: nil, &block)
|
|
151
163
|
raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if @closed_all.value
|
|
152
164
|
scheduler = scoped_value
|
|
153
165
|
raise SchedulerClosedError, "cannot schedule task on a closed scheduler" if scheduler.closed?
|
|
154
166
|
raise LocalJumpError, "Cannot schedule without a block" unless block
|
|
155
167
|
|
|
168
|
+
deadline = scheduling_deadline(timeout)
|
|
156
169
|
validate_local_mode(mode, auto_local)
|
|
157
170
|
register_scheduler(scheduler)
|
|
158
171
|
|
|
@@ -160,7 +173,7 @@ module Farce
|
|
|
160
173
|
if non_block
|
|
161
174
|
scheduler.try_schedule(*, mode:, auto_local:, &block)
|
|
162
175
|
else
|
|
163
|
-
scheduler.schedule(*, mode:, auto_local:, &block)
|
|
176
|
+
scheduler.schedule(*, mode:, auto_local:, timeout: Clock.remaining(deadline), &block)
|
|
164
177
|
end
|
|
165
178
|
|
|
166
179
|
submitted && self
|
data/lib/farce/local/vector.rb
CHANGED
|
@@ -8,10 +8,11 @@ module Farce
|
|
|
8
8
|
#
|
|
9
9
|
# @!method initialize(source = nil, compare_by_identity: false, scope: :ractor)
|
|
10
10
|
# @!macro scopes
|
|
11
|
-
# @param source [Array, nil]
|
|
11
|
+
# @param source [Array, Integer, nil] Initial values or a size filled with nil.
|
|
12
12
|
# @param compare_by_identity [Boolean] whether values are compared by identity
|
|
13
13
|
# @param scope [Symbol] the scope of the vector
|
|
14
14
|
# @return [Vector]
|
|
15
|
+
# @raise [ArgumentError] If the size is negative.
|
|
15
16
|
class Vector < Abstract::Vector
|
|
16
17
|
include Shareable::Tracked
|
|
17
18
|
include Scoped::Tracked
|
|
@@ -24,7 +25,7 @@ module Farce
|
|
|
24
25
|
self.class.new(values, compare_by_identity: compare_by_identity?, scope:)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
private def new_scoped_value(
|
|
28
|
+
private def new_scoped_value(source = nil, **) = Internal::UnsharedVector.new(normalize_vector_source(source), **)
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
end
|