farce 0.4.1 → 0.6.0
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/ext/farce/atom.c +23 -7
- data/ext/farce/containers.h +3 -0
- data/ext/farce/counter.c +4 -6
- data/ext/farce/depend +3 -1
- data/ext/farce/extconf.rb +19 -14
- data/ext/farce/farce.c +110 -3
- data/ext/farce/map.c +15 -7
- data/ext/farce/priority_queue.c +46 -1
- data/ext/farce/queue.c +141 -7
- data/ext/farce/trie.c +140 -72
- data/ext/farce/trie_search.h +114 -0
- data/ext/farce/vector.c +53 -21
- data/ext/farce/weak_map.c +16 -11
- data/ext/farce/weak_references.h +23 -0
- data/ext/java/org/farce/FiberScheduler.java +8 -6
- data/ext/java/org/farce/PriorityQueue.java +18 -2
- 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 +23 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Strict
|
|
7
|
+
# Strict variant of {Farce::Publisher}.
|
|
8
|
+
class Publisher < Farce::Abstract::Publisher
|
|
9
|
+
include Shareable::Unfreezable
|
|
10
|
+
|
|
11
|
+
# @overload initialize(*subscribers)
|
|
12
|
+
# (see Farce::Abstract#initialize)
|
|
13
|
+
def initialize(*subscribers)
|
|
14
|
+
@map = Strict::Map.new
|
|
15
|
+
super()
|
|
16
|
+
subscribers.each { subscribe(it) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# (see Farce::Abstract::Publisher#publish)
|
|
20
|
+
def publish(message = nil)
|
|
21
|
+
return super if Ractor.shareable?(message)
|
|
22
|
+
raise Ractor::IsolationError, "value must be Ractor-shareable"
|
|
23
|
+
end
|
|
24
|
+
alias send publish
|
|
25
|
+
alias << publish
|
|
26
|
+
alias broadcast publish
|
|
27
|
+
|
|
28
|
+
private attr_reader :map
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/farce/strict/vector.rb
CHANGED
|
@@ -8,10 +8,11 @@ module Farce
|
|
|
8
8
|
class Vector < Abstract::Vector
|
|
9
9
|
include Shareable::Delegated
|
|
10
10
|
|
|
11
|
-
# @param source [Array, nil] Initial values. The source array is not retained.
|
|
11
|
+
# @param source [Array, Integer, nil] Initial values or a size filled with nil. The source array is not retained.
|
|
12
12
|
# @param compare_by_identity [Boolean] Whether values are compared by identity.
|
|
13
|
+
# @raise [ArgumentError] If the size is negative.
|
|
13
14
|
def initialize(source = nil, compare_by_identity: false)
|
|
14
|
-
@vector = Internal::Vector.new(source, compare_by_identity:)
|
|
15
|
+
@vector = Internal::Vector.new(normalize_vector_source(source), compare_by_identity:)
|
|
15
16
|
super()
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Strict
|
|
7
|
+
# A weak cache that allows reusing previously generated values without holding strong references to them.
|
|
8
|
+
# Only shareable keys and values can be used.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# cache = Farce::Strict::WeakCache.new
|
|
12
|
+
# value = cache.store_if_absent(:key) { ExpensiveClass.new.freeze }
|
|
13
|
+
class WeakCache
|
|
14
|
+
include Shareable::Delegated
|
|
15
|
+
include Internal::Inspect
|
|
16
|
+
|
|
17
|
+
# Creates a new WeakCache instance if it outperforms {Strict::WeakValueMap} on the current platform.
|
|
18
|
+
# Otherwise it creates a standard WeakValueMap instance.
|
|
19
|
+
# @return [WeakCache, Strict::WeakValueMap] The created cache instance.
|
|
20
|
+
def self.create(...) = Internal.native_weak_maps? ? Strict::WeakValueMap.new(...) : WeakCache.new(...)
|
|
21
|
+
|
|
22
|
+
# @param initial_mapping [Hash, Farce::Abstract::Map, #each, nil]
|
|
23
|
+
# The entries to store initially. Each iteration must yield a key-value pair.
|
|
24
|
+
# Nil creates an empty map.
|
|
25
|
+
# @param compare_by_identity [Boolean]
|
|
26
|
+
# Whether keys use object identity instead of `hash` and `eql?`.
|
|
27
|
+
def initialize(initial_mapping = nil, compare_by_identity: false)
|
|
28
|
+
@map = Strict::WeakValueMap.new(initial_mapping, compare_keys_by_identity: compare_by_identity)
|
|
29
|
+
initialize_cache
|
|
30
|
+
super()
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @api private
|
|
34
|
+
def marshal_dump = [1, @map]
|
|
35
|
+
|
|
36
|
+
# @api private
|
|
37
|
+
def marshal_load(data)
|
|
38
|
+
@map, = Internal::MarshalSupport.payload(data, 1)
|
|
39
|
+
initialize_cache
|
|
40
|
+
publish_shareable
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Retrieves or stores a value for the given key.
|
|
44
|
+
# The block is called to generate the value if the key is absent.
|
|
45
|
+
# Concurrent calls for the same key reuse the generated value.
|
|
46
|
+
# Once that value is collected, a later call generates it again.
|
|
47
|
+
#
|
|
48
|
+
# @param key [BasicObject] The key to look up.
|
|
49
|
+
# @yield Called without arguments when the key is absent.
|
|
50
|
+
# @yieldreturn [BasicObject] The value to store and return.
|
|
51
|
+
# @return [BasicObject] The associated value.
|
|
52
|
+
def store_if_absent(key, &)
|
|
53
|
+
raise LocalJumpError, "no block given" unless block_given?
|
|
54
|
+
@cache.store_if_absent(key, &)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
alias compute_if_absent store_if_absent
|
|
58
|
+
|
|
59
|
+
# Store a value only when the key is absent.
|
|
60
|
+
# Existing nil and false values count as present.
|
|
61
|
+
# @param key [BasicObject] The key to look up or store.
|
|
62
|
+
# @param value [BasicObject] The value to store if the key is absent.
|
|
63
|
+
# @return [BasicObject, nil] The existing value, or nil when a value was inserted.
|
|
64
|
+
def put_if_absent(key, value)
|
|
65
|
+
inserted = false
|
|
66
|
+
previous = store_if_absent(key) do
|
|
67
|
+
inserted = true
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
inserted ? nil : previous
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Look up a value without generating it.
|
|
74
|
+
# @param key [BasicObject] The key to look up.
|
|
75
|
+
# @return [BasicObject, nil] The associated value, or nil if the key is absent.
|
|
76
|
+
def [](key) = @cache.fetch(key) { nil } # rubocop:disable Style/RedundantFetchBlock
|
|
77
|
+
|
|
78
|
+
# Return a lookup key and its associated value if present.
|
|
79
|
+
# @param key [BasicObject] The key to look up.
|
|
80
|
+
# @return [Array(BasicObject, BasicObject), nil] The pair `[key, value]`, or nil if the key is absent.
|
|
81
|
+
def assoc(key)
|
|
82
|
+
value = fetch(key) { return nil }
|
|
83
|
+
[key, value]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Return whether a live entry exists, including entries with nil or false values.
|
|
87
|
+
# @param key [BasicObject] The key to look up.
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def key?(key)
|
|
90
|
+
@cache.fetch(key) { return false }
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
alias has_key? key?
|
|
95
|
+
alias include? key?
|
|
96
|
+
alias member? key?
|
|
97
|
+
|
|
98
|
+
# Look up a value in nested maps or arrays.
|
|
99
|
+
# @param key [BasicObject] The key to look up.
|
|
100
|
+
# @param rest [Array<BasicObject>] Additional keys or indices for the nested value.
|
|
101
|
+
# @return [BasicObject, nil] The nested value, or nil if an intermediate value is absent or nil.
|
|
102
|
+
def dig(key, *rest)
|
|
103
|
+
value = self[key]
|
|
104
|
+
return value if rest.empty? || value.nil?
|
|
105
|
+
value.dig(*rest)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Return the stored key matching a lookup key.
|
|
109
|
+
# @param key [BasicObject] The key to look up.
|
|
110
|
+
# @return [BasicObject, nil] The stored key, or nil if the key is absent.
|
|
111
|
+
def getkey(key) = @map.getkey(key)
|
|
112
|
+
|
|
113
|
+
# Fetch the value associated with a key, using the same missing-key behavior as Hash#fetch.
|
|
114
|
+
# If both a default and a block are provided, the block takes precedence.
|
|
115
|
+
#
|
|
116
|
+
# @overload fetch(key)
|
|
117
|
+
# @param key [BasicObject] The key to look up.
|
|
118
|
+
# @return [BasicObject] The associated value.
|
|
119
|
+
# @raise [KeyError] If the key is absent.
|
|
120
|
+
#
|
|
121
|
+
# @overload fetch(key, default)
|
|
122
|
+
# @param key [BasicObject] The key to look up.
|
|
123
|
+
# @param default [BasicObject] The value to return if the key is absent.
|
|
124
|
+
# @return [BasicObject] The associated value or `default`.
|
|
125
|
+
#
|
|
126
|
+
# @overload fetch(key)
|
|
127
|
+
# @param key [BasicObject] The key to look up.
|
|
128
|
+
# @yield [key] Called if the key is absent.
|
|
129
|
+
# @yieldparam key [BasicObject] The missing key.
|
|
130
|
+
# @yieldreturn [BasicObject] The value to return.
|
|
131
|
+
# @return [BasicObject] The associated value or the block result.
|
|
132
|
+
def fetch(*arguments)
|
|
133
|
+
unless arguments.length.between?(1, 2)
|
|
134
|
+
raise ArgumentError, "wrong number of arguments (given #{arguments.length}, expected 1..2)"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
key, default = arguments
|
|
138
|
+
warn "block supersedes default value argument", uplevel: 1 if block_given? && arguments.length == 2
|
|
139
|
+
@cache.fetch(key) do
|
|
140
|
+
return yield(key) if block_given?
|
|
141
|
+
return default if arguments.length == 2
|
|
142
|
+
raise KeyError.new("key not found: #{key.inspect}", receiver: self, key: key)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Fetch values in the requested order, using {#fetch} for missing keys.
|
|
147
|
+
# @param keys [Array<BasicObject>] The keys to look up.
|
|
148
|
+
# @yield [key] Called for each missing key.
|
|
149
|
+
# @yieldparam key [BasicObject] The missing key.
|
|
150
|
+
# @yieldreturn [BasicObject] The fallback value, which is not stored.
|
|
151
|
+
# @return [Array<BasicObject>] The associated values or block results.
|
|
152
|
+
def fetch_values(*keys, &) = keys.map { fetch(it, &) }
|
|
153
|
+
|
|
154
|
+
# Look up values in the requested order, returning nil for missing keys.
|
|
155
|
+
# @param keys [Array<BasicObject>] The keys to look up.
|
|
156
|
+
# @return [Array<BasicObject>] The associated values, with nil for missing keys.
|
|
157
|
+
def values_at(*keys) = keys.map { self[it] }
|
|
158
|
+
|
|
159
|
+
# @return [Boolean] Whether keys are compared by identity instead of `hash` and `eql?`.
|
|
160
|
+
def compare_keys_by_identity? = @map.compare_keys_by_identity?
|
|
161
|
+
|
|
162
|
+
# (see #compare_keys_by_identity?)
|
|
163
|
+
def compare_by_identity? = compare_keys_by_identity?
|
|
164
|
+
|
|
165
|
+
# @return [Boolean] Whether keys must be Ractor-shareable, including copied mutable strings.
|
|
166
|
+
def shareable_keys? = true
|
|
167
|
+
|
|
168
|
+
# @return [Boolean] Whether values must be Ractor-shareable.
|
|
169
|
+
def shareable_values? = true
|
|
170
|
+
|
|
171
|
+
# @return [Boolean] Whether keys are referenced weakly.
|
|
172
|
+
def weak_keys? = false
|
|
173
|
+
|
|
174
|
+
# @return [Boolean] Whether values are referenced weakly.
|
|
175
|
+
def weak_values? = true
|
|
176
|
+
|
|
177
|
+
# @api private
|
|
178
|
+
def inspect_with(inspector)
|
|
179
|
+
super do
|
|
180
|
+
yield if block_given?
|
|
181
|
+
inspector.breakable
|
|
182
|
+
inspector.group("{", "}") do
|
|
183
|
+
inspector.breakable ""
|
|
184
|
+
inspector.seplist(@map, nil, :each) do |key, value|
|
|
185
|
+
inspector.group { inspector.hash_pair(key, value) }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
alias to_s inspect
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def initialize_cache = @cache = Internal.native_weak_maps? ? @map : Internal::WeakCacheMirror.new(@map)
|
|
196
|
+
def freeze_backend = @map
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -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
|