FatDog has asked for the wisdom of the Perl Monks concerning the following question:

Please help me fork my 'children'...(that went to a dark place)..

My problem: Every day - we would like to run ~140 backup commands that our DBA's have come up with.

(This is under Linux)

The problem is that we cannot run all backup commands at the same time or it would clobber our performance. We do not really know one day to the next what commands will take 2 hours and what commands will take 10 minutes. So we cannot do batches.

My plan: Take an array of commands and fork each one to a child process up to 6. Every so often - see if any finished and fire up the next command. This keeps things running, but keeps things from running away.

The down side - I have never done this type of thing in perl. I've done it with bash scripts but Perl should be able to do a better job - right?

An afternoon of study allowed me to come up with the following test code that seems to work. But I need advice from people more experienced to look at things and tell me how to head off problems I dont know about yet.

Question: If a command fails, does "waitpid" put the exit code into the $? variable or how do I get the exit code of a command after it finishes?

The code below can be cut and pasted:

use POSIX ":sys_wait_h"; ... #--------------------------------------------------------------------- +---------------------------------------- #--------------------------------------------------------------------- +---------------------------------------- sub S_TestForking2 { my ($lSleepTime); my ($lProcessLabel, @lPidArray); my $lFinished = 0; my $lStillRunning = 0; my $isAlive; my $kid; my $lpid; my $i; my %lRunningPidHash = (); my @lCmdArray; my $lCmdIndex; # Create a bunch of fake commands that take time push (@lCmdArray, "sleep 20"); push (@lCmdArray, "sleep 25"); push (@lCmdArray, "sleep 38"); push (@lCmdArray, "sleep 20"); push (@lCmdArray, "sleep 17"); push (@lCmdArray, "sleep 15"); push (@lCmdArray, "sleep 25"); push (@lCmdArray, "sleep 30"); push (@lCmdArray, "sleep 15"); $lFinished = 0; while ($lFinished == 0) { # Count the running processes using the pid array we filled wh +en we forked the child $lStillRunning = 0; foreach $lpid (sort keys %lRunningPidHash) { waitpid($lpid, WNOHANG); if ( $? == -1 ) { $lStillRunning++; } elsif ($? == 0) { # If we ask about this id again we get -1 forever delete $lRunningPidHash{$lpid}; } print "\t$lpid = $?\n"; } print "Running processes: $lStillRunning\n"; # If our command array is zero and our running processes is ze +ro - we can just exit if ( $lStillRunning == 0 and scalar (@lCmdArray) == 0 ) { $lFinished = 1; last; } # Pop the commands off the command array so we always have 6 p +rocesses running for ($i = $lStillRunning; $i < 3; $i++) { my $lCmd = pop (@lCmdArray); if ( $lCmd ) { $lpid = fork; die "Couldn't fork: $!" unless defined $lpid; if ( $lpid ) { # parent print "Forked off a child: $lCmd - $lpid\n"; $lRunningPidHash{$lpid} = 1; } else { # child close (STDIN); close (STDOUT); close (STDERR); exec ($lCmd) or die ("Error: could not exec comman +d : $lCmd : $!\n"); } } } # Sleep a few seconds and re scan sleep (5); } print "Parent exiting\n"; } # S_TestForking2

Replies are listed 'Best First'.
Re: Using perl to manage child processes - critique my code.
by locked_user sundialsvc4 (Abbot) on Dec 18, 2008 at 03:22 UTC

    Also within CPAN are “work queue” packages, which really are a much-better description of what you are really trying to do here.

    The bottom line is:   you've got about 140 units-of-work to do, and although you (of course) want to get them done “as soon as possible,” the ruling constraint is that you need to limit the number of units that are being attempted at any one time. Hence, you could reasonably express this objective as:   “up to n workers, burning their way through a queue with 140 entries on it.”

    So... model it that way.

    Have a pool of n processes or threads, each one of which “retrieves a unit-of-work from the queue, and then performs it.” They continue doing this until they find that the queue is empty; then they terminate. Because the number of processes/threads is limited (and adjustable...), you've got “knob” you're looking for.

    If you wanted to get more-elaborate about it, then of-course you could do so... defining several queues and placing “short” tasks in one and “long” tasks in another, for example, and dedicating a certain number of threads to each queue. The possibilities are endless, and (the bottom line is...) no matter how you choose to do it, predictable and controllable.

Re: Using perl to manage child processes - critique my code.
by BrowserUk (Patriarch) on Dec 18, 2008 at 06:57 UTC

    T'is easy with threads:

    Updated: Changed < to <= and made final loop wait for all kids to finish.

    #! perl -slw use strict; use threads qw[ yield ]; use threads::shared; our $MAXTIME ||= 7200; our $CMDS ||= 140; our $CONCURRENT ||= 6; my $running :shared = 0; my @cmds = map { my $time = int rand $MAXTIME; qq[ perl -e"sleep $time" ]; } 1 .. $CMDS; for my $cmd ( @cmds ) { sleep 1 while do{ lock $running; $running >= $CONCURRENT }; async{ my $n = do{ lock $running; ++$running; }; my $tid = threads->tid; print "$tid: starting '$cmd' ($n)"; system $cmd; print "$tid: ending"; { lock $running; --$running; } }->detach; yield; } sleep 1 while do{ lock $running; $running };

    Sample run:


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Using perl to manage child processes - critique my code.
by toolic (Bishop) on Dec 18, 2008 at 01:24 UTC
    Completely unrelated to your question...

    Here is a less verbose way to populate your @lCmdArray array:

    my @lCmdArray = map {"sleep $_"} qw(20 25 38 20 17 15 25 30 15);
Re: Using perl to manage child processes - critique my code.
by ig (Vicar) on Dec 18, 2008 at 00:22 UTC

    There are several modules that do this. Searching CPAN for fork manage processes will give you some leads.

      I was not sure what search terms to use so thank you.
Re: Using perl to manage child processes - critique my code.
by kyle (Abbot) on Dec 18, 2008 at 04:51 UTC

    I don't think there's anything wrong with polling with waitpid, but I wonder if it can be avoided somehow. Perhaps you could create children with "open my $child_fh, '-|'" and have the parent use IO::Select to wait on them. The child does a fork,exec and a blocking wait (or just system), and it writes a status line back to the parent before it exits.

    You may want to store a start time for each process and give it a time limit in case something hangs one day.

    Looking at what you have, it seems as if it could be simpler. I haven't tested this, but it's how I'd start...

Re: Using perl to manage child processes - critique my code.
by salva (Canon) on Dec 18, 2008 at 08:53 UTC
Re: Using perl to manage child processes - critique my code.
by Corion (Patriarch) on Dec 18, 2008 at 07:29 UTC

    If you can wrap the backup for a single run into a shell script, Dominus' runN script provides you with a very simple (and simplicistic) way of running n processes in parallel to work on a queue.

      The real backup command IS a ksh script written by our DBA's for their own use so that might be a perfect thing for me to use. Thanks.
Re: Using perl to manage child processes - critique my code.
by imrags (Monk) on Dec 18, 2008 at 07:34 UTC
    You can use
    use threads qw(yield); use Thread::Semaphore;
    threads to spawn processes...and semaphore to control the treads...
    Raghu