Production Cron Tasks

Useful if you run scheduled tasks in production and want to leverage cron, via Orchestrate.io. The Gist shell script for cron_helper.sh is below.

#!/bin/bash

usage() {
  cat << EOF
Usage: $0 [OPTION]... COMMAND
Execute the given command in a way that works safely with cron. This should
typically be used inside of a cron job definition like so:
* * * * * $(which "$0") [OPTION]... COMMAND

Arguments:
  -c           Ensure that the job never exits non zero.
  -e EXITFILE  The exit file that will have a time stamp written to it if the
               job succeeds.
  -h           This help message.
  -i           Nice the job so that it doesn't over consume resources.
  -k LOCKFILE  The lock file to hold while the job is running.
  -l LOGFILE   The log file to write stdout and stderr too.
  -n NAME      The name of this cron job. Giving the cron job a name causes
               log lines to include it in the output, and automatically sets
               EXITFILE, LOCKFILE, and LOGFILE to some simple default values.
               
               EXITFILE will be set to /var/run/\$USER/NAME.exit if
               /var/run/\$USER exists, otherwise it will write to
               /var/tmp/\$USER/NAME.exit

               LOCKFILE will be set to /var/run/\$USER/NAME.lock if
               /var/run\$USER exists, otherwise it will write to
               /var/tmp/\$USER/NAME.lock

               LOGFILE will be set to /var/log/\$USER/NAME.log if
               /var/log/\$USER exists, otherwise it will write to
               /var/tmp/$USER/NAME.log
  -s           If set the script will sleep a random time between 0 and 60
               seconds before starting the command.
  -t           If set then the output from the script will be automatically
               timestamped when being written to the log file.

COMMAND is the command that should be executed.
EOF
}

SAFE_EXIT=0
EXIT_FILE=""
NICE=0
LOCK_FILE=""
LOG_FILE=""
NAME=""
SLEEP=0
TIMESTAMP=0

# This is a cach of all the groups the user is a member of. We use it with
# canwrite() later.
GROUPS=""

# Checks to see if the shell array ($2) contains $1.
contains() {
  local i
  for i in "${@:2}"; do
    [[ "$i" == "$1" ]] && return 0
  done
  return 1
}

# Checks to see if the current user can write to the given file. This will check the
# file permissions first, and if the file does not exist then it will check the
# directory permissions.
canwrite() {
  local perm
  local owner
  local group

  if [ -f "$1" ] ; then
    read perm owner group < <(stat -Lc "%a %G %U" "$1" 2> /dev/null)
  else
    read perm owner group < <(stat -Lc "%a %G %U" "$(dirname $1)" 2> /dev/null)
  fi
  if [ $? -ne 0 ] ; then
    return 1
  fi
 
  if [ "$owner" == "$USER" ] ; then
    if [ $((perm&0200)) -ne 0 ] ; then
      return 1
    fi
    return 0
  elif contains "$group" "${GROUPS[@]}" ; then
    if [ $((perm&0020)) -ne 0 ] ; then
      return 1
    fi
    return 0
  else
    if [ $((perm&0002)) -ne 0 ] ; then
      return 1
    fi
    return 0
  fi
}

name() {
  NAME="$1"

  # Exit file
  if [ -z "$EXIT_FILE" ] ; then
    if canwrite "/var/run/${USER}/${NAME}.exit" ; then
      EXIT_FILE="/var/run/${USER}/${NAME}.exit"
    else
      mkdir -p "/var/tmp/${USER}"
      EXIT_FILE="/var/tmp/${USER}/${NAME}.exit"
    fi
  fi
  
  # Lock File
  if [ -z "$LOCK_FILE" ] ; then
    if canwrite "/var/run/${USER}/${NAME}.lock" ; then
      LOCK_FILE="/var/run/${USER}/${NAME}.lock"
    else
      mkdir -p "/var/tmp/${USER}"
      LOCK_FILE="/var/tmp/${USER}/${NAME}.lock"
    fi
  fi
  
  # Log File
  if [ -z "$LOG_FILE" ] ; then
      if canwrite "/var/run/${USER}/${NAME}.lock" ; then
      LOG_FILE="/var/log/${USER}/${NAME}.log"
    else
      mkdir -p "/var/tmp/${USER}"
      LOG_FILE="/var/tmp/${USER}/${NAME}.log"
    fi
  fi
}

while getopts "ce:hik:l:n:st" arg; do
  case $arg in
    c) SAFE_EXIT=1 ;;
    e) EXIT_FILE="$OPTARG" ;;
    h) usage ; exit ;;
    i) NICE=1 ;;
    k) LOCK_FILE="$OPTARG" ;;
    l) LOG_FILE="$OPTARG" ;;
    n) name "$OPTARG" ;;
    s) SLEEP=1 ;;
    t) TIMESTAMP=1 ;;
  esac
done
shift $((OPTIND-1))


# This function will write a log line to the output. This can either be called by the
# timestamper, or internally.
log() {
  if [ $TIMESTAMP -eq 1 ] ; then
    echo "$(date) $*"
  else
    echo "$*"
  fi
}

# Setup logging first so we can report to the user what is happening.
if [ -n "$LOG_FILE" ] ; then
  exec > "$LOG_FILE" 2>&1
fi

# Attempt to lock the lock file if it is set.
if [ -n "$LOCK_FILE" ] ; then
  exec 200>> "$LOCK_FILE"
  flock -n -x 200
  if [ $? -ne 0 ] ; then
    log "Unable to obtain a lock, is a job already running?"
    if [ $SAFE_EXIT -eq 1 ] ; then
      exit 0
    else
      exit 1
    fi
  fi
fi

# Sleep a random amount between 0 and 60 seconds.
if [ $SLEEP -eq 1 ] ; then
  sleep $((RANDOM%60))
fi

# Get the pre-command in case we need to nice the job.
PRECOMMAND=""
if [ $NICE -eq 1 ] ; then
  PRECOMMAND="nice"
fi

# Run the command.
if [ $TIMESTAMP -eq 1 ] ; then
  $PRECOMMAND "$@" 2>&1 | while read line ; do log "$line" ; done
else
  $PRECOMMAND "$@" 2>&1
fi
EXIT_STATUS=$?

# Process the exit file.
if [ -n "$EXIT_FILE" -a $EXIT_STATUS -eq 0 ] ; then
  date > "$EXIT_FILE"
fi

# Exit status
if [ $SAFE_EXIT -eq 1 ] ; then
  exit 0
else
  exit $EXIT_STATUS
fi

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *