Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Running a cron every 30 seconds

Ok so I have a cron that I need to run every 30 seconds.

Here is what I have:

*/30 * * * * /bin/bash -l -c 'cd /srv/last_song/releases/20120308133159 && script/rails runner -e production '\''Song.insert_latest'\''' 

It runs, but is this running every 30 minutes or 30 seconds?

Also, I have been reading that cron might not be the best tool to use if I run it that often. Is there another better tool that I can use or install on Ubuntu 11.04 that will be a better option? Is there a way to fix the above cron?

like image 236
Matt Elhotiby Avatar asked Mar 08 '12 14:03

Matt Elhotiby


2 Answers

You have */30 in the minutes specifier - that means every minute but with a step of 30 (in other words, every half hour). Since cron does not go down to sub-minute resolutions, you will need to find another way.

One possibility, though it's a bit of a kludge(a), is to have two jobs, one offset by 30 seconds:

# Need these to run on 30-sec boundaries, keep commands in sync. * * * * *              /path/to/executable param1 param2 * * * * * ( sleep 30 ; /path/to/executable param1 param2 ) 

You'll see I've added comments and formatted to ensure it's easy to keep them synchronised.

Both cron jobs actually run every minute but the latter one will wait half a minute before executing the "meat" of the job, /path/to/executable.

For other (non-cron-based) options, see the other answers here, particularly the ones mentioning fcron and systemd. These are probably preferable assuming your system has the ability to use them (such as installing fcron or having a distro with systemd in it).


If you don't want to use the kludgy solution, you can use a loop-based solution with a small modification. You'll still have to manage keeping your process running in some form but, once that's sorted, the following script should work:

#!/bin/env bash  # Debug code to start on minute boundary and to # gradually increase maximum payload duration to # see what happens when the payload exceeds 30 seconds.  ((maxtime = 20)) while [[ "$(date +%S)" != "00" ]]; do true; done  while true; do     # Start a background timer BEFORE the payload runs.      sleep 30 &      # Execute the payload, some random duration up to the limit.     # Extra blank line if excess payload.      ((delay = RANDOM % maxtime + 1))     ((maxtime += 1))     echo "$(date) Sleeping for ${delay} seconds (max ${maxtime})."     [[ ${delay} -gt 30 ]] && echo     sleep ${delay}      # Wait for timer to finish before next cycle.      wait done 

The trick is to use a sleep 30 but to start it in the background before your payload runs. Then, after the payload is finished, just wait for the background sleep to finish.

If the payload takes n seconds (where n <= 30), the wait after the payload will then be 30 - n seconds. If it takes more than 30 seconds, then the next cycle will be delayed until the payload is finished, but no longer.

You'll see that I have debug code in there to start on a one-minute boundary to make the output initially easier to follow. I also gradually increase the maximum payload time so you'll eventually see the payload exceed the 30-second cycle time (an extra blank line is output so the effect is obvious).

A sample run follows (where cycles normally start 30 seconds after the previous cycle):

Tue May 26 20:56:00 AWST 2020 Sleeping for 9 seconds (max 21). Tue May 26 20:56:30 AWST 2020 Sleeping for 19 seconds (max 22). Tue May 26 20:57:00 AWST 2020 Sleeping for 9 seconds (max 23). Tue May 26 20:57:30 AWST 2020 Sleeping for 7 seconds (max 24). Tue May 26 20:58:00 AWST 2020 Sleeping for 2 seconds (max 25). Tue May 26 20:58:30 AWST 2020 Sleeping for 8 seconds (max 26). Tue May 26 20:59:00 AWST 2020 Sleeping for 20 seconds (max 27). Tue May 26 20:59:30 AWST 2020 Sleeping for 25 seconds (max 28). Tue May 26 21:00:00 AWST 2020 Sleeping for 5 seconds (max 29). Tue May 26 21:00:30 AWST 2020 Sleeping for 6 seconds (max 30). Tue May 26 21:01:00 AWST 2020 Sleeping for 27 seconds (max 31). Tue May 26 21:01:30 AWST 2020 Sleeping for 25 seconds (max 32). Tue May 26 21:02:00 AWST 2020 Sleeping for 15 seconds (max 33). Tue May 26 21:02:30 AWST 2020 Sleeping for 10 seconds (max 34). Tue May 26 21:03:00 AWST 2020 Sleeping for 5 seconds (max 35). Tue May 26 21:03:30 AWST 2020 Sleeping for 35 seconds (max 36).  Tue May 26 21:04:05 AWST 2020 Sleeping for 2 seconds (max 37). Tue May 26 21:04:35 AWST 2020 Sleeping for 20 seconds (max 38). Tue May 26 21:05:05 AWST 2020 Sleeping for 22 seconds (max 39). Tue May 26 21:05:35 AWST 2020 Sleeping for 18 seconds (max 40). Tue May 26 21:06:05 AWST 2020 Sleeping for 33 seconds (max 41).  Tue May 26 21:06:38 AWST 2020 Sleeping for 31 seconds (max 42).  Tue May 26 21:07:09 AWST 2020 Sleeping for 6 seconds (max 43). 

If you want to avoid the kludgy solution, this is probably better. You'll still need a cron job (or equivalent) to periodically detect if this script is running and, if not, start it. But the script itself then handles the timing.


(a) Some of my workmates would say that kludges are my specialty :-)

like image 109
paxdiablo Avatar answered Sep 18 '22 20:09

paxdiablo


If you are running a recent Linux OS with SystemD, you can use the SystemD Timer unit to run your script at any granularity level you wish (theoretically down to nanoseconds), and - if you wish - much more flexible launching rules than Cron ever allowed. No sleep kludges required

It takes a bit more to set up than a single line in a cron file, but if you need anything better than "Every minute", it is well worth the effort.

The SystemD timer model is basically this: timers are units that start service units when a timer elapses.

So for every script/command that you want to schedule, you must have a service unit and then an additional timer unit. A single timer unit can include multiple schedules, so you normally wouldn't need more than one timer and one service.

Here is a simple example that logs "Hello World" every 10 seconds:

/etc/systemd/system/helloworld.service:

[Unit] Description=Say Hello [Service] ExecStart=/usr/bin/logger -i Hello World 

/etc/systemd/system/helloworld.timer:

[Unit] Description=Say Hello every 10 seconds [Timer] OnBootSec=10 OnUnitActiveSec=10 AccuracySec=1ms [Install] WantedBy=timers.target 

After setting up these units (in /etc/systemd/system, as described above, for a system-wide setting, or at ~/.config/systemd/user for a user-specific setup), you need to enable the timer (not the service though) by running systemctl enable --now helloworld.timer (the --now flag also starts the timer immediately, otherwise, it will only start after the next boot, or user login).

The [Timer] section fields used here are as follows:

  • OnBootSec - start the service this many seconds after each boot.
  • OnUnitActiveSec - start the service this many seconds after the last time the service was started. This is what causes the timer to repeat itself and behave like a cron job.
  • AccuracySec - sets the accuracy of the timer. Timers are only as accurate as this field sets, and the default is 1 minute (emulates cron). The main reason to not demand the best accuracy is to improve power consumption - if SystemD can schedule the next run to coincide with other events, it needs to wake the CPU less often. The 1ms in the example above is not ideal - I usually set accuracy to 1 (1 second) in my sub-minute scheduled jobs, but that would mean that if you look at the log showing the "Hello World" messages, you'd see that it is often late by 1 second. If you're OK with that, I suggest setting the accuracy to 1 second or more.

As you may have noticed, this timer doesn't mimic Cron all that well - in the sense that the command doesn't start at the beginning of every wall clock period (i.e. it doesn't start on the 10th second on the clock, then the 20th and so on). Instead is just happens when the timer ellapses. If the system booted at 12:05:37, then the next time the command runs will be at 12:05:47, then at 12:05:57, etc. If you are interested in actual wall clock accuracy, then you may want to replace the OnBootSec and OnUnitActiveSec fields and instead set an OnCalendar rule with the schedule that you want (which as far as I understand can't be faster than 1 second, using the calendar format). The above example can also be written as:

OnCalendar=*-*-* *:*:00,10,20,30,40,50 

Last note: as you probably guessed, the helloworld.timer unit starts the helloworld.service unit because they have the same name (minus the unit type suffix). This is the default, but you can override that by setting the Unit field for the [Timer] section.

More gory details can be found at:

  • Arch Linux Wiki page about SystemD timers which gives a very good overview of the topic, with examples.
  • man systemd.timer
  • man systemd.time
  • man systemd.service
  • man systemd.exec
like image 20
Guss Avatar answered Sep 17 '22 20:09

Guss