Using tar on crontab causes it to use multiple processes which consume all cpu

The following line is the only line in crontab:

* 13 * * * sh /Users/gentaliaru/Dropbox/work/scripts/backup.sh >/tmp/stdout.log 2>/tmp/stderr.log

And the script contents are

rm -rf /Volumes/X5/backup/git.tar.tz
tar -cvzf /Volumes/X5/tmp/git.tar.tz /Users/gentaliaru/ws/git/
terminal-notifier -message "git Backup is complete" -title "Backup notifier"

As soon as cron starts after10-20 seconds will initiate 10+ bsdtar processes which will never end. But if I start the same command manually then it finises normally in 5 minutes or so.

I had similar problems before the macOS Monterey upgrade.


Solution 1:

* 13 * * * sh /Users/gentaliaru/Dropbox/work/scripts/backup.sh >/tmp/stdout.log 2>/tmp/stderr.log

This cron job will start at 13:00 UTC and run every minute. According to your note "it takes usually around 5 minutes" cron is starting every minute a new job before the last one has finished. In fact, its a kind of loop.

The first * has to be replaced by 0 in order to start the job every day at 13:00 only once:

0 13 * * * sh /Users/gentaliaru/Dropbox/work/scripts/backup.sh >/tmp/stdout.log 2>/tmp/stderr.log >/dev/null 2>&1

Aside the cron issue the following hints are worth to be considered. It shows how to improve the robustness and notification (even for failed jobs) for your cron job.

Always check resources (files, path, and so on) first

One common pitfall with cron are missing resources (files or paths) during the execution of a job.

Therefore, a better approach is to always specify full paths and perform some checks if files and directories exist. The whole script should look like this in order to handle existing/non-exsting paths and files properly:

#!/bin/sh

mntPoint="$(/usr/sbin/diskutil info /Volumes/X5/ | /usr/bin/grep 'Mount Point' |  /usr/bin/tr -s ' ' | /usr/bin/cut -d ' ' -f 4)"
if [ $mntPoint != "/Volumes/X5" ]
  then
    /usr/bin/osascript -e 'display notification "/Volumes/X5 has not been mounted" with title "Cron job aborted"'
    exit;
fi

[[ -f '/Volumes/X5/backup/git.tar.tz' ]] && /bin/rm -rf '/Volumes/X5/backup/git.tar.tz'
[[ ! -d '/Volumes/X5/backup' ]] && mkdir '/Volumes/X5/backup'

if /usr/bin/tar -czf /Volumes/X5/backup/git.tar.tz /Users/gentaliaru/ws/git/ 2> /Volumes/X5/backup/.tar.log
  then
    /usr/bin/osascript -e "display notification \"Backup written to /Volumes/X5/backup/git.tar.tz\" with title \"Cron job successfully completed\""
else
    # do not use /usr/bin/read as the internal read command is required!
    read errMsg < /Volumes/X5/backup/.tar.log
    /usr/bin/osascript -e "display notification \"$errMsg\" with title \"Cron job failed\""
    # /path/to/terminal-notifier -message "git Backup is complete" -title "Backup notifier"
fi

Explanation

First of all I recommend to convert the "plain" ascii file into a real shell script by adding a shebang line at the top of the file.

#!/bin/sh

Check if the backup drive is properly mounted

Lines 3-8 ensures the backup drive is mounted; otherwise throw a notification. A check with just [[ -d /Volumes/X5 ]] is not reliable enough as I faced sometimes (very rarely, but it happened) the mount path exists but without attached drive. Checking it with diskutil circumvent this occasional oddities.

Then, rm -rf /Volumes/X5/backup/git.tar.tz without checking the existance of the file is another pitfall. Better check the existence first:

[[ -f '/Volumes/X5/backup/git.tar.tz' ]] && /bin/rm -rf '/Volumes/X5/backup/git.tar.tz'

Furthermore, your script shows two different paths. The remove command rm in line 1 refers to /Volumes/X5/backup but the tar command in line 2 refers to /Volumes/X5/tmp/git.tar.tz. Is this really what you want?

Invoking tar to write an archive to a non-existing path/subdirectory throws the following error (at least here on HighSierra 10.13.6):

tar: Failed to open '/Volumes/X5/backup/git.tar.tz'

Therefore, check first the existence of tar's target directory and create it if it doesn't exist:

[[ ! -d '/Volumes/X5/backup' ]] && mkdir '/Volumes/X5/backup'

Line 13 does finally the backup and throws - on success - the notification (line 15). Otherwise a notification about the failure is dropped (line 19).

Use of macOS internal resources for notification

My example uses internal macOS resources without the necessity of third party tools like terminal-notifier. Its done by an AppleScript one-liner which is invoked by osascript.

If you would like to keep the solution with terminal-notifier just replace the lines (15 and 19 with osascript) with the following:

/path/to/terminal-notifier -message "Backup written to /Volumes/X5/backup/git.tar.tz" -title "Cron job successfully completed" # Line 15
/path/to/terminal-notifier -message "${errMsg}" -title "Cron job failed" # Line 19

It is also a good practice to specify the full path for terminal-notifier. In case its installed by Homebrew use something like /usr/local/bin/terminal-notifier.

Then make it executable:

chmod u+x /Users/gentaliaru/Dropbox/work/scripts/backup.sh

Sidenode for Homebrew

Paths differ between the platforms (check the documentation here):

Its /usr/local on macOS Intel and /opt/homebrew on Apple Silicon