Class: Farce::Task
- Inherits:
-
Object
- Object
- Farce::Task
- Includes:
- Abstract::Value, Internal::Freeze::Delegated, Internal::MarshalSupport::Reject
- Defined in:
- lib/farce/task.rb
Overview
A unit of work that executes once and retains its result or failure.
# You can create and schedule a task with one call:
task = Farce::Task.schedule { 1 + 2 }
task.value # => 3
# There is also a shorthand for running a task on the global Ractor pool:
task = Farce::Task.in_parallel { sleep 1 }
task.join
task.alive? # => false
# schedule accepts a custom scheduler:
scheduler = Farce::Scheduler.create
task = Farce::Task.schedule(scheduler:) { 1 + 2 }
# or alternatively you can manually create a task and hand it to the scheduler
task = Farce::Task.new { 1 + 2 }
scheduler.schedule(&task)
# or execute it manually
task = Farce::Task.new { Ractor.current.name.upcase }
Ractor.new(task, name: "Herbert") { it.call }
task.value # => "HERBERT"
Constant Summary collapse
- InvalidStateError =
Raised when an operation is attempted on a task that is in an incorrect state.
Class.new(RuntimeError)
Instance Attribute Summary collapse
-
#mode ⇒ Symbol
readonly
The default mode used for task arguments and the return value.
Class Method Summary collapse
-
.in_parallel(*args, mode: :smart) {|*args| ... } ⇒ Task
Creates a task and schedules it for execution on the globally shared executor pool.
-
.on_main(*args, mode: :smart, auto_local: true) {|*args| ... } ⇒ Task
Creates a task and schedules it for execution on the main scheduler.
-
.schedule(*args, mode: :smart, auto_local: true, scheduler: nil) {|*args| ... } ⇒ Task
Creates a task and schedules it for execution.
-
.try_schedule(*args, mode: :smart, auto_local: true, scheduler: nil) {|*args| ... } ⇒ Task?
Creates a task and attempts to schedule it for execution.
Instance Method Summary collapse
-
#add_argument(argument, mode: nil) ⇒ self
(also: #<<)
Adds an argument to the task.
-
#alive? ⇒ Boolean
Whether the task is currently alive (initialized or running).
-
#call ⇒ self
Executes the task by invoking its callback with the added arguments.
-
#initialize(*args, mode: :smart) {|*args| ... } ⇒ Task
constructor
Creates a new task with the given arguments and callback.
-
#join(timeout = nil) ⇒ self?
Waits for the task to finish, with an optional timeout.
-
#local? ⇒ Boolean
Whether the task is local (i.e., its mode is
:local). -
#to_proc ⇒ Proc
Converts the task to a Proc object.
-
#value(timeout = nil) ⇒ BasicObject?
Waits for the task to finish and returns its value, with an optional timeout.
Methods included from Abstract::Value
Constructor Details
#initialize(*args, mode: :smart) {|*args| ... } ⇒ Task
Creates a new task with the given arguments and callback.
This does not schedule the task for execution, nor does it associate it with any executor.
In fact, the task object itself is a proc-like object, that can be called like a regular proc, but will track its execution state and result.
Valid modes are:
:smart- Applies smart rules or a dedicated proxy. Raises for unsupported values. This is the default for most mode-backed classes.:smart_raise- Applies smart rules or a dedicated proxy, with a raising fallback.:smart_copy- Applies smart rules or a dedicated proxy, then falls back to copying or Marshal. This is the default for Port.:smart_move- Applies smart rules or a dedicated proxy, then falls back to moving.:copy- The value will be copied between Ractors.:marshal- The value will be serialized immediately and reconstructed once per Ractor with Ruby's Marshal protocol. Contents must support Marshal. Nested shareable values may be reconstructed too.:make_shareable- The value will be made Ractor-shareable using Ractor.make_shareable.:move- The value will be moved between Ractors. This saves memory compared to copying, and supports values that can't be copied but moved (like IO objects). However, the value will no longer be accessible on the Ractor that pushed it.:mutable- A Mutable instance will be created for the value. This isn't done recursively and thus will fail for nested unshareable values.:local- The value will be kept local to the Ractor that pushed it. Another ractor trying to receive it will get an error. Useful for usage contained within a single Ractor.:proxy- The value will be wrapped in aFarce::Proxythat executes calls in the original Ractor.:raise- An error will be raised if the value is not Ractor-shareable. Useful for enforcing shareability.:dedup- The value will be deduplicated using Farce.dedup, then made Ractor-shareable. This may update and freeze the original. Already-shareable values pass through unchanged.:shareable_copy- The value will be copied and the copy will be made Ractor-shareable.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/farce/task.rb', line 143 def initialize(*args, mode: :smart, &callback) raise ArgumentError, "invalid mode: #{mode}" unless MODES.include?(mode) raise LocalJumpError, "no callback provided" unless block_given? @mode = mode @ractor = Strict::Atom.new @state = Strict::Atom.new(:initialized) if mode == :local @arguments = args @callback = callback @manager = nil @ractor.value = Ractor.current @result = Unshared::Atom.new else @arguments = Strict::Vector.new @callback = Ractor.shareable?(callback) ? callback : Ractor.shareable_proc(&callback) @manager = ModeManager.new(mode: mode) @result = Strict::Atom.new args.each { add_argument(it) } end __freeze__ end |
Instance Attribute Details
#mode ⇒ Symbol (readonly)
Returns the default mode used for task arguments and the return value.
120 121 122 |
# File 'lib/farce/task.rb', line 120 def mode @mode end |
Class Method Details
.in_parallel(*args, mode: :smart) {|*args| ... } ⇒ Task
54 |
# File 'lib/farce/task.rb', line 54 def self.in_parallel(*, **, &) = enqueue(false, Farce.in_parallel, *, auto_local: false, **, &) |
.on_main(*args, mode: :smart, auto_local: true) {|*args| ... } ⇒ Task
63 |
# File 'lib/farce/task.rb', line 63 def self.on_main(*, **, &) = enqueue(false, Farce.on_main, *, **, &) |
.schedule(*args, mode: :smart, auto_local: true, scheduler: nil) {|*args| ... } ⇒ Task
78 |
# File 'lib/farce/task.rb', line 78 def self.schedule(*, scheduler: nil, **, &) = enqueue(false, scheduler, *, **, &) |
.try_schedule(*args, mode: :smart, auto_local: true, scheduler: nil) {|*args| ... } ⇒ Task?
89 |
# File 'lib/farce/task.rb', line 89 def self.try_schedule(*, scheduler: nil, **, &) = enqueue(true, scheduler, *, **, &) |
Instance Method Details
#add_argument(argument, mode: nil) ⇒ self Also known as: <<
Adds an argument to the task. Can only be called before the task is frozen and before #call is invoked.
Allows for arguments to use different modes than the return value (or other arguments). Mode is ignored for local tasks.
For non-local tasks, mode may only be set to :local for arguments added by the Ractor that will
execute the task.
189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/farce/task.rb', line 189 def add_argument(argument, mode: nil) check_frozen! unless local? raise ArgumentError, "invalid mode: #{mode}" unless mode.nil? || MODES.include?(mode) raise InvalidStateError, "cannot add local argument to a non-local task" if mode == :local && !claim_for_ractor? argument = @manager.wrap(argument, mode:) end @arguments << argument self end |
#alive? ⇒ Boolean
Returns whether the task is currently alive (initialized or running).
205 |
# File 'lib/farce/task.rb', line 205 def alive? = alive_state?(@state.value) |
#call ⇒ self
Executes the task by invoking its callback with the added arguments.
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/farce/task.rb', line 210 def call raise InvalidStateError, "task already claimed by another ractor" unless claim_for_ractor? raise InvalidStateError, "call may only be called once" unless @state.compare_and_set(:initialized, :running) begin arguments = local? ? @arguments.freeze : @arguments.freeze.to_a.map! { @manager.unwrap(it) } result = @callback.call(*arguments) @result.value = local? ? result : @manager.wrap(result) @state.value = :finished rescue Exception => e # rubocop:disable Lint/RescueException # Every callback failure must release waiters, including ScriptError and SystemExit. begin @result.value = e rescue Ractor::Error @result.value = [RuntimeError, -"#{e.class}: #{e.}", Ractor.make_shareable(e.backtrace)].freeze end @state.value = :error ensure if @state.value == :running @result.value = [RuntimeError, -"task execution aborted"].freeze @state.value = :error end end self end |
#join(timeout = nil) ⇒ self?
Waits for the task to finish, with an optional timeout.
239 240 241 242 |
# File 'lib/farce/task.rb', line 239 def join(timeout = nil) return unless @state.wait_while(timeout:) { alive_state?(it) } self end |
#local? ⇒ Boolean
Returns whether the task is local (i.e., its mode is :local).
245 |
# File 'lib/farce/task.rb', line 245 def local? = @mode == :local |
#to_proc ⇒ Proc
Converts the task to a Proc object. If the task is Ractor-shareable, then so is the returned Proc.
266 267 268 269 270 |
# File 'lib/farce/task.rb', line 266 def to_proc return method(:call).to_proc if local? task = self Ractor.make_shareable(-> { task.call }) end |
#value(timeout = nil) ⇒ BasicObject?
Waits for the task to finish and returns its value, with an optional timeout. If the task raises an error, it will be propagated.
256 257 258 259 260 261 |
# File 'lib/farce/task.rb', line 256 def value(timeout = nil) case @state.wait_while(timeout:) { alive_state?(it) } when :finished then local? ? @result.value : @manager.unwrap(@result.value) when :error then raise(*@result.value) end end |