README.textile

SmokeSignals

SmokeSignals is an implementation of Lisp-style conditions and restarts as a Ruby library. Conditions and restarts make it easy to separate policy of error recovery from implementation of error recovery. If you’re unfamiliar with the concept, check out the chapter from Practical Common Lisp.

SmokeSignals is different because:

conditions are not errors (although they can be)

signaling a condition does not unravel the stack (although it can)

conditions can be handled multiple times at different levels of the call stack (or not at all)

restarts can be established at any level in the call stack, not just where the condition is signaled

implementation of signaling, handling, and restarting is completely hidden. (The only possible exception to this is a design decision which allows ensure blocks to work, making this usable with real side-effectful programs.)

# If no handlers are set, this will do nothing.
SmokeSignals::Condition.new.signal

The bang flavor will raise unless it is rescued or restarted.

# This is a fatal signal.
SmokeSignals::Condition.new.signal!

Since you can handle signals multiple times by different handlers at multiple levels in the call stack, simply handling a fatal signal and returning normally is not enough. You must either rescue it or restart it.

Rescuing a condition is just like rescuing an exception with a rescue block. It returns the value from the entire handle block.

By default MyCondition1 === condition that was signaled is used to determine whether a handler applies or not, kind of like a case. You can change the default behavior by overriding Condition#handle_by(handler). Either return a Proc to handle it or nil.

You can handle a signal multiple times by returning normally from your handler. Doing this you can, for example, observe the fact that a condition has been signaled without otherwise having any effect on control flow.

SmokeSignals.handle(lambda {|c| puts 'this is run 2nd' }) do
SmokeSignals.handle(lambda {|c| puts 'this is run 1st' }) do
begin
SmokeSignals::Condition.new.signal
puts 'this is run 3rd because no handlers called rescue or restart'
end
end
end

In the case of an ensure block, it is executed after any handlers. It must be executed afterwards because the whole point of signal handlers is that they are run before the stack is unwound. At that point, a signal handler may choose to rescue, restart, or return normally to allow other handlers to execute. In contrast, by the time an exception is caught, rescuing is not an option; it’s a necessity.

SmokeSignals.handle(lambda {|c| puts 'this is run 2nd' }) do
SmokeSignals.handle(lambda {|c| puts 'this is run 1st' }) do
begin
SmokeSignals::Condition.new.signal
puts 'this is run 3rd because no handlers called rescue or restart'
ensure
puts 'this is run last'
end
end
end

ensure blocks are executed after handlers, but they are executed before restarts. To see why this design decision was made, consider this example.

If this function were called and restarted many times, and the stack were not unwound before each restart, then you would have many files open at once. This is why SmokeSignals unwinds the stack before executing restarts, meaning that ensure blocks are run before restarts.

If you like, you can use defs to define restarts. This allows you to use default arguments, etc.

def parse_file(filename)
SmokeSignals.with_restarts(lambda {
def use_new_filename(f)
parse_file(f)
end
def log_and_abort(logger=Rails.logger)
logger.error("File could not be parsed: #{filename}")
# When called, this will return nil from parse_file.
nil
end
}) do
# Do stuff...
end
end

You can pass arguments to restarts the same way you would when calling Object#send.

Is SmokeSignals a replacement for Ruby exceptions?

Short answer: no, they’re an extension.

Long answer… As shown above, you can achieve all the functionality of exceptions with SmokeSignals.

However, you’re probably using some code that doesn’t know about SmokeSignals and raises exceptions instead. Setting a condition handler will not handle these raised exceptions. They couldn’t because in such a case, restarting would be impossible and rescuing would be a necessity. By the time an exception is handled, the stack has already been unwound.

Thread Safety

This library is thread-safe because each thread has its own handlers and restarts. You cannot signal in one thread and handle it in another thread.