Practical mod_perl, from O'Reilly. | 33
Practical mod_perl: Chapter 6: Coding with mod_perl in Mind
[The following is the conclusion of our series of excerpts from Chapter 6 of the O'Reilly title, Practical mod_perl.]
The Importance of Cleanup Code
Cleanup code is a critical issue with aborted scripts. For example, what happens to locked resources, if there are any? Will they be freed or not? If not, scripts using these resources and the same locking scheme might hang forever, waiting for these resources to be freed.
And what happens if a file was opened and never closed? In some cases, this might lead to a file-descriptor leakage. In the long run, many leaks of this kind might make your system unusable: when all file descriptors are used, the system will be unable to open new files.
First, let's take a step back and recall what the problems and
solutions for these issues are under mod_cgi. Under mod_cgi, the resource-locking
issue is a problem only if you use external lock files and use them for lock
indication, instead of using flock( )
. If the script
running under mod_cgi is aborted between the lock and the unlock code, and you
didn't bother to write cleanup code to remove old, dead locks, you're in big
trouble.
The solution is to place the cleanup code in an END
block:
END {
# code that ensures that locks are removed
}
When the script is aborted, Perl will run the END
block while shutting down.
If you use flock( )
, things are much
simpler, since all opened files will be closed when the script exits. When the
file is closed, the lock is removed as well--all the locked resources are freed.
There are systems where flock( )
is unavailable;
on those systems, you can use Perl's emulation of this function.
With mod_perl, things can be more complex when you use global
variables as filehandles. Because processes don't exit after processing a request,
files won't be closed unless you explicitly close( )
them or reopen them with the open( )
call, which
first closes the file. Let's see what problems we might encounter and look at
some possible solutions.
Critical section
First, we want to take a little detour to discuss the "critical section" issue. Let's start with a resource-locking scheme. A schematic representation of a proper locking technique is as follows:
- Lock a resource
- Do something with the resource
- Unlock the resource
<critical section starts>
<critical section ends>
If the locking is exclusive, only one process can hold the resource at any given time, which means that all the other processes will have to wait. The code between the locking and unlocking functions cannot be interrupted and can therefore become a service bottleneck. That's why this code section is called critical. Its execution time should be as short as possible.
Even if you use a shared locking scheme, in which many processes are allowed to concurrently access the resource, it's still important to keep the critical section as short as possible, in case a process requires an exclusive lock.
Example 6-30 uses a shared lock but has a poorly designed critical section.
Example 6-30: critical_section_sh.pl
use Fcntl qw(:flock);
use Symbol;
my $fh = gensym;
open $fh, "/tmp/foo" or die $!;
# start critical section
flock $fh, LOCK_SH; # shared lock, appropriate for reading
seek $fh, 0, 0;
my @lines = <$fh>;
for (@lines) {
print if /foo/;
}
close $fh; # close unlocks the file
# end critical section
The code opens the file for reading, locks and rewinds it to the beginning, reads all the lines from the file, and prints out the lines that contain the string "foo".
The gensym( )
function imported by
the Symbol
module creates an anonymous glob data
structure and returns a reference to it. Such a glob reference can be used as
a file or directory handle. Therefore, it allows lexically scoped variables
to be used as filehandles.
Fcntl
imports file-locking symbols,
such as LOCK_SH
, LOCK_EX
,
and others with the :flock
group tag, into the
script's namespace. Refer to the Fcntl
manpage
for more information about these symbols.
If the file being read is big, it will take a relatively long time for this code to complete printing out the lines. During this time, the file remains open and locked with a shared lock. While other processes may access this file for reading, any process that wants to modify the file (which requires an exclusive lock) will be blocked waiting for this section to complete.
We can optimize the critical section as follows. Once the file has been read, we have all the information we need from it. To make the example simpler, we've chosen to just print out the matching lines. In reality, the code might be much longer.
We don't need the file to be open while the loop executes, because we don't access it inside the loop. Closing the file before we start the loop will allow other processes to obtain exclusive access to the file if they need it, instead of being blocked for no reason.
Example 6-31 is an improved version of the previous example, in which we only read the contents of the file during the critical section and process it afterward, without creating a possible bottleneck.
Example 6-31: critical_section_sh2.pl
use Fcntl qw(:flock);
use Symbol;
my $fh = gensym;
open $fh, "/tmp/foo" or die $!;
# start critical section
flock $fh, LOCK_SH;
seek $fh, 0, 0;
my @lines = <$fh>;
close $fh; # close unlocks the file
# end critical section
for (@lines) {
print if /foo/;
}
Example 6-32 is a similar example that uses an exclusive lock. The script reads in a file and writes it back, prepending a number of new text lines to the head of the file.
Example 6-32: critical_section_ex.pl
use Fcntl qw(:flock);
use Symbol;
my $fh = gensym;
open $fh, "+>>/tmp/foo" or die $!;
# start critical section
flock $fh, LOCK_EX;
seek $fh, 0, 0;
my @add_lines =
(
qq{Complete documentation for Perl, including FAQ lists,\n},
qq{should be found on this system using 'man perl' or\n},
qq{'perldoc perl'. If you have access to the Internet, point\n},
qq{your browser at https://www.perl.com/, the Perl Home Page.\n},
);
my @lines = (@add_lines, <$fh>);
seek $fh, 0, 0;
truncate $fh, 0;
print $fh @lines;
close $fh; # close unlocks the file
# end critical section
Since we want to read the file, modify it, and write it back without
anyone else changing it in between, we open it for reading and writing with
the help of "+>>"
and lock it with
an exclusive lock. You cannot safely accomplish this task by opening the file
first for reading and then reopening it for writing, since another process might
change the file between the two events. (You could get away with "+<"
as well; please refer to the perlfunc manpage for
more information about the open( )
function.)
Next, the code prepares the lines of text it wants to prepend
to the head of the file and assigns them and the content of the file to the
@lines
array. Now we have our data ready to be
written back to the file, so we seek( )
to the
start of the file and truncate( )
it to zero size.
Truncating is necessary when there's a chance the file might shrink. In our
example, the file always grows, so in this case there is actually no need to
truncate it; however, it's good practice to always use truncate(
)
, as you never know what changes your code might undergo in the future,
and truncate( )
doesn't significantly affect performance.
Finally, we write the data back to the file and close it, which unlocks it as well.
Did you notice that we created the text lines to be prepended as close to the place of usage as possible? This complies with good "locality of code" style, but it makes the critical section longer. In cases like this, you should sacrifice style in order to make the critical section as short as possible. An improved version of this script with a shorter critical section is shown in Example 6-33.
Example 6-33: critical_section_ex2.pl
use Fcntl qw(:flock);
use Symbol;
my @lines =
(
qq{Complete documentation for Perl, including FAQ lists,\n},
qq{should be found on this system using 'man perl' or\n},
qq{'perldoc perl'. If you have access to the Internet, point\n},
qq{your browser at https://www.perl.com/, the Perl Home Page.\n},
);
my $fh = gensym;
open $fh, "+>>/tmp/foo" or die $!;
# start critical section
flock $fh, LOCK_EX;
seek $fh, 0, 0;
push @lines, <$fh>;
seek $fh, 0, 0;
truncate $fh, 0;
print $fh @lines;
close $fh; # close unlocks the file
# end critical section
There are two important differences. First, we prepared the text
lines to be prepended before the file is locked. Second,
rather than creating a new array and copying lines from one array to another,
we appended the file directly to the @lines
array.
Created: March 27, 2003
Revised: July 23, 2003
URL: https://webreference.com/programming/perl/mod_perl/chap6/4