In Progress
Unit 1, Lesson 21
In Progress

At Exit

How can you be sure a resource will be cleaned up, when you don’t control how a program will exit? In this episode we’ll examine Ruby’s at_exit call for establishing exit handlers. In typical RubyTapas fashion, we’ll look at practical applications, and go beyond the basics to answer questions about the behavior of at_exit in various scenarios.

Video transcript & code

In programming, as in real life, it's important to remember to clean up after ourselves. For instance, if we open a file we ought to close it later.

myfile = open("myfile.txt")
# ...
myfile.close

Ruby makes it exceptionally easy to make sure that resources like files are closed when were done with them, through the use of blocks. When we use the block form of opening a file or some other resource, Ruby closes the file automatically once the block completes.

open("myfile.txt") do |myfile|
  # ...
end

There are times, however, when we can't make use of this convenience.

For instance, back in Episode #485, we looked at an example of a command-line program which should pipe long output through a pager process when it is run at the console.

In order to do this, when it is first starting up, it checks to see if it is being run from the console, and if it is, substitutes a pipe to a pager program for the usual standard output stream.

require "faker"
require "lovely_rufus"

if $stdout.tty?
  pager = open("|more", "w")
  $stdout = pager
end

if ARGV[0] == "--help"
  text = Faker::Lorem.paragraphs(30).join("\n\n")
  puts LovelyRufus::TextWrapper.wrap(text, width: 67)
end

In case like this, it's often not practical or even possible to wrap the entire program in a resource managing block. But, in this particular case it turned out to be essential that the pipe handle be closed at program exit in order to ensure that the whole program ran correctly.

Our solution was to use a Ruby method called at_exit to close the pager pipe when the program ended.

require "faker"
require "lovely_rufus"

if ARGV[0] == "--help"
  if $stdout.tty?
    pager = open("|more", "w")
    at_exit do pager.close end
    $stdout = pager
  end

  if ARGV[0] == "--help"
    text = Faker::Lorem.paragraphs(30).join("\n\n")
    puts LovelyRufus::TextWrapper.wrap(text, width: 67)
  end
end

We've used this feature in a few episodes, but we've never really talked about it in detail.

In a nutshell, at_exit allows us to establish an "exit handler". Whatever code we supply in the at_exit block will be run by Ruby when the program comes to an end.

Here's a very simple demonstration. We set up an at_exit block which will output a goodbye message. Then we output a greeting.

When we run this , the output is in the opposite order from how it appears in the program. That's because Ruby saved the goodbye-printing code to be run only when the program finished.

at_exit do
  puts "Bye bye!"
end

puts "Hello!"

# >> Hello!
# >> Bye bye!

When looking at a simple example like this, it's easy to wonder why we would bother with at_exit when we could just put the farewell code at the end of the program instead.

puts "Hello!"
puts "Bye bye!"

The magic of at_exit is that we can use it even in contexts where we have no control over where the program ends.

For instance, Ruby has a library for managing temporary files, but imagine that we are writing our own replacement for it.

Our method for getting a new temp file is in library code inside a Ruby gem.

def get_temp_file
  open("#{$$}-#{rand(10000)}.tmp", "w+")
end

Applications using this code require the library and then invoke the get_temp_file method.

$LOAD_PATH.unshift(".")

require "mytemp"
temp = get_temp_file
# => #<File:37400-1207.tmp>

Now, what if we wanted to ensure that the temp files from our are always closed and deleted when the program ends? In our library code we have no control over where or how the program exits.

That's where at_exit comes in. In the get_temp_file method we can assign our temp file object to a local variable. then, inside an at_exit block, we can close the file and delete it. Then we just have to remember to return the file object.

def get_temp_file
  file = open("#{$$}-#{rand(10000)}.tmp", "w+")
  at_exit do
    file.close
    File.delete(file.path)
  end
  file
end

By registering an at_exit handler, we are able to exert some control over what happens when the program ends. Even though we're writing code inside a library which has no control over when or how the Ruby process will exit.

A method like this may well be called multiple times in a single program run. You might be wondering how Ruby handles multiple invocations of at_exit.

Well, let's find out. We'll line up three at_exit calls in a row.

Then we'll execute the code.

at_exit do puts "First at_exit" end
at_exit do puts "Second at_exit" end
at_exit do puts "Third at_exit" end

# >> Third at_exit
# >> Second at_exit
# >> First at_exit

When we look at the output, we can see that all of the at_exit blocks are invoked. And they are run in last-in first-out order. A useful way to think of this is as each successive at_exit setting up a resource scope "inside" the scope of the last at_exit. And then when exiting, Ruby works its way back up through these scopes.

Something else you might wonder about is whether there any circumstances in which at_exit handlers are not invoked. For instance,what about when the program exits due to an unhandled exception?

Let's register an at_exit handler.

Then, before the program can exit naturally, let's raise an exception. (If you're wondering why we're using fail instead of raise here, see Episode #188.)

at_exit do
  puts "Rearranging deck chairs"
end

fail "Abandon ship!" # ~> RuntimeError: Abandon ship!

# >> Rearranging deck chairs

# ~> RuntimeError
# ~> Abandon ship!
# ~>
# ~> xmptmp-in15504_7e.rb:5:in `<main>'

When we run this and look at the output, we can see that even though the program failed due to an exception, the at_exit handler was still run.

There is one case in which at_exit handlers will not be run.

When we want to immediately terminate a Ruby program, we use the exit! method. Unlike the non-bang exit method, this method forces an instant termination of the process.

When we run this code, we can see that this time, the at_exit code was never executed.

at_exit do
  puts "I'll be back."
end

puts "Hasta la vista, baby!"
exit!

# >> Hasta la vista, baby!

The at_exit feature might not be something you use in everyday application code. But when we're writing libraries that allocate resources—whether files, network connections, external processes, or even threads— at_exit is an essential tool for writing robust code that always cleans up after itself. Happy hacking!

Responses