CrackedRuby logo

CrackedRuby

require in Ractors

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