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
-
.create(object = UNDEFINED, register: nil, scheduler: nil) ⇒ BasicObject, Farce::Proxy
The proxied object or the original object if it is Ractor-shareable.
-
.default_register ⇒ Farce::Proxy::Register
The default register used by the proxy class.
-
.define(klass, layer: :outer) ⇒ BasicObject
Created a new module that will be included in the proxy for the specified class and layer.
Instance Method Summary collapse
-
#initialize(object, register: nil, scheduler: nil) ⇒ Proxy
constructor
A new instance of Proxy.
-
#inspect ⇒ String
A string representation of the proxy object.
-
#ractor_shareable? ⇒ true
Proxies can be shared even when their targets cannot.
Constructor Details
#initialize(object, register: nil, scheduler: nil) ⇒ Proxy
Returns a new instance of Proxy.
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.
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.
166 |
# File 'lib/farce/proxy.rb', line 166 def self.default_register = REGISTER |
.define(klass, layer: :outer) { ... } ⇒ BasicObject
172 |
# File 'lib/farce/proxy.rb', line 172 def self.define(...) = REGISTER.define(...) |
Instance Method Details
#inspect ⇒ String
Does not call #inspect on the proxied object itself, to avoid Ractor round-trips
Returns 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.
264 |
# File 'lib/farce/proxy.rb', line 264 def ractor_shareable? = true |