farce 0.1.0-java → 0.3.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 +127 -1
- data/README.md +111 -17
- data/docs/gems/dry-types.md +8 -8
- data/docs/modes.md +75 -15
- data/docs/scopes.md +3 -1
- data/docs/variants.md +18 -0
- data/lib/farce/_yard/macros.rb +18 -3
- data/lib/farce/abstract/executor.rb +130 -0
- data/lib/farce/abstract/flag.rb +73 -0
- data/lib/farce/abstract/pool.rb +334 -0
- data/lib/farce/abstract/queue.rb +4 -3
- data/lib/farce/abstract/scheduler.rb +539 -93
- data/lib/farce/abstract/set.rb +17 -5
- data/lib/farce/abstract/sorted_set.rb +2 -2
- data/lib/farce/abstract/timer_queue.rb +49 -10
- data/lib/farce/abstract/vector.rb +510 -28
- data/lib/farce/atom.rb +1 -1
- data/lib/farce/class_mirror.rb +20 -7
- data/lib/farce/config.rb +7 -1
- data/lib/farce/engine/jruby/signal.rb +7 -4
- data/lib/farce/engine/jvm/flag.rb +15 -3
- data/lib/farce/engine/ruby/shared/require_hook.rb +18 -0
- data/lib/farce/engine/ruby.rb +7 -0
- data/lib/farce/engine/shared/main_scheduler.rb +2 -2
- data/lib/farce/engine/shared/parallel_scheduler.rb +1 -1
- data/lib/farce/engine/shared/proxy_owner.rb +1 -1
- data/lib/farce/engine/shared/unshared_atom.rb +9 -8
- data/lib/farce/engine/shared/unshared_vector.rb +31 -4
- data/lib/farce/engine/shared/vector.rb +8 -0
- data/lib/farce/engine/shared/weak_map/index.rb +14 -1
- data/lib/farce/engine/shared.rb +9 -0
- data/lib/farce/engine/truffleruby/native/flag.rb +15 -3
- data/lib/farce/envelope.rb +54 -7
- data/lib/farce/exchanger.rb +1 -1
- data/lib/farce/flag.rb +2 -0
- data/lib/farce/integrations/active_support.rb +1 -1
- data/lib/farce/integrations/bson.rb +1 -1
- data/lib/farce/integrations/cbor.rb +1 -1
- data/lib/farce/integrations/concurrent.rb +3 -3
- data/lib/farce/integrations/date.rb +8 -0
- data/lib/farce/integrations/dry_types.rb +7 -4
- data/lib/farce/integrations/json.rb +2 -2
- data/lib/farce/integrations/msgpack.rb +1 -1
- data/lib/farce/integrations/oj.rb +3 -4
- data/lib/farce/integrations/psych.rb +1 -1
- data/lib/farce/integrations/ractor_sharing.rb +1 -1
- data/lib/farce/integrations/sorted_set.rb +1 -1
- data/lib/farce/integrations/weakref.rb +9 -1
- data/lib/farce/integrations/yajl.rb +3 -3
- data/lib/farce/integrations.rb +33 -23
- data/lib/farce/internal/flag_waiting.rb +30 -0
- data/lib/farce/internal/freeze.rb +8 -3
- data/lib/farce/internal/lease_initialization.rb +1 -1
- data/lib/farce/internal/local_executor_methods.rb +33 -0
- data/lib/farce/internal/local_scheduled_task.rb +27 -0
- data/lib/farce/internal/map_value_modes.rb +1 -1
- data/lib/farce/internal/scheduled_task.rb +1 -1
- data/lib/farce/internal/scheduler_lifecycle.rb +1 -1
- data/lib/farce/internal/thread_pool_supervisor.rb +45 -0
- data/lib/farce/internal/unshared_pool_worker.rb +14 -0
- data/lib/farce/internal.rb +14 -0
- data/lib/farce/lazy.rb +4 -4
- data/lib/farce/lazy_ref.rb +3 -3
- data/lib/farce/lfu_map.rb +1 -1
- data/lib/farce/local/flag.rb +4 -0
- data/lib/farce/local/pool.rb +111 -0
- data/lib/farce/local/scheduler.rb +219 -0
- data/lib/farce/local/scoped.rb +1 -1
- data/lib/farce/lru_map.rb +1 -1
- data/lib/farce/map.rb +2 -2
- data/lib/farce/mode_manager/smart_rule.rb +138 -0
- data/lib/farce/mode_manager.rb +12 -3
- data/lib/farce/molecule.rb +2 -2
- data/lib/farce/pool.rb +2 -290
- data/lib/farce/port.rb +4 -4
- data/lib/farce/priority_queue.rb +1 -1
- data/lib/farce/proxy/register.rb +6 -0
- data/lib/farce/proxy/supervisor.rb +1 -1
- data/lib/farce/proxy.rb +9 -4
- data/lib/farce/queue.rb +1 -1
- data/lib/farce/scheduler.rb +1 -502
- data/lib/farce/set.rb +1 -1
- data/lib/farce/sorted_set.rb +1 -1
- data/lib/farce/task.rb +285 -0
- data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
- data/lib/farce/timer_queue.rb +20 -6
- data/lib/farce/transaction/vector.rb +43 -9
- data/lib/farce/transaction/wrapper.rb +14 -0
- data/lib/farce/tree_map.rb +1 -1
- data/lib/farce/unsafe/lfu_map.rb +2 -0
- data/lib/farce/unsafe/lru_map.rb +2 -0
- data/lib/farce/unsafe/tree_map.rb +2 -0
- data/lib/farce/unshareable.rb +8 -19
- data/lib/farce/unshared/atom.rb +3 -0
- data/lib/farce/unshared/lfu_map.rb +2 -0
- data/lib/farce/unshared/lru_map.rb +2 -0
- data/lib/farce/unshared/map.rb +3 -0
- data/lib/farce/unshared/pool.rb +44 -0
- data/lib/farce/unshared/scheduler.rb +89 -0
- data/lib/farce/unshared/set.rb +1 -0
- data/lib/farce/unshared/sorted_set.rb +1 -0
- data/lib/farce/unshared/tree_map.rb +5 -1
- data/lib/farce/unshared/vector.rb +3 -0
- data/lib/farce/unshared/weak_atom.rb +5 -1
- data/lib/farce/unshared/weak_key_map.rb +3 -0
- data/lib/farce/unshared/weak_map.rb +3 -0
- data/lib/farce/unshared/weak_set.rb +1 -0
- data/lib/farce/unshared/weak_value_map.rb +3 -0
- data/lib/farce/vector.rb +44 -19
- data/lib/farce/version.rb +1 -1
- data/lib/farce.rb +20 -25
- metadata +18 -3
data/lib/farce/task.rb
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# A unit of work that executes once and retains its result or failure.
|
|
7
|
+
#
|
|
8
|
+
# ```ruby
|
|
9
|
+
# # You can create and schedule a task with one call:
|
|
10
|
+
# task = Farce::Task.schedule { 1 + 2 }
|
|
11
|
+
# task.value # => 3
|
|
12
|
+
#
|
|
13
|
+
# # There is also a shorthand for running a task on the global Ractor pool:
|
|
14
|
+
# task = Farce::Task.in_parallel { sleep 1 }
|
|
15
|
+
# task.join
|
|
16
|
+
# task.alive? # => false
|
|
17
|
+
#
|
|
18
|
+
# # schedule accepts a custom scheduler:
|
|
19
|
+
# scheduler = Farce::Scheduler.create
|
|
20
|
+
# task = Farce::Task.schedule(scheduler:) { 1 + 2 }
|
|
21
|
+
#
|
|
22
|
+
# # or alternatively you can manually create a task and hand it to the scheduler
|
|
23
|
+
# task = Farce::Task.new { 1 + 2 }
|
|
24
|
+
# scheduler.schedule(&task)
|
|
25
|
+
#
|
|
26
|
+
# # or execute it manually
|
|
27
|
+
# task = Farce::Task.new { Ractor.current.name.upcase }
|
|
28
|
+
# Ractor.new(task, name: "Herbert") { it.call }
|
|
29
|
+
# task.value # => "HERBERT"
|
|
30
|
+
# ```
|
|
31
|
+
class Task
|
|
32
|
+
include Abstract::Value
|
|
33
|
+
include Internal::Freeze::Delegated
|
|
34
|
+
include Internal::MarshalSupport::Reject
|
|
35
|
+
|
|
36
|
+
# Raised when an operation is attempted on a task that is in an incorrect state.
|
|
37
|
+
InvalidStateError = Class.new(RuntimeError)
|
|
38
|
+
|
|
39
|
+
# @!macro [new] task_schedule
|
|
40
|
+
# @!macro modes
|
|
41
|
+
# @param args [Array] the arguments to pass to the task
|
|
42
|
+
# @param mode [Symbol] the mode for arguments and the successful result
|
|
43
|
+
# Arguments can be overridden by {#add_argument}.
|
|
44
|
+
# @yield [*args] the block to execute with the given arguments
|
|
45
|
+
# @yieldparam args [Array] the arguments passed to the block
|
|
46
|
+
# @yieldreturn [Object] the result of the block execution, will become the task's {#value}
|
|
47
|
+
# @raise ArgumentError if the provided mode is invalid
|
|
48
|
+
# @raise LocalJumpError if no callback is provided
|
|
49
|
+
|
|
50
|
+
# @overload in_parallel(*args, mode: :smart)
|
|
51
|
+
# Creates a task and schedules it for execution on the {Farce.in_parallel globally shared executor pool}.
|
|
52
|
+
# @!macro task_schedule
|
|
53
|
+
# @return [Task] the created task
|
|
54
|
+
def self.in_parallel(*, **, &) = enqueue(false, Farce.in_parallel, *, auto_local: false, **, &)
|
|
55
|
+
|
|
56
|
+
# @overload on_main(*args, mode: :smart, auto_local: true)
|
|
57
|
+
# Creates a task and schedules it for execution on the {Farce.on_main main scheduler}.
|
|
58
|
+
# If `auto_local` is true (default), `mode` will be set to `:local` if the method is called from the main Ractor.
|
|
59
|
+
#
|
|
60
|
+
# @!macro task_schedule
|
|
61
|
+
# @param auto_local [Boolean] whether to automatically switch to local mode if called from the main Ractor
|
|
62
|
+
# @return [Task] the created task
|
|
63
|
+
def self.on_main(*, **, &) = enqueue(false, Farce.on_main, *, **, &)
|
|
64
|
+
|
|
65
|
+
# @overload schedule(*args, mode: :smart, auto_local: true, scheduler: nil, &)
|
|
66
|
+
# Creates a task and schedules it for execution.
|
|
67
|
+
# A scheduler may optionally be provided.
|
|
68
|
+
#
|
|
69
|
+
# If no scheduler is provided, then it will look for a Ractor-local scheduler if mode is `:local` or `auto_local`
|
|
70
|
+
# is true. If none is found, one will be created automatically if `:mode` is `:local`, otherwise the
|
|
71
|
+
# {Farce.in_parallel globally shared executor pool} will be used.
|
|
72
|
+
#
|
|
73
|
+
# The scheduler may be a {Abstract::Executor executor}, or a Fiber scheduler.
|
|
74
|
+
#
|
|
75
|
+
# @!macro task_schedule
|
|
76
|
+
# @param scheduler [Abstract::Executor, Fiber::Scheduler, nil] the scheduler to use for execution
|
|
77
|
+
# @return [Task] the created task
|
|
78
|
+
def self.schedule(*, scheduler: nil, **, &) = enqueue(false, scheduler, *, **, &)
|
|
79
|
+
|
|
80
|
+
# @overload try_schedule(*args, mode: :smart, auto_local: true, scheduler: nil, &)
|
|
81
|
+
# Creates a task and attempts to schedule it for execution.
|
|
82
|
+
#
|
|
83
|
+
# Behaves exactly like {.schedule}, except it uses {Abstract::Executor#try_schedule} instead of
|
|
84
|
+
# {Abstract::Executor#schedule} if the scheduler is an executor, and that it returns `nil` if it fails to
|
|
85
|
+
# schedule the task.
|
|
86
|
+
#
|
|
87
|
+
# @!macro task_schedule
|
|
88
|
+
# @return [Task, nil] the created task, or `nil` if it failed to schedule
|
|
89
|
+
def self.try_schedule(*, scheduler: nil, **, &) = enqueue(true, scheduler, *, **, &)
|
|
90
|
+
|
|
91
|
+
# @api private
|
|
92
|
+
def self.enqueue(non_block, scheduler, *, mode: :smart, auto_local: true, &) # :nodoc:
|
|
93
|
+
unless scheduler
|
|
94
|
+
scheduler = Internal.local_scheduler(create: mode == :local) if auto_local || mode == :local
|
|
95
|
+
unless scheduler
|
|
96
|
+
scheduler = Internal::ParallelScheduler
|
|
97
|
+
auto_local = false
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if scheduler.is_a?(Abstract::Executor)
|
|
102
|
+
mode = :local if auto_local && scheduler.local?(wait: false)
|
|
103
|
+
task = new(*, mode:, &)
|
|
104
|
+
|
|
105
|
+
if non_block
|
|
106
|
+
return unless scheduler.try_schedule(mode:, auto_local: false, &task)
|
|
107
|
+
else
|
|
108
|
+
scheduler.schedule(mode:, auto_local: false, &task)
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
task = new(*, mode: :local, &)
|
|
112
|
+
scheduler.fiber(&task)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
task
|
|
116
|
+
end
|
|
117
|
+
private_class_method :enqueue
|
|
118
|
+
|
|
119
|
+
# @return [Symbol] the default mode used for task arguments and the return value
|
|
120
|
+
attr_reader :mode
|
|
121
|
+
|
|
122
|
+
# Creates a new task with the given arguments and callback.
|
|
123
|
+
#
|
|
124
|
+
# This does not schedule the task for execution, nor does it associate it with any executor.
|
|
125
|
+
#
|
|
126
|
+
# In fact, the task object itself is a proc-like object, that can be {call called} like a regular proc,
|
|
127
|
+
# but will track its execution state and result.
|
|
128
|
+
#
|
|
129
|
+
# @example Using a Task without an Executor
|
|
130
|
+
# task = Farce::Task.new(1, 2) { |a, b| a + b }
|
|
131
|
+
#
|
|
132
|
+
# task.alive? # => true
|
|
133
|
+
# task.call == task # => true
|
|
134
|
+
# task.alive? # => false
|
|
135
|
+
# task.value # => 3
|
|
136
|
+
#
|
|
137
|
+
# @example Passing a task to an executor
|
|
138
|
+
# task = Farce::Task.new { puts "Hello, world!" }
|
|
139
|
+
# Farce.in_parallel(&task)
|
|
140
|
+
# task.join # waits for the task to complete
|
|
141
|
+
#
|
|
142
|
+
# @!macro task_schedule
|
|
143
|
+
def initialize(*args, mode: :smart, &callback)
|
|
144
|
+
raise ArgumentError, "invalid mode: #{mode}" unless MODES.include?(mode)
|
|
145
|
+
raise LocalJumpError, "no callback provided" unless block_given?
|
|
146
|
+
|
|
147
|
+
@mode = mode
|
|
148
|
+
@ractor = Strict::Atom.new
|
|
149
|
+
@state = Strict::Atom.new(:initialized)
|
|
150
|
+
|
|
151
|
+
if mode == :local
|
|
152
|
+
@arguments = args
|
|
153
|
+
@callback = callback
|
|
154
|
+
@manager = nil
|
|
155
|
+
@ractor.value = Ractor.current
|
|
156
|
+
@result = Unshared::Atom.new
|
|
157
|
+
else
|
|
158
|
+
@arguments = Strict::Vector.new
|
|
159
|
+
@callback = Ractor.shareable?(callback) ? callback : Ractor.shareable_proc(&callback)
|
|
160
|
+
@manager = ModeManager.new(mode: mode)
|
|
161
|
+
@result = Strict::Atom.new
|
|
162
|
+
args.each { add_argument(it) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
__freeze__
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Adds an argument to the task.
|
|
169
|
+
# Can only be called before the task is frozen and before {#call} is invoked.
|
|
170
|
+
#
|
|
171
|
+
# Allows for arguments to use different modes than the return value (or other arguments).
|
|
172
|
+
# Mode is ignored for {#local? local} tasks.
|
|
173
|
+
#
|
|
174
|
+
# For non-local tasks, `mode` may only be set to `:local` for arguments added by the Ractor that will
|
|
175
|
+
# {#call execute} the task.
|
|
176
|
+
#
|
|
177
|
+
# @example A task using `move` for the return value, but `copy` for its argument
|
|
178
|
+
# task = Farce::Task.new(mode: :move) { |arg| arg.upcase }
|
|
179
|
+
# task.add_argument("hello", mode: :copy)
|
|
180
|
+
# Farce.in_parallel(&task)
|
|
181
|
+
# task.value # => "HELLO"
|
|
182
|
+
#
|
|
183
|
+
# @param argument [BasicObject] the argument to add to the task
|
|
184
|
+
# @param mode [Symbol, nil] the mode to use for the argument, or `nil` to use the task's default mode
|
|
185
|
+
# @raise ArgumentError if the provided mode is invalid
|
|
186
|
+
# @raise FrozenError if the task has already been frozen or called
|
|
187
|
+
# @raise InvalidStateError if trying to add a local argument from more than one ractor
|
|
188
|
+
# @return [self] the task itself, for chaining
|
|
189
|
+
def add_argument(argument, mode: nil)
|
|
190
|
+
check_frozen!
|
|
191
|
+
|
|
192
|
+
unless local?
|
|
193
|
+
raise ArgumentError, "invalid mode: #{mode}" unless mode.nil? || MODES.include?(mode)
|
|
194
|
+
raise InvalidStateError, "cannot add local argument to a non-local task" if mode == :local && !claim_for_ractor?
|
|
195
|
+
argument = @manager.wrap(argument, mode:)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
@arguments << argument
|
|
199
|
+
self
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
alias << add_argument
|
|
203
|
+
|
|
204
|
+
# @return [Boolean] whether the task is currently alive (initialized or running)
|
|
205
|
+
def alive? = alive_state?(@state.value)
|
|
206
|
+
|
|
207
|
+
# Executes the task by invoking its callback with the added arguments.
|
|
208
|
+
# @raise InvalidStateError if the task has already been claimed by another ractor or has already been called
|
|
209
|
+
# @return [self] the task itself, for chaining
|
|
210
|
+
def call
|
|
211
|
+
raise InvalidStateError, "task already claimed by another ractor" unless claim_for_ractor?
|
|
212
|
+
raise InvalidStateError, "call may only be called once" unless @state.compare_and_set(:initialized, :running)
|
|
213
|
+
begin
|
|
214
|
+
arguments = local? ? @arguments.freeze : @arguments.freeze.to_a.map! { @manager.unwrap(it) }
|
|
215
|
+
result = @callback.call(*arguments)
|
|
216
|
+
@result.value = local? ? result : @manager.wrap(result)
|
|
217
|
+
@state.value = :finished
|
|
218
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
219
|
+
# Every callback failure must release waiters, including ScriptError and SystemExit.
|
|
220
|
+
begin
|
|
221
|
+
@result.value = e
|
|
222
|
+
rescue Ractor::Error
|
|
223
|
+
@result.value = [RuntimeError, -"#{e.class}: #{e.message}", Ractor.make_shareable(e.backtrace)].freeze
|
|
224
|
+
end
|
|
225
|
+
@state.value = :error
|
|
226
|
+
ensure
|
|
227
|
+
if @state.value == :running
|
|
228
|
+
@result.value = [RuntimeError, -"task execution aborted"].freeze
|
|
229
|
+
@state.value = :error
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
self
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Waits for the task to finish, with an optional timeout.
|
|
237
|
+
# @param timeout [Numeric, nil] the maximum time to wait, or `nil` to wait indefinitely
|
|
238
|
+
# @return [self, nil] the task itself if it finished within the timeout, or `nil` otherwise
|
|
239
|
+
def join(timeout = nil)
|
|
240
|
+
return unless @state.wait_while(timeout:) { alive_state?(it) }
|
|
241
|
+
self
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# @return [Boolean] whether the task is local (i.e., its mode is `:local`)
|
|
245
|
+
def local? = @mode == :local
|
|
246
|
+
|
|
247
|
+
# Waits for the task to finish and returns its value, with an optional timeout.
|
|
248
|
+
# If the task raises an error, it will be propagated.
|
|
249
|
+
#
|
|
250
|
+
# @param timeout [Numeric, nil] the maximum time to wait, or `nil` to wait indefinitely
|
|
251
|
+
# @return [BasicObject, nil]
|
|
252
|
+
# the value returned by the task's callback, or `nil` if the task did not finish within the timeout
|
|
253
|
+
# @raise Exception if the task raises an error
|
|
254
|
+
# @raise RuntimeError with the original class, message, and backtrace if the exception cannot be shared
|
|
255
|
+
# @raise RuntimeError if execution is aborted before producing a result or exception
|
|
256
|
+
def value(timeout = nil)
|
|
257
|
+
case @state.wait_while(timeout:) { alive_state?(it) }
|
|
258
|
+
when :finished then local? ? @result.value : @manager.unwrap(@result.value)
|
|
259
|
+
when :error then raise(*@result.value)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Converts the task to a Proc object.
|
|
264
|
+
# If the task is Ractor-shareable, then so is the returned Proc.
|
|
265
|
+
# @return [Proc] a Proc that calls the task's `call` method
|
|
266
|
+
def to_proc
|
|
267
|
+
return method(:call).to_proc if local?
|
|
268
|
+
task = self
|
|
269
|
+
Ractor.make_shareable(-> { task.call })
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
def alive_state?(state) = state == :initialized || state == :running
|
|
275
|
+
|
|
276
|
+
def claim_for_ractor?
|
|
277
|
+
current_ractor = Ractor.current
|
|
278
|
+
@ractor.compare_and_set(nil, current_ractor)
|
|
279
|
+
@ractor.value == current_ractor
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
define_method(:__freeze__, Kernel.instance_method(:freeze))
|
|
283
|
+
def freeze_backend = @arguments
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
# warn_indent: true
|
|
4
4
|
|
|
5
5
|
module Farce
|
|
6
|
-
# A
|
|
6
|
+
# A task executor that starts a new thread for each scheduled task. Executes synchronous tasks inline.
|
|
7
7
|
# Creating an instance starts no threads. Blocks and arguments stay local to the caller.
|
|
8
|
-
# Tasks are not tracked, and closing this stateless
|
|
8
|
+
# Tasks are not tracked, and closing this stateless executor has no effect.
|
|
9
9
|
# Default instances support Marshal. Instances with a custom thread factory reject dumping.
|
|
10
|
-
class
|
|
10
|
+
class ThreadExecutor < Farce::Abstract::Executor
|
|
11
11
|
include Shareable::Immutable
|
|
12
12
|
include Internal::Inspect
|
|
13
13
|
|
|
@@ -18,7 +18,7 @@ module Farce
|
|
|
18
18
|
|
|
19
19
|
# @api private
|
|
20
20
|
def marshal_dump
|
|
21
|
-
raise TypeError, "
|
|
21
|
+
raise TypeError, "thread factories cannot be marshaled" if @factory
|
|
22
22
|
[1]
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -32,7 +32,7 @@ module Farce
|
|
|
32
32
|
#
|
|
33
33
|
# @overload schedule(*args, **options)
|
|
34
34
|
# @param args [Array<Object>] Arguments passed to the block.
|
|
35
|
-
# @param options [Hash]
|
|
35
|
+
# @param options [Hash] Execution options are ignored. All tasks use local arguments.
|
|
36
36
|
# @yield [*args] The task to run.
|
|
37
37
|
# @return [self]
|
|
38
38
|
def schedule(*, **, &)
|
|
@@ -45,7 +45,7 @@ module Farce
|
|
|
45
45
|
#
|
|
46
46
|
# @overload execute(*args, **options)
|
|
47
47
|
# @param args [Array<Object>] Arguments passed to the block.
|
|
48
|
-
# @param options [Hash]
|
|
48
|
+
# @param options [Hash] Execution options are ignored. All tasks use local arguments.
|
|
49
49
|
# @yield [*args] The task to run.
|
|
50
50
|
# @return [self]
|
|
51
51
|
def execute(*, **)
|
|
@@ -57,7 +57,7 @@ module Farce
|
|
|
57
57
|
# @return [self] Does not wait for or stop task threads.
|
|
58
58
|
def close = self
|
|
59
59
|
|
|
60
|
-
# @return [false] This stateless
|
|
60
|
+
# @return [false] This stateless executor is always available.
|
|
61
61
|
def closed? = false
|
|
62
62
|
|
|
63
63
|
# @return [Symbol] Always `:running`.
|
data/lib/farce/timer_queue.rb
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
module Farce
|
|
6
6
|
# A shareable timer queue with transfer modes for unshareable values.
|
|
7
|
-
class TimerQueue < Abstract::TimerQueue
|
|
7
|
+
class TimerQueue < Farce::Abstract::TimerQueue
|
|
8
8
|
include Internal::ManagedQueue
|
|
9
9
|
include Shareable::Unfreezable
|
|
10
10
|
|
|
11
11
|
# (see Farce::Abstract::TimerQueue#initialize)
|
|
12
12
|
# @!macro modes
|
|
13
13
|
# @param mode [Symbol] the transfer mode for unshareable values
|
|
14
|
-
def initialize(capacity: nil, mode: :
|
|
14
|
+
def initialize(capacity: nil, mode: :smart, track_age: false)
|
|
15
15
|
@manager = ModeManager.new(mode:)
|
|
16
16
|
super(capacity:, track_age:)
|
|
17
17
|
end
|
|
@@ -20,16 +20,20 @@ module Farce
|
|
|
20
20
|
# @!macro modes
|
|
21
21
|
# @param mode [Symbol] the transfer mode for unshareable values
|
|
22
22
|
def push(value, non_block = false, mode: nil, timeout: nil, **time_options) # rubocop:disable Style/OptionalBooleanParameter
|
|
23
|
-
at
|
|
24
|
-
|
|
23
|
+
at = Clock.parse(time_options)
|
|
24
|
+
value = @manager.wrap(value, mode:)
|
|
25
|
+
entry = Entry.new(value, at, self)
|
|
26
|
+
push_to_storage(at, non_block, value, timeout:) && entry
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
# (see Farce::Abstract::TimerQueue#try_push)
|
|
28
30
|
# @!macro modes
|
|
29
31
|
# @param mode [Symbol] the transfer mode for unshareable values
|
|
30
32
|
def try_push(value, mode: nil, **time_options)
|
|
31
|
-
at
|
|
32
|
-
|
|
33
|
+
at = Clock.parse(time_options)
|
|
34
|
+
value = @manager.wrap(value, mode:)
|
|
35
|
+
entry = Entry.new(value, at, self)
|
|
36
|
+
return entry if @queue.push(at, value)
|
|
33
37
|
block_given? ? yield : false
|
|
34
38
|
end
|
|
35
39
|
|
|
@@ -62,5 +66,15 @@ module Farce
|
|
|
62
66
|
return @manager.unwrap(value) unless empty
|
|
63
67
|
yield if block_given?
|
|
64
68
|
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def delete_from_storage(priority, value, compare_by_identity:)
|
|
73
|
+
if compare_by_identity && @manager.managed_envelope?(value)
|
|
74
|
+
@queue.delete_identity(priority, value)
|
|
75
|
+
else
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
end
|
|
65
79
|
end
|
|
66
80
|
end
|
|
@@ -18,7 +18,12 @@ module Farce
|
|
|
18
18
|
|
|
19
19
|
# (see Abstract::Vector#[])
|
|
20
20
|
# @see Abstract::Vector#[]
|
|
21
|
-
def [](index
|
|
21
|
+
def [](index, length = UNDEFINED)
|
|
22
|
+
access do
|
|
23
|
+
next unwrap_stored(@working[index]) if length.equal?(UNDEFINED) && !index.is_a?(Range)
|
|
24
|
+
slice(index, length)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
22
27
|
|
|
23
28
|
# (see Abstract::Vector#get)
|
|
24
29
|
# @see Abstract::Vector#get
|
|
@@ -34,8 +39,12 @@ module Farce
|
|
|
34
39
|
|
|
35
40
|
# (see Abstract::Vector#[]=)
|
|
36
41
|
# @see Abstract::Vector#[]=
|
|
37
|
-
def []=(index, value)
|
|
38
|
-
|
|
42
|
+
def []=(index, value, replacement = UNDEFINED)
|
|
43
|
+
if replacement.equal?(UNDEFINED) && !index.is_a?(Range)
|
|
44
|
+
store(index, value)
|
|
45
|
+
else
|
|
46
|
+
write { assign_vector_slice(index, value, replacement) }
|
|
47
|
+
end
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
# (see Abstract::Vector#store)
|
|
@@ -52,15 +61,24 @@ module Farce
|
|
|
52
61
|
|
|
53
62
|
# (see Abstract::Vector#push)
|
|
54
63
|
# @see Abstract::Vector#push
|
|
55
|
-
def push(value, mode: nil)
|
|
56
|
-
write
|
|
64
|
+
def push(value = UNDEFINED, *rest, mode: nil)
|
|
65
|
+
write do
|
|
66
|
+
if rest.empty? && !value.equal?(UNDEFINED)
|
|
67
|
+
@working.push(wrap(value, mode:))
|
|
68
|
+
else
|
|
69
|
+
push_values(value.equal?(UNDEFINED) ? rest : [value, *rest], mode:)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
57
72
|
self
|
|
58
73
|
end
|
|
59
|
-
|
|
74
|
+
|
|
75
|
+
def <<(value) = push(value)
|
|
60
76
|
|
|
61
77
|
# (see Abstract::Vector#pop)
|
|
62
78
|
# @see Abstract::Vector#pop
|
|
63
|
-
def pop =
|
|
79
|
+
def pop(count = UNDEFINED)
|
|
80
|
+
write { count.equal?(UNDEFINED) ? unwrap_stored(@working.pop) : pop_values(count) }
|
|
81
|
+
end
|
|
64
82
|
|
|
65
83
|
# (see Abstract::Vector#clear)
|
|
66
84
|
# @see Abstract::Vector#clear
|
|
@@ -120,6 +138,17 @@ module Farce
|
|
|
120
138
|
def internal_vector = @working
|
|
121
139
|
def logical_value(value) = unwrap_stored(value)
|
|
122
140
|
|
|
141
|
+
def deletion_comparison(value)
|
|
142
|
+
return value unless @manager
|
|
143
|
+
@manager.wrap(value, mode: compare_by_identity? ? :local : :copy)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def deletion_match?(stored, expected)
|
|
147
|
+
return super unless @manager
|
|
148
|
+
unwrap_stored(stored)
|
|
149
|
+
@manager.same_value?(stored, expected, identity: compare_by_identity?)
|
|
150
|
+
end
|
|
151
|
+
|
|
123
152
|
# Slice helpers return independent data rather than another participant.
|
|
124
153
|
# Use an ordinary vector that remains usable after the attempt ends.
|
|
125
154
|
def build_derived_vector(values)
|
|
@@ -130,10 +159,15 @@ module Farce
|
|
|
130
159
|
|
|
131
160
|
def inspect_value(inspector, value) = super(inspector, value, @manager)
|
|
132
161
|
def derived_storage(value) = wrap(value)
|
|
162
|
+
def prepare_vector_value(value, mode: nil) = wrap(value, mode:)
|
|
133
163
|
|
|
134
|
-
Wrapper.inherit(self, :each_index, :reverse_each, :to_a, :deconstruct, :to_h, :join, :dig, :assoc,
|
|
164
|
+
Wrapper.inherit(self, :each_index, :reverse_each, :to_a, :to_ary, :deconstruct, :to_h, :join, :dig, :assoc,
|
|
135
165
|
:rassoc, :at, :fetch, :fetch_values, :values_at, :slice, :rfind, :bsearch, :bsearch_index, :pack,
|
|
136
|
-
:length, :count, :compare_by_identity?, :shareable_values
|
|
166
|
+
:length, :count, :compare_by_identity?, :shareable_values?, :flatten, :transpose, :product,
|
|
167
|
+
:combination, :repeated_combination, :permutation, :repeated_permutation, :<=>,
|
|
168
|
+
writes: %i[delete delete_if map! collect! keep_if select! filter! reject! compact! uniq!
|
|
169
|
+
reverse! rotate! shuffle! sort! sort_by! flatten! shift unshift prepend insert delete_at slice!
|
|
170
|
+
replace fill append])
|
|
137
171
|
end
|
|
138
172
|
end
|
|
139
173
|
end
|
|
@@ -15,6 +15,20 @@ module Farce
|
|
|
15
15
|
module Wrapper
|
|
16
16
|
include Internal::MarshalSupport::Reject
|
|
17
17
|
|
|
18
|
+
# Freezing an attempt view must not freeze its staged data or participant.
|
|
19
|
+
module Unfreezable
|
|
20
|
+
def append_features(mod)
|
|
21
|
+
super
|
|
22
|
+
if mod.is_a?(Class)
|
|
23
|
+
mod.class_eval { undef freeze if method_defined?(:freeze) }
|
|
24
|
+
else
|
|
25
|
+
mod.extend(Unfreezable)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
private_constant :Unfreezable
|
|
30
|
+
extend Unfreezable
|
|
31
|
+
|
|
18
32
|
# Wrappers inherit abstract container classes for their interfaces, but
|
|
19
33
|
# must not initialize another ordinary container. This constructor ends
|
|
20
34
|
# Unshareable's super chain after binding the existing transaction snapshot.
|
data/lib/farce/tree_map.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Farce
|
|
|
20
20
|
# @param entries [Hash, Array<Array(BasicObject, BasicObject)>, Abstract::Map, #each, nil]
|
|
21
21
|
# Optional initial entries for the map.
|
|
22
22
|
# @param mode [Symbol] The default value transfer mode.
|
|
23
|
-
def initialize(entries = nil, mode: :
|
|
23
|
+
def initialize(entries = nil, mode: :smart, normalize_keys: nil, **keyword_entries)
|
|
24
24
|
unless keyword_entries.empty?
|
|
25
25
|
raise ArgumentError, "entries given as both positional and keyword arguments" unless entries.nil?
|
|
26
26
|
entries = keyword_entries
|
data/lib/farce/unsafe/lfu_map.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Farce
|
|
|
10
10
|
# does not coordinate competing callers, including callers loading the same key.
|
|
11
11
|
class LFUMap < Abstract::LFUMap
|
|
12
12
|
include Unshareable
|
|
13
|
+
include Internal::Freeze::Delegated
|
|
13
14
|
|
|
14
15
|
# @!method store_if_absent(key)
|
|
15
16
|
# Read an existing value or construct and store a value for an absent key.
|
|
@@ -29,6 +30,7 @@ module Farce
|
|
|
29
30
|
private
|
|
30
31
|
|
|
31
32
|
def new_bounded_map(...) = Internal::LFUMap.new(...)
|
|
33
|
+
def freeze_backend = @map
|
|
32
34
|
def new_key_locks(**) = nil
|
|
33
35
|
def with_key_lock(_key) = yield
|
|
34
36
|
end
|
data/lib/farce/unsafe/lru_map.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Farce
|
|
|
10
10
|
# does not coordinate competing callers, including callers loading the same key.
|
|
11
11
|
class LRUMap < Abstract::LRUMap
|
|
12
12
|
include Unshareable
|
|
13
|
+
include Internal::Freeze::Delegated
|
|
13
14
|
|
|
14
15
|
# @!method store_if_absent(key)
|
|
15
16
|
# Read an existing value or construct and store a value for an absent key.
|
|
@@ -29,6 +30,7 @@ module Farce
|
|
|
29
30
|
private
|
|
30
31
|
|
|
31
32
|
def new_bounded_map(...) = Internal::LRUMap.new(...)
|
|
33
|
+
def freeze_backend = @map
|
|
32
34
|
def new_key_locks(**) = nil
|
|
33
35
|
def with_key_lock(_key) = yield
|
|
34
36
|
end
|
|
@@ -8,12 +8,14 @@ module Farce
|
|
|
8
8
|
# A {Abstract::TreeMap tree map} that is not thread-safe and cannot be shared between Ractors.
|
|
9
9
|
class TreeMap < Abstract::TreeMap
|
|
10
10
|
include Unshareable
|
|
11
|
+
include Internal::Freeze::Delegated
|
|
11
12
|
|
|
12
13
|
undef_method :transaction_wrapper
|
|
13
14
|
|
|
14
15
|
private
|
|
15
16
|
|
|
16
17
|
def new_tree_map(...) = Internal::UnsafeTreeMap.new(...)
|
|
18
|
+
def freeze_backend = @map
|
|
17
19
|
def new_key_locks = nil
|
|
18
20
|
def with_key_lock(_key) = yield
|
|
19
21
|
end
|
data/lib/farce/unshareable.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
module Farce
|
|
6
6
|
# Mixin that makes sure instances cannot be shared between Ractors.
|
|
7
|
-
#
|
|
7
|
+
# Instances may be frozen without becoming Ractor-shareable.
|
|
8
8
|
#
|
|
9
9
|
# @example
|
|
10
10
|
# class MyClass
|
|
@@ -12,26 +12,10 @@ module Farce
|
|
|
12
12
|
# end
|
|
13
13
|
#
|
|
14
14
|
# object = MyClass.new
|
|
15
|
-
# object.freeze #
|
|
15
|
+
# object.freeze # Preserves the Ractor restrictions.
|
|
16
16
|
#
|
|
17
17
|
# Ractor.shareable?(object) # => false
|
|
18
18
|
module Unshareable
|
|
19
|
-
# @api private
|
|
20
|
-
module UndefFreeze # :nodoc: all
|
|
21
|
-
# @api private
|
|
22
|
-
def append_features(mod)
|
|
23
|
-
super
|
|
24
|
-
if mod.is_a?(Class)
|
|
25
|
-
mod.class_eval { undef freeze if method_defined?(:freeze) }
|
|
26
|
-
else
|
|
27
|
-
mod.extend(UndefFreeze)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private_constant :UndefFreeze
|
|
33
|
-
extend UndefFreeze
|
|
34
|
-
|
|
35
19
|
# Mixin for classes that cannot be shared across Ractors, but may be moved between them.
|
|
36
20
|
module Movable
|
|
37
21
|
include Unshareable
|
|
@@ -58,7 +42,12 @@ module Farce
|
|
|
58
42
|
def pin_unshareable
|
|
59
43
|
return unless Internal.native_ractors?
|
|
60
44
|
case self
|
|
61
|
-
when Movable
|
|
45
|
+
when Movable
|
|
46
|
+
if is_a?(Copyable)
|
|
47
|
+
Internal::Unshareable.prevent_shareable(self)
|
|
48
|
+
else
|
|
49
|
+
Internal::Unshareable.prevent_copyable(self)
|
|
50
|
+
end
|
|
62
51
|
when Copyable then Internal::Unshareable.prevent_movable(self)
|
|
63
52
|
else Internal::Unshareable.pin_to_current_ractor(self)
|
|
64
53
|
end
|
data/lib/farce/unshared/atom.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Farce
|
|
|
12
12
|
# atom.update { |items| items + [:job] }
|
|
13
13
|
class Atom < Abstract::Atom
|
|
14
14
|
include Unshareable
|
|
15
|
+
include Internal::Freeze::Delegated
|
|
15
16
|
|
|
16
17
|
# @param value [BasicObject, nil] the initial value
|
|
17
18
|
# @param compare_by_identity [Boolean] whether comparisons use object identity
|
|
@@ -19,6 +20,8 @@ module Farce
|
|
|
19
20
|
@atom = Internal::UnsharedAtom.new(value, compare_by_identity:)
|
|
20
21
|
super()
|
|
21
22
|
end
|
|
23
|
+
|
|
24
|
+
private def freeze_backend = @atom
|
|
22
25
|
end
|
|
23
26
|
end
|
|
24
27
|
end
|
|
@@ -7,10 +7,12 @@ module Farce
|
|
|
7
7
|
# A thread-safe LFU map for mutable keys and values used within one Ractor.
|
|
8
8
|
class LFUMap < Abstract::LFUMap
|
|
9
9
|
include Unshareable
|
|
10
|
+
include Internal::Freeze::Delegated
|
|
10
11
|
|
|
11
12
|
private
|
|
12
13
|
|
|
13
14
|
def new_bounded_map(...) = Internal::LFUMap.new(...)
|
|
15
|
+
def freeze_backend = @map
|
|
14
16
|
|
|
15
17
|
def new_key_locks(compare_keys_by_identity:)
|
|
16
18
|
Internal::KeyLockMap.new(registry_class: Farce::Unshared::Map, compare_keys_by_identity:)
|
|
@@ -7,10 +7,12 @@ module Farce
|
|
|
7
7
|
# A thread-safe LRU map for mutable keys and values used within one Ractor.
|
|
8
8
|
class LRUMap < Abstract::LRUMap
|
|
9
9
|
include Unshareable
|
|
10
|
+
include Internal::Freeze::Delegated
|
|
10
11
|
|
|
11
12
|
private
|
|
12
13
|
|
|
13
14
|
def new_bounded_map(...) = Internal::LRUMap.new(...)
|
|
15
|
+
def freeze_backend = @map
|
|
14
16
|
|
|
15
17
|
def new_key_locks(compare_keys_by_identity:)
|
|
16
18
|
Internal::KeyLockMap.new(registry_class: Farce::Unshared::Map, compare_keys_by_identity:)
|
data/lib/farce/unshared/map.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Farce
|
|
|
12
12
|
# `dup` and `clone` copy entries into independent storage while sharing stored values.
|
|
13
13
|
class Map < Abstract::ConcurrentMap
|
|
14
14
|
include Unshareable
|
|
15
|
+
include Internal::Freeze::Delegated
|
|
15
16
|
|
|
16
17
|
# @!macro map_initialization
|
|
17
18
|
#
|
|
@@ -44,6 +45,8 @@ module Farce
|
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
Internal.prepare_map_access(self, :unshared)
|
|
48
|
+
|
|
49
|
+
private def freeze_backend = @map
|
|
47
50
|
end
|
|
48
51
|
end
|
|
49
52
|
end
|