CrackedRuby logo

CrackedRuby

Signal Trapping

Comprehensive guide to handling operating system signals in Ruby applications using the Signal module and trap handlers.

Core Modules Signal Module
3.8.1

Overview

Signal trapping in Ruby provides a mechanism for handling operating system signals sent to running processes. Ruby's Signal module offers methods to register handlers that execute when specific signals arrive, enabling applications to respond to external events like user interrupts, system shutdowns, or custom process communication.

The Signal module operates by registering trap handlers that execute when the Ruby interpreter receives designated signals. These handlers run asynchronously, interrupting normal program flow when signals arrive. Ruby supports most POSIX signals and provides platform-specific signal handling capabilities.

# Basic signal handler registration
Signal.trap("INT") do |signo|
  puts "Received signal #{signo}"
  exit(0)
end

# Alternative syntax using signal numbers
Signal.trap(2) do |signo|
  puts "SIGINT received: #{signo}"
end

# Check current trap handler
current_handler = Signal.trap("INT", "DEFAULT")
puts current_handler.class  # => String or Proc

Ruby processes inherit signal dispositions from their parent process, but applications can override these settings using Signal.trap. The module handles signal delivery across threads and manages the execution context for trap handlers.

Signal handlers execute in the main thread regardless of which thread receives the signal. This design choice simplifies signal handling but requires careful coordination when multiple threads access shared resources. Ruby queues signals that arrive during handler execution, processing them after the current handler completes.

Basic Usage

Signal trapping begins with registering handlers using Signal.trap, which accepts signal names or numbers and handler objects. Ruby supports string signal names, integer signal numbers, and symbolic signal names.

# String-based signal names
Signal.trap("TERM") { puts "Termination requested" }
Signal.trap("USR1") { puts "User signal 1" }
Signal.trap("HUP") { puts "Hangup signal" }

# Numeric signal identifiers
Signal.trap(15) { puts "SIGTERM received" }
Signal.trap(10) { puts "SIGUSR1 received" }

# Symbolic references using Signal constants
Signal.trap(Signal::SIGINT) { puts "Interrupt signal" }

Handler blocks receive the signal number as their first argument, enabling generic signal processing logic. Ruby passes the signal number even when handlers are registered using string names.

# Generic handler processing multiple signals
handler = proc do |signal|
  case signal
  when 2, 15  # SIGINT or SIGTERM
    puts "Shutdown signal #{signal} received"
    cleanup_resources
    exit(0)
  when 10     # SIGUSR1
    puts "Reloading configuration due to signal #{signal}"
    reload_config
  else
    puts "Unknown signal: #{signal}"
  end
end

Signal.trap("INT", &handler)
Signal.trap("TERM", &handler)
Signal.trap("USR1", &handler)

Ruby provides predefined handler strings for common signal processing patterns. The "DEFAULT" string restores the system's default signal handling behavior, while "IGNORE" prevents the signal from affecting the process.

# Restore default signal handling
Signal.trap("INT", "DEFAULT")

# Ignore specific signals
Signal.trap("PIPE", "IGNORE")

# Save and restore previous handlers
old_int_handler = Signal.trap("INT") { puts "Custom handler" }
Signal.trap("INT", old_int_handler)  # Restore previous handler

Signal.list returns a hash mapping signal names to their numeric values, providing a comprehensive reference for available signals on the current platform.

# Enumerate available signals
Signal.list.each do |name, number|
  puts "#{name}: #{number}"
end

# Check for platform-specific signals
if Signal.list.key?("WINCH")
  Signal.trap("WINCH") { handle_window_resize }
end

Thread Safety & Concurrency

Signal handlers execute in the main thread regardless of the thread that receives the signal, creating specific concurrency considerations for multithreaded Ruby applications. Ruby's signal handling mechanism queues signals during handler execution and processes them sequentially after each handler completes.

Thread safety becomes critical when signal handlers access shared data structures or modify global state. Ruby's Global Interpreter Lock (GIL) provides some protection, but signal handlers can still interrupt critical sections and create race conditions.

require 'thread'

class SafeSignalHandler
  def initialize
    @mutex = Mutex.new
    @shutdown_requested = false
    @worker_threads = []
  end

  def setup_signals
    Signal.trap("INT") do |signal|
      @mutex.synchronize do
        puts "Shutdown requested via signal #{signal}"
        @shutdown_requested = true
      end
      wake_workers
    end

    Signal.trap("USR1") do |signal|
      @mutex.synchronize do
        puts "Status requested via signal #{signal}"
        report_status
      end
    end
  end

  def shutdown_requested?
    @mutex.synchronize { @shutdown_requested }
  end

  private

  def wake_workers
    @worker_threads.each(&:wakeup)
  end

  def report_status
    puts "Active threads: #{@worker_threads.size}"
    puts "Memory usage: #{GC.stat[:heap_live_slots]} objects"
  end
end

Signal delivery timing creates challenges when handlers modify data structures accessed by other threads. Atomic operations and careful synchronization prevent data corruption in multithreaded signal handling scenarios.

require 'atomic'

class AtomicSignalCounter
  def initialize
    @signal_count = Atomic.new(0)
    @last_signal = Atomic.new(0)
    @processing = Atomic.new(false)
  end

  def setup_counting_handler
    Signal.trap("USR1") do |signal|
      return if @processing.compare_and_swap(false, true)

      begin
        current_count = @signal_count.update { |v| v + 1 }
        @last_signal.update { |_| Time.now.to_f }
        puts "Signal #{signal} count: #{current_count}"
      ensure
        @processing.update { |_| false }
      end
    end
  end

  def stats
    {
      count: @signal_count.value,
      last_signal: Time.at(@last_signal.value),
      processing: @processing.value
    }
  end
end

Thread coordination requires careful attention to blocking operations in signal handlers. Ruby signal handlers should avoid operations that might block indefinitely, as this can prevent signal processing and create deadlocks in multithreaded applications.

class NonBlockingSignalHandler
  def initialize
    @signal_queue = Queue.new
    @shutdown = false
    setup_signal_processor
  end

  def setup_signals
    ["INT", "TERM", "USR1", "USR2"].each do |sig|
      Signal.trap(sig) do |signal_num|
        # Non-blocking signal queuing
        @signal_queue.push([sig, signal_num, Time.now]) rescue nil
      end
    end
  end

  private

  def setup_signal_processor
    Thread.new do
      until @shutdown
        begin
          signal_data = @signal_queue.pop(non_block: true)
          process_queued_signal(*signal_data)
        rescue ThreadError
          sleep(0.1)  # Queue empty, brief pause
        end
      end
    end
  end

  def process_queued_signal(signal_name, signal_num, timestamp)
    case signal_name
    when "INT", "TERM"
      puts "Shutdown signal #{signal_name} (#{signal_num}) at #{timestamp}"
      @shutdown = true
    when "USR1"
      puts "Status request via #{signal_name} at #{timestamp}"
      print_status
    when "USR2"
      puts "Config reload via #{signal_name} at #{timestamp}"
      reload_configuration
    end
  end
end

Error Handling & Debugging

Signal trap registration can fail when attempting to handle non-trappable signals or when platform restrictions prevent handler installation. Ruby raises ArgumentError for invalid signal specifications and Errno exceptions for system-level signal handling failures.

class RobustSignalHandler
  RESTRICTED_SIGNALS = %w[KILL STOP SEGV].freeze
  
  def register_handler(signal, &block)
    return false if RESTRICTED_SIGNALS.include?(signal.to_s.upcase)
    
    begin
      old_handler = Signal.trap(signal, &block)
      puts "Registered handler for #{signal}"
      old_handler
    rescue ArgumentError => e
      warn "Invalid signal specification: #{signal} - #{e.message}"
      nil
    rescue SystemCallError => e
      warn "System error registering #{signal} handler: #{e.message}"
      nil
    end
  end

  def safe_signal_list
    available_signals = {}
    
    Signal.list.each do |name, number|
      begin
        # Test if signal can be trapped by temporarily setting default
        old_handler = Signal.trap(name, "DEFAULT")
        Signal.trap(name, old_handler)
        available_signals[name] = number
      rescue ArgumentError, SystemCallError
        # Signal cannot be trapped, skip it
      end
    end
    
    available_signals
  end
end

Debugging signal handler execution requires special techniques because handlers run asynchronously and can interrupt normal program flow at arbitrary points. Signal delivery timing issues often manifest as intermittent bugs that are difficult to reproduce.

class DebuggableSignalHandler
  def initialize(debug: false)
    @debug = debug
    @signal_log = []
    @handler_stack = []
  end

  def debug_trap(signal, &handler)
    wrapped_handler = proc do |signo|
      entry_time = Time.now
      
      if @debug
        @signal_log << {
          signal: signal,
          number: signo,
          timestamp: entry_time,
          thread: Thread.current.object_id,
          call_stack: caller[0..5]
        }
      end
      
      @handler_stack.push(signal)
      
      begin
        result = handler.call(signo)
        
        if @debug
          @signal_log.last[:duration] = Time.now - entry_time
          @signal_log.last[:result] = result.class.name
        end
        
        result
      rescue => e
        if @debug
          @signal_log.last[:error] = e.message
          @signal_log.last[:error_class] = e.class.name
        end
        
        warn "Error in #{signal} handler: #{e.message}"
        warn e.backtrace.first(3).join("\n")
        raise
      ensure
        @handler_stack.pop
      end
    end
    
    Signal.trap(signal, &wrapped_handler)
  end

  def signal_report
    return "Debug mode disabled" unless @debug
    
    report = ["Signal Handler Debug Report"]
    report << "=" * 30
    
    @signal_log.each_with_index do |entry, idx|
      report << "#{idx + 1}. Signal: #{entry[:signal]} (#{entry[:number]})"
      report << "   Time: #{entry[:timestamp]}"
      report << "   Thread: #{entry[:thread]}"
      report << "   Duration: #{entry[:duration]&.round(4)}s"
      report << "   Error: #{entry[:error]}" if entry[:error]
      report << ""
    end
    
    report.join("\n")
  end
end

Signal handler exceptions require careful management because uncaught exceptions in handlers can terminate the entire process. Ruby's exception handling mechanisms work within signal handlers, but exception propagation follows different rules than normal method execution.

module SignalErrorHandling
  def self.safe_handler(&block)
    proc do |signal|
      begin
        block.call(signal)
      rescue => e
        error_info = {
          signal: signal,
          error: e.class.name,
          message: e.message,
          backtrace: e.backtrace&.first(5),
          timestamp: Time.now
        }
        
        log_signal_error(error_info)
        
        # Attempt graceful degradation
        case e
        when SystemCallError
          warn "System error in signal handler: #{e.message}"
        when StandardError
          warn "Application error in signal handler: #{e.message}"
        else
          warn "Unexpected error in signal handler: #{e.class} - #{e.message}"
          raise  # Re-raise serious errors
        end
      end
    end
  end

  def self.log_signal_error(info)
    File.open("signal_errors.log", "a") do |f|
      f.puts "[#{info[:timestamp]}] Signal #{info[:signal]} Error:"
      f.puts "  #{info[:error]}: #{info[:message]}"
      info[:backtrace]&.each { |line| f.puts "    #{line}" }
      f.puts
    end
  rescue
    # Fallback if logging fails
    warn "Failed to log signal error: #{info[:error]}"
  end
end

# Usage with error handling
Signal.trap("USR1", &SignalErrorHandling.safe_handler do |signal|
  risky_operation_that_might_fail
  puts "Signal #{signal} processed successfully"
end)

Production Patterns

Production Ruby applications commonly use signal trapping for graceful shutdown sequences, configuration reloading, and runtime diagnostics. These patterns require robust signal handling that coordinates with application lifecycle management and external monitoring systems.

Graceful shutdown implementation involves capturing termination signals and performing cleanup operations before process termination. This pattern prevents data loss and ensures proper resource cleanup in production environments.

class GracefulShutdownManager
  def initialize
    @shutdown_callbacks = []
    @shutdown_timeout = 30
    @shutdown_initiated = false
    @shutdown_mutex = Mutex.new
  end

  def register_shutdown_callback(name, timeout: 10, &block)
    @shutdown_callbacks << {
      name: name,
      callback: block,
      timeout: timeout
    }
  end

  def setup_signal_handlers
    ["INT", "TERM"].each do |signal|
      Signal.trap(signal) do |signo|
        initiate_shutdown(signal, signo)
      end
    end

    Signal.trap("USR2") do |signo|
      dump_shutdown_status
    end
  end

  private

  def initiate_shutdown(signal_name, signal_number)
    @shutdown_mutex.synchronize do
      return if @shutdown_initiated
      @shutdown_initiated = true
    end

    puts "Graceful shutdown initiated by #{signal_name} (#{signal_number})"
    shutdown_start = Time.now

    @shutdown_callbacks.each do |callback_info|
      callback_start = Time.now
      
      begin
        Timeout.timeout(callback_info[:timeout]) do
          puts "Executing shutdown callback: #{callback_info[:name]}"
          callback_info[:callback].call
        end
        
        elapsed = Time.now - callback_start
        puts "Shutdown callback #{callback_info[:name]} completed in #{elapsed.round(2)}s"
        
      rescue Timeout::Error
        warn "Shutdown callback #{callback_info[:name]} timed out after #{callback_info[:timeout]}s"
      rescue => e
        warn "Error in shutdown callback #{callback_info[:name]}: #{e.message}"
      end
    end

    total_elapsed = Time.now - shutdown_start
    puts "Graceful shutdown completed in #{total_elapsed.round(2)}s"
    exit(0)
  end

  def dump_shutdown_status
    puts "Shutdown Status:"
    puts "  Initiated: #{@shutdown_initiated}"
    puts "  Callbacks registered: #{@shutdown_callbacks.size}"
    puts "  Process uptime: #{Time.now - $PROGRAM_START_TIME}s"
  end
end

# Production usage example
shutdown_manager = GracefulShutdownManager.new

shutdown_manager.register_shutdown_callback("database_cleanup") do
  ActiveRecord::Base.connection_pool.disconnect!
end

shutdown_manager.register_shutdown_callback("worker_shutdown", timeout: 15) do
  WorkerPool.shutdown_gracefully
end

shutdown_manager.register_shutdown_callback("metrics_flush") do
  MetricsCollector.flush_pending_metrics
end

shutdown_manager.setup_signal_handlers

Configuration reloading using signals enables production applications to update settings without process restart. This pattern requires thread-safe configuration management and atomic updates to prevent inconsistent application state.

class ConfigurationReloader
  def initialize(config_paths)
    @config_paths = Array(config_paths)
    @current_config = load_configuration
    @config_mutex = Mutex.new
    @reload_callbacks = []
    @last_reload = Time.now
  end

  def setup_reload_signals
    Signal.trap("HUP") do |signo|
      reload_configuration("HUP", signo)
    end

    Signal.trap("USR1") do |signo|
      validate_configuration_files
    end
  end

  def register_reload_callback(name, &block)
    @reload_callbacks << { name: name, callback: block }
  end

  def current_config
    @config_mutex.synchronize { @current_config.dup }
  end

  private

  def reload_configuration(signal_name, signal_number)
    puts "Configuration reload triggered by #{signal_name} (#{signal_number})"
    reload_start = Time.now

    begin
      new_config = load_configuration
      
      @config_mutex.synchronize do
        old_config = @current_config
        @current_config = new_config
        @last_reload = reload_start
        
        notify_reload_callbacks(old_config, new_config)
      end

      elapsed = Time.now - reload_start
      puts "Configuration reloaded successfully in #{elapsed.round(3)}s"
      
    rescue => e
      warn "Configuration reload failed: #{e.message}"
      warn "Continuing with previous configuration"
    end
  end

  def load_configuration
    config = {}
    
    @config_paths.each do |path|
      if File.exist?(path)
        case File.extname(path)
        when '.json'
          config.merge!(JSON.parse(File.read(path)))
        when '.yml', '.yaml'
          config.merge!(YAML.load_file(path))
        else
          warn "Unknown configuration file format: #{path}"
        end
      else
        warn "Configuration file not found: #{path}"
      end
    end
    
    config.freeze
  end

  def notify_reload_callbacks(old_config, new_config)
    @reload_callbacks.each do |callback_info|
      begin
        callback_info[:callback].call(old_config, new_config)
      rescue => e
        warn "Error in reload callback #{callback_info[:name]}: #{e.message}"
      end
    end
  end

  def validate_configuration_files
    puts "Configuration File Validation:"
    
    @config_paths.each do |path|
      if File.exist?(path)
        begin
          load_configuration_file(path)
          puts "#{path} - Valid"
        rescue => e
          puts "#{path} - Error: #{e.message}"
        end
      else
        puts "#{path} - File not found"
      end
    end
    
    puts "Last successful reload: #{@last_reload}"
  end
end

Runtime diagnostics through signal handlers provide production visibility into application state without external dependencies. This pattern enables debugging and monitoring through signal-triggered reporting.

class RuntimeDiagnostics
  def initialize
    @diagnostic_handlers = {}
    @report_history = []
    @max_history = 50
  end

  def register_diagnostic(signal, name, &block)
    @diagnostic_handlers[signal] = {
      name: name,
      handler: block,
      call_count: 0,
      last_called: nil
    }

    Signal.trap(signal) do |signo|
      generate_diagnostic_report(signal, signo)
    end
  end

  private

  def generate_diagnostic_report(signal, signal_number)
    handler_info = @diagnostic_handlers[signal]
    return unless handler_info

    report_start = Time.now
    handler_info[:call_count] += 1
    handler_info[:last_called] = report_start

    puts "=" * 50
    puts "DIAGNOSTIC REPORT: #{handler_info[:name]}"
    puts "Signal: #{signal} (#{signal_number})"
    puts "Timestamp: #{report_start}"
    puts "Report ##{handler_info[:call_count]}"
    puts "=" * 50

    begin
      report_data = handler_info[:handler].call
      
      if report_data.is_a?(Hash)
        report_data.each { |key, value| puts "#{key}: #{value}" }
      else
        puts report_data
      end
      
    rescue => e
      puts "Error generating report: #{e.message}"
    end

    elapsed = Time.now - report_start
    puts "-" * 50
    puts "Report generated in #{elapsed.round(3)}s"
    puts

    # Store report in history
    store_report_history(signal, handler_info[:name], report_start, elapsed)
  end

  def store_report_history(signal, name, timestamp, duration)
    @report_history << {
      signal: signal,
      name: name,
      timestamp: timestamp,
      duration: duration
    }

    @report_history.shift if @report_history.size > @max_history
  end
end

# Production diagnostic setup
diagnostics = RuntimeDiagnostics.new

diagnostics.register_diagnostic("USR1", "Memory and Thread Status") do
  {
    "Ruby Version" => RUBY_VERSION,
    "Process PID" => Process.pid,
    "Memory Usage" => "#{GC.stat[:heap_live_slots]} live objects",
    "GC Stats" => "#{GC.stat[:count]} collections, #{GC.stat[:total_time]}ms total",
    "Active Threads" => Thread.list.size,
    "Load Average" => File.read("/proc/loadavg").strip rescue "N/A",
    "Uptime" => "#{(Time.now - $PROGRAM_START_TIME).round(2)}s"
  }
end

diagnostics.register_diagnostic("USR2", "Application State") do
  {
    "Active Connections" => ConnectionPool.active_count,
    "Queue Sizes" => JobQueue.size,
    "Cache Hit Rate" => CacheMetrics.hit_rate,
    "Request Rate" => RequestCounter.requests_per_second,
    "Error Rate" => ErrorTracker.error_rate
  }
end

Common Pitfalls

Signal handler execution creates numerous opportunities for subtle bugs and unexpected behavior. These issues often manifest intermittently in production environments, making them particularly challenging to diagnose and resolve.

Platform-specific signal behavior differences cause compatibility issues when Ruby applications run across different operating systems. Signal numbers, available signals, and default behaviors vary between platforms, requiring defensive programming techniques.

class PlatformAwareSignalHandler
  COMMON_SIGNALS = {
    'INT' => 2,    # Interrupt (Ctrl+C)
    'TERM' => 15,  # Termination request
    'HUP' => 1,    # Hangup (Unix only)
    'QUIT' => 3    # Quit (Unix only)
  }.freeze

  def self.setup_cross_platform_handlers
    # Handle signals that exist on current platform
    available_signals = Signal.list
    
    COMMON_SIGNALS.each do |name, expected_number|
      if available_signals.key?(name)
        actual_number = available_signals[name]
        
        if actual_number != expected_number
          warn "Signal #{name} number differs: expected #{expected_number}, got #{actual_number}"
        end
        
        Signal.trap(name) do |signo|
          handle_termination_signal(name, signo)
        end
      else
        warn "Signal #{name} not available on this platform"
      end
    end

    # Platform-specific signal handling
    case RUBY_PLATFORM
    when /linux/
      setup_linux_specific_signals
    when /darwin/
      setup_macos_specific_signals
    when /mswin|mingw/
      setup_windows_specific_signals
    end
  end

  private

  def self.setup_linux_specific_signals
    Signal.trap("USR1") { handle_user_signal(1) } if Signal.list.key?("USR1")
    Signal.trap("USR2") { handle_user_signal(2) } if Signal.list.key?("USR2")
  end

  def self.setup_macos_specific_signals
    Signal.trap("INFO") { handle_info_signal } if Signal.list.key?("INFO")
  end

  def self.setup_windows_specific_signals
    # Windows has limited signal support
    warn "Running on Windows: limited signal support available"
  end
end

Signal handler reentrancy issues occur when signal handlers are interrupted by additional signals before completing execution. Ruby queues signals during handler execution, but complex handlers can create race conditions and inconsistent state.

class ReentrantSignalProblem
  def initialize
    @counter = 0
    @processing = false
  end

  def setup_problematic_handler
    # PROBLEMATIC: Not reentrant-safe
    Signal.trap("USR1") do |signal|
      puts "Handler entry, processing: #{@processing}"
      @processing = true
      
      # Simulate work that could be interrupted
      5.times do |i|
        @counter += 1
        puts "Counter: #{@counter}, iteration: #{i}"
        sleep(0.1)  # Dangerous: allows signal interruption
      end
      
      @processing = false
      puts "Handler exit"
    end
  end

  def setup_safe_handler
    # BETTER: Reentrant-safe with proper guards
    Signal.trap("USR1") do |signal|
      # Quick check without blocking
      if @processing
        puts "Handler busy, signal #{signal} ignored"
        next
      end
      
      @processing = true
      
      begin
        # Atomic operations only
        current_value = @counter
        new_value = current_value + 1
        @counter = new_value
        
        puts "Signal #{signal}: #{current_value} -> #{new_value}"
      ensure
        @processing = false
      end
    end
  end
end

Signal delivery timing creates race conditions when handlers modify shared state accessed by other parts of the application. These timing-dependent bugs are particularly difficult to reproduce and debug.

class SignalRaceCondition
  def initialize
    @shared_resource = []
    @access_count = 0
  end

  def problematic_signal_handling
    # PROBLEMATIC: Race condition between signal handler and main thread
    Signal.trap("USR1") do
      # Signal handler modifies shared state
      @shared_resource << Time.now
      @access_count += 1
      puts "Signal handler: #{@shared_resource.size} items"
    end

    # Main thread also accesses shared state
    Thread.new do
      loop do
        # Race condition: size might change during iteration
        @shared_resource.each_with_index do |item, index|
          puts "Processing item #{index}: #{item}"
          sleep(0.01)  # Simulation of work
        end
        sleep(1)
      end
    end
  end

  def safe_signal_handling
    require 'thread'
    @mutex = Mutex.new
    
    # BETTER: Synchronized access to shared state
    Signal.trap("USR1") do
      @mutex.synchronize do
        @shared_resource << Time.now
        @access_count += 1
        puts "Signal handler: #{@shared_resource.size} items"
      end
    end

    Thread.new do
      loop do
        items_to_process = @mutex.synchronize { @shared_resource.dup }
        
        items_to_process.each_with_index do |item, index|
          puts "Processing item #{index}: #{item}"
          sleep(0.01)
        end
        
        sleep(1)
      end
    end
  end
end

Signal handler complexity creates maintenance and debugging challenges when handlers perform extensive processing or interact with multiple subsystems. Simple, focused handlers reduce the likelihood of bugs and improve reliability.

# PROBLEMATIC: Complex signal handler
class ComplexSignalHandler
  def setup_complex_handler
    Signal.trap("USR1") do |signal|
      begin
        # Too much processing in signal handler
        update_configuration_from_file
        recalculate_worker_pool_sizes
        refresh_database_connections
        send_metrics_to_monitoring_system
        log_detailed_system_state
        notify_external_services
        
        puts "Complex processing completed for signal #{signal}"
      rescue => e
        # Error handling becomes complex
        log_error("Signal handler error", e)
        attempt_error_recovery
        send_error_notification(e)
      end
    end
  end
end

# BETTER: Simple signal handler with delegation
class SimpleSignalHandler
  def initialize
    @signal_queue = Queue.new
    setup_signal_processor
  end

  def setup_simple_handler
    # Signal handler only queues work
    Signal.trap("USR1") do |signal|
      @signal_queue.push([:usr1_received, signal, Time.now])
    end
  end

  private

  def setup_signal_processor
    Thread.new do
      loop do
        begin
          work_item = @signal_queue.pop
          process_signal_work(work_item)
        rescue => e
          warn "Error processing signal work: #{e.message}"
        end
      end
    end
  end

  def process_signal_work(work_item)
    action, signal, timestamp = work_item
    
    case action
    when :usr1_received
      # Complex processing happens in dedicated thread
      ConfigurationManager.reload
      WorkerPool.adjust_sizes
      DatabasePool.refresh_connections
      MetricsCollector.send_updates
      SystemLogger.log_state
      NotificationService.send_updates
    end
  end
end

Reference

Signal Module Methods

Method Parameters Returns Description
Signal.trap(signal, command) signal (String/Integer), command (String/Proc/nil) String or Proc Register signal handler, returns previous handler
Signal.list None Hash Returns hash mapping signal names to numbers
Signal.signame(signo) signo (Integer) String or nil Returns signal name for given number

Common POSIX Signals

Signal Number Default Action Description
SIGHUP 1 Terminate Hangup detected on controlling terminal
SIGINT 2 Terminate Interrupt from keyboard (Ctrl+C)
SIGQUIT 3 Core dump Quit from keyboard (Ctrl+)
SIGKILL 9 Terminate Kill signal (cannot be trapped)
SIGTERM 15 Terminate Termination signal
SIGUSR1 10 Terminate User-defined signal 1
SIGUSR2 12 Terminate User-defined signal 2
SIGCHLD 17 Ignore Child process terminated or stopped
SIGPIPE 13 Terminate Write to broken pipe
SIGALRM 14 Terminate Timer signal from alarm()

Predefined Handler Strings

Handler String Behavior
"DEFAULT" Restore system default signal handling
"IGNORE" Ignore the signal completely
"EXIT" Terminate process immediately
"SYSTEM_DEFAULT" Restore original system handler

Platform Signal Availability

Platform Available Signals Notes
Linux Full POSIX set Includes SIGRTMIN+n real-time signals
macOS Full POSIX set Includes BSD-specific signals like SIGINFO
Windows Limited set Only SIGINT, SIGTERM, SIGBREAK, SIGABRT
FreeBSD Full POSIX set Similar to Linux with BSD extensions
Solaris Full POSIX set Includes Solaris-specific extensions

Signal Handler Execution Context

Aspect Behavior
Thread Context Handlers always execute in main thread
Signal Queuing Signals queued during handler execution
Exception Propagation Uncaught exceptions can terminate process
Interrupt Timing Handlers can interrupt any Ruby operation
Memory Allocation Safe to allocate objects in handlers
File Operations Generally safe but avoid blocking operations

Error Conditions

Exception Cause Resolution
ArgumentError Invalid signal name/number Use Signal.list to verify available signals
Errno::EINVAL Invalid signal operation Check platform signal support
SecurityError Insufficient privileges Run with appropriate permissions
SystemCallError System-level signal failure Check system resources and limits

Best Practices Summary

Practice Rationale
Keep handlers simple Reduces complexity and potential for errors
Use signal queuing for complex operations Prevents blocking signal delivery
Synchronize shared state access Prevents race conditions in multithreaded apps
Test signal behavior on target platforms Ensures compatibility across deployments
Handle exceptions in signal handlers Prevents process termination from handler errors
Avoid blocking operations in handlers Maintains signal responsiveness
Use atomic operations when possible Reduces need for explicit synchronization