18 May 2007

Semi-Reliable Periodic Commands

Impromptu

cron is great. Using cron, you can schedule commands to be run at regular intervals or at specific times. One of the major drawbacks of cron is that it doesn't generally keep state regarding job's status. If a job is scheduled to run at midnight, but the system is powered down at midnight, the command will never be run.

There are a few solutions out there to take care of this shortcomings, but many are designed for system-wide use only. What if you don't want to intermingle your personal jobs with the system-wide ones? Here is a fairly simple script that works as a wrapper, providing a little insurance to help make sure jobs get run.

#!/bin/bash

export P_ENV=~/.profile
export P_TRACK=~/.p_track

if [ -e "$P_ENV" ]; then
. $P_ENV
fi

case "$1" in
h)
export P_NOW=$(date +%Y-%m-%d-%H)
;;
d)
export P_NOW=$(date +%Y-%m-%d)
;;
w)
export P_NOW=$(date +%Y-%U)
;;
m)
export P_NOW=$(date +%Y-%m)
;;
*)
echo ERROR: Term not specified. Must be one of h, d, w, m .
exit 1
esac

if [ -z "$2" ]; then
echo ERROR: Command to run not specified.
exit 2
fi

export P_TAG=$(echo $2 | sed -e 's/[^A-Za-z0-9]/-/g')
export P_FILE=$P_TRACK/$P_TAG-$P_NOW

if [ -e "$P_FILE" ]; then
exit 0
else
rm -f $P_TRACK/$P_TAG*
echo Executing $2 at $(date)
$2 $3 $4 $5 $6 $7 $8 $9
echo Done

if [ "$?" -eq "0" ]; then
touch $P_FILE
fi
fi


The script has 4 operating modes. It can help ensure command are run hourly, daily, weekly, or monthly. It uses empty files in a configurable directory (~/.p_track by default) to keep tabs on the last time the command was run. Entries in the tracking directory are only created if the command run returns exit status 0 (no error.)

To use this wrapper, place it and a period (h for hourly, d for daily, and so on) in front of commands in your crontab like so:

*/20 * * * * ~/bin/periodic h ~/bin/command param1 param2


The above will check to see if the command needs to be run every 20 minutes, but will only execute it every hour. The advantage of wrapping individual commands instead using periodic command directories (check out the run-parts command) is less administrative overhead.

Note: The "real" periodic command is generally used to run system-wide periodic commands, often stored in /etc/periodic. UNIX-y systems tend to use other mechanisms and directories to do something similar.

Update 1: Changed the script from a group of if/elif to case (thanks for pointing that out Dave) and added a quick import of .profile or another file to set up environment.

4 comments:

David A. Harding said...

For many modern crons, I find it sufficent to run important jobs with the @reboot option. For example:

## Normal run
0 * * * * /bin/command param1 param2
## In case computer was off
@reboot /bin/command param1 param2

This way, if my command failed to run while the computer was off, it will be run as soon as the computer is restarted. That might mean the command will be run more than once per hour, but its easy to see how long the computer was off with the following command:

last -x shutdown

A small script can check the final field outputed by the command above and see if its longer than N amount of time, and if so, return true, but if not, return false.

Then:

@reboot off-time 1h && /bin/command ...

-Dave

Christopher Ingram said...

That is probably good for most user, but I tend to go from 2 days to 2 weeks without a reboot. Thankfully, there is no shortage of options in this area of the sysadmin's universe.

David A. Harding said...

True. Shell scripts rock.

Do you know about the bourne shell's case builtin? I think you could rewrite the long if,elif stanza like the following:

case "$1" in
h)
export P_NOW=$(date +%Y-%m-%d-%h)
;;
d)
...
;;
....
*)
echo "Error: command to run not specified. Dying"
exit 2
esac

(Blogger kills the whitespace I tried to insert. Sorry)

-Dave

Christopher Ingram said...

Yes, although 95% of the time I never use case in shell scripts? Why? No idea.