Skip to content

Instantly share code, notes, and snippets.

@coderofsalvation
Last active December 5, 2023 12:38
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save coderofsalvation/1102e56d3d4dcbb1e36f to your computer and use it in GitHub Desktop.
Save coderofsalvation/1102e56d3d4dcbb1e36f to your computer and use it in GitHub Desktop.
cronjob wrapper with locking support
# portable cronjob wrapper to ensure :
#
# * only one process at the time (prevents process-overlap, handy for unique cron workers)
# * reverse cron's email behaviour (only emails on error)
# * ultraportable: only relies on flock, not on debians 'start-stop-daemon' or centos 'daemon'
# * nicelevel to tame cpu usage
#
# usage: cronjoblock <application> [args]
# example: cronjoblock /home/foo/myscript &
# cronjoblock /home/foo/myscript & <--- 'myscript' will only run when the previous run is finished
#
# there's is no output unless there are errors. This is handy in respect to cron's MAILTO variable:
# the stdout/stderr output will be suppressed (so cron will not send mails) *unless* the given process
# has an exitcode > 0
#
[[ ! -n $1 ]] && { head -n11 $0 | sed 's/^#/ /g' | grep -v 'bin/bash'; exit; }
EXITCODE=0
STDOUT="/tmp/.cronjoblock.$( echo "$*" | tr A-Z a-z | sed -e 's/[^a-zA-Z0-9\-]/-/g')" # logfilename containing cronjob args
LOCKFILE="$STDOUT.lock"
NICELEVEL=10 # don't overload cpu (higher is slower)
exec nice -n $NICELEVEL /usr/bin/flock -w 0 "$LOCKFILE" "$@" | tee -a "$STDOUT" # remove -a to flush log on each run
EXITCODE=$$; [[ ! $EXITCODE == 0 ]] && cat "$STDOUT" # output stdout to cron *only* on error
exit $EXITCODE # let cron know (trigger email)
SHELL=/bin/bash
MAILTO="onlybugs@mycompany.com"
# -------------- min (0 - 59)
# | --------------- hour (0 - 23)
# | | ---------------- day of month (1 - 31)
# | | | ----------------- month (1 - 12)
# | | | | ------------------ day of week (0 - 6) (0 to 6 are Sunday to Saturday, or use names; 7 is Sunday, the same as 0)
# | | | | |
# | | | | |
# * * * * * command to execute
*/5 * * * * cd /my/path && ./cronjoblock foo bar # run every 5 mins if not running
*/5 * * * * cd /my/path && ./cronjoblock foo bar &> /dev/null # run every 5 mins if not running (no output)
@shafiquejamal
Copy link

Nice. Just wanted to point out a typo:

only reliest on flock

should be:

only relies on flock

@Asahi21
Copy link

Asahi21 commented Sep 26, 2022

Hi, thanks for making this script, I wanted to know if you could make a variation that only locks the cronjob to 1 process at the time (like the original), but still outputs everything to the console, so I can log everything into a file using the tee command, thanks!

@buhtz
Copy link

buhtz commented Dec 5, 2023

Thanks for that script.
Can you please add more comments to your code. It is always hard to understand bash. I am not sure what happens here.

EDIT: What do you think of that?

#!/bin/bash
# Credits: https://gist.github.com/coderofsalvation/1102e56d3d4dcbb1e36f
# License: Unknown
# Author: Coder of Salvation / Leon van Kammen (https://gist.github.com/coderofsalvation)
# portable cronjob wrapper to ensure :
#
#   * only one process at the time (prevents process-overlap, handy for unique cron workers)
#   * reverse cron's email behaviour (only emails on error)
#   * ultraportable: only reliest on flock, not on debians 'start-stop-daemon' or centos 'daemon'
#   * nicelevel to tame cpu usage
#
# usage: cronjoblock <application> [args]
# example: cronjoblock /home/foo/myscript &
#          cronjoblock /home/foo/myscript &   <--- 'myscript' will only run when the previous run is finished
#
# there's is no output unless there are errors. This is handy in respect to cron's MAILTO variable:
# the stdout/stderr output will be suppressed (so cron will not send mails) *unless* the given process
# has an exitcode > 0
#

# If no arguments given just print the comment lines of that script and exit.
[[ ! -n $1 ]] && { head -n11 $0 | sed 's/^#/ /g' | grep -v 'bin\/bash'; exit; }

# Default exit code "success"
EXITCODE=0

# Create a sanitized version of all givin arguments
SANITIZED_ARGS=$( echo "$*" | tr A-Z a-z | sed -e 's/[^a-zA-Z0-9\-]/-/g')

STDOUT="/tmp/.cronjoblock.$SANITIZED_ARGS"

LOCKFILE="$STDOUT.lock"

# Execute
NICELEVEL=10
exec nice -n $NICELEVEL /usr/bin/flock --timeout 0 "$LOCKFILE" "$@" > "$STDOUT"

# output stdout to cron *only* on error
EXITCODE=$$; [[ ! $EXITCODE == 0 ]] && cat "$STDOUT"                       

# let cron know (trigger email)
exit $EXITCODE

@coderofsalvation
Copy link
Author

coderofsalvation commented Dec 5, 2023

Hi, thanks for making this script, I wanted to know if you could make a variation that only locks the cronjob to 1 process at the time (like the original), but still outputs everything to the console, so I can log everything into a file using the tee command, thanks!

done, I've added tee, so it'll always log to a file + stdout (and you can always tee afterwards) :D
(or add &>/dev/null to supress stdout+stderr, see example)

Thanks for that script. Can you please add more comments to your code. It is always hard to understand bash. I am not sure what happens here.

done, I've annotated it.

Keep on cronning! ❤

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment