8.2. Common Mistakes in Sending EmailNow we can begin using email as a notification method. However, when we start to write code that performs this function, we quickly find that the how to send mail is not nearly as interesting as the when and what to send. This section explores those questions by taking a contrary approach. If we look at what and how not to send mail we'll get a deeper insight into these issues. Let's talk about some of the most common mistakes made when writing system administration programs that send mail. 8.2.1. Overzealous Message SendingBy far, the most common mistake is sending too much mail. It is a great idea to have scripts send mail. If there's a service disruption, normal email or email sent to a pager are good ways to bring this problem to the attention of a human. But under most circumstances it is a very bad idea to have your program send mail about the problem every five minutes or so. Overzealous mail generators are quickly added to the mail filters of the very humans who should be reading the mail. The end result is that important mail is routinely ignored. 8.2.1.1. Controlling the frequency of mailThe easiest way to avoid what I call "mail beaconing" is to build safeguards into the programs to gate the delay between messages. If your script runs constantly, it is easy to stash the time of the last mail message sent in a variable like this: $last_sent = time; If your program is started up every N minutes or hours via Unix's cron or NT scheduler service mechanisms, this information can be written to a one-line file and read again the next time the program is run. Be sure in this case to pay attention to some of the security precautions listed in Chapter 1, "Introduction". Depending on the situation, you can get fancy about your delay times. This code shows an exponential backoff:
The subroutine expbackoff( ) returns true (1) if email should be sent and false (0) if not. It begins by returning true the first time it is called, then rapidly increases the delay time until eventually true is only returned once a day. To make this code more interesting, I've used a peculiar programming construct called a closure to stash away the last message-sent time and the last power of two used to compute the delay. We're using the closure as a way of hiding our important variables from the rest of the program. In this small program it is just a curiosity, but the usefulness of this technique becomes readily apparent in a larger program where it is more likely that other code might inadvertently stomp on our variables. In brief, here's how closures work. The subroutine &time_closure( ) returns a reference to an anonymous subroutine, essentially a little piece of code without a name. Later on we'll use that reference to run this code using the standard symbolic reference syntax: &$last_data. The code in our anonymous subroutine returns a reference to an array, hence the punctuation parking lot in this line used to access the returned data:
Here's the magic that makes a closure: because the reference is created in the same enclosing block as the my( )ed variables $stored_sent and $stored_power, it traps those variables in a unique context. $stored_sent and $stored_power can be read and changed only while the code in this reference is executing. They also retain their values between invocations of the code reference. For instance:
will print "1 1" even though it appears we changed the values of $stored_sent and $stored_power in the third line of code. We certainly changed the value of the global variables with those names, but we couldn't touch the copies protected by the closure. It may help you to think of a variable in a closure as a satellite in orbit around a wandering planet. The satellite is trapped by the gravity of the planet; where the planet goes, so too goes the satellite. The satellite's position can be described only in reference to the planet: to find the satellite, you first locate the planet. Each time you find this particular planet, the satellite should be there, just where you left it. Think of the variables in a closure as being in orbit around their anonymous subroutine code reference, separate from the rest of your program's galaxy. Setting astrophysics aside, let's return to our discussion of mail sending. Sometimes it is more appropriate to have your program act like a two-year-old, complaining more often as time goes by. Here's some code similar to the previous example. This time we increase the number of messages sent over time. It starts off giving the go-ahead to send mail once a day and then rapidly decreases the delay time until it hits a minimum delay of five minutes:
In both examples we called an additional subroutine (&$last_data) to find when the last message was sent and how the delay was computed. Later, if we decide to change how the program is run, this compartmentalization will allow us to change how we store that state. For example, if we change our program to run periodically rather than running all the time, we could easily replace the closure with a normal subroutine that saves and retrieves the data to and from a plain text file. 8.2.1.2. Controlling the amount of mailAnother subclass of the "overzealous message sending" syndrome is the "everybody on the network for themselves" problem. If all of the machines on your network decide to send you a piece of mail, you may miss something important in the subsequent message blizzard. A better approach is to have them all report to a central repository of some sort. The information can then be collated and mailed out later in a single mail message. Let's consider a moderately contrived example. For this scenario, assume each machine in your network drops a one-line file into a shared directory.[1] Named for each machine, that file will contain each machine's summary of the results of last night's scientific computation. It would have a single line of this form:
hostname success-or-failure number-of-computations-completed A program that collates the information and mails the results might look like this:
The code first reads a list of the machine names that will be participating in this scheme. Later on it will use a hash based on this list to check if there are any machines that have not placed a file in the central reporting directory. We open each file in this directory and extract the status information. Once we've collated the results, we construct a mail message and send it out. Here's an example of the resulting mail: Date: Wed, 14 Apr 1999 13:06:09 -0400 (EDT) Message-Id: <199904141706.NAA08780@example.com> Subject: [report] Partial: 3 ACK, 4 NACK, 1 MIA To: project@example.com From: project@example.com Run report from reportscript on Wed Apr 14 13:06:08 1999 ==Succeeded== barney: computed 23123 oogatrons betty: computed 6745634 oogatrons fred: computed 56344 oogatrons ==Failed== bambam: computed 0 oogatrons dino: computed 0 oogatrons pebbles: computed 0 oogatrons wilma: computed 0 oogatrons ==Missing== mrslate Another way to collate results like this is to create a custom logging daemon and have each machine report in over a network socket. Let's look at code for the server first. This example reuses code from the previous example. We'll talk about the important new code right after you see the listing:
Besides moving some of the code sections to their own subroutines, the key change is the addition of the networking code. The IO::Socket module makes the process of opening and using sockets pretty painless. Sockets are usually described using a telephone metaphor. We start by setting up our side of the socket (IO::Socket->new( )), essentially turning on our phone, and then wait for a call from a network client (IO::Socket->accept( )). Our program will pause (or "block") until a connection comes in. As soon as it arrives, we note the name of the connecting client. We then read a line of input from the socket. This line of input is expected to look just like those we read from individual files in our previous example. The one difference is the magic hostname DUMPNOW. If we see this hostname, we print the subject and body of a ready-to-mail message to the connecting client and reset all of our counters and hash tables. The client is then responsible for actually sending the mail it receives from the server. Let's look at our sample client and what it can do with this message:
This code is simpler. First, we open up a socket to the server. In most cases, we pass it our status information (received on the command line as $ARGV[0]) and drop the connection. If we were really going to set up a logging client-server like this, we would probably encapsulate this client code in a subroutine and call it from within a much larger program after its processing had been completed. If this script is passed an -m flag, it instead sends "DUMPNOW" to the server and reads the subject line and body returned by the server. Then this output is fed to Mail::Mailer and sent out via mail using the same code we've seen earlier. To limit the example code size and keep the discussion on track, the server and client code presented here is as bare bones as possible. There's no error or input checking, access control or authentication (anyone on the Net who can get to our server can feed and receive data from it), persistent storage (what if the machine goes down?), or any of a number of routine precautions in place. On top of this, we can only handle a single request at a time. If a client should stall in the middle of a transaction, we're sunk. For more sophisticated server examples, I recommend the client-server treatments in Sriram Srinivasan's Advanced Perl Programming, and Tom Christiansen and Nathan Torkington's Perl Cookbook, both published by O'Reilly. Jochen Wiedmann's Net::Daemon module will also help you write more sophisticated daemon programs. Let's move on to the other common mistakes made when writing system administration programs that send mail. 8.2.2. Subject Line WasteA Subject: line is a terrible thing to waste. When sending mail automatically, it is possible to generate a useful Subject: line on the fly for each message. This means there is very little excuse to leave someone with a mailbox that looks like this: Super-User File history database merge report Super-User File history database merge report Super-User File history database merge report Super-User File history database merge report Super-User File history database merge report Super-User File history database merge report Super-User File history database merge report when it could look like this: Super-User Backup OK, 1 tape, 1.400 GB written. Super-User Backup OK, 1 tape, 1.768 GB written. Super-User Backup OK, 1 tape, 2.294 GB written. Super-User Backup OK, 1 tape, 2.817 GB written. Super-User Backup OK, 1 tape, 3.438 GB written. Super-User Backup OK, 3 tapes, 75.40 GB written. Your Subject: line should be a concise and explicit summary of the situation. It should be very clear from the subject line whether the program generating the message is reporting success, failure, or something in between. A little more programming effort will pay off handsomely in reduced time reading mail. 8.2.3. Insufficient Information in the Message BodyThis falls into the same "a little verbosity goes a long way" category as the previous mistake. If your script is going to complain about problems or error conditions in email, there are certain pieces of information it should provide in that mail. They boil down to the canonical questions of journalism:
Here's some simple Perl code that covers all of these bases:
&problemreport will output a problem report, subject line first, suitable for feeding to Mail::Mailer as per our previous examples. &fireperson is an example test of this subroutine. Now that we've explored sending mail, let's see the other edge of the sword. ![]() Copyright © 2001 O'Reilly & Associates. All rights reserved. |
|
|