farce 0.2.0 → 0.3.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 +47 -0
- data/README.md +39 -1
- data/docs/modes.md +8 -5
- data/docs/scopes.md +3 -1
- data/docs/variants.md +18 -0
- data/lib/farce/abstract/executor.rb +130 -0
- data/lib/farce/abstract/pool.rb +334 -0
- data/lib/farce/abstract/scheduler.rb +539 -93
- data/lib/farce/engine/jruby/signal.rb +7 -4
- 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.rb +9 -0
- 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/scheduled_task.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/local/pool.rb +111 -0
- data/lib/farce/local/scheduler.rb +219 -0
- data/lib/farce/pool.rb +2 -290
- data/lib/farce/proxy.rb +2 -2
- data/lib/farce/scheduler.rb +1 -502
- data/lib/farce/task.rb +285 -0
- data/lib/farce/{thread_scheduler.rb → thread_executor.rb} +7 -7
- data/lib/farce/unshared/pool.rb +44 -0
- data/lib/farce/unshared/scheduler.rb +89 -0
- data/lib/farce/version.rb +1 -1
- data/lib/farce.rb +9 -17
- metadata +13 -2
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`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Unshared
|
|
7
|
+
# An elastic pool of thread workers within one Ractor.
|
|
8
|
+
#
|
|
9
|
+
# Uses the same scaling and fiber schedulers as {Farce::Pool}. Tasks keep their
|
|
10
|
+
# blocks and argument references. `auto_local: true` ignores `mode`. With
|
|
11
|
+
# `auto_local: false`, only `:local` and smart modes are accepted.
|
|
12
|
+
# The pool cannot be copied or moved to another Ractor.
|
|
13
|
+
# A scheduler constructor may capture objects from this Ractor.
|
|
14
|
+
#
|
|
15
|
+
# @example Submit work using local state
|
|
16
|
+
# results = Farce::Unshared::Queue.new
|
|
17
|
+
# pool = Farce::Unshared::Pool.new(max_size: 2)
|
|
18
|
+
# pool.schedule { results.push(:done) }
|
|
19
|
+
# results.pop # => :done
|
|
20
|
+
# pool.close
|
|
21
|
+
class Pool < Abstract::Pool
|
|
22
|
+
include Unshareable
|
|
23
|
+
include Internal::LocalExecutorMethods
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def publish_pool = pin_unshareable
|
|
28
|
+
def prepare_constructor(constructor) = constructor
|
|
29
|
+
def new_queue(capacity:) = Internal::UnsharedQueue.new(capacity:, track_age: true)
|
|
30
|
+
def new_worker = Internal::UnsharedPoolWorker.new(self)
|
|
31
|
+
def new_scheduler(**, &) = Scheduler.new(**, &)
|
|
32
|
+
def launch_scheduler(scheduler) = scheduler.launch_thread
|
|
33
|
+
|
|
34
|
+
def prepare_task(args, block, mode, auto_local)
|
|
35
|
+
validate_local_mode(mode, auto_local)
|
|
36
|
+
Internal::LocalScheduledTask.new(args, block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def schedule_scaler(started, delay = @grow_after)
|
|
40
|
+
Internal::ThreadPoolSupervisor.current.schedule(self, started, delay)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
module Unshared
|
|
7
|
+
# A task scheduler that runs work within its creating Ractor.
|
|
8
|
+
#
|
|
9
|
+
# Use {.create} to start a background thread, or install an instance with
|
|
10
|
+
# `Fiber.set_scheduler` on the current thread. Tasks retain their blocks and
|
|
11
|
+
# argument references. The scheduler cannot be copied or moved to another Ractor.
|
|
12
|
+
#
|
|
13
|
+
# `auto_local: true` ignores `mode`. With `auto_local: false`, only `:local`
|
|
14
|
+
# and smart modes are accepted. All accepted modes keep tasks local.
|
|
15
|
+
#
|
|
16
|
+
# @example Run tasks with captured local state
|
|
17
|
+
# results = Farce::Unshared::Queue.new
|
|
18
|
+
# scheduler = Farce::Unshared::Scheduler.create
|
|
19
|
+
# scheduler.schedule { results.push(:done) }
|
|
20
|
+
# results.pop # => :done
|
|
21
|
+
# scheduler.close
|
|
22
|
+
#
|
|
23
|
+
# @!method schedule(*args, mode: :smart, auto_local: true)
|
|
24
|
+
# Enqueue a task while retaining its block and argument references.
|
|
25
|
+
# `mode` is ignored when `auto_local` is true. Otherwise, only local and
|
|
26
|
+
# smart modes are accepted.
|
|
27
|
+
# @param (see Farce::Abstract::Executor#schedule)
|
|
28
|
+
# @return [self]
|
|
29
|
+
class Scheduler < Abstract::Scheduler
|
|
30
|
+
include Unshareable
|
|
31
|
+
include Internal::LocalExecutorMethods
|
|
32
|
+
|
|
33
|
+
# Start a worker thread in this Ractor. Returns the running scheduler.
|
|
34
|
+
# Additional options and the block are forwarded to {#initialize}.
|
|
35
|
+
# @param executor [Class, nil] Thread or a custom executor that stays in this Ractor.
|
|
36
|
+
# @param name [String, nil] Optional worker name.
|
|
37
|
+
# @param priority [Integer, nil] Optional worker thread priority.
|
|
38
|
+
# @yieldreturn [Object] Fiber scheduler to install in the worker.
|
|
39
|
+
# @return [Scheduler]
|
|
40
|
+
def self.create(executor = Thread, name: nil, priority: nil, **, &) = super
|
|
41
|
+
|
|
42
|
+
# Create a scheduler without starting a worker.
|
|
43
|
+
# A constructor block builds the fiber scheduler when launched or installed.
|
|
44
|
+
# It may capture objects from this Ractor.
|
|
45
|
+
#
|
|
46
|
+
# @overload initialize(capacity: 1024, backend: Farce.config.io_backend, &constructor)
|
|
47
|
+
# @param capacity [Integer, nil] Pending-task capacity, or nil for an unbounded queue.
|
|
48
|
+
# @param backend [Symbol] IO backend for the built-in scheduler.
|
|
49
|
+
# @yieldreturn [Object] Fiber scheduler to install in the worker.
|
|
50
|
+
def initialize(...)
|
|
51
|
+
super
|
|
52
|
+
self.owner = Ractor.current
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Start dispatching tasks in a new thread or a custom executor in this Ractor.
|
|
56
|
+
# Rejects Ractor executors before changing the scheduler's state.
|
|
57
|
+
# @param executor [Class, nil] Thread or a custom executor that stays in this Ractor.
|
|
58
|
+
# @param name [String, nil] Optional worker name.
|
|
59
|
+
# @param priority [Integer, nil] Optional worker thread priority.
|
|
60
|
+
# @return [Thread, Object] The worker.
|
|
61
|
+
# @raise [ArgumentError] If a Ractor executor is supplied.
|
|
62
|
+
def launch(executor = Thread, name: nil, priority: nil)
|
|
63
|
+
raise ArgumentError, "Unshared schedulers cannot launch Ractor workers" if executor && executor <= Ractor
|
|
64
|
+
super(executor || Thread, name:, priority:)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @raise [ArgumentError] Unshared schedulers cannot launch Ractor workers.
|
|
68
|
+
def launch_ractor(...) = launch(Ractor, ...)
|
|
69
|
+
|
|
70
|
+
# @param value [Ractor] The creating Ractor.
|
|
71
|
+
# @raise [ArgumentError] If another Ractor is supplied or the assigned owner changes.
|
|
72
|
+
def owner=(value)
|
|
73
|
+
raise ArgumentError, "owner must be the current Ractor" unless value == Ractor.current
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def prepare_constructor(constructor) = constructor
|
|
80
|
+
def new_queue(capacity:) = Internal::UnsharedQueue.new(capacity:)
|
|
81
|
+
def launch_callback = ->(_scheduler, priority, name) { launch!(name, priority) }
|
|
82
|
+
|
|
83
|
+
def prepare_task(args, block, mode, auto_local)
|
|
84
|
+
validate_local_mode(mode, auto_local)
|
|
85
|
+
Internal::LocalScheduledTask.new(args, block)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/farce/version.rb
CHANGED
data/lib/farce.rb
CHANGED
|
@@ -360,7 +360,7 @@ module Farce
|
|
|
360
360
|
# This allows scheduling without blocking the current thread.
|
|
361
361
|
#
|
|
362
362
|
# On JRuby and TruffleRuby, blocks run inline because there is no native Ractor isolation.
|
|
363
|
-
# The returned {
|
|
363
|
+
# The returned {ThreadExecutor} starts a new thread for each scheduled task.
|
|
364
364
|
#
|
|
365
365
|
# @example
|
|
366
366
|
# $results = []
|
|
@@ -408,9 +408,9 @@ module Farce
|
|
|
408
408
|
#
|
|
409
409
|
# @overload on_main
|
|
410
410
|
# Returns a scheduler that executes tasks on the main ractor.
|
|
411
|
-
# @return [Abstract::
|
|
411
|
+
# @return [Abstract::Executor]
|
|
412
412
|
#
|
|
413
|
-
# @return [Abstract::
|
|
413
|
+
# @return [Abstract::Executor, nil]
|
|
414
414
|
# @see .in_parallel
|
|
415
415
|
def self.on_main(*args, mode: UNDEFINED, &)
|
|
416
416
|
unless block_given?
|
|
@@ -448,9 +448,9 @@ module Farce
|
|
|
448
448
|
# @return [nil]
|
|
449
449
|
#
|
|
450
450
|
# @overload in_parallel
|
|
451
|
-
# @return [Abstract::
|
|
451
|
+
# @return [Abstract::Executor] The shared scheduler.
|
|
452
452
|
#
|
|
453
|
-
# @return [Abstract::
|
|
453
|
+
# @return [Abstract::Executor, nil]
|
|
454
454
|
# @see .on_main
|
|
455
455
|
def self.in_parallel(*args, mode: UNDEFINED, &)
|
|
456
456
|
unless block_given?
|
|
@@ -493,22 +493,14 @@ module Farce
|
|
|
493
493
|
raise LocalJumpError, "no block given" unless block_given?
|
|
494
494
|
|
|
495
495
|
if auto_local || mode == :local
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
scheduler = Internal.local_scheduler(create: mode == :local)
|
|
497
|
+
if scheduler && !scheduler.is_a?(Abstract::Executor)
|
|
498
|
+
scheduler.fiber(**) { yield(*) }
|
|
498
499
|
return
|
|
499
500
|
end
|
|
500
|
-
scheduler = Ractor.main? ? Internal::MainScheduler : Internal::Storage[:local_scheduler]
|
|
501
501
|
end
|
|
502
502
|
|
|
503
|
-
scheduler ||=
|
|
504
|
-
if mode == :local
|
|
505
|
-
Internal::Storage.store_if_absent(:local_scheduler) do
|
|
506
|
-
Internal.native_ractors? ? Scheduler.create(Thread) : ThreadScheduler.new
|
|
507
|
-
end
|
|
508
|
-
else
|
|
509
|
-
Internal::ParallelScheduler
|
|
510
|
-
end
|
|
511
|
-
|
|
503
|
+
scheduler ||= Internal::ParallelScheduler
|
|
512
504
|
scheduler.schedule(*, mode:, auto_local:, **, &)
|
|
513
505
|
nil
|
|
514
506
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: farce
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Haase
|
|
@@ -85,6 +85,7 @@ files:
|
|
|
85
85
|
- lib/farce/abstract/counter.rb
|
|
86
86
|
- lib/farce/abstract/duplicable_map.rb
|
|
87
87
|
- lib/farce/abstract/exchanger.rb
|
|
88
|
+
- lib/farce/abstract/executor.rb
|
|
88
89
|
- lib/farce/abstract/flag.rb
|
|
89
90
|
- lib/farce/abstract/lazy.rb
|
|
90
91
|
- lib/farce/abstract/lease.rb
|
|
@@ -94,6 +95,7 @@ files:
|
|
|
94
95
|
- lib/farce/abstract/lru_map.rb
|
|
95
96
|
- lib/farce/abstract/map.rb
|
|
96
97
|
- lib/farce/abstract/molecule.rb
|
|
98
|
+
- lib/farce/abstract/pool.rb
|
|
97
99
|
- lib/farce/abstract/port.rb
|
|
98
100
|
- lib/farce/abstract/priority_queue.rb
|
|
99
101
|
- lib/farce/abstract/queue.rb
|
|
@@ -262,6 +264,8 @@ files:
|
|
|
262
264
|
- lib/farce/internal/lease_pool_state.rb
|
|
263
265
|
- lib/farce/internal/lease_state.rb
|
|
264
266
|
- lib/farce/internal/lease_waiting.rb
|
|
267
|
+
- lib/farce/internal/local_executor_methods.rb
|
|
268
|
+
- lib/farce/internal/local_scheduled_task.rb
|
|
265
269
|
- lib/farce/internal/managed_queue.rb
|
|
266
270
|
- lib/farce/internal/map_value_modes.rb
|
|
267
271
|
- lib/farce/internal/marshal_support.rb
|
|
@@ -287,12 +291,14 @@ files:
|
|
|
287
291
|
- lib/farce/internal/strict_lease_map.rb
|
|
288
292
|
- lib/farce/internal/strict_lease_pool.rb
|
|
289
293
|
- lib/farce/internal/thread_pool.rb
|
|
294
|
+
- lib/farce/internal/thread_pool_supervisor.rb
|
|
290
295
|
- lib/farce/internal/transaction_conflict.rb
|
|
291
296
|
- lib/farce/internal/transaction_freeze_guard.rb
|
|
292
297
|
- lib/farce/internal/transaction_map_snapshot.rb
|
|
293
298
|
- lib/farce/internal/undefined.rb
|
|
294
299
|
- lib/farce/internal/unshared_lease.rb
|
|
295
300
|
- lib/farce/internal/unshared_lease_pool.rb
|
|
301
|
+
- lib/farce/internal/unshared_pool_worker.rb
|
|
296
302
|
- lib/farce/internal/unshared_queue_waiting.rb
|
|
297
303
|
- lib/farce/internal/value_serialization.rb
|
|
298
304
|
- lib/farce/internal/weak_map_value_modes.rb
|
|
@@ -316,8 +322,10 @@ files:
|
|
|
316
322
|
- lib/farce/local/lru_map.rb
|
|
317
323
|
- lib/farce/local/map.rb
|
|
318
324
|
- lib/farce/local/molecule.rb
|
|
325
|
+
- lib/farce/local/pool.rb
|
|
319
326
|
- lib/farce/local/priority_queue.rb
|
|
320
327
|
- lib/farce/local/queue.rb
|
|
328
|
+
- lib/farce/local/scheduler.rb
|
|
321
329
|
- lib/farce/local/scoped.rb
|
|
322
330
|
- lib/farce/local/set.rb
|
|
323
331
|
- lib/farce/local/sorted_set.rb
|
|
@@ -382,7 +390,8 @@ files:
|
|
|
382
390
|
- lib/farce/strict/weak_set.rb
|
|
383
391
|
- lib/farce/strict/weak_value_map.rb
|
|
384
392
|
- lib/farce/system.rb
|
|
385
|
-
- lib/farce/
|
|
393
|
+
- lib/farce/task.rb
|
|
394
|
+
- lib/farce/thread_executor.rb
|
|
386
395
|
- lib/farce/timer_queue.rb
|
|
387
396
|
- lib/farce/transaction.rb
|
|
388
397
|
- lib/farce/transaction/atom.rb
|
|
@@ -415,8 +424,10 @@ files:
|
|
|
415
424
|
- lib/farce/unshared/lru_map.rb
|
|
416
425
|
- lib/farce/unshared/map.rb
|
|
417
426
|
- lib/farce/unshared/molecule.rb
|
|
427
|
+
- lib/farce/unshared/pool.rb
|
|
418
428
|
- lib/farce/unshared/priority_queue.rb
|
|
419
429
|
- lib/farce/unshared/queue.rb
|
|
430
|
+
- lib/farce/unshared/scheduler.rb
|
|
420
431
|
- lib/farce/unshared/set.rb
|
|
421
432
|
- lib/farce/unshared/sorted_set.rb
|
|
422
433
|
- lib/farce/unshared/timer_queue.rb
|