23

I need to run a script every 64 hours. I couldn't find the answer with cron. Is it possible with it, or should I use a loop in a shell script?

6

5 Answers 5

47

Just run every hour and check that the number of hours since some arbitrary instant (for instance, instant 0 of the Unix epoch time) is a multiple of 64:

0 * * * * t=$(date +\%s); [ "$(( (t / 3600) \% 64 ))" -eq 0 ] && your-command
11
  • 16
    could check every 8 hours too (highest common factor of 24 and 64)
    – somebody
    Sep 19, 2021 at 22:22
  • 8
    There is also the issue that date %s gives seconds in UTC and cron runs on local time. At DST changes a whole hour is erased or added to the local time. That should not be a problem here as the script is called "every hour", but worth noting for longer intervals.
    – user232326
    Sep 20, 2021 at 3:50
  • 7
    @somebody, I did consider it doing it every 8 hours, but that wouldn't work in locales that implement DST. For instance in mainland UK and currently, that would have to be 0 0,8,16 * * * in winter and 0 1,9,17 * * * in summer. Sep 20, 2021 at 6:16
  • 2
    @Griffin, by crontab(5) man page has in the LIMITATIONS section: Even if a user specifies the TZ environment variable in his crontab this will affect only the commands executed in the crontab, not the execution of the crontab tasks themselves.. date +%s itself is independent on timezone. Sep 20, 2021 at 14:16
  • 1
    @Griffin A 4 3 * * * will be executed at 03:04:00 local time, even with a TZ set for the crontab. Only the actual commands, like date here: 4 3 * * * date, will be affected by such TZ environment variable.
    – user232326
    Sep 20, 2021 at 14:32
27

I suggest perhaps using a crontab "front end" like crontab.guru for figuring out crontab if you're a beginner.

However, as in your case, the hour setting only allows for values of 0 to 23, so you can't use crontab here.

Instead, I'd suggest using at. In your case, I'd probably use something like:

at now + 64 hours

and then enter your command or

echo "<your command>" | at now + 64 hours

at the beginning of your script, etc. Basically, you'll be scheduling running the command right when the command has been invoked the last time. Also, if you don't want a time delta, rather the exact time, I suggest doing a bit of time arithmetic, and then use an exact time with at to have the command run.

I highly suggest reading the man page of at, as it is fairly comprehensive.

5
  • 17
    I'd suggest to add the new at-scheduling at the beginning of the script, otherwise a failure during script execution will also stop scheduling the next run.
    – FelixJN
    Sep 19, 2021 at 10:21
  • 1
    crontab.guru is nice and pretty, but isn't worth anything for any of the special cases that cron can't directly do, but that crop up from time to time. Like intervals that don't divide cleanly into days or months, or running something on the first/last $weekday of a month.
    – ilkkachu
    Sep 19, 2021 at 10:56
  • @FelixJN That's actually a good idea, I've edited my answer using your suggestion.
    – polemon
    Sep 19, 2021 at 18:45
  • @FelixJN: That also reduces the amount of timing error this will accumulate, but still not to zero. A fraction of a second every 64 hours is trivial for most use-cases, but if it matters you could maybe round down to the start of the hour somehow, maybe parsing data output. (More timing error can happen if the system is sometimes swap thrashing or mid reboot, or worse hibernated, when the at time comes. Big delay in commands running.) Sep 20, 2021 at 14:45
  • On my *nix systems. "AT" only runs if the user is logged in to the machine at the time.
    – Scottie H
    Sep 20, 2021 at 17:20
26

cron is made to run things on date or clock events, i.e. every 1st of a month or week or every third hour of a day, respectively.

For running things in time intervals that cannot be simply expressed by arithmetics on clock time or dates, I'd suggest using a systemd timer - which of course works only if you have systemd running on your respective device.

You will need two files, the service file to run the script and the timer file for managing the events.

$cat /etc/systemd/system/myscript.service

[Unit]
Description=this service runs my script

[Service]
ExecStart=/full/path/to/myscript.sh

#[Install]
#WantedBy=multi-user.target

This service simply executes the script without checking if it succeeded or anything. Optionally enable the service via systemctl enable myscript.service which means it will be executed once during boot time - I commented this out, because the timer will take care of boot events. You may manually run your script by systemctl start myscript.service.


$cat /etc/systemd/system/myscript.timer

[Unit]
Description=this timer runs myscript.service every 64h

[Timer]
OnBootSec=120
OnUnitActiveSec=64h


[Install]
WantedBy=timers.target

This is the timer to run the myscript.service every 64h since the last activation of the service as well as two minutes after boot time (to ensure that the system is in the right state for the service to run). Note that the timer will not remember the 64h if you reboot/power off. Enable and start the timer accordingly:

systemctl enable myscript.timer
systemctl start myscript.timer

Since the timer only starts after a boot or since the last unit activation, you will have to run the service once to ensure the timer runs:

systemctl start myscript.service

Check the status of the timers by

systemctl list-timers
4
  • 1
    Also, consider OnActiveSec= instead of OnUnitActiveSec= if you don't want the timer to be dependent on when the unit was last run, e.g. if you want the timer to run every 64h even if you manually run the unit at other times.
    – Bob
    Sep 21, 2021 at 9:31
  • 2
    Also, using a fixed 120s timer to try to "ensure that the system is in the right state" is a bad idea. Use After= with Wants= or Requires= to more explicitly specify dependencies so that you know when they are available, rather than guessing 120s is enough in all situations.
    – Bob
    Sep 21, 2021 at 9:33
  • 1
    @Bob Agreed regarding After= etc.. Be careful: Persistent=true only applies to events scheduled with OnCalendar, a missed 64h-interval via On(Unit)ActiveSec will not be accounted for. See official documentation
    – FelixJN
    Sep 21, 2021 at 10:18
  • Ah, I had missed that line. Good point! I'll delete my other comment to avoid confusion. That does restrict options, then, since OnCalendar= runs into the same problems cron does -- leaving your post-boot catchup the best option if it must run on boot.
    – Bob
    Sep 21, 2021 at 14:08
6

64 hours is 2 and 2/3 days, so 3 * 64 hours = 192 hours = 8 days. So you could almost do

0 0  1-25/8 * * foo
0 16 3-27/8 * * foo
0 8  6-30/8 * * foo

except that you get a short pattern at the end of the month (if the month has 30 days, then there will be only 16 hours between the run at 08:00 on the 30th and the one at 00:00 on the 1st; if the month has 31 days, it will be 40 hours; if the month has 28 days, it will be 32 hours between the run at 16:00 on the 27th and the one at 00:00 on the 1st; if the month has 29 days, it will be 56), and also things will be off by 1 hour whenever your locality goes on or comes off of summer time (assuming it does).

Doesn't quite meet your specs (and I endorse any of the other answers that do), but it is an interesting exercise in making do.

0

Surprised no one has mentioned either of:-

Anacron

It's supplied as standard on most distro's.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.