Shims, Polyfills, and Extensions

Farce adapts Ruby APIs for use across Ractors and with Fiber schedulers. Its Ractor and Port implementations also provide fallbacks on Ruby engines without native Ractors.

Ractor

Farce includes Farce::Ractor, which will either delegate to Ractor or supply a polyfill for it.

On platforms that don't support Ractors, a Thread-based implementation is provided that tracks Ractor-membership via Thread groups.

ractor = Farce::Ractor.new do
  value = receive
  puts "Received #{value.inspect}"
end

ractor.send 42

Port

Farce includes Farce::Port, which is either an extended subclass of Ractor::Port if it is available, or a polyfill if it isn't.

In addition to Ractor::Port it supports the following features:

  • Sharing modes both for .new and #send.
  • Opt-in auto-local sharing: If the port belongs to the current Ractor, any object can be sent to it without copying or moving it.
  • #receive supports a timeout option on all Ruby implementations and versions, not just CRuby 4.1+
  • You can use #owned? to check if the current Ractor owns a port.
  • #receive does not block a Fiber scheduler when called from a non-blocking Fiber.
# reject unshareable values except if they are being sent from the ractor owning the port
port   = Farce::Port.new(mode: :raise, auto_local: true)
object = Object.new

port.send(object)
port.receive.equal?(object) # => true

Backports

While Farce includes Farce::Ractor and Farce::Port, it does not backport any methods to the standard Ractor class.

Instead, backports are provided by Farce::Backports.

Available Backports

From Ruby 4.0

The following methods are added to Ractor on Ruby versions prior to 4.0:

From Ruby 4.1

The following methods are added to Ractor on Ruby versions prior to 4.1:

It also extends the following methods with optional timeout support:

No breaking changes are backported. This includes the updated monitor behavior. The polyfill will choose its monitor implementation based on RUBY_VERSION (so that any code checking the Ruby version will see the behavior it should).

Without Activating Backports

You can call these methods on Farce::Backports, directly, passing in the Ractor instance as a first argument.

ractor     = Farce::Ractor.new { sleep 1; raise "I'm not happy!" }
supervisor = Farce::Ractor.new { puts "status: #{receive}" }
port       = Farce::Backports.default_port(supervisor)

# This will print "status: aborted" in approximately one second
Farce::Backports.monitor(ractor, port)

# Wait for the supervisor to receive the status
Farce::Backports.join(supervisor)

Using Backports via Refinements

You can activate the backports for a lexical scope (within a file, or even just within a module within a file, or starting with Ruby 4.1 for an individual block) using them as a refinement:

# only activates the backports for the current file
using Farce::Backports

# Refinements can't install constants, so we still need to use Farce::Ractor for JRuby/TruffleRuby
# If you know you're on CRuby, you can replace Farce::Ractor with Ractor
ractor     = Farce::Ractor.new { sleep 1; raise "I'm not happy!" }
supervisor = Farce::Ractor.new { puts "status: #{receive}" }

# These methods now work regardless of whether you used Ractor or Farce::Ractor
port = supervisor.default_port
ractor.monitor(port)
supervisor.join

Activating Backports Globally

You can activate backports globally. As these don't override built-in methods, doing so should only cause issues for code expecting these methods not to be defined based on the RUBY_VERSION string.

Anywhere before executing your code, like an initializer:

Farce::Backports.install

Now you can do this in any file:

# After installing backports globally, Ractor will always be defined
ractor     = Ractor.new { sleep 1; raise "I'm not happy!" }
supervisor = Ractor.new { puts "status: #{receive}" }
ractor.monitor(supervisor.default_port)
supervisor.join

WeakRef

Farce::WeakRef is a drop-in replacement for Ruby's WeakRef, which at the time of writing cannot be used outside of the main Ractor at all.

Farce implements a version based on weak atoms. Not only can it be used on non-main Ractors, Farce::WeakRef instances themselves are Ractor-shareable if the value they reference is also Ractor-shareable (or has been garbage collected). Moreover, calling Ractor.make_shareable(weak_ref) will propagate through to the referenced value.

Resolv

Farce::Resolv is a version of Ruby's Resolv, using counters instead of a global instance variable, and can therefore be used outside the main Ractor.

Concurrent

Farce::Concurrent includes the following classes:

Class Description
Array Alias for Farce::Vector
Atom Alias for Farce::Atom
AtomicBoolean Alias for Farce::Flag
AtomicFixnum Alias for Farce::Counter
CountDownLatch Drop-in replacement for concurrent-ruby's CountDownLatch
CyclicBarrier Replacement for concurrent-ruby's CyclicBarrier
Event Drop-in replacement for concurrent-ruby's Event
Exchanger Alias for Farce::Exchanger
FiberLocalVar Drop-in replacement for concurrent-ruby's FiberLocalVar
Hash Alias for Farce::Map
Lock Alias for Farce::Lock
LockLocalVar Drop-in replacement for concurrent-ruby's LockLocalVar
ReadWriteLock Alias for Farce::ReadWriteLock
ThreadLocalVar Drop-in replacement for concurrent-ruby's ThreadLocalVar
Tuple Replacement for concurrent-ruby's Tuple

The nice thing is that these are either always Ractor-shareable, or in the case of FiberLocalVar and ThreadLocalVar, are Ractor-shareable if their default values or blocks are.

For instance, you can replace threads with ractors in concurrent-ruby's own CountDownLatch example:

latch = Farce::Concurrent::CountDownLatch.new(3)

waiter = Ractor.new(latch) do |latch|
  latch.wait
  puts "Waiter released"
end

decrementer = Ractor.new(latch) do |latch|
  sleep 1
  latch.count_down
  puts latch.count

  sleep 1
  latch.count_down
  puts latch.count

  sleep 1
  latch.count_down
  puts latch.count
end

waiter.join
decrementer.join

README · Sharing Unshareable Data · Scheduling Code