Class: Farce::Task

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Abstract::Value

#blank?, #unwrap

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 a Farce::Proxy that 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.

Examples:

Using a Task without an Executor

task = Farce::Task.new(1, 2) { |a, b| a + b }

task.alive?       # => true
task.call == task # => true
task.alive?       # => false
task.value        # => 3

Passing a task to an executor

task = Farce::Task.new { puts "Hello, world!" }
Farce.in_parallel(&task)
task.join # waits for the task to complete

Parameters:

  • args (Array) —

    the arguments to pass to the task

  • mode (Symbol) (defaults to: :smart) —

    the mode for arguments and the successful result Arguments can be overridden by #add_argument.

Yields:

  • (*args) —

    the block to execute with the given arguments

Yield Parameters:

  • args (Array) —

    the arguments passed to the block

Yield Returns:

  • (Object) —

    the result of the block execution, will become the task's #value

Raises:

  • ArgumentError if the provided mode is invalid

  • LocalJumpError if no callback is provided



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.

Returns:

  • (Symbol) —

    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

Creates a task and schedules it for execution on the globally shared executor pool. 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 a Farce::Proxy that 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.

Parameters:

  • args (Array) —

    the arguments to pass to the task

  • mode (Symbol) (defaults to: :smart) —

    the mode for arguments and the successful result Arguments can be overridden by #add_argument.

Yields:

  • (*args) —

    the block to execute with the given arguments

Yield Parameters:

  • args (Array) —

    the arguments passed to the block

Yield Returns:

  • (Object) —

    the result of the block execution, will become the task's #value

Returns:

  • (Task) —

    the created task

Raises:

  • ArgumentError if the provided mode is invalid

  • LocalJumpError if no callback is provided



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

Creates a task and schedules it for execution on the main scheduler. If auto_local is true (default), mode will be set to :local if the method is called from the main Ractor.

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 a Farce::Proxy that 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.

Parameters:

  • args (Array) —

    the arguments to pass to the task

  • mode (Symbol) (defaults to: :smart) —

    the mode for arguments and the successful result Arguments can be overridden by #add_argument.

  • auto_local (Boolean) (defaults to: true) —

    whether to automatically switch to local mode if called from the main Ractor

Yields:

  • (*args) —

    the block to execute with the given arguments

Yield Parameters:

  • args (Array) —

    the arguments passed to the block

Yield Returns:

  • (Object) —

    the result of the block execution, will become the task's #value

Returns:

  • (Task) —

    the created task

Raises:

  • ArgumentError if the provided mode is invalid

  • LocalJumpError if no callback is provided



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

Creates a task and schedules it for execution. A scheduler may optionally be provided.

If no scheduler is provided, then it will look for a Ractor-local scheduler if mode is :local or auto_local is true. If none is found, one will be created automatically if :mode is :local, otherwise the globally shared executor pool will be used.

The scheduler may be a executor, or a Fiber scheduler.

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 a Farce::Proxy that 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.

Parameters:

  • args (Array) —

    the arguments to pass to the task

  • mode (Symbol) (defaults to: :smart) —

    the mode for arguments and the successful result Arguments can be overridden by #add_argument.

  • scheduler (Abstract::Executor, Fiber::Scheduler, nil) (defaults to: nil) —

    the scheduler to use for execution

Yields:

  • (*args) —

    the block to execute with the given arguments

Yield Parameters:

  • args (Array) —

    the arguments passed to the block

Yield Returns:

  • (Object) —

    the result of the block execution, will become the task's #value

Returns:

  • (Task) —

    the created task

Raises:

  • ArgumentError if the provided mode is invalid

  • LocalJumpError if no callback is provided



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?

Creates a task and attempts to schedule it for execution.

Behaves exactly like schedule, except it uses Abstract::Executor#try_schedule instead of Abstract::Executor#schedule if the scheduler is an executor, and that it returns nil if it fails to schedule the task.

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 a Farce::Proxy that 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.

Parameters:

  • args (Array) —

    the arguments to pass to the task

  • mode (Symbol) (defaults to: :smart) —

    the mode for arguments and the successful result Arguments can be overridden by #add_argument.

Yields:

  • (*args) —

    the block to execute with the given arguments

Yield Parameters:

  • args (Array) —

    the arguments passed to the block

Yield Returns:

  • (Object) —

    the result of the block execution, will become the task's #value

Returns:

  • (Task, nil) —

    the created task, or nil if it failed to schedule

Raises:

  • ArgumentError if the provided mode is invalid

  • LocalJumpError if no callback is provided



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.

Examples:

A task using move for the return value, but copy for its argument

task = Farce::Task.new(mode: :move) { |arg| arg.upcase }
task.add_argument("hello", mode: :copy)
Farce.in_parallel(&task)
task.value # => "HELLO"

Parameters:

  • argument (BasicObject) —

    the argument to add to the task

  • mode (Symbol, nil) (defaults to: nil) —

    the mode to use for the argument, or nil to use the task's default mode

Returns:

  • (self) —

    the task itself, for chaining

Raises:

  • ArgumentError if the provided mode is invalid

  • FrozenError if the task has already been frozen or called

  • InvalidStateError if trying to add a local argument from more than one ractor



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).

Returns:

  • (Boolean) —

    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.

Returns:

  • (self) —

    the task itself, for chaining

Raises:

  • InvalidStateError if the task has already been claimed by another ractor or has already been called



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.message}", 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.

Parameters:

  • timeout (Numeric, nil) (defaults to: nil) —

    the maximum time to wait, or nil to wait indefinitely

Returns:

  • (self, nil) —

    the task itself if it finished within the timeout, or nil otherwise



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).

Returns:

  • (Boolean) —

    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.

Returns:

  • (Proc) —

    a Proc that calls the task's call method



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.

Parameters:

  • timeout (Numeric, nil) (defaults to: nil) —

    the maximum time to wait, or nil to wait indefinitely

Returns:

  • (BasicObject, nil) —

    the value returned by the task's callback, or nil if the task did not finish within the timeout

Raises:

  • Exception if the task raises an error

  • RuntimeError with the original class, message, and backtrace if the exception cannot be shared

  • RuntimeError if execution is aborted before producing a result or exception



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