Overview
Ruby's Ractor system introduces parallel execution through isolated execution contexts, fundamentally changing how require
operates compared to traditional single-threaded environments. Each Ractor maintains its own isolated copy of loaded files, constants, and global variables, creating distinct execution environments that prevent typical sharing mechanisms.
The require
method in Ractors loads files independently within each execution context. When a Ractor calls require
, Ruby loads the specified file exclusively for that Ractor's namespace, maintaining complete isolation from other Ractors. This isolation extends to constants, global variables, and class definitions created during the loading process.
# Main Ractor loads a library
require 'json'
# Create new Ractor that must load JSON independently
worker = Ractor.new do
require 'json' # Loads JSON again for this Ractor
JSON.parse('{"key": "value"}')
end
result = worker.take
# => {"key"=>"value"}
Ractor isolation prevents shared state contamination but requires careful consideration of loading strategies. Each Ractor must explicitly require needed libraries, even if they were loaded in the main Ractor or other parallel contexts.
# Each Ractor needs its own require statements
workers = 3.times.map do |i|
Ractor.new(i) do |worker_id|
require 'digest'
require 'securerandom'
Digest::SHA256.hexdigest("worker-#{worker_id}")
end
end
results = workers.map(&:take)
Ruby enforces strict boundaries between Ractors to prevent data races and ensure thread safety. The require
mechanism respects these boundaries by creating independent loading contexts, though this comes with memory overhead and initialization complexity.
Basic Usage
Standard require operations in Ractors follow familiar patterns but with mandatory isolation requirements. Every Ractor must independently load required libraries, regardless of what has been loaded in other contexts.
# Basic require in Ractor
calculator = Ractor.new do
require 'bigdecimal'
BigDecimal('123.456') * BigDecimal('2.0')
end
result = calculator.take
# => 0.246912e3
Multiple Ractors requiring the same library each receive independent copies. Ruby does not share loaded code between Ractors, maintaining complete isolation at the cost of memory duplication.
# Multiple Ractors with same requirements
processors = 5.times.map do |i|
Ractor.new(i) do |id|
require 'time'
require 'uri'
{
id: id,
timestamp: Time.now.iso8601,
processed: URI.parse("https://api.example.com/worker/#{id}")
}
end
end
results = processors.map(&:take)
Relative require paths work within Ractors but resolve relative to the current working directory, not the Ractor's creation context. This behavior can cause confusion when Ractors are created from different locations.
# Relative requires in Ractors
Dir.chdir('/app/lib') do
worker = Ractor.new do
require_relative 'helpers/data_processor'
require_relative 'validators/input_validator'
DataProcessor.new.process('sample data')
end
result = worker.take
end
Gem loading follows the same isolation principles. Each Ractor must independently require gems, including their dependencies. Bundler integration works within individual Ractors but does not share loaded gems across boundaries.
# Gem loading in Ractors
require 'bundler/setup' # In main context
data_workers = 3.times.map do
Ractor.new do
require 'httparty'
require 'nokogiri'
response = HTTParty.get('https://api.example.com/data')
Nokogiri::HTML(response.body)
end
end
parsed_data = data_workers.map(&:take)
Thread Safety & Concurrency
Ractor require operations maintain thread safety through complete isolation rather than traditional locking mechanisms. Each Ractor loads files into its own namespace, eliminating the need for synchronization around require statements.
The isolation model ensures that concurrent require calls never interfere with each other. Multiple Ractors can simultaneously load the same file without race conditions, as each receives an independent copy of the loaded code.
# Concurrent require operations
start_time = Time.now
workers = 10.times.map do |i|
Ractor.new(i) do |worker_id|
require 'net/http'
require 'openssl'
require 'base64'
# Each worker has independent Net::HTTP
uri = URI("https://httpbin.org/delay/1")
response = Net::HTTP.get_response(uri)
{
worker: worker_id,
status: response.code,
duration: Time.now - start_time
}
end
end
results = workers.map(&:take)
File loading synchronization occurs at the individual Ractor level. Ruby ensures that require operations within a single Ractor are atomic, preventing partial loading states that could cause inconsistencies.
# Ractor-internal require synchronization
complex_loader = Ractor.new do
# These requires are synchronized within this Ractor
require 'active_support'
require 'active_support/core_ext/string'
require 'active_support/core_ext/hash'
# Safe to use immediately after require
"hello_world".camelize
end
result = complex_loader.take
# => "HelloWorld"
Global state isolation prevents typical concurrency issues around shared constants and variables. Each Ractor maintains its own copy of globals, eliminating race conditions that would occur in shared-state systems.
# Global state isolation demonstration
$global_counter = 0
counters = 5.times.map do |i|
Ractor.new(i) do |worker_id|
require 'singleton'
# Each Ractor has independent $global_counter
10.times { $global_counter += 1 }
{
worker: worker_id,
counter: $global_counter,
singleton_id: Singleton.instance.object_id
}
end
end
results = counters.map(&:take)
# Each worker reports counter: 10, with different singleton_id
Error Handling & Debugging
Require failures in Ractors generate standard LoadError exceptions but with additional complexity due to isolation boundaries. Error handling must account for independent loading contexts and potential inconsistencies between Ractors.
LoadError exceptions in Ractors behave identically to single-threaded contexts but only affect the individual Ractor where they occur. Other Ractors continue executing normally even when one encounters loading failures.
# Isolated error handling
workers = [
Ractor.new do
begin
require 'nonexistent_gem'
rescue LoadError => e
{ status: 'error', message: e.message }
end
end,
Ractor.new do
require 'json'
{ status: 'success', data: JSON.parse('{"test": true}') }
end
]
results = workers.map(&:take)
# One succeeds, one fails independently
Debugging require issues in Ractors requires understanding the isolation model. Standard debugging techniques work within individual Ractors but cannot inspect state across Ractor boundaries.
# Debugging require paths in Ractors
debug_worker = Ractor.new do
original_load_path = $LOAD_PATH.dup
begin
require 'custom_library'
rescue LoadError => e
{
error: e.message,
load_path: $LOAD_PATH,
working_directory: Dir.pwd,
caller: caller
}
end
end
debug_info = debug_worker.take
Error recovery strategies must handle the isolated nature of Ractor execution. Failed requires in one Ractor do not affect others, but communication of failure states requires explicit messaging.
# Error recovery with fallback loading
resilient_processor = Ractor.new do
processors = []
# Try preferred processor
begin
require 'fast_processor'
processors << FastProcessor
rescue LoadError
# Fallback to standard library
require 'csv'
processors << CSV
end
# Try optional enhancement
begin
require 'optimization_library'
processors << OptimizationLibrary
rescue LoadError
# Continue without optimization
end
{
loaded_processors: processors.map(&:name),
processor_count: processors.length
}
end
config = resilient_processor.take
Version compatibility errors manifest differently in Ractors due to independent loading. Each Ractor loads its own version of gems, potentially creating inconsistencies across the application.
# Version compatibility handling
version_checker = Ractor.new do
compatibility_info = {}
begin
require 'example_gem'
compatibility_info[:example_gem] = {
version: ExampleGem::VERSION,
loaded: true
}
rescue LoadError, NameError => e
compatibility_info[:example_gem] = {
error: e.class.name,
message: e.message,
loaded: false
}
end
compatibility_info
end
version_info = version_checker.take
Common Pitfalls
Ractor require operations contain several subtle gotchas that can cause unexpected behavior. Understanding these pitfalls prevents common mistakes and design problems in parallel applications.
Assuming shared state between Ractors represents the most frequent mistake. Developers often expect constants or globals defined in required files to be shared, but Ractor isolation prevents this sharing.
# PITFALL: Expecting shared constants
require 'logger'
SHARED_LOGGER = Logger.new(STDOUT)
# This creates independent loggers in each Ractor
workers = 3.times.map do |i|
Ractor.new(i) do |id|
require 'logger'
# SHARED_LOGGER is nil here - must recreate
local_logger = Logger.new(STDOUT)
local_logger.info("Worker #{id} starting")
end
end
workers.each(&:take)
Memory consumption multiplies due to independent loading in each Ractor. Large libraries loaded in multiple Ractors consume significantly more memory than single-threaded applications.
# PITFALL: Memory multiplication
memory_before = GC.stat(:total_allocated_objects)
# Loading large library in multiple Ractors
memory_workers = 5.times.map do
Ractor.new do
require 'rails' # Large library
GC.stat(:total_allocated_objects)
end
end
ractor_memory = memory_workers.map(&:take)
# Memory usage is 5x single-threaded loading
File system dependencies can cause loading failures when Ractors access files relative to different working directories. This issue particularly affects applications with complex directory structures.
# PITFALL: Working directory confusion
original_dir = Dir.pwd
Dir.chdir('/tmp') do
# Ractor created while in /tmp
worker = Ractor.new do
# But loads relative to original working directory
begin
require_relative 'config/application'
rescue LoadError => e
{
error: e.message,
pwd: Dir.pwd,
expected_path: File.expand_path('config/application.rb')
}
end
end
result = worker.take
# Likely fails unless config exists in /tmp
end
Singleton pattern violations occur when singletons are expected to be shared but each Ractor creates independent instances. This breaks singleton semantics and can cause logic errors.
# PITFALL: Broken singleton pattern
require 'singleton'
class DatabaseConnection
include Singleton
def initialize
@connection_id = SecureRandom.hex(8)
end
attr_reader :connection_id
end
# Main Ractor singleton
main_connection = DatabaseConnection.instance
# Each Ractor gets different singleton
connection_workers = 3.times.map do
Ractor.new do
require 'singleton'
require 'securerandom'
# Independent singleton instance
DatabaseConnection.instance.connection_id
end
end
connection_ids = connection_workers.map(&:take)
# All different IDs, breaking singleton expectations
Gem initialization side effects can cause unexpected behavior when gems perform global modifications during loading. Each Ractor independently triggers these side effects.
# PITFALL: Repeated initialization side effects
# Some gems modify global state during require
workers = 3.times.map do
Ractor.new do
# Each require triggers gem initialization
require 'gem_with_side_effects'
# Side effects execute multiple times
GemWithSideEffects.configuration.initialized_count
end
end
initialization_counts = workers.map(&:take)
# Each Ractor shows independent initialization
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
require(string) |
string (String) |
Boolean |
Loads library in current Ractor context |
require_relative(string) |
string (String) |
Boolean |
Loads file relative to current file location |
load(filename, wrap=false) |
filename (String), wrap (Boolean) |
true |
Loads and executes file in current Ractor |
Ractor-Specific Behavior
Aspect | Behavior | Implication |
---|---|---|
Loading Scope | Per-Ractor isolation | Each Ractor loads independent copies |
Constants | Isolated per Ractor | No sharing of loaded constants |
Global Variables | Isolated per Ractor | Independent global state |
Memory Usage | Multiplied by Ractor count | Higher memory consumption |
Thread Safety | Isolation-based | No synchronization needed |
Error Types
Exception | Cause | Recovery Strategy |
---|---|---|
LoadError |
File not found or syntax error | Verify file paths and syntax |
NameError |
Missing constant after load | Check constant definitions |
Ractor::IsolationError |
Attempted shared object access | Use Ractor-safe communication |
Ractor::MovedError |
Object moved between Ractors | Recreate objects in target Ractor |
Loading States
State | Description | Detection Method |
---|---|---|
Not Loaded | Library not required in current Ractor | defined?(Constant) returns nil |
Loading | Currently executing require statement | Rare, atomic operation |
Loaded | Library available in current Ractor | defined?(Constant) returns "constant" |
Failed | Previous require attempt failed | Catch LoadError on retry |
Path Resolution
Path Type | Resolution Context | Ractor Behavior |
---|---|---|
Absolute | File system root | Consistent across Ractors |
Relative | Current working directory | May differ between Ractors |
Gem paths | GEM_PATH/LOAD_PATH | Isolated per Ractor |
require_relative | File location | Consistent if same source file |
Memory Patterns
Pattern | Single Ractor | Multiple Ractors | Scaling Factor |
---|---|---|---|
Small library (< 1MB) | 1x | Nx | Linear |
Medium library (1-10MB) | 1x | Nx | Linear |
Large library (> 10MB) | 1x | Nx | Linear with overhead |
Native extensions | 1x | Nx + shared objects | Near-linear |
Performance Characteristics
Operation | Complexity | Ractor Impact | Optimization |
---|---|---|---|
First require | O(file size) | Independent per Ractor | Minimize library size |
Subsequent require | O(1) | Per-Ractor caching | Same-Ractor reuse |
Constant access | O(1) | No cross-Ractor access | Local storage |
Global access | O(1) | Isolated per Ractor | Minimize global usage |