Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I need to start a cronjob every day, but an hour later each day. What I have so far works for the most part, except for 1 day of the year:

0 0 * * * sleep $((3600 * (10#$(date +\%j) \% 24))) && /usr/local/bin/myprog

When the day of year is 365 the job will start at 5:00, but the next day (not counting a leap year) will have a day of year as 1, so the job will start at 1:00. How can I get rid of this corner case?

share|improve this question
1  
Any reason to not just start it every 25 hours? –  HalosGhost Aug 10 at 2:52
7  
And how exactly would you do that? */25 in the hour position will not solve it. –  birch Aug 10 at 3:52
    
@HalosGhost Thanks for your suggestion! I wrote a simple implementation based on at. –  Giulio Muscarello Aug 10 at 13:19

3 Answers 3

up vote 21 down vote accepted

My preferred solution would be to start the job every hour but have the script itself check whether it's time to run or not and exit without doing anything 24 times out of 25.

crontab:

0 * * * *    /usr/local/bin/myprog

at the top of myprog:

[ 0 -eq $(( $(date +%s) / 3600 % 25 )) ] || exit 0

If you don't want to make any changes to the script itself, you can also put the "time to run" check in the crontab entry but it makes for a long unsightly line:

0 * * * *    [ 0 -eq $(( $(date +\%s) / 3600 \% 25 )) ] && /usr/local/bin/myprog
share|improve this answer
1  
'%'s need to be escaped with a backslash in the crontab. –  birch Aug 10 at 4:22
    
@birch I never knew that! I guess I never before tried to include a % in a crontab. Thanks for the correction, I have edited the answer. –  Celada Aug 10 at 5:51
1  
This looks good to me. I have no idea what the OP wants to do with respect to Daylight Savings Time (spring ahead, fall behind), but the OP should make adjustments accordingly. –  emory Aug 10 at 14:04
    
I'd be slightly worried about rounding in the division. If for some reason the time that date got back from the kernel was 1ms before the time you expected the script to run, the check would give an incorrect result. –  kasperd Aug 10 at 16:44
1  
@kasperd I don't know, I suppose you might be right about different CPUs reporting different time. As for ntpd, it does try hard to only slew the clock, not jump it, specifically to avoid this kind of problem, but you're right, it (or ntpdate) sometimes can jump the time backwards. As for cron miscalculating its sleep delay, I'm pretty sure that would be considered a bug! Still, point taken, and a workaround would be to schedule the job 30 minutes past the hour which is less likely to cause the problem... Or add ±1800 in the arithmetic expression before taking the reamainder mod 3600. –  Celada Aug 11 at 6:06

If your system has systemd, you can use timers events for this. Just define a new service, which should contain the command/task you want to execute, and then create a timer event with the OnUnitActiveSec option:

[Unit]
Description=daily + 1 hour task

[Timer]
OnUnitActiveSec=25h # run 25 hours after service was last started
AccuracySec=10min

[Install]
WantedBy=timers.target

Use the same name for the files, except that instead of .service you use .timer.

Synthesizing:

  1. Create a file called job.service in the /etc/systemd/system/ directory.
  2. Fill it with the necessary information. You can verify the configuration, using systemctl status job.service.
  3. Create a file called job.timer in /etc/systemd/system/.
  4. Fill it with the required information:

    [Unit]
    Description=daily + 1 hour task
    
    [Timer]
    OnUnitActiveSec=25h # run 25 hours after service was last started
    AccuracySec=10min
    
    [Install]
    WantedBy=timers.target
    
  5. Verify the timer using systemctl list-timers
  6. Done.
share|improve this answer
    
If I was going to stray from the simplicity of cron, I'd use launchd, which would require less work than your example for systemd. –  birch Aug 11 at 0:03

If you don't mind using something other than cronjobs, I would suggest the lesser-known utility at . Simply write a wrapper script that both schedules itself to be run in 25 hours, and then calls your program. This seems to be the cleanest solution.
For example, you could write this in ~/script.sh:

echo "bash ~/script.sh" | at now + 25 hours
/usr/bin/yourprogram

And then simply run bash ~/script.sh once.

Thanks to @HalosGhost for the idea of scheduling the job once in 25 hours.

share|improve this answer
2  
There are 2 problems with using at for this purpose: (1) if the job fails to execute correctly even once, it also probably fails to reschedule itself and then not just the next execution but all future executions are effectively cancelled until a human notices, and (2) since the job takes some non-zero time to run, using the naive now + 25 hours means it will run a few seconds (or more) later each time, anf this lag will build up over time, making it eventualy run at completely the wrong time. –  Celada Aug 10 at 13:21
2  
You are correct about #1; I'm not so sure about #2. Although I don't have any data, I don't think the delay between the clock changing and the at job being triggered and subsequently rescheduled is large enough to be noticeable - especially considering that the resolution of at is limited to minutes and starts all jobs at [time]:00. –  Giulio Muscarello Aug 10 at 13:33
1  
@Celada: Scheduling the next at job first thing in the at script avoids those problems. Notwithstanding, if an error occurs then the whole chain is broken, which may or may not be desired: if the use case is "only run this when it works", not restarting is a great feature. But if the use case is "always run, even if the last failed" then at is not the right tool. –  bishop Aug 10 at 15:00

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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