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,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
class Subscription
|
|
7
|
+
Farce.on_main do
|
|
8
|
+
ractors = Internal.native_ractors? ? [::Ractor] : [Internal::FakeRactor::Main, Internal::FakeRactor::NotMain]
|
|
9
|
+
|
|
10
|
+
define(Internal::BasePort, *ractors) { subscriber.send(it) }
|
|
11
|
+
define(Abstract::Publisher) { subscriber.publish(it) }
|
|
12
|
+
define(Abstract::Queue) { subscriber.try_push(it) }
|
|
13
|
+
define(Internal::Counter, Local::Counter) { |_| subscriber.increment }
|
|
14
|
+
define(Internal::Flag, Local::Flag) { |_| subscriber.set }
|
|
15
|
+
define(Abstract::Atom) { subscriber.value = it }
|
|
16
|
+
define(Abstract::Set, Abstract::Vector) { subscriber << it }
|
|
17
|
+
define(Signal) { |_| subscriber.broadcast }
|
|
18
|
+
|
|
19
|
+
define(::Thread::Queue) { subscriber.push(it) }
|
|
20
|
+
define(::Thread::SizedQueue) do |payload|
|
|
21
|
+
subscriber.push(payload, true)
|
|
22
|
+
rescue ThreadError
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# A publisher subscription that can be paused or permanently canceled.
|
|
7
|
+
# Disabling skips new messages. Unsubscribing drains an existing callback queue before stopping it.
|
|
8
|
+
class Subscription
|
|
9
|
+
include Internal::Freeze::Unfreezable
|
|
10
|
+
include Internal::Noncopyable
|
|
11
|
+
|
|
12
|
+
# Raised when an operation is attempted on an unsubscribed subscription.
|
|
13
|
+
UnsubscribedError = Class.new(StandardError)
|
|
14
|
+
|
|
15
|
+
REGISTER = ClassMirror.new(self)
|
|
16
|
+
private_constant :REGISTER
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
protected :new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Register delivery behavior for subscriber classes from the main Ractor.
|
|
23
|
+
# Subclasses inherit the nearest registered delivery method.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# Farce::Subscription.define(MyInbox) { |payload| subscriber.push(payload) }
|
|
27
|
+
#
|
|
28
|
+
# @param classes [Array<Class>] the subscriber classes to support
|
|
29
|
+
# @yieldparam payload [BasicObject] the published payload
|
|
30
|
+
# @yieldreceiver [Subscription] the subscription whose {#subscriber} receives the payload
|
|
31
|
+
# @note The block must be convertible to a shareable proc.
|
|
32
|
+
# @return [void]
|
|
33
|
+
def self.define(*classes, &)
|
|
34
|
+
raise LocalJumpError, "no block given" unless block_given?
|
|
35
|
+
definition = Internal.prepare_method_definition(&)
|
|
36
|
+
classes.each do |klass|
|
|
37
|
+
REGISTER.define(klass) do
|
|
38
|
+
define_method(:deliver, definition)
|
|
39
|
+
private :deliver
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @api private
|
|
45
|
+
def self.build(publisher, subscriber, ...)
|
|
46
|
+
handler = REGISTER[subscriber.class]
|
|
47
|
+
unless handler.private_method_defined?(:deliver)
|
|
48
|
+
raise ArgumentError, "Unsupported subscriber type: #{subscriber.class}"
|
|
49
|
+
end
|
|
50
|
+
handler.new(publisher, subscriber, ...)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [BasicObject] the subscriber associated with this subscription
|
|
54
|
+
attr_reader :subscriber
|
|
55
|
+
|
|
56
|
+
# @return [Farce::Abstract::Publisher] the publisher associated with this subscription
|
|
57
|
+
attr_reader :publisher
|
|
58
|
+
|
|
59
|
+
# @api private
|
|
60
|
+
attr_reader :key
|
|
61
|
+
|
|
62
|
+
# @!visibility private
|
|
63
|
+
def initialize(publisher, subscriber, map, callback: false, source: nil)
|
|
64
|
+
# Keep Proc identities alive without making their captured state shareable.
|
|
65
|
+
@source = source && Envelope.new(source, mode: :local)
|
|
66
|
+
@callback = callback
|
|
67
|
+
@enabled = Flag.new(true)
|
|
68
|
+
@key = (source || subscriber).__id__
|
|
69
|
+
@map = map
|
|
70
|
+
@publisher = publisher
|
|
71
|
+
@subscribed = Flag.new(true)
|
|
72
|
+
@subscriber = subscriber
|
|
73
|
+
super()
|
|
74
|
+
__freeze__
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @api private
|
|
78
|
+
def publish(payload)
|
|
79
|
+
return unless enabled?
|
|
80
|
+
deliver(payload)
|
|
81
|
+
true
|
|
82
|
+
rescue ::ClosedQueueError, Ractor::ClosedError, FrozenError
|
|
83
|
+
unsubscribe
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Boolean] whether the subscription is currently enabled
|
|
88
|
+
def enabled? = @subscribed.value && @enabled.value
|
|
89
|
+
|
|
90
|
+
# @return [Boolean] whether the subscription has not been permanently canceled
|
|
91
|
+
def subscribed? = @subscribed.value
|
|
92
|
+
|
|
93
|
+
# Enables the subscription, allowing it to receive published payloads.
|
|
94
|
+
# @raise [UnsubscribedError] if the subscription has been unsubscribed
|
|
95
|
+
# @return [void]
|
|
96
|
+
def enable
|
|
97
|
+
raise UnsubscribedError, "Cannot enable a subscription that has been unsubscribed" unless @subscribed.value
|
|
98
|
+
@enabled.value = true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Disables the subscription, preventing it from receiving published payloads.
|
|
102
|
+
# @return [void]
|
|
103
|
+
def disable = @enabled.value = false
|
|
104
|
+
|
|
105
|
+
# Removes the subscription permanently, allowing it and the subscriber to be garbage collected.
|
|
106
|
+
# @return [BasicObject, nil] the subscriber, or nil if already unsubscribed
|
|
107
|
+
def unsubscribe
|
|
108
|
+
return unless @subscribed.compare_and_set(true, false)
|
|
109
|
+
@map.delete_pair(key, self)
|
|
110
|
+
subscriber.seal if @callback
|
|
111
|
+
subscriber
|
|
112
|
+
end
|
|
113
|
+
alias cancel unsubscribe
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
define_method(:__freeze__, Kernel.instance_method(:freeze))
|
|
118
|
+
|
|
119
|
+
require "farce/subscription/definitions"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -16,8 +16,42 @@ module Farce
|
|
|
16
16
|
class Map < Abstract::ConcurrentMap
|
|
17
17
|
include MapOperations
|
|
18
18
|
|
|
19
|
+
# (see Abstract::ConcurrentMap#store_if_present)
|
|
20
|
+
# @param mode [Symbol, nil] The result's transfer mode, or nil to use the default.
|
|
21
|
+
def store_if_present(key, mode: nil)
|
|
22
|
+
require_block!(block_given?)
|
|
23
|
+
write do
|
|
24
|
+
canonical = prepare(key)
|
|
25
|
+
next unless @working.key?(canonical)
|
|
26
|
+
replacement = yield(unwrap_stored(@working[canonical]))
|
|
27
|
+
unwrap_stored(store_value(canonical, wrap(replacement, mode:)))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# (see Abstract::ConcurrentMap#replace_if_exists)
|
|
32
|
+
# @param mode [Symbol, nil] The replacement's transfer mode, or nil to use the default.
|
|
33
|
+
def replace_if_exists(key, replacement, mode: nil)
|
|
34
|
+
write do
|
|
35
|
+
canonical = prepare(key)
|
|
36
|
+
next unless @working.key?(canonical)
|
|
37
|
+
previous = @working[canonical]
|
|
38
|
+
store_value(canonical, wrap(replacement, mode:))
|
|
39
|
+
unwrap_stored(previous)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# (see Abstract::ConcurrentMap#delete_pair)
|
|
44
|
+
# A failed comparison invalidates the transaction attempt.
|
|
45
|
+
def delete_pair(key, expected)
|
|
46
|
+
removed = modify_entry(key) do |present, value|
|
|
47
|
+
present && values_equal?(value, expected) ? Internal::MAP_DELETE : Internal::MAP_KEEP
|
|
48
|
+
end
|
|
49
|
+
compared(removed)
|
|
50
|
+
end
|
|
51
|
+
|
|
19
52
|
Wrapper.inherit(self, *MapOperations::READ_HELPERS, :delete_if, :reject!, :compact!, :keep_if,
|
|
20
|
-
:select!, :filter!, :transform_values!, :merge
|
|
53
|
+
:select!, :filter!, :transform_values!, :merge!, :put, :get_and_set, :compute_if_absent,
|
|
54
|
+
:compute_if_present, :replace_pair, :compute, :merge_pair, :fetch_or_store, :put_if_absent)
|
|
21
55
|
|
|
22
56
|
private def enlist(backend) = @transaction.enlist(backend, per_key: true)
|
|
23
57
|
end
|
data/lib/farce/unshared/pool.rb
CHANGED
|
@@ -36,7 +36,7 @@ module Farce
|
|
|
36
36
|
Internal::LocalScheduledTask.new(args, block)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def schedule_scaler(started, delay =
|
|
39
|
+
def schedule_scaler(started, delay = settings.grow_after)
|
|
40
40
|
Internal::ThreadPoolSupervisor.current.schedule(self, started, delay)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Unshared
|
|
7
|
+
# Unshared variant of {Farce::Publisher}.
|
|
8
|
+
class Publisher < Abstract::Publisher
|
|
9
|
+
include Unshareable
|
|
10
|
+
|
|
11
|
+
# @overload initialize(*subscribers)
|
|
12
|
+
# (see Farce::Abstract#initialize)
|
|
13
|
+
def initialize(...)
|
|
14
|
+
@map = Unshared::Map.new
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :map
|
|
21
|
+
|
|
22
|
+
def default_mode = :local
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -20,12 +20,12 @@ module Farce
|
|
|
20
20
|
# results.pop # => :done
|
|
21
21
|
# scheduler.close
|
|
22
22
|
#
|
|
23
|
-
# @!method schedule(*args, mode: :smart, auto_local: true)
|
|
23
|
+
# @!method schedule(*args, mode: :smart, auto_local: true, timeout: nil)
|
|
24
24
|
# Enqueue a task while retaining its block and argument references.
|
|
25
25
|
# `mode` is ignored when `auto_local` is true. Otherwise, only local and
|
|
26
26
|
# smart modes are accepted.
|
|
27
27
|
# @param (see Farce::Abstract::Executor#schedule)
|
|
28
|
-
# @return [self]
|
|
28
|
+
# @return [self, nil] self when submitted, or nil when the scheduling timeout expires.
|
|
29
29
|
class Scheduler < Abstract::Scheduler
|
|
30
30
|
include Unshareable
|
|
31
31
|
include Internal::LocalExecutorMethods
|
|
@@ -80,7 +80,7 @@ module Farce
|
|
|
80
80
|
def new_queue(capacity:) = Internal::UnsharedQueue.new(capacity:)
|
|
81
81
|
def launch_callback = ->(_scheduler, priority, name) { launch!(name, priority) }
|
|
82
82
|
|
|
83
|
-
def prepare_task(args, block, mode, auto_local)
|
|
83
|
+
def prepare_task(args, block, mode, auto_local, **)
|
|
84
84
|
validate_local_mode(mode, auto_local)
|
|
85
85
|
Internal::LocalScheduledTask.new(args, block)
|
|
86
86
|
end
|
|
@@ -9,10 +9,11 @@ module Farce
|
|
|
9
9
|
include Unshareable
|
|
10
10
|
include Internal::Freeze::Delegated
|
|
11
11
|
|
|
12
|
-
# @param source [Array, nil] Initial values. Values are retained directly.
|
|
12
|
+
# @param source [Array, Integer, nil] Initial values or a size filled with nil. Values are retained directly.
|
|
13
13
|
# @param compare_by_identity [Boolean] Whether values are compared by identity.
|
|
14
|
+
# @raise [ArgumentError] If the size is negative.
|
|
14
15
|
def initialize(source = nil, compare_by_identity: false)
|
|
15
|
-
@vector = Internal::UnsharedVector.new(source, compare_by_identity:)
|
|
16
|
+
@vector = Internal::UnsharedVector.new(normalize_vector_source(source), compare_by_identity:)
|
|
16
17
|
super()
|
|
17
18
|
end
|
|
18
19
|
|
data/lib/farce/vector.rb
CHANGED
|
@@ -20,18 +20,21 @@ module Farce
|
|
|
20
20
|
include Shareable::Delegated
|
|
21
21
|
|
|
22
22
|
# @!macro modes
|
|
23
|
-
# @param source [Array, nil] Initial values. The source array is not retained.
|
|
23
|
+
# @param source [Array, Integer, nil] Initial values or a size filled with nil. The source array is not retained.
|
|
24
24
|
# @param mode [Symbol] The default value transfer mode.
|
|
25
25
|
# @param compare_by_identity [Boolean] Whether values are compared by identity.
|
|
26
|
+
# @raise [ArgumentError] If the size is negative.
|
|
26
27
|
def initialize(source = nil, mode: :smart, compare_by_identity: false)
|
|
27
|
-
|
|
28
|
-
unless
|
|
29
|
-
raise ArgumentError, "compare_by_identity must be true or false"
|
|
30
|
-
end
|
|
28
|
+
source = normalize_vector_source(source)
|
|
29
|
+
raise TypeError, "source must be an Array" unless source.nil? || Integer === source || source.is_a?(Array)
|
|
31
30
|
|
|
32
|
-
@compare_by_identity = compare_by_identity
|
|
31
|
+
@compare_by_identity = compare_by_identity ? true : false
|
|
33
32
|
@manager = ModeManager.new(mode:)
|
|
34
|
-
|
|
33
|
+
if source && !(Integer === source)
|
|
34
|
+
source = source.map { @manager.wrap(it) }
|
|
35
|
+
raise TypeError, "source must be an Array" unless source.nil? || source.is_a?(Array)
|
|
36
|
+
end
|
|
37
|
+
@vector = Internal::Vector.new_for_modes(source)
|
|
35
38
|
super()
|
|
36
39
|
end
|
|
37
40
|
|
|
@@ -51,7 +54,7 @@ module Farce
|
|
|
51
54
|
def marshal_load(data)
|
|
52
55
|
entries, @manager, identity, frozen = Internal::MarshalSupport.payload(data, 4)
|
|
53
56
|
@compare_by_identity = identity
|
|
54
|
-
@vector = Internal::Vector.
|
|
57
|
+
@vector = Internal::Vector.new_for_modes(nil)
|
|
55
58
|
publish_shareable
|
|
56
59
|
entries.each { @vector.push(Internal::MarshalSupport.restore_value(it)) }
|
|
57
60
|
Internal::MarshalSupport.freeze(self, frozen)
|
|
@@ -176,11 +179,18 @@ module Farce
|
|
|
176
179
|
replacement = @manager.wrap(replacement, mode:)
|
|
177
180
|
wrapped = true
|
|
178
181
|
end
|
|
179
|
-
return true if @vector.compare_and_set(index, current, replacement, timeout:
|
|
182
|
+
return true if @vector.compare_and_set(index, current, replacement, timeout: Clock.remaining(deadline))
|
|
180
183
|
return false if nil.equal?(current) || (deadline && Clock.now >= deadline)
|
|
181
184
|
end
|
|
182
185
|
end
|
|
183
186
|
|
|
187
|
+
# (see Farce::Abstract::Vector#wait_until)
|
|
188
|
+
def wait_until(index, timeout: nil)
|
|
189
|
+
raise LocalJumpError, "no block given" unless block_given?
|
|
190
|
+
result = Internal.wait_until(@vector, index, timeout:) { |current| yield(@manager.unwrap(current)) }
|
|
191
|
+
@manager.unwrap(result)
|
|
192
|
+
end
|
|
193
|
+
|
|
184
194
|
# (see Farce::Abstract::Vector#wait_until_changed)
|
|
185
195
|
def wait_until_changed(index, expected, timeout: nil)
|
|
186
196
|
deadline = timeout_deadline(timeout)
|
|
@@ -189,7 +199,7 @@ module Farce
|
|
|
189
199
|
current = @vector[index]
|
|
190
200
|
return @manager.unwrap(current) unless values_equal?(current, expected)
|
|
191
201
|
return if deadline && Clock.now >= deadline
|
|
192
|
-
@vector.wait_until_changed(index, current, timeout:
|
|
202
|
+
@vector.wait_until_changed(index, current, timeout: Clock.remaining(deadline))
|
|
193
203
|
end
|
|
194
204
|
end
|
|
195
205
|
|
|
@@ -243,11 +253,5 @@ module Farce
|
|
|
243
253
|
raise ArgumentError, "timeout must be finite and non-negative" unless timeout.finite? && !timeout.negative?
|
|
244
254
|
Clock.now + timeout
|
|
245
255
|
end
|
|
246
|
-
|
|
247
|
-
def remaining_timeout(deadline)
|
|
248
|
-
return unless deadline
|
|
249
|
-
remaining = deadline - Clock.now
|
|
250
|
-
remaining.negative? ? 0 : remaining
|
|
251
|
-
end
|
|
252
256
|
end
|
|
253
257
|
end
|
data/lib/farce/version.rb
CHANGED
|
@@ -118,8 +118,19 @@ module Farce
|
|
|
118
118
|
if !key.equal?(new_key)
|
|
119
119
|
entries << [key, new_key, result]
|
|
120
120
|
elsif !value.equal?(result)
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
loop do
|
|
122
|
+
retry_update = false
|
|
123
|
+
send_to(target, :modify_entry, key, canonical: true) do |present, current|
|
|
124
|
+
next Internal::MAP_KEEP unless present
|
|
125
|
+
if Internal.value_changed?(current, value, true)
|
|
126
|
+
value = current
|
|
127
|
+
retry_update = true
|
|
128
|
+
next Internal::MAP_KEEP
|
|
129
|
+
end
|
|
130
|
+
result
|
|
131
|
+
end
|
|
132
|
+
break unless retry_update
|
|
133
|
+
# Traverse replacements after releasing the key's update reservation.
|
|
123
134
|
result = walker.visit(value)
|
|
124
135
|
end
|
|
125
136
|
end
|
|
@@ -179,6 +190,24 @@ module Farce
|
|
|
179
190
|
super(object, walker)
|
|
180
191
|
end
|
|
181
192
|
|
|
193
|
+
define(Struct) do |object, walker|
|
|
194
|
+
result = walker.update(object, object.to_a) do |target, results|
|
|
195
|
+
results.each_with_index { |value, index| target[index] = value unless target[index].equal?(value) }
|
|
196
|
+
target
|
|
197
|
+
end
|
|
198
|
+
super(result, walker)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
define(Range) do |object, walker|
|
|
202
|
+
names = object.instance_variables
|
|
203
|
+
values = [object.begin, object.end, *names.map { object.instance_variable_get(it) }]
|
|
204
|
+
walker.update(object, values) do |target, results|
|
|
205
|
+
names.each_with_index { |name, index| target.instance_variable_set(name, results[index + 2]) }
|
|
206
|
+
Internal.initialize_range(target, results[0], results[1], object.exclude_end?)
|
|
207
|
+
target
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
182
211
|
define(Data) do |object, walker|
|
|
183
212
|
walker.rebuild_data(object)
|
|
184
213
|
end
|
|
@@ -172,14 +172,14 @@ module Farce
|
|
|
172
172
|
node ? node.changed == true : (@seen.key?(object) && !@seen[object].equal?(object))
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
-
# Follow pending references to check that hashing cannot reach unfinished
|
|
175
|
+
# Follow pending references to check that hashing cannot reach unfinished values.
|
|
176
176
|
def hashable?(object)
|
|
177
|
-
return true unless @
|
|
177
|
+
return true unless @unfinished_values && !@unfinished_values.empty?
|
|
178
178
|
!Walker.visit(object, constants:, class_variables:) do |value, traversal|
|
|
179
|
-
traversal.return(true) if @
|
|
179
|
+
traversal.return(true) if @unfinished_values.key?(value)
|
|
180
180
|
node = @nodes[value] || @destinations&.[](value)
|
|
181
181
|
if node && node.plans
|
|
182
|
-
traversal.return(true) if @
|
|
182
|
+
traversal.return(true) if @unfinished_values.key?(node.destination)
|
|
183
183
|
node.plans.each { |plan| plan.children.each { traversal.visit(resolved(it)) } }
|
|
184
184
|
else
|
|
185
185
|
traversal.traverse
|
|
@@ -188,10 +188,10 @@ module Farce
|
|
|
188
188
|
end
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
-
# Reject keys and set elements whose
|
|
191
|
+
# Reject keys and set elements whose immutable dependencies are still being built.
|
|
192
192
|
def check_hash_key(object)
|
|
193
193
|
return object if hashable?(object)
|
|
194
|
-
raise ArgumentError, "hash key or set element refers to an unfinished Data value"
|
|
194
|
+
raise ArgumentError, "hash key or set element refers to an unfinished Data or Range value"
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
private
|
|
@@ -217,19 +217,19 @@ module Farce
|
|
|
217
217
|
dirty ||= plan.hash_keys && (index % plan.key_stride).zero? && child && child.changed
|
|
218
218
|
end
|
|
219
219
|
node.changed ||= dirty
|
|
220
|
-
plan.dirty = dirty
|
|
220
|
+
plan.dirty = dirty || (Range === node.destination && @unfinished_values&.key?(node.destination))
|
|
221
221
|
end
|
|
222
222
|
|
|
223
|
-
# Choose a writable destination, copying lazily or allocating unfinished
|
|
223
|
+
# Choose a writable destination, copying lazily or allocating unfinished values.
|
|
224
224
|
def prepare(node, rebuild: false)
|
|
225
225
|
previous = node.destination
|
|
226
226
|
return previous unless rebuild || previous.equal?(node.original)
|
|
227
227
|
object = node.original
|
|
228
228
|
return object if Internal::Noncopyable === object || Abstract::LeaseMap === object
|
|
229
229
|
destination =
|
|
230
|
-
if Data === object
|
|
230
|
+
if Data === object || Range === object
|
|
231
231
|
value = object.class.allocate
|
|
232
|
-
(@
|
|
232
|
+
(@unfinished_values ||= {}.compare_by_identity)[value] = true
|
|
233
233
|
value
|
|
234
234
|
elsif @modify == true
|
|
235
235
|
send_to(object, :frozen?) ? send_to(object, :clone, freeze: false) : object
|
|
@@ -241,6 +241,7 @@ module Farce
|
|
|
241
241
|
(@destinations ||= {}.compare_by_identity)[destination] = node unless destination.equal?(object)
|
|
242
242
|
node.destination = destination
|
|
243
243
|
node.result = destination if !node.skipped && (node.result.equal?(object) || node.result.equal?(previous))
|
|
244
|
+
node.plans&.each { it.dirty = true } if Range === destination
|
|
244
245
|
destination
|
|
245
246
|
end
|
|
246
247
|
|
|
@@ -259,7 +260,7 @@ module Farce
|
|
|
259
260
|
node.destination = result
|
|
260
261
|
node.result = result if node.result.equal?(old)
|
|
261
262
|
end
|
|
262
|
-
@
|
|
263
|
+
@unfinished_values&.delete(node.destination) if Data === node.destination || Range === node.destination
|
|
263
264
|
return unless node.cyclic
|
|
264
265
|
plan.target = node.destination
|
|
265
266
|
plan.applied = plan.results.dup
|
|
@@ -281,6 +282,9 @@ module Farce
|
|
|
281
282
|
if freeze
|
|
282
283
|
node.changed = true unless send_to(node.result, :frozen?)
|
|
283
284
|
prepare(node) if @modify != true && node.result.equal?(node.original) && !send_to(node.result, :frozen?)
|
|
285
|
+
if Range === node.destination && @unfinished_values&.key?(node.destination)
|
|
286
|
+
node.plans&.each { apply(node, it) }
|
|
287
|
+
end
|
|
284
288
|
send_to(node.result, :freeze)
|
|
285
289
|
end
|
|
286
290
|
finalize_node(node) if finalize
|
|
@@ -308,15 +312,25 @@ module Farce
|
|
|
308
312
|
freezing = !node.skipped && (node.freeze_result || @freeze == true)
|
|
309
313
|
needed = node.plans&.any?(&:dirty) || (freezing && @modify != true && !send_to(node.original, :frozen?))
|
|
310
314
|
prepare(node) if needed
|
|
311
|
-
next unless Data === node.destination
|
|
312
|
-
|
|
315
|
+
next unless Data === node.destination || Range === node.destination
|
|
316
|
+
# Rebuild only after this destination's own assignments become stale.
|
|
317
|
+
next unless node.plans&.any? do |plan|
|
|
318
|
+
plan.target.equal?(node.destination) && plan.applied &&
|
|
319
|
+
plan.results.each_with_index.any? { |value, index| !value.equal?(plan.applied[index]) }
|
|
313
320
|
end
|
|
314
321
|
prepare(node, rebuild: true)
|
|
315
322
|
end
|
|
316
323
|
break if component.each_with_index.all? { |node, index| node.destination.equal?(destinations[index]) }
|
|
317
324
|
end
|
|
318
325
|
|
|
319
|
-
component.sort_by
|
|
326
|
+
ordered = component.sort_by do |node|
|
|
327
|
+
if Data === node.destination || Range === node.destination
|
|
328
|
+
2
|
|
329
|
+
else
|
|
330
|
+
node.plans&.any?(&:hash_keys) ? 1 : 0
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
ordered.each do |node|
|
|
320
334
|
@node = node
|
|
321
335
|
@current_object = node.original
|
|
322
336
|
node.plans&.each do |plan|
|
data/lib/farce/walker.rb
CHANGED
|
@@ -140,14 +140,18 @@ module Farce
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
# Transform a graph using the callback's return values.
|
|
143
|
+
#
|
|
143
144
|
# Call {#traverse} to transform an object's children. Returning another value
|
|
144
145
|
# replaces the object without automatically visiting that replacement.
|
|
145
146
|
# Unchanged branches retain their identity, including when copy is enabled.
|
|
147
|
+
#
|
|
146
148
|
# Changed cycles are connected before results are frozen or finalized.
|
|
147
149
|
# A cyclic callback may run again as child replacements become known. Keep
|
|
148
150
|
# callbacks repeatable and put publication or caching in {#finalize}.
|
|
149
151
|
# Use {#freeze_result} instead of freezing a preliminary traversal result.
|
|
152
|
+
#
|
|
150
153
|
# Data values are rebuilt only when members change, using their normal initializer.
|
|
154
|
+
# Ranges are rebuilt when endpoints or instance variables change.
|
|
151
155
|
# Noncopyable coordination objects, such as leases, are updated in place.
|
|
152
156
|
# Constants and class variables are assigned only when their value
|
|
153
157
|
# changes identity. Constant replacement can emit Ruby redefinition warnings.
|
|
@@ -156,6 +160,7 @@ module Farce
|
|
|
156
160
|
#
|
|
157
161
|
# A walk is not an atomic snapshot or update. Coordinate concurrent writers
|
|
158
162
|
# when transforming container structure, especially map keys.
|
|
163
|
+
#
|
|
159
164
|
# @param object [BasicObject] the root object
|
|
160
165
|
# @!macro walker_module_options
|
|
161
166
|
# @param copy [Boolean, Symbol] false to edit mutable objects in place, true to
|
|
@@ -171,7 +176,7 @@ module Farce
|
|
|
171
176
|
# @raise [ArgumentError] if copy is neither a boolean nor a Symbol
|
|
172
177
|
# @raise [ArgumentError] if cyclic callbacks do not converge within 32 passes,
|
|
173
178
|
# or a cyclic finalizer replaces its result
|
|
174
|
-
# @raise [ArgumentError] if a hash key or set element refers to a Data value
|
|
179
|
+
# @raise [ArgumentError] if a hash key or set element refers to a Data or Range value
|
|
175
180
|
# still being constructed. Identity-based containers do not hash their keys.
|
|
176
181
|
def self.modify(object, copy: false, freeze: nil, constants: true, class_variables: true, &callback)
|
|
177
182
|
raise LocalJumpError, "no block given" unless callback
|
|
@@ -263,9 +268,11 @@ module Farce
|
|
|
263
268
|
def remember(object, result) = @seen[object] = result
|
|
264
269
|
|
|
265
270
|
# Visit child values and describe how to assign their replacements.
|
|
271
|
+
#
|
|
266
272
|
# The assignment block only runs during modification and may run again for cycles.
|
|
267
273
|
# It receives a writable target and the resolved children. Do not mutate object
|
|
268
274
|
# outside this block. Return the result of update from a traversal definition.
|
|
275
|
+
#
|
|
269
276
|
# @param object [BasicObject] the object being traversed
|
|
270
277
|
# @param values [Array] child references in assignment order
|
|
271
278
|
# @param hash_keys [Boolean] rebuild when key descendants change in place
|
|
@@ -288,6 +295,7 @@ module Farce
|
|
|
288
295
|
# and requested freezing. Registering another finalizer replaces the first.
|
|
289
296
|
# Cyclic finalizers must preserve identity. Acyclic finalizers may return a
|
|
290
297
|
# canonical replacement, which is propagated to the parent.
|
|
298
|
+
#
|
|
291
299
|
# @yieldparam result [BasicObject] the settled result
|
|
292
300
|
# @yieldparam cyclic [Boolean] whether the node belongs to a cycle
|
|
293
301
|
# @yieldreturn [BasicObject] the final result
|
data/lib/farce/weak_atom.rb
CHANGED
|
@@ -100,14 +100,17 @@ module Farce
|
|
|
100
100
|
matched
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
# (see Abstract::Atom#wait_until)
|
|
104
|
+
def wait_until(timeout: nil, &) = Internal.wait_until(@atom, timeout:, &)
|
|
105
|
+
|
|
103
106
|
# (see Abstract::Atom#wait_until_changed)
|
|
104
107
|
def wait_until_changed(expected, timeout: nil, &fallback)
|
|
105
108
|
deadline = Internal.timeout_deadline(timeout)
|
|
106
109
|
while true
|
|
107
|
-
current = @atom.get(timeout:
|
|
110
|
+
current = @atom.get(timeout: Clock.remaining(deadline)) { TIMED_OUT }
|
|
108
111
|
return fallback&.call if TIMED_OUT.equal?(current)
|
|
109
112
|
return current unless values_equal?(current, expected)
|
|
110
|
-
result = @atom.wait_until_changed(current, timeout:
|
|
113
|
+
result = @atom.wait_until_changed(current, timeout: Clock.remaining(deadline)) { TIMED_OUT }
|
|
111
114
|
return fallback&.call if TIMED_OUT.equal?(result)
|
|
112
115
|
end
|
|
113
116
|
end
|
data/lib/farce/weak_key_map.rb
CHANGED
data/lib/farce/weak_map.rb
CHANGED
data/lib/farce/weak_value_map.rb
CHANGED
data/lib/farce.rb
CHANGED
|
@@ -429,7 +429,9 @@ module Farce
|
|
|
429
429
|
end
|
|
430
430
|
|
|
431
431
|
# Schedules work without waiting for the block to finish.
|
|
432
|
-
# On CRuby, uses a shared Ractor pool with at most {System.cpu_count} workers.
|
|
432
|
+
# On CRuby, uses a shared Ractor pool with at most {System.cpu_count} workers by default.
|
|
433
|
+
# Set {Config}'s `parallel_pool_*` options before creating a scheduler to configure
|
|
434
|
+
# its worker limits, growth and shrink delays, per-worker task limit, and queue capacity.
|
|
433
435
|
# On JRuby and TruffleRuby, starts a new thread for each task.
|
|
434
436
|
#
|
|
435
437
|
# Without a block, returns the shared scheduler. The CRuby pool starts workers
|