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
data/lib/farce/map.rb
CHANGED
|
@@ -48,7 +48,14 @@ module Farce
|
|
|
48
48
|
|
|
49
49
|
private
|
|
50
50
|
|
|
51
|
-
def new_map(
|
|
51
|
+
def new_map(
|
|
52
|
+
compare_by_identity: false,
|
|
53
|
+
compare_keys_by_identity: compare_by_identity,
|
|
54
|
+
compare_values_by_identity: compare_by_identity
|
|
55
|
+
)
|
|
56
|
+
Internal::StrictMap.new(compare_by_identity:, compare_keys_by_identity:, compare_values_by_identity:)
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
def freeze_backend = @map
|
|
53
60
|
|
|
54
61
|
Internal.prepare_map_access(self, :modes)
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# Ractor-friendly memoization module.
|
|
7
|
+
#
|
|
8
|
+
# Replaces the following pattern:
|
|
9
|
+
#
|
|
10
|
+
# ```ruby
|
|
11
|
+
# class MyClass
|
|
12
|
+
# def time
|
|
13
|
+
# @time ||= Time.now
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
# ```
|
|
17
|
+
#
|
|
18
|
+
# Using `Farce::Memoize` instead gives you:
|
|
19
|
+
#
|
|
20
|
+
# * Allows memoization after freezing
|
|
21
|
+
# * Is Ractor-safe (with optional scoping or transfer modes) and thread-safe
|
|
22
|
+
# * Adds one instance variable per memoized method (avoids hash lookup and doesn't bust Ruby's data structures)
|
|
23
|
+
# * Is Marshal-dump/load friendly
|
|
24
|
+
# * Allows weak memoization, where the memoized value is kept until it is garbage collected
|
|
25
|
+
# * Does not need to run code on the main Ractor for memoization
|
|
26
|
+
#
|
|
27
|
+
# Methods with arguments cannot be memoized. Explicitly use a {Farce::Abstract::Map map} instead.
|
|
28
|
+
#
|
|
29
|
+
# @example Using `memoize` method
|
|
30
|
+
# class MyClass
|
|
31
|
+
# include Farce::Memoize
|
|
32
|
+
# memoize def time = Time.now
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# instance = MyClass.new
|
|
36
|
+
# first_time = instance.time
|
|
37
|
+
# second_time = instance.time
|
|
38
|
+
#
|
|
39
|
+
# first_time == second_time # => true
|
|
40
|
+
# first_time == MyClass.new.time # => false
|
|
41
|
+
#
|
|
42
|
+
# @example Scope-specific memoization
|
|
43
|
+
# class MyClass
|
|
44
|
+
# include Farce::Memoize
|
|
45
|
+
#
|
|
46
|
+
# def first = expensive_computation(0)
|
|
47
|
+
# def last = expensive_computation(-1)
|
|
48
|
+
#
|
|
49
|
+
# # Memoize separately per Ractor
|
|
50
|
+
# memoize :first, :last, scope: :ractor
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# @example Setting a mode
|
|
54
|
+
# class MyClass
|
|
55
|
+
# include Farce::Memoize
|
|
56
|
+
# memoize :expensive, mode: :make_shareable
|
|
57
|
+
# def expensive = expensive_computation
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example Without `memoize` extension
|
|
61
|
+
# class MyClass
|
|
62
|
+
# include Farce::Memoize[:time]
|
|
63
|
+
# def time = Time.now
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# @see Farce::Lazy
|
|
67
|
+
# @see Farce::LazyRef
|
|
68
|
+
#
|
|
69
|
+
# @!macro memoize_params
|
|
70
|
+
# @overload $0(*methods, scope: :global, mode: :smart, weak: false, visibility: nil)
|
|
71
|
+
# @param methods [Array<Symbol>]
|
|
72
|
+
# The methods to memoize
|
|
73
|
+
#
|
|
74
|
+
# @param scope [Symbol]
|
|
75
|
+
# The scope of the memoization.
|
|
76
|
+
# In addition to the {Farce::SCOPES normal scopes} it may also be set to `:global` or `:unshared`.
|
|
77
|
+
#
|
|
78
|
+
# @param mode [Symbol]
|
|
79
|
+
# The mode of the memoization. Ignored for any scope other than `:global`.
|
|
80
|
+
# Use `:strict` to cache shareable results directly, including with `weak: true`.
|
|
81
|
+
# Defaults to `:raise` with `weak: true`, which also supports `:make_shareable` and `:dedup`.
|
|
82
|
+
#
|
|
83
|
+
# @param weak [Boolean]
|
|
84
|
+
# Whether to use weak memoization.
|
|
85
|
+
# If enabled, memoized values may be garbage collected when no longer referenced.
|
|
86
|
+
# In this case, next access to the memoized method may recompute the value.
|
|
87
|
+
#
|
|
88
|
+
# @param visibility [Symbol, nil]
|
|
89
|
+
# The visibility of the memoized methods.
|
|
90
|
+
# May be one of `:public`, `:protected`, or `:private`.
|
|
91
|
+
# If set to `nil`, {Farce::Memoize#memoize} will attempt to infer the visibility based on the context.
|
|
92
|
+
# Other methods will default to `:public`.
|
|
93
|
+
module Memoize
|
|
94
|
+
# A named module preserves the nil marker's identity across Marshal round trips.
|
|
95
|
+
NIL_VALUE = Module.new.freeze
|
|
96
|
+
private_constant :NIL_VALUE
|
|
97
|
+
|
|
98
|
+
# Mixins used to implement memoization for different methods, scopes, and modes
|
|
99
|
+
# @see Farce::Memoize
|
|
100
|
+
class Mixin < Module
|
|
101
|
+
KNOWN = Strict::Map.new
|
|
102
|
+
private_constant :KNOWN
|
|
103
|
+
|
|
104
|
+
# @!macro memoize_params
|
|
105
|
+
# Creates or retrieves a memoization mixin for the given methods and options.
|
|
106
|
+
# @return [Mixin] the memoization mixin
|
|
107
|
+
def self.new(*methods, **options)
|
|
108
|
+
return super unless self == Mixin
|
|
109
|
+
methods = Farce.dedup(methods.map(&:to_sym).uniq.sort)
|
|
110
|
+
options = Farce.dedup(normalize_options(options))
|
|
111
|
+
map = KNOWN.store_if_absent(options) { Strict::Map.new }
|
|
112
|
+
map.store_if_absent(methods) { super(methods, **options) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @api private
|
|
116
|
+
def self.normalize_options(options)
|
|
117
|
+
options[:visibility] &&= options[:visibility].to_sym
|
|
118
|
+
|
|
119
|
+
options[:scope] ||= :global
|
|
120
|
+
options[:visibility] ||= :public
|
|
121
|
+
options[:weak] ||= false
|
|
122
|
+
|
|
123
|
+
if options[:scope] == :global
|
|
124
|
+
options[:mode] ||= options[:weak] ? :raise : :smart
|
|
125
|
+
else
|
|
126
|
+
options.delete(:mode)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
options
|
|
130
|
+
end
|
|
131
|
+
private_class_method :normalize_options
|
|
132
|
+
|
|
133
|
+
# @return [Array<Symbol>] the memoized methods
|
|
134
|
+
attr_reader :memoized_methods
|
|
135
|
+
|
|
136
|
+
# @return [Symbol] the scope of the memoization
|
|
137
|
+
attr_reader :scope
|
|
138
|
+
|
|
139
|
+
# @return [Symbol, nil] the transfer mode, or nil for a scoped cache
|
|
140
|
+
attr_reader :mode
|
|
141
|
+
|
|
142
|
+
# @return [Symbol] the visibility of the memoized methods
|
|
143
|
+
attr_reader :visibility
|
|
144
|
+
|
|
145
|
+
# @api private
|
|
146
|
+
def initialize(methods, scope:, weak:, mode: nil, visibility: :public)
|
|
147
|
+
@memoized_methods = methods
|
|
148
|
+
@scope = scope
|
|
149
|
+
@mode = mode
|
|
150
|
+
@weak = !!weak # rubocop:disable Style/DoubleNegation
|
|
151
|
+
@visibility = visibility
|
|
152
|
+
|
|
153
|
+
super()
|
|
154
|
+
|
|
155
|
+
unless %i[public protected private].include?(visibility)
|
|
156
|
+
raise ArgumentError, "unsupported visibility: #{visibility.inspect}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
atom_class = weak ? "WeakAtom" : "Atom"
|
|
160
|
+
|
|
161
|
+
case scope
|
|
162
|
+
when :global
|
|
163
|
+
if mode == :strict
|
|
164
|
+
init = "::Farce::Strict::#{atom_class}.new"
|
|
165
|
+
else
|
|
166
|
+
modes = weak ? Internal::WeakModeManager::MODES : MODES
|
|
167
|
+
raise ArgumentError, "unsupported mode: #{mode.inspect}" unless modes.include?(mode)
|
|
168
|
+
init = "::Farce::#{atom_class}.new(mode: #{mode.inspect})"
|
|
169
|
+
end
|
|
170
|
+
args = methods.join(", ")
|
|
171
|
+
args += ", mode: #{mode.inspect}" unless mode == :smart
|
|
172
|
+
when :unshared
|
|
173
|
+
init = "::Farce::Unshared::#{atom_class}.new"
|
|
174
|
+
args = "#{methods.join(", ")}, scope: :unshared"
|
|
175
|
+
when *SCOPES
|
|
176
|
+
init = "::Farce::Local::#{atom_class}.new(scope: #{scope.inspect})"
|
|
177
|
+
args = "#{methods.join(", ")}, scope: #{scope.inspect}"
|
|
178
|
+
else
|
|
179
|
+
raise ArgumentError, "unsupported scope: #{scope.inspect}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
set_temporary_name(-"Farce::Memoize[#{args}]") if instance_of?(Mixin)
|
|
183
|
+
initializer = +""
|
|
184
|
+
# Overlapping mixins need separate slots. Stable names also survive Marshal across processes.
|
|
185
|
+
slot = "#{methods.join(",").unpack1("H*")}_#{scope}_#{mode}_#{@weak}_#{visibility}"
|
|
186
|
+
|
|
187
|
+
methods.each do |method|
|
|
188
|
+
unless method =~ /\A[a-z_]\w*[!?]?\z/i && method != :initialize
|
|
189
|
+
raise ArgumentError, "unsuitable method name: #{method.inspect}"
|
|
190
|
+
end
|
|
191
|
+
ivar = "@_#{method.to_s.unpack1("H*")}_memoize_#{slot}_atom"
|
|
192
|
+
initializer << " #{ivar} = #{init}\n"
|
|
193
|
+
module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
194
|
+
#{visibility} def #{method}
|
|
195
|
+
value = #{ivar}.value || #{ivar}.store_if_absent do
|
|
196
|
+
result = super()
|
|
197
|
+
nil.equal?(result) ? NIL_VALUE : result
|
|
198
|
+
end
|
|
199
|
+
value unless NIL_VALUE.equal?(value)
|
|
200
|
+
end
|
|
201
|
+
RUBY
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Explicit forwarding preserves nested constructor keywords on JRuby.
|
|
205
|
+
module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
206
|
+
def initialize(*, **, &)
|
|
207
|
+
#{initializer}
|
|
208
|
+
super
|
|
209
|
+
end
|
|
210
|
+
RUBY
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# @return [Boolean] whether the memoization is weak
|
|
214
|
+
def weak? = @weak
|
|
215
|
+
|
|
216
|
+
# allow `include Farce::Memoize[...]` to work like `prepend`
|
|
217
|
+
private def append_features(base) = base.prepend(self)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @api private
|
|
221
|
+
def self.append_features(base) = base.extend(self)
|
|
222
|
+
private_class_method :append_features
|
|
223
|
+
|
|
224
|
+
# Creates or retrieves the memoization mixin for the given methods, scope, and mode
|
|
225
|
+
# @!macro memoize_params
|
|
226
|
+
# @return [Mixin] memoization mixin for the given methods, scope, and mode
|
|
227
|
+
def self.[](...) = Mixin.new(...)
|
|
228
|
+
|
|
229
|
+
# @!macro memoize_params
|
|
230
|
+
# Creates a memoization mixin for the given arguments and adds it to the current class.
|
|
231
|
+
#
|
|
232
|
+
# Can be used as a prefix to `def`:
|
|
233
|
+
#
|
|
234
|
+
# ```ruby
|
|
235
|
+
# memoize def some_method = expensive_computation
|
|
236
|
+
# ```
|
|
237
|
+
#
|
|
238
|
+
# Or called separately with the method names, useful for passing additional options:
|
|
239
|
+
#
|
|
240
|
+
# ```ruby
|
|
241
|
+
# def some_method = expensive_computation
|
|
242
|
+
#
|
|
243
|
+
# # memoize some_method per thread
|
|
244
|
+
# memoize :some_method, scope: :thread
|
|
245
|
+
# ```
|
|
246
|
+
#
|
|
247
|
+
# ## Visibility of Memoized Methods
|
|
248
|
+
#
|
|
249
|
+
# If no visibility is provided, it will determine the visibility based on the existing method definitions.
|
|
250
|
+
# If none exist, it will default to public. This happens when `memoize` is called.
|
|
251
|
+
# Make sure you set the visibility before calling this method.
|
|
252
|
+
#
|
|
253
|
+
# This works:
|
|
254
|
+
#
|
|
255
|
+
# ```ruby
|
|
256
|
+
# memoize private def some_method = expensive_computation
|
|
257
|
+
# ```
|
|
258
|
+
#
|
|
259
|
+
# As does this:
|
|
260
|
+
#
|
|
261
|
+
# ```ruby
|
|
262
|
+
# private
|
|
263
|
+
# memoize def some_method = expensive_computation
|
|
264
|
+
# ```
|
|
265
|
+
#
|
|
266
|
+
# Or this:
|
|
267
|
+
#
|
|
268
|
+
# ```ruby
|
|
269
|
+
# private def some_method = expensive_computation
|
|
270
|
+
# memoize :some_method
|
|
271
|
+
# ```
|
|
272
|
+
#
|
|
273
|
+
# But this doesn't, as the mixin's method will end up public:
|
|
274
|
+
#
|
|
275
|
+
# ```ruby
|
|
276
|
+
# private memoize def some_method = expensive_computation
|
|
277
|
+
# ```
|
|
278
|
+
#
|
|
279
|
+
# Same for this one, as the mixin's method will also end up public:
|
|
280
|
+
#
|
|
281
|
+
# ```ruby
|
|
282
|
+
# memoize :some_method
|
|
283
|
+
# private def some_method = expensive_computation
|
|
284
|
+
# ```
|
|
285
|
+
#
|
|
286
|
+
# You can of course set visibility explicitly when calling `memoize`:
|
|
287
|
+
#
|
|
288
|
+
# ```ruby
|
|
289
|
+
# memoize :some_method, visibility: :private
|
|
290
|
+
# ```
|
|
291
|
+
#
|
|
292
|
+
# @return [Array<Symbol>] the list of memoized methods
|
|
293
|
+
def memoize(*methods, visibility: nil, warn: true, **)
|
|
294
|
+
if visibility.nil?
|
|
295
|
+
groups = methods.group_by do |method|
|
|
296
|
+
next :protected if protected_method_defined?(method)
|
|
297
|
+
next :private if private_method_defined?(method)
|
|
298
|
+
:public
|
|
299
|
+
end
|
|
300
|
+
return groups.flat_map { memoize(*_2, visibility: _1, warn:, **) }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
if warn
|
|
304
|
+
methods.each do |name|
|
|
305
|
+
method = instance_method(name)
|
|
306
|
+
next if method.parameters.empty?
|
|
307
|
+
warn "#{method.owner}##{name} expects parameters and may be unsuitable for memoization"
|
|
308
|
+
rescue NameError => e
|
|
309
|
+
raise e unless e.name == name.to_sym
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
mixin = Farce::Memoize[*methods, visibility:, **]
|
|
314
|
+
prepend mixin
|
|
315
|
+
|
|
316
|
+
mixin.memoized_methods
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
data/lib/farce/pool.rb
CHANGED
|
@@ -11,6 +11,11 @@ module Farce
|
|
|
11
11
|
# `min_size` retire after `shrink_after` without queued or running work.
|
|
12
12
|
# By default, the pool starts with no workers and can shrink back to zero.
|
|
13
13
|
#
|
|
14
|
+
# Worker limits, task limits, scaling delays, and queue capacity can change while
|
|
15
|
+
# the pool runs. Lowering `max_size` retires extra workers as they become idle.
|
|
16
|
+
# Busy workers keep queued work runnable. Raising `min_size` starts workers
|
|
17
|
+
# immediately. Queued tasks are retained.
|
|
18
|
+
#
|
|
14
19
|
# Most importantly, it exposes a {#schedule} method compatible with {Scheduler#schedule}.
|
|
15
20
|
#
|
|
16
21
|
# @example
|
data/lib/farce/priority_queue.rb
CHANGED
|
@@ -42,7 +42,7 @@ module Farce
|
|
|
42
42
|
result = @reverse_order ? @queue.pop_last { empty = true } : @queue.pop { empty = true }
|
|
43
43
|
|
|
44
44
|
return @manager.unwrap(result) unless empty
|
|
45
|
-
remaining =
|
|
45
|
+
remaining = Clock.remaining(deadline)
|
|
46
46
|
return block_given? ? yield : nil unless wait_pop(timeout: remaining)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# A message multiplexer.
|
|
7
|
+
# Implements the publisher-subscriber protocol.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# publisher = Farce::Publisher.new
|
|
11
|
+
#
|
|
12
|
+
# # Count how many messages have been received
|
|
13
|
+
# counter = Farce::Counter.new
|
|
14
|
+
# publisher.subscribe(counter)
|
|
15
|
+
#
|
|
16
|
+
# # Trigger a callback whenever a message is published
|
|
17
|
+
# publisher.subscribe do |message|
|
|
18
|
+
# puts "Received message: #{message}"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # Send messages to a queue
|
|
22
|
+
# queue = Farce::Queue.new
|
|
23
|
+
# publisher.subscribe(queue)
|
|
24
|
+
#
|
|
25
|
+
# # Keep track of the last published value
|
|
26
|
+
# atom = Farce::Atom.new
|
|
27
|
+
# publisher.subscribe(atom)
|
|
28
|
+
#
|
|
29
|
+
# # Send messages to a Ractor
|
|
30
|
+
# ractor = Ractor.new do
|
|
31
|
+
# while message = Ractor.receive
|
|
32
|
+
# puts "Received message in Ractor: #{message}"
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
# publisher.subscribe(ractor)
|
|
36
|
+
#
|
|
37
|
+
# # Publish some messages
|
|
38
|
+
# publisher.publish("Hello, world!")
|
|
39
|
+
# publisher.publish("Another message")
|
|
40
|
+
#
|
|
41
|
+
# queue.pop # => "Hello, world!"
|
|
42
|
+
# atom.value # => "Another message"
|
|
43
|
+
# counter.value # => 2
|
|
44
|
+
class Publisher < Farce::Abstract::Publisher
|
|
45
|
+
include Shareable::Unfreezable
|
|
46
|
+
|
|
47
|
+
# @overload initialize(*subscribers)
|
|
48
|
+
# (see Farce::Abstract#initialize)
|
|
49
|
+
def initialize(*subscribers)
|
|
50
|
+
@map = Strict::Map.new
|
|
51
|
+
super()
|
|
52
|
+
subscribers.each { subscribe(it) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private attr_reader :map
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/farce/shareable.rb
CHANGED
|
@@ -13,6 +13,9 @@ module Farce
|
|
|
13
13
|
# object = MyClass.new
|
|
14
14
|
# Ractor.shareable?(object) # => true
|
|
15
15
|
module Shareable
|
|
16
|
+
# @api private
|
|
17
|
+
def self.included(base) = Internal.prepare_shareable_class(base)
|
|
18
|
+
|
|
16
19
|
# A structurally frozen wrapper whose logical frozen state is owned by its backend.
|
|
17
20
|
# The including class must implement a private `freeze_backend` method.
|
|
18
21
|
#
|
data/lib/farce/signal.rb
CHANGED
|
@@ -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
|