Class: Chore::Signal

Inherits:
Object
  • Object
show all
Defined in:
lib/chore/signal.rb

Overview

Provides smarter signal handling capabilities than Ruby's built-in Signal class. Specifically it runs callbacks in a separate thread since:

(1) Ruby 2.0 cannot obtain locks in the main Signal thread
(2) Doing so can result in deadlocks in Ruby 1.9.x.

Ruby's core implementation can be found at: ruby-doc.org/core-1.9.3/Signal.html

Differences

There are a few important differences with the way signals trapped through this class behave than through Ruby's Signal class.

Sequential processing

In Ruby, signals are interrupt-driven – the thread is executing at the time will be interrupted at that point in the call stack and start executing the signal handler. This increases the potential for deadlocks if mutexes are in use by both the thread and the signal handler.

In Chore, signal handlers are executed sequentially. When a handler is started, it must complete before the next signal is processed. These handlers are also executed in their own thread and, therefore, will compete for resources with the rest of the application.

Forking

In Ruby, forking does not disrupt the ability to process signals. Signals trapped in the master process will continue to be trapped in forked child processes.

In Chore, this is not the case. When a process is forked, any trapped signals will no longer get processed. This is because the thread that processes those incoming signals gets killed.

In order to process these signals, `Chore::Signal.reset` must be called, followed by additional calls to re-register those signal handlers.

Signal ordering

It is important to note that in Ruby, signals are essentially processed as LIFO (Last-In, First-Out) since they are interrupt driven. Similar behaviors is present in Chore's implementation.

Having LIFO behavior is the reason why this class uses a queue for tracking the list of incoming signals, instead of writing them out to a pipe.

Constant Summary collapse

PRIORITIES =

The priorities of signals to handle. If not defined, the signal is considered high-priority.

{
  'CHLD' => :secondary
}

Class Method Summary collapse

Class Method Details

.resetObject

Resets signals and handlers back to their defaults. Any unprocessed signals will be discarded.

This should be called after forking a processing in order to ensure that signals continue to get processed. Note, however, that new handlers must get registered after forking.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/chore/signal.rb', line 106

def reset
  # Reset traps back to their default behavior.  Note that this *must*
  # be done first in order to prevent trap handlers from being called
  # while the wake pipe / listener are being reset.  If this is run
  # out of order, then it's possible for those callbacks to hit errors.
  @handlers.keys.each {|signal| trap(signal, 'DEFAULT')}

  # Reset signals back to their empty state
  @listener = nil
  @primary_signals.clear
  @secondary_signals.clear
  @wake_out.close
  @wake_in.close
  @wake_in, @wake_out = IO.pipe
end

.trap(signal, command = nil, &block) ⇒ Object

Traps the given signal and runs the block when the signal is sent to this process. This will run the block outside of the trap thread.

Only a single handler can be registered for a signal at any point. If a signal has already been trapped, a warning will be generated and the previous handler for the signal will be returned.

See ::Signal#trap @ ruby-doc.org/core-1.9.3/Signal.html#method-c-trap for more information.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/chore/signal.rb', line 77

def trap(signal, command = nil, &block)
  if command
    # Command given for Ruby to interpret -- pass it directly onto Signal
    @handlers.delete(signal)
    ::Signal.trap(signal, command)
  else
    # Ensure we're listening for signals
    listen

    if @handlers[signal]
      Chore.logger.debug "#{signal} signal has been overwritten:\n#{caller * "\n"}"
    end

    # Wrap handlers so they run in the listener thread
    signals = PRIORITIES[signal] == :secondary ? @secondary_signals : @primary_signals
    @handlers[signal] = block
    ::Signal.trap(signal) do
      signals << signal
      wakeup
    end
  end
end