Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Lord Protector of your scripts
Ruby
branch: master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
examples
lib
test
.document
.gitignore
LICENSE
README.rdoc
Rakefile
VERSION
cromwell.gemspec

README.rdoc

Cromwell

Lord Protector of your scripts.

Description

This is a very simple wrapper over Signal#trap method that allows you to easily protect your scripts from being killed while they are doing something that should not be interrupted (e.g. interacting with some non-transactional service) or is too costly to restart (e.g. long computations).

While inside the protected block, your script will ignore certain signals and continue its work, but if a signal was caught, it will terminate once the protected block is over. By default, only following signals are ignored: INT (keyboard interrupt ^C), TERM (sent by kill by default), HUP (sent when shell terminates), and QUIT (a “dump core” signal, sent with <code>^</code>), but you can specify any list of them (except for KILL and STOP, of course).

For a full signal list supported on your operating system, run Signal.list in your irb. For more info on signals and their meaning, check your local man signal.

This gem is based on real-life production code. It is especially useful for protecting various daemon-like scripts in Rails application that are (might be) restarted with every deploy.

Installation procedure

Thanks to Gemcutter, installation is as simple as:

sudo gem install cromwell

If you plan on changing anything, you should run tests and tests require two additional gems: shoulda and mocha. Install them with:

sudo gem install thoughtbot-shoulda mocha

Usage examples

The most important in Cromwell API is the protect method. It can be called in two ways: with a block and without a block.

Block form

When used with a block, Cromwell executes the code inside the block protecting it from being interrupted with a signal:

puts 'See you in a while...'
Cromwell.protect {
  sleep 10
}
puts "You're still here?"

When you run this script (which lives in examples/example1.rb), you won't be able to interrupt it with ^C or simple kill while it's sleeping for ten seconds:

$ ruby examples/example1.rb
See you in a while...
^C^C^C^C
[ ten seconds pass... ]
$

Because I tried to interrupt the script, it was terminated once the protected block was over. Had I not pressed ^C, the last line would be executed:

$ ruby examples/example1.rb
See you in a while...
[ ten seconds pass... ]
You're still here?
$

The script cannot be killed, too (I run it in background to be able to run other commands in the same shell):

$ ruby examples/example1.rb &
[1] 70300
See you in a while...
$ kill 70300
[ ten seconds pass... ]
$
[1]+  Done                    ruby examples/example1.rb

If you really want to kill it, use kill -9:

$ ruby examples/example1.rb &
[1] 70328
See you in a while...
$ kill -9 70328
$
[1]+  Killed                  ruby examples/example1.rb

Non-block form

If you want to have more control over what's protected in your script, you can use protect without the block. In that case your code will be protected until you call unprotect method:

puts 'See you in a while...'
Cromwell.protect
sleep 10
Cromwell.unprotect
puts "You're still here?"

The above code lives in examples/example2.rb and behaves in the same way as previous example.

In general it might be good idea to place the call to unprotect in an ensure block. Or, if you want your script to just run until it finishes on its own, don't call unprotect at all.

Specifying other signals

If you want to protect from other signals than the default list, specify them as parameters to protect method:

puts "You can't stop me with ^C but you can kill me. My pid is #{$$}."
Cromwell.protect("INT") {
  sleep 10
}
puts "You're still here?"

This script is still immune to ^C:

$ ruby examples/example3.rb
You can't stop me with ^C but you can kill me. My pid is 70243.
^C^C^C^C
[ ten seconds pass... ]
$

But can be killed:

$ ruby examples/example3.rb &
[1] 70245
You can't stop me with ^C but you can kill me. My pid is 70245.
$ kill 70245
[1]+  Terminated              ruby examples/example3.rb
$

Inspecting state

You can inspect Cromwell's state with two methods:

  • Cromwell.protected? returns true when your code is protected, false otherwise.

  • Cromwell.should_exit? returns true when a signal was caught and termination will occur after the protected code is over.

Preventing termination

Since version 0.1.2, you can prevent termination of your script even when a signal was caught. To do so, use

Cromwell.should_exit = false

like that:

puts 'You can try to kill me but I will survive!'
Cromwell.protect {
  begin
    sleep 10
  ensure
    puts "Oh noes! You wanted to kill me! But I'll continue my work!" if Cromwell.should_exit?
    Cromwell.should_exit = false
  end
}
puts "You're still here?"

This script will continue working even after ^C:

$ ruby examples/example4.rb
You can try to kill me but I will survive!
^C^C^C
[ ten seconds pass... ]
Oh noes! You wanted to kill me! But I'll continue my work!
You're still here?
$

Removing traps

As of version 0.2, any traps that were previously installed are restored by the unprotect method (which is also called by the protect method if a block was given to it, of course). This means if you had some other code installed to handle signals (profiling, debugging etc.) it should be restored. For example:

Signal.trap("INT") { puts "Original signal handler!" }

puts 'See you in a while...'
Cromwell.protect {
  sleep 1
}
puts "Try to ^C now to see original signal handler in action!"
sleep 10

When run, it goes like this:

$ ruby examples/example5.rb
See you in a while...
[ 1 second passes... ]
Try to ^C now to see original signal handler in action!
^COriginal signal handler!
^COriginal signal handler!
^COriginal signal handler!
[ ten seconds pass... ]
$

Of course, had I press ^C right after “See you in a while…”, the script would terminate after 1 second at the end of the protected block.

Logging

If want to see what is going on with Cromwell or have some problems with it, you can use a logger (requires cromwell gem version >= 0.3). If Cromwell is used inside an application that already uses logging, you can use your app's logger. Or you might prefer to use a separate logger.

Cromwell uses two levels of log messages:

  • Logger::INFO – when a signal was caught or script is terminated.

  • Logger::DEBUG – all sorts of debugging information: setting up and restoring traps, calling methods and yielding block. Probably not useful for you, unless you have some problems with Cromwell or are messing with the code.

Here's an example of logger usage:

Cromwell.logger = Logger.new(STDOUT)
Cromwell.logger.level = Logger::INFO

puts 'See you in a while...'
Cromwell.protect {
  sleep 10
}
puts "You're still here?"

When run, this script will log messages on STDOUT:

$ ruby examples/example6.rb
See you in a while...
^CI, [2010-01-14T11:59:29.246851 #19011]  INFO -- : Caught signal INT -- ignoring.
^CI, [2010-01-14T11:59:31.558532 #19011]  INFO -- : Caught signal INT -- ignoring.
^CI, [2010-01-14T11:59:36.270395 #19011]  INFO -- : Caught signal INT -- ignoring.
[ ten seconds pass... ]
I, [2010-01-14T11:59:37.872514 #19011]  INFO -- : Exiting because should_exit is true
$

Custom traps

Starting with version 0.4, you can provide your own trap to handle signal. Possible uses of this feature that I can imagine:

  • ask user for password so only admin can kill the script,

  • setup some counter and exit the script after 3 signals,

  • handle some signals differently.

In the following example (examples/example7.rb), SIGINT is ignored completely, while SIGQUIT is handled normally (exit after protected block), but with a message:

Cromwell.custom_traps["INT"] = proc {
  puts "Trying your ^C skills, are you?"
}

Cromwell.custom_traps["QUIT"] = proc {
  puts "We'll be leaving soon!"
  Cromwell.should_exit = true
}

puts 'See you in a while...'
Cromwell.protect {
  sleep 10
}
puts "You're still here?"

Let's see it in action:

$ ruby examples/example7.rb
See you in a while...
^CTrying your ^C skills, are you?
[ ten seconds pass... ]
You're still here?
$

The last message printed means that the script was not terminated after protected block because the signal trap did not set should_exit to true. And now, let's try sending SIGQUIT:

$ ruby examples/example7.rb
See you in a while...
^\We'll be leaving soon!
[ ten seconds pass... ]
$

This time the script didn't get to execute the last puts statement.

Compatibility

Works for me. Tested on Mac OS X 10.4–10.6 and a little bit on Debian Linux. If it works for you too, I'd be glad to know. Cromwell's reliability depends heavily on your operating system's signals implementation reliability (which may not be very stable on some systems).

To Do list

  • Empty for now… If you miss some feature, let me know.

Changelog

0.4

  • Added custom traps.

0.3

  • Added logger support.

0.2

  • Remove traps when they are not needed anymore and restore original traps.

0.1.2

  • Allow to prevent termination of your script even when a signal was caught.

  • Ensure that examples use ../lib/cromwell.rb not the installed gem

Note on Patches/Pull Requests

  • Fork the project.

  • Make your feature addition or bug fix.

  • Add tests for it. This is important so I don't break it in a future version unintentionally.

  • Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)

  • Send me a pull request. Bonus points for topic branches.

Note on terminology

The protection from signals provided by Cromwell and the method names protect, unprotect, and protected? have nothing to do with Ruby's protected keyword and the general concept of a protected method in Ruby and other object-oriented languages.

Copyright

Copyright © 2009 Przemyslaw Kowalczyk. See LICENSE for details.

Something went wrong with that request. Please try again.