From perldoc perlmod
…
An "END" code block is executed as late as possible, that is, after perl
has finished running the program and just before the interpreter is
being exited, even if it is exiting as a result of a die() function.
(But not if it's morphing into another program via "exec", or being
blown out of the water by a signal--you have to trap that yourself (if
you can).) You may have multiple "END" blocks within a file--they will
execute in reverse order of definition; that is: last in, first out
(LIFO). "END" blocks are not executed when you run perl with the "-c"
switch, or if compilation fails....
Perl’s END
blocks are useful inside your script for doing things
like cleaning up after itself, closing files or disconnecting from
databases. In many cases you use an END
block to guarantee certain
behaviors like a commit or rollback of a transaction. You’ll
typically see END
blocks in scripts, but occassionally you might find
one in a Perl module.
Over the last four years I’ve done a lot of maintenance work on legacy Perl applications. I’ve learned more about Perl in these four years than I learned in the previous 20. Digging into bad code is the best way to learn how to write good code. It’s sometimes hard to decide if code is good or bad but to paraphrase a supreme court justice, I can’t always define bad code, but I know it when I see it.
One of the gems I’ve stumbled upon was a module that provided needed
functionality for many scripts that included its own END
block.
Putting an END
block in a Perl module is an anti-pattern and just bad
mojo. A module should never contain an END
block. Here are
some alternatives:
cleanup()
methodDESTROY
) in an object-oriented moduleModules should provide functionality - not take control of your script’s shutdown behavior.
The first and most obvious answer is that they were unaware of how
DESTROY
blocks can be employed. If you know something about the
author and you’re convinced they know better, then why else?
I theorize that the author was trying to create a module that would encapsulate functionality that he would use in EVERY script he wrote for the application. While the faux pas might be forgiven I’m not ready to put my wagging finger back in its holster.
If you want to write a wrapper for all your scripts and you’ve already settled on using a Perl module to do so, then please for all that is good and holy do it right. Here are some potential guidelines for that wrapper:
new()
method that instantiates your wrapperinit()
method that encapsulates the common
startup operations with options to control whether some are executed
or notrun()
method that executes the functionality for the scriptfinalize()
method for executing cleanup proceduresAll of these methods could be overridden if you just use a plain ‘ol Perl
module. For those that prefer composition over inheritance, use a
role with something like Role::Tiny
to provide those universally
required methods. Using Role::Tiny
provides better flexibility
by allowing you to use those methods before or after your
modifications to their behavior.
The particular script I was working on included a module(whose name I
shall not speak) that included such an END
block. My script
should have exited cleanly and quietly. Instead, it produced
mysterious messages during shutdown. Worse yet I feared some
undocumented behaviors and black magic might have been conjured up
during that process! After a bit of debugging, I found the culprit:
END
block baked into itEND
block printed debug messages to STDERR while doing other
cleanup operationsMy initial attempt to suppress the module’s END
block:
END {
use POSIX;
POSIX::_exit(0);
}
This works as long as my script exits normally. But if my
script dies due to an error, the rogue END
block still
executes. Not exactly the behavior I want.
Here’s what I want to happen:
END
block from executing.Ensure my script always exits with a meaningful status code.
A better method for scripts that need an END
block to claw back
control:
use English qw(-no_match_vars);
use POSIX ();
my $retval = eval {
return main();
};
END {
if ($EVAL_ERROR) {
warn "$EVAL_ERROR"; # Preserve exact error message
}
POSIX::_exit( $retval // 1 );
}
END
blocks – Since POSIX::_exit()
terminates
the process immediatelymain()
throws an exception, we
log it without modifying the messagemain()
forgets to return a
status, we default to 1
, ensuring no silent failures.Of course, you should know what behavior you are bypassing if you
decide to wrestle control back from some misbehaving module. In my
case, I knew that the behaviors being executed in the END
block
could safely be ignored. Even if they couldn’t be ignored, I can still
provide those behaviors in my own cleanup procedures.
Isn’t this what future me or the poor wretch tasked with a dumpster dive into a legacy application would want? Explicitly seeing the whole shebang without hours of scratching your head looking for mysterious messages that emanate from the depths is priceless. It’s gold, Jerry! Gold!
Next up…a better wrapper.
Previous post: How to Fix Apache 2.4 Broken Directory Requests (Part III)