Class: Farce::Proxy

Inherits:
BasicObject
Includes:
Farce.const_get(:Internal)::Autoloads, Farce.const_get(:Internal)::Delegation
Defined in:
lib/farce/proxy.rb,
lib/farce/proxy/wrapper.rb,
lib/farce/proxy/register.rb,
lib/farce/proxy/supervisor.rb

Overview

A proxy mimics the API of another object, executing method calls within the Ractor that created it. This is an easy way to have drop-in replacements for objects that cannot be shared across Ractors. Delegated calls raise Ractor::RemoteError after the owner exits.

# Mutable arrays aren't Ractor-shareable
array   = []
proxied = []
proxy   = Farce::Proxy.new(proxied)

Farce::Ractor.new(array, proxy) do |*list|
  list.each { it << 42 }
end.join

# The array got copied instead of being modified in place
array # => []

# The proxy didn't get copied
proxied # => [42]

Customizing Proxy Behavior

Imagine we have an unshareable class:

class MyClass
  include Farce::Unshareable
  attr_reader :record

  def initialize = @record = []
  def <<(value) = @record << value
end

We can use a proxy to still allow cross-ractor access:

my_instance = MyClass.new
my_proxy    = Farce::Proxy.create(my_instance)

Ractor.new(my_proxy) { it << 42 }.join

my_instance.record # => [42]

The above example has one issue: We cannot chain the calls, because << returns an array, which actually gets copied.

my_instance = MyClass.new
my_proxy    = Farce::Proxy.create(my_instance)

Ractor.new(my_proxy) { it << 42 << 43 }.join

my_instance.record # => [42]

We could easily fix this inside MyClass by returning self from <<, but maybe we don't actually control MyClass? In that case, we can add a proxy definition:

# The "inner" layer runs in the same ractor that the proxied object lives in.
Farce::Proxy.define(MyClass, layer: :inner) do
  # Also proxy the value returned by `<<`
  def <<(...) = __proxy__(super)
end

my_instance = MyClass.new
my_proxy    = Farce::Proxy.create(my_instance)

Ractor.new(my_proxy) { it << 42 << 43 }.join

my_instance.record # => [42, 43]

Advanced customization

The proxy wrappers define methods for all supported modes. Use the outer wrapper for argument handling and memoization, use the inner wrapper for handling return values:

# This class cannot easily be shared across Ractors, as it uses a Mutex and a mutable array.
class MyClass
  attr_reader :capacity

  def initialize(capacity = 100)
    @capacity = Integer(capacity)
    @record   = []
    @mutex    = Mutex.new
  end

  def add(value)
    @mutex.synchronize do
      return false if @record.size >= @capacity
      @record << value
      true
    end
  end

  def to_a = @record.dup
end

# Lets define some rules

Farce::Proxy.define(MyClass, layer: :outer) do
  # Capacity doesn't change, so we can memoize it.
  def capacity = @capacity ||= super

  # Maybe a common use-case is adding MyClass instances to MyClass instances?
  # If so, maybe we want to proxy these as well? Make everything else shareable.
  def add(value)
    value = __proxy__(value) if value.is_a? MyClass
    super __make_shareable__(value)
  end
end

Farce::Proxy.define(MyClass, layer: :inner) do
  # to_a already creates a new array every time, we don't have to copy it again
  def to_a = __move__(super)
end

Proxies should mimic the original object's interface as closely as possible, but nothing keeps you from adding custom methods:

class MyClass
  def is_proxy? = false
end

Farce::Proxy.define(MyClass) do
  def is_proxy? = true
  def proxy_method = "Hi from the proxy!!!"
end

my_instance = MyClass.new
my_proxy    = Farce::Proxy.create(my_instance)

my_instance.is_proxy? # => false
my_proxy.is_proxy?    # => true

my_proxy.proxy_method if my_proxy.is_proxy? # => "Hi from the proxy!!!"

Defined Under Namespace

Classes: Register

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(object, register: nil, scheduler: nil) ⇒ Proxy

Returns a new instance of Proxy.

Parameters:



253
254
255
256
257
258
259
260
261
# File 'lib/farce/proxy.rb', line 253

def initialize(object, register: nil, scheduler: nil)
  @token        = ::Object.new.freeze # cross-ractor finalizers don't work reliably across Ruby versions
  @supervisor   = Supervisor.new(register || REGISTER, scheduler || ::Farce, ::Farce::WeakValue.new(@token))
  @proxy_class  = ::Kernel.instance_method(:class).bind_call(self)
  @object_class = ::Kernel.instance_method(:class).bind_call(object)
  @object_id    = ::BasicObject.instance_method(:__id__).bind_call(object)
  __freeze__
  @supervisor.run(object)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing ⇒ BasicObject (private)

Dispatches method calls to be processed by the Ractor the proxied object resides in.



284
285
286
287
# File 'lib/farce/proxy.rb', line 284

def method_missing(...)
  result = @supervisor.send(...)
  SELF.equal?(result) ? self : result
end

Class Method Details

.create(object, register: nil, scheduler: nil) ⇒ BasicObject, Farce::Proxy .create(register: nil) { ... } ⇒ BasicObject, Farce::Proxy .create(scheduler:, register: nil) { ... } ⇒ BasicObject, Farce::Proxy

Returns the proxied object or the original object if it is Ractor-shareable.

Overloads:

  • .create(object, register: nil, scheduler: nil) ⇒ BasicObject, Farce::Proxy

    Creates a new proxy for the given object if it is not Ractor-shareable. Returns the given object otherwise.

    Parameters:

  • .create(register: nil) { ... } ⇒ BasicObject, Farce::Proxy

    Creates a new Ractor, runs the given block within it, and returns the result. If the result is not Ractor-shareable, it will be proxied. The new Ractor stops after its proxies are collected.

    Yields:

    • The block to be executed within the new Ractor.

    Yield Receiver:

    • (nil)
  • .create(scheduler:, register: nil) { ... } ⇒ BasicObject, Farce::Proxy

    Schedules the given block to be executed by the specified scheduler. Returns its result. If the result is not Ractor-shareable, it will be proxied.

    Yields:

    • The block to be executed by the scheduler.

    Yield Receiver:

    • (nil)

Returns:

  • (BasicObject, Farce::Proxy) —

    the proxied object or the original object if it is Ractor-shareable



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/farce/proxy.rb', line 195

def self.create(object = UNDEFINED, register: nil, scheduler: nil, &)
  if UNDEFINED.equal?(object)
    initializer = ::Farce::Strict::Atom.new(::Farce::Ractor.shareable_proc(&))
    success     = ::Farce::Strict::Atom.new
    result      = ::Farce::Strict::Atom.new
    owned       = scheduler.nil?
    scheduler ||= owner_scheduler

    schedule_initializer(scheduler, initializer, register, success, result, owned)

    success.wait_until_non_nil
    return result.swap(nil) if success.value
    raise ::Farce::Ractor::RemoteError, result.swap(nil)
  end

  return object if Ractor.shareable?(object)
  new(object, register:, scheduler:)
end

.default_register ⇒ Farce::Proxy::Register

Returns the default register used by the proxy class.

Returns:



166
# File 'lib/farce/proxy.rb', line 166

def self.default_register = REGISTER

.define(klass, layer: :outer) { ... } ⇒ BasicObject

Created a new module that will be included in the proxy for the specified class and layer. You can define methods using def within the block. These methods may call super (which will ultimately delegate to the proxied object).

The inner layer is invoked inside the Ractor the proxy resides in, while the outer layer is invoked in the caller's Ractor.

The wrapper objects do not have to be Ractor-shareable, so you may use instance variables for memoization.

Parameters:

  • klass (Class) —

    the class to define wrapper methods for

  • layer (Symbol) (defaults to: :outer) —

    the layer of the proxy to define methods for (:inner or :outer)

Yields:

  • block to define wrapper methods for the proxied object

See Also:

Yield Receiver:

  • (Module) —

    the module representing the wrapper layer, extended by Definition



172
# File 'lib/farce/proxy.rb', line 172

def self.define(...) = REGISTER.define(...)

Instance Method Details

#inspect ⇒ String

Note:

Does not call #inspect on the proxied object itself, to avoid Ractor round-trips

Returns a string representation of the proxy object.

Returns:

  • (String) —

    a string representation of the proxy object



268
# File 'lib/farce/proxy.rb', line 268

def inspect = "#<#{@proxy_class.inspect} object=#<#{@object_class.inspect}:0x#{@object_id.to_s(16)}>>"

#ractor_shareable? ⇒ true

Returns proxies can be shared even when their targets cannot.

Returns:

  • (true) —

    proxies can be shared even when their targets cannot



264
# File 'lib/farce/proxy.rb', line 264

def ractor_shareable? = true