Document Home


Samples for this Section


Previous: Rounding Out the CGI-Based Entry Script

Cleaning Up the Insert Script

When I launched into this version of the insert script, my primary intent beyond adding half_inning to the storage function was to lean out the code a bit, clean up some pieces that I put in as I was attempting to track the source of a couple of bugs, and generally render it a little more legible and ultimately more amenable to modularization. For example, as I was initially developing the previous version of the script I was trying to track down a series of "unitialized value" errors in the comparisons that determine whether a viable file handle can be established on a given target. I changed the destination for the split functions preceeding those comparisons from arrays to a series of temporary variables to isolate potential sources of that problem. Not considered good form, and not pretty, but certainly functional. (In fact, if you are developing a script under time pressure and a rough version delivers the required functionality with acceptable performance, and support is not an issue over the relevant time frame, it is perfectly acceptable to leave that script running indefinitely. However, if support by someone else is or becomes an issue it is appropriate to revisit the code and clean it up, because legibility is an important component of maintainability.)



As I was thinking about this, however, I realized that I really did not like the manner in which the previous script handled the association of host names with mosix numbers, and since one of the stated reasons for this clean-up phase is to prepare for modularization, I decided that this is the appropriate place to do something about that. I was very pleasantly surprised to discover that nothing exploded in my face as I made the revision, although I will not discount the possibility that there is something ticking back there, waiting to go off. There almost always is <grin>.



You may recall from the previous incarnation that I mentioned that if I were to adopt a more automated approach to this issue I would probably traverse the structure under the mfs mount point, and use that as the basis for performing the actions that establish the environment required to support what follows. In a fashion similar to the previous version, the script tests for the presence of key files and the subroutine that creates them is run if one or the other is not found. (As you will see, in this case that information is stored in two files rather than in one as in the previous version.) Again as in the previous version, the number_config subroutine is executed if that condition is not met. Before I get into the subroutine itself, however, note that I have added the socket module to the operating environment - I'll get to the reason for that in just a bit.



In a sense, the implementation of log and repository file destination management incorporated into this version is an intermediate step on the evolution of this functionality from the explicit designation of machine name and mosix number used in the previous incarnation to an upcoming version in which the names of the destination machines are read in from external files that are written as part of a configuration management component. (I briefly considered storing directory locations similarly, but decided that at that point the additional functionality provided might be counter-productive. Writing these files is a system function, not a user function, and while the flexibility to designate different machines as alternative locations is a real enhancement to the stability of the system, I do not want them to get scattered all over the cluster file system to the extent that it would become difficult to track down the appropriate file to use in database recovery.) In this version I still use the $controller_num and $server_num scalars to establish destination directories, but after that point the information used to perform system operations is pulled entirely from the cluster environment.



Just as before, the number_config subroutine writes the address information required for execution of the remote shell commands that I use to determine the space available on the target machine. Since, however, I have removed explicit specification of machine names from the script, I have to create a third file that holds the names of the machines, and ultimately have to be able to associate those hostnames with the mosix number. I will get to the manner in which I do that in just a bit ... for now I have to get the source information written.



As I said above, the strategy employed by this script involves reading the directory under the mfs mount point and using that as the source of the cluster mosix numbers. As long as the mfs filesystem is mounted, all of those numbers will be there ... that's just the way mosix works.

 
			my ($address,$host,@host);
			my $mfs_mount='/mfs';
			opendir(MFS,$mfs_mount);
			my @machines=readdir(MFS);
			my $out_host_addr="/home/www/host_addr";
			my $out_numbers="/home/www/numbers";
			my $out_hosts="/home/www/hosts";
			open(OUT,">>$out_numbers");
			open(OUT_HOSTS,">>$out_hosts");
			my $machine;
			close MFS;
As you can see, after initializing a few scalars and an array I initialize the scalar $mfs_mount holding the mount point for the mfs filesystem and use that to create a directory handle on that location. I then dump the contents of that directory into the @machines array with the readdir function on the defined directoruy handle. After initializing three scalars holding the names of destination files, I create filehandles on the two destination files that will hold the final results and close the mfs directory handle.



Now that I have an array with the contents of the mfs mount point directory as elements, I iterate over that array and for each element that had a digit in the first position use a system command to the mosctl utility with the whois option and the $machine scalar.

			foreach $machine(@machines)	{
	
						if ($machine =~ /^\d/) {
				
							system("mosctl whois $machine >> $out_host_addr");
							print OUT $machine."\n";
						}
				}
The $machine scalar, of course, will on each iteration through the loop hold the pertinent array element from the array of directory contents, and when that element is a number it is also the mosix number of a machine. That is what the pattern match in the if statement does ... the commands within the if statement are executed only if the string in the $machine scalar has a digit in the first position. The output of this command is the tcpip address of the pertinent machine, which I append to the file whose name is held in the $out_host_addr scalar. I then print a line to the filehandle on the numbers file, thus establishing the relationship between mosix number and tcpip address in the order in which the lines are written into the target files. (While the set of mosix numbers in ralphzilla has always been contiguous and continuous, I have no basis on which to assume that this is always true. Even if this were to be always true, it is a good idea to get into the habit of never assuming that something that seems to be true is true.) After these two files are written,
			open(ADDR,"<$out_host_addr");
			while (<ADDR>)	{
				
				$address=inet_aton($_);
				@host=gethostbyaddr($address,AF_INET);
				print OUT_HOSTS $host[0]."\n"; 
			}
			close OUT_HOSTS;
			close OUT;
I open the file containing the tcpip addresses and step through it, using the inet_aton() and gethostbyaddr() functions from the socket module as I did in the cluster update script I discussed in that earlier section. After each lookup is performed, the first element of the array returned by gethostbyaddr(), holding the name of the target host, is printed to the output filehandle.



As with all things perlish, there are a multitude of ways to do this, from writing off a cookie with this information to writing to a small text-based database. I will likely play around with something like this in a later version, because I am not totally comfortable with relying on the order of the lines in the files to establish the association between related items, but since this is done by the program I can be reasonably sure that it will work the same each time <ahem>, and is therefore perfectly adequate. The importance of the information lies in the way I'm going to access it within the script, and that means that the important part now lies in how I pull that data into an accessible structure. That's where the fun in this revision appears<grin>



Think a little about what is going to have to happen to set up the environment that will allow the remote shell commands to be executed. The mosix number of the target machine will be present, and from that the script will have to be able to get the host name. The best way to do that is with a hash, a special form of array, formerly called an associative array, in which elements are paired and one of the elements is used as an index to access the value of the other. You can find find quite a lot of material on hashes in the various perl tutorials that are on the internet, here, for example, but the basic concept is really quite simple. In this context I'm going to construct a hash in which the mosix number is the key value used to access the host name for the machine with that mosix number, and that is what happens in the hash_assign subroutine.



While this subroutine is generally simple, it does introduce two new perl tools. First, as I mentioned in the paragraph above, in this subroutine I associate the mosix numbers and the host names in a has, which will result in a data structure that will look something like this:

					{ "1" => "ralphzilla-raider",
					  "2"=>"ralphzilla-faxa",
									.
									.
					  "5"=>"ralphzilla-b-free"}
The way I get to that is straightforward. After initializing the hash and a scalar, I create file handles on the two files created by the number_config subroutine.
			my %hostnum;
			open(NUMBERS,"/home/www/numbers");
			open(HOSTS,"/home/www/hosts");
			my @mos_num=<NUMBERS>
			my @hosts=<HOSTS>
I then dump the contents of these files into two arrays, one holding the mosix numbers for the cluster and the second the host names. I then initialize a counter variable, and iterate over each element in the array holding the mosix numbers.
			my $mos_num;
			my $i=0;

			foreach $mos_num(@mos_num)	{
				chomp $mos_num;
				$hostnum{$mos_num}=$hosts[$i];
				$i++;
		
			}
Note that the first thing I do within the loop is to chomp the value of the mosix number array element that is the current subject in the loop. This is always a good idea with a value that represents a line read from a file and which will be used for some sort of comparison. The trailing blanks and control characters that can be embedded in such a string will often cause such a comparison to fail without a readily recognizable reason why the failure occurred. While there are ways of doing such comparisons without doing the chomp, you have to know that the problem exists to trap it. Therefore, you might just as well do the chomp and not worry about it. The following line
		$hostnum{$mos_num}=$hosts[$i];
represents the standard method for adding a set of values to a hash, and may be interpreted as saying that the value held in the $ith element of the @hosts array should be stored in the %hostnum hash and accessed with the value held in the $mos_num scalar. In other words, if the value of @mos_num is "2" , and this is the second trip through the loop, and the value of the counter $i==1 and the second element of the @hosts array holds the string "ralphzilla-faxa", this statement will be inserting a hash slice that looks like
"2"=>"ralphzilla-faxa",.
Recognize that I initialize the $i counter to 0. That's because the first element of perl arrays are accessed with the value 0, remember? Since the association of the mosix number with host names is established by the order of the lines in the two files, the counter $i provides a way to track where I am in the @mos_num array, and which element in the @hosts array is therefore relevant. In this fashion the value of the mosix number is not required to be in any specific order in the directory structure that is the source of this array, even though I might expect that it is. Make sense? After the hash slice is stored, the counter scalar is incremented and the loop closed.


The second new thing in this subroutine lies in the way %hostnum is going to be accessed. Remember my brief discussion of references in the discussion of the previous version of this script? Well, as I was putting this together it occurred to me that this is a relatively straightforward context in which to get started on them. You may have noted in the main body of the script that I defined the scalar $hostref. I did that at that point to get the $hostref scalar established in the scope of the main body of the script, and as a result, the scalar should be visible throughout the script. In the line immediately following the loop that populates the has, $hostref is assigned a reference to the %hostnum hash.

		$hostref=\%hostnum;
Since the scalar was initialized at the calling level, there is no need to pass it back to that level, and the subroutine ends.

(In my first revision of this version I did explicitly return $hostref, to remove any doubt that the scalar would hold the appropriate reference as I tested the script. When I determined that the reference was working as I wished, I removed the return statement and verified that the reference was still visible. This was the behavior that I expected from the way I constructed this. But let me tell you a secret. Sometimes things do not work the way you expect them to work.<grin> I think of doing things like this as proactive debugging. As I discussed in the short discussion on debugging, the process of isolating a bug involves controlling the environment until the specific context that is creating the bug can be identified. Bugs are relatively rarely the result of a single inappropriately specified statement, in perl the interpreter would kick out a script with a syntax error anyway. They are generally the result of interaction between elements of code that may be behaving unexpectedly or may contain a logical error in its expression. For example, the expression $==11 tests whether what is stored in the scalar $x is equal to 11, while the expression $x=11 assigns the value 11 to $x. You might think that you would catch the result of leaving out the second = sign readily, but if it happens in the context of an if statement, i.e.,
if ($x==11){};

vs.

if ($x=11) {};
the first stement will evaluate to 1 (remember, any defined and non-zero expression represents true) only when $x is arithmetically equal to 11, while the second in effect tests whether it is possible to assign 11 to $x, which should happen all of the time as long as the scalar is defined. (If the expression read my $x=11, it would happen all of the time.) Each of these statements are perfectly legal, and while you are likely to use the first type far more frequently there will be times when you want to do things like what is done in the second. But if you intended the first, and through typo or simple absent-mindedness get the second, the code within the block following the if() will always be executed. Even when you realize that conditions are not being appropriately trapped, you can read right past that problem several times before you spot it.


This has been a somewhat long-winded digression, but the context I've created is precisely the kind of context in which many bugs occur, and it illustrates the virtues of doing the kind of over-building, as it were, that I'm talking about. If you were absolutely confident with the remainder of the structures that surrounded the code that included the if statement, you would sooner or later recognize that the second = sign was missing. Building an application is inherently incremental. One creates a foundation and then starts adding functionality to that structures. The fewer elements of uncertainty you add as you implement a bit of functionality, the easier it will be to track down malfunctions. As as a side note, you can find out a lot of interesting things as you take out the little pieces that you think you do not really need.)


Anyway, back to the script. I'm now at the point at which I am going to start combining the two loops used to check for filehandle viability and space availability for the log file and the repository file. The essential strategy that I employ is to embed the logic that was incorporated in the two loops previously into a single loop that is itself embedded into a larger loop that executes only twice ... once for the log file and once for the repository.


I begin by initializing the various structures that I use to control the loop and to hold information as the loop executes.

my @prefix=('l','r');
my ($loop,$prefix,$fh,$fh_name,$dir,@destinations,$free);
Of particular interest here is the two-element array @prefix, which is initialized with the values "l" and "r" ... this array will provide the control for the outer level loop.


After initializing the Filesys::DiskFree object as I have done in previous versions, the script enters the loop that will execute once for each element in the array.

my $df=Filesys::DiskFree->new();
$df->df();

foreach $prefix(@prefix)	{

		$loop=1;
		
		if ($prefix eq 'l')	{
		
				@destinations = ("/mfs/$server_num/data/bb_system/",
									"/home/www/");
				$min_space=100;
				$name="bb.log";
				}
				
		elsif ($prefix  eq 'r')	{
		
				@destinations = ("/mfs/$server_num/data/bb_system/",
								  "/home/www/",
								  "/mfs/$controller_num/home/www");
				$min_space=500;
				$name="repository";
				}
Within this outer loop, the first command sets the value of the $loop scalar to 1 ... as before this scalar is used to control exist from what is now the inner loop. Following this, a simple if statement determines what set of values are loaded into the @destinations array, the $min_space scalar, and the $name scalar. At that point, the script actually enters the inner loop, which is much the same as the loops were in the previous version, except for the face that the scalars like $r_min and $l_dir have been replaced by generic equivalents and populated within the context of the outer loop.
		while ($loop)	{
		
				$dir = shift @destinations;
				
				if (! $dir)		{exit;}
				
				free_space($dir);
				
				$fh = new FileHandle($dir.$name,'>>');
				
				if (($free > $min_space) and ($fh)) 	{
				
					$loop=0;
					if ($prefix eq "l") {$log_fh=new FileHandle($dir.$name,'>>');}
					elsif ($prefix eq "r") {$repo_fh=new FileHandle($dir.$name,'>>');}
				}
				$fh->close;
			}
One minor change required by this implementation is that I can no longer rely on the filehandle created as part of the test loop as the output filehandle for either the repository file or the log file, because the filehandle created in the loop has to be generic and disposable so it can be used in any given iteration. I can envision at least a couple of potential solutions to this. The one I adopted is the most straightforward: if both the filehandle creation and disk space availability checks pass, the $repo_fh filehandle is created if the $prefix value is "r", or the $log_fh filehandle is created if the $prefix value is "l". This gets the job done, but is probably not the approach I would want to take if there were more than the two potential states in the loop. While I can see alternative formulations for contexts in which there were more options, since I have not had the opportunity to test them I will keep them to myself for now.


Beyond that, the loop functions pretty much as did each of the loops in the previous version ... the only way out of the loop is to either successfully pass the two tests, in which case the script continues, or the end of the @destinations array is reached, leading to exit from the script. As before, the script terminate if none of the specified locations for either the repository file or the log file are viable. While the previous version, in which it would have been relatively easy to execute different action upon failure if desired, in this version I would implement such by calling a subroutine within the block defined within the (! $dir) if statement that is now comprised simply of exit;.


The changes I made in how host names can be determined are actually put to use in the free_space() subroutine. But first, if you take a look at the early part of that subroutine you will see that the temporary variables that previously held the results of the directory split have been replaced by the array @full_path, and individual elements now are referenced by their position in the array.

	my $dir=shift @_;
	my @full_path=split('/',$dir,4);
	my @result;	
As before, if this is a directory mounted under the mfs mount point a command must be executed on the remote machine to determine the available space at the target location.
	if ($full_path[1] eq 'mfs')	{
Also as before I assemble a scalar which holds the full path to the target directory relevant to the root of the remote machine.
		my $path='/'.$full_path[3];
When it comes time to determine the name of that remote machine, however, I now use the hash reference to look up the host name given the index value, which of course if the mosix number of the target machine embedded in the original target directory specification. The syntax of the command to assign the result of that lookup to the scalar $rhost is
		$rhost=$hostref->{$full_path[2]};
as the mosix number is the third element in the array returned from the split function. (Remember, since the string that is split begins with a "/", which is the delimiter specified for the split function, the first element of that array is empty, or null.) There are other ways to go about formulating this lookup ... this is perl, after all. I considered this the clearest manner in which to express the lookup, and will likely use alternative formulations in similar contexts as I proceed.


This is, I think, a good example of using data structures to retrieve information that I previously specified explicitly, and how they can be used to implement code that is both cleaner and more maintainable. What I went through earlier with the new version of the number_config and hash_assign subroutines may seem like a great deal of trouble compared with simply hard-coding the relationship between mosix numbers and host names, but that approach would require that the code be changed if the cluster were to be modified, and that is the kind of thing that might not be caught by inexperienced system maintenance personnel. The number_config subroutine may take a few seconds to run, but it only has to run when the source files are not present. (Actually, in the current state of the system the administrator would have to know to delete those files when a change is made in the cluster in order to force the script to re-create them. This is a substrantial improvement over having to edit the script, but it is obviously not bullet-proof. While it is not unusual for real-world systems to have similarly quirky actions that are required to be performed periodically, the best solution is to have the system somehow sense that requirement and take the action on its own. I am putting that on my to-do list, right now <grin>.) As I get into building modules, you will see that a large part of the rationale for doing things like this lies not so much in their immediate context, but in extending the accessibility of that information beyond the scope of that context, thus enabling the same functionality in the wider framework of the application and, as you will see, beyond.


As I said earlier, the most substantive changes made in this version of the script are those in the number_config, the hash_assign, and the free_space subroutines. Beyond these, the only changes to the script are the relatively trivial ones required to have the half_inning value added to the database. I will leave tracking down those changes as a homework exercise. As in previous incarnations in which a change I made required a change in the structure of the database, it is appropriate to take the steps specified in the database revisionpage.


In the next section, I will start creating modules that will hold many of the subroutines I've used to this point.




Changes to the Interface Script