1. Introduction
Linux systems offer various ways to schedule tasks. Typically, jobs are executed at a time point specified in a deterministic way. However, sometimes, we need to add a random factor to the schedule. Hence, we can, for example, avoid exhausting the system’s resources when a number of processes start at exactly the same hour.
In this tutorial, we’ll learn how to dispatch tasks at random time intervals.
2. Prerequisites
Our goal is to start tasks at random times, during some predefined time window. Let’s assume that it’ll be between 9 AM and 6 PM.
First, let’s prepare a simple script rscript.sh to be executed randomly. It prints the process’ PID and the start time extracted from the date output:
#!/bin/bash
echo "Process $$ starting at: " $(LANG=en_US date +%T)
For using the script with cron, we define a variable WORKDIR in crontab to keep the location of our script and its output:
$ crontab -l
# Edit this file to introduce tasks to be run by cron.
# ...
# m h dom mon dow command
WORKDIR=/home/joe/prj/random
3. Ubuntu and vixie
The default Ubuntu cron implementation is vixie. Because it doesn’t offer any built-in random capacities, we’re going to examine workarounds.
Before we start, note that we’ll use the RANDOM variable as a source of random numbers. If we want to use it directly in the crontab, we should change the cron’s shell to Bash. That’s because cron’s default shell sh doesn’t maintain this variable. Thus, let’s set the SHELL variable in the crontab:
$ crontab -l
# Edit this file to introduce tasks to be run by cron.
# ...
# m h dom mon dow command
WORKDIR=/home/joe/prj/random
SHELL=/bin/bash
3.1. Random Time Offset Over a Regular Schedule
As the first possibility, let’s delay the execution of the actual command by a random interval:
0,30 9-17 * * * sleep $((RANDOM \% 30))m ; $WORKDIR/rscript.sh >> $WORKDIR/jobs_log.txt
The entry starts rscript.sh every hour and every 30 minutes after an hour, from 9 AM to 6:59 PM. This gives us 18 executions. Each run begins with sleep for a random amount of time under 30 minutes. Note that a modulo % sign has a special meaning in crontab, so we need to escape it with *\*.
3.2. Starting a Batch of Jobs With crontab
As another approach, let’s start all tasks almost simultaneously. However, the actual job will be postponed. So, the random delay is set to trigger the script within eight hours (or 480 minutes):
1-21/1 9 * * * sleep $((RANDOM \% 480))m ; $WORKDIR/rscript.sh >> $WORKDIR/jobs_log.txt
This entry launches 20 tasks every minute after 9 AM. Of course, for some time, we’ll have a lot of sleeping processes in our system.
3.3. Starting a Batch of Jobs With a Script
A slight modification of the previous solution consists of assigning the dispatching job to a single script, which we’ve named launcher.sh:
#!/bin/bash
WORKDIR=/home/joe/prj/random
n_jobs=$1 # number of jobs
time_span=$2 #launching period in minutes
for i in $(seq 1 $n_jobs)
do
shift=$((RANDOM%time_span)) # draw delay up to time_span
(sleep ${shift}m; $WORKDIR/rscript.sh >> $WORKDIR/jobs_log_launcher.txt) & # move task background, sleep for shift minutes
done
Then, in crontab, we can schedule the script at 9 AM with the entry:
0 9 * * * $WORKDIR/launcher.sh 20 480
4. Fedora and cronie
cronie is the cron implementation used in Fedora. With it, we can add a random delay to the job’s start time. We can pass its upper limit in the RANDOM_DELAY variable in crontab.
Because cronie isn’t installed by default, we should do that with dnf:
$ sudo dnf install cronie
After installing and enabling the service, let’s check its status:
$ systemctl status crond
● crond.service - Command Scheduler
Loaded: loaded (/usr/lib/systemd/system/crond.service; enabled; preset: enabled)
Drop-In: /usr/lib/systemd/system/service.d
└─10-timeout-abort.conf
Active: active (running) since Tue 2023-07-18 06:51:30 PDT; 7min ago
Main PID: 5453 (crond)
Tasks: 1 (limit: 5720)
Memory: 976.0K
CPU: 43ms
CGroup: /system.slice/crond.service
└─5453 /usr/sbin/crond -n
Jul 18 06:51:30 10.0.2.15 crond[5453]: (CRON) STARTUP (1.6.1)
Jul 18 06:51:30 10.0.2.15 systemd[1]: Started crond.service - Command Scheduler.
Jul 18 06:51:30 10.0.2.15 crond[5453]: (CRON) INFO (Syslog will be used instead of sendmail.)
Jul 18 06:51:30 10.0.2.15 crond[5453]: (CRON) INFO (RANDOM_DELAY will be scaled with factor 91% if used.)
Jul 18 06:51:30 10.0.2.15 crond[5453]: (CRON) INFO (running with inotify support)
Jul 18 06:51:30 10.0.2.15 crond[5453]: (CRON) INFO (@reboot jobs will be run at computer's startup.)
Note the RANDOM_DELAY information in the output. From here, we can learn that this delay will be scaled down to 91%. This random scaling factor is set once during the start of the crond service. It’s the only random part of this setup.
4.1. The crontab Example
So, let’s schedule tasks to be started every hour, on the hour and half-past the hour, between 9 AM and 6 PM. Additionally, let’s use RANDOM_DELAY to add a random wait no longer than 30 minutes:
$ crontab -l
WORKDIR=/home/joe/prj/random
RANDOM_DELAY=30
0,30 9-17 * * * $WORKDIR/rscript.sh >> $WORKDIR/jobs_log.txt
Let’s check the start times in the log file:
$ cat jobs_log.txt
Process 1786 starting at: 09:27:02
Process 1971 starting at: 09:57:01
# ...
Process 4750 starting at: 17:27:01
Process 4932 starting at: 17:57:01
This 27-minute shift comes from the RANDOM_DELAY multiplied by the 91% scale factor.
The declared delay affects all crontab entries below the declaration. However, we can change its values for subsequent entries. Let’s take a look at this crontab content:
$ crontab -l
WORKDIR=/home/joe/prj/random
0,30 9-17 * * * $WORKDIR/rscript.sh >> $WORKDIR/jobs_log.txt
RANDOM_DELAY=30
0,30 9-17 * * * $WORKDIR/rscript.sh >> $WORKDIR/jobs_log1.txt
RANDOM_DELAY=
0,30 9-17 * * * $WORKDIR/rscript.sh >> $WORKDIR/jobs_log2.txt
The first entry isn’t delayed at all, while the subsequent one is delayed up to 30 minutes. Finally, the last entry has no offset.
5. The systemd Timers
Most of the popular, modern Linux distributions control services with systemd. We can replace the cron scheduling with the systemd.timer service. It offers a time granularity of one second, compared to the one-minute granularity of cron. Before we start creating the service, let’s make our script rscript.sh available system-wide by putting it into the /usr/local/bin folder.
Now, let’s prepare the unit file test_scheduler for our service:
[Unit]
Description=Periodic scheduler with random shift service
Wants=test_scheduler.timer
[Service]
Type=oneshot
ExecStart=/usr/local/bin/rscript.sh
Note that we don’t bother about redirecting the script’s output. It isn’t necessary, because the service’s STDOUT and STDERR show up in the journaling system.
Next, we need the corresponding timer file, test_scheduler.timer:
[Unit]
Description=Test periodic scheduler with random delay
[Timer]
Unit=test_scheduler.service
OnCalendar=*-*-* *:0/10:0
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Thanks to the OnCalendar entry, the timer will trigger every 10 minutes of each hour. Subsequently, we delay each execution by a random interval with an upper limit of 600 seconds using the RandomizedDelaySec=600 directive. This interval is calculated individually for each planned execution.
Finally, let’s copy both files to the /etc/systemd/system folder and start the service:
$ sudo systemctl start test_scheduler
Afterward, let’s take a look at the status of the running service:
$ systemctl status test_scheduler.timer
● test_scheduler.timer - Test periodic scheduler with random delay
Loaded: loaded (/etc/systemd/system/test_scheduler.timer; enabled; vendor preset: enabled)
Active: active (waiting) since Sun 2023-07-16 12:36:11 CEST; 5min ago
Trigger: Sun 2023-07-16 12:55:36 CEST; 13min left
# ...
The Trigger line informs us about the time of the next event and the remaining waiting time. We see the 5:36 minutes long deviation from the regular start at 12:50. The latter time would be dictated by the OnCalendar expression.
5.1. More on the OnCalendar Format
OnCalendar specifies time points in a similar manner as the crontab entry does. Basically, it means:
DOW YYYY-MM-DD HH24:MI:SS
Here, DOW stands for the day of the week. We need to use English abbreviations or full names for them. The remaining two, date and time parts of the format, are self-explanatory. We can skip either of these parts if we don’t want to specify them. Next, let’s list the operators we’re going to use:
- * – an asterisk to denote any time
- / – a slash to set an increment
- .. – double dots to define a range
Moreover, we can test the expression with the systemd-analyze calendar command.
Now, let’s accomplish our goal to start jobs at random between 9 AM and 6 PM. Let’s try a 09..17:0/30 expression, which means every multiple of 30 minutes, between 9 AM and 5:59 PM. We’ll start the simulation at 8 AM with the –base-time option and simulate 18 following events with the –iterations option to systemd-analyze:
$ systemd-analyze calendar --base-time=08:00:00 --iterations=18 '09..17:0/30'
Original form: 09..17:0/30
Normalized form: *-*-* 09..17:00/30:00
Next elapse: Sun 2023-07-16 09:00:00 CEST
(in UTC): Sun 2023-07-16 07:00:00 UTC
From now: 6h ago
Iter. #2: Sun 2023-07-16 09:30:00 CEST
(in UTC): Sun 2023-07-16 07:30:00 UTC
From now: 6h ago
# ...
Iter. #18: Sun 2023-07-16 17:30:00 CEST
(in UTC): Sun 2023-07-16 15:30:00 UTC
From now: 1h 42min left
Of course, the systemd-analyze calendar command doesn’t take the randomness into account. Then, we can modify the test_scheduler.timer unit file, setting RandomizedDelaySec to a half hour (1800 seconds):
[Timer]
Unit=test_scheduler.service
OnCalendar=09..17:0/30
RandomizedDelaySec=1800
Finally, let’s collect the results of a real run with journalctl:
$ journalctl -S "2023-07-17" -f -u test_scheduler.service
Jul 17 09:29:05 ubuntu systemd[1]: Starting Test periodic scheduler with random delay...
Jul 17 09:29:05 ubuntu rscript.sh[2266]: Process 2266 starting at: 09:29:05
Jul 17 09:29:05 ubuntu systemd[1]: test_scheduler.service: Deactivated successfully.
Jul 17 09:29:05 ubuntu systemd[1]: Finished Test periodic scheduler with random delay.
Jul 17 09:54:26 ubuntu systemd[1]: Starting Test periodic scheduler with random delay...
Jul 17 09:54:26 ubuntu rscript.sh[2334]: Process 2334 starting at: 09:54:26
Jul 17 09:54:26 ubuntu systemd[1]: test_scheduler.service: Deactivated successfully.
Jul 17 09:54:26 ubuntu systemd[1]: Finished Test periodic scheduler with random delay.
# ...
Jul 17 17:41:55 ubuntu systemd[1]: Starting Test periodic scheduler with random delay...
Jul 17 17:41:55 ubuntu rscript.sh[3727]: Process 3727 starting at: 17:41:55
Jul 17 17:41:55 ubuntu systemd[1]: test_scheduler.service: Deactivated successfully.
Jul 17 17:41:55 ubuntu systemd[1]: Finished Test periodic scheduler with random delay.
5.2. Fixing the Random Delay
With the FixedRandomDelay directive accompanying the RandomizedDelaySec, we can fix the value of the random delay. This value will be defined once for the timer unit and won’t change during the restarts of the service:
[Timer]
Unit=test_scheduler.service
OnCalendar=09..17:0/30
RandomizedDelaySec=1800
FixedRandomDelay=true
6. The at Command
The at command starts a task at a specific moment in time. The command is a front end for the atd daemon managed by systemctl:
$ systemctl status atd
● atd.service - Deferred execution scheduler
Loaded: loaded (/lib/systemd/system/atd.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2023-07-13 12:44:29 CEST; 2s ago
Docs: man:atd(8)
Process: 10695 ExecStartPre=find /var/spool/cron/atjobs -type f -name =* -not -newercc /run/systemd -delete (code=exited, status=0/SUCCESS)
Main PID: 10696 (atd)
Tasks: 1 (limit: 18957)
Memory: 244.0K
CPU: 3ms
CGroup: /system.slice/atd.service
└─10696 /usr/sbin/atd -f
6.1. Random Scheduling With at
The idea is to spread our tasks randomly by feeding the at command with uniformly drawn time points. Thus, let’s use at‘s now time specifier and a random offset time in minutes:
$ at -f <script_name> now + <time> minutes
Let’s write a simple script rscript_at.sh to be scheduled with the -f option:
#!/bin/bash
WORKDIR=/home/joe/prj/random
echo "Process $$ starting at: " $(LANG=en_EN date +%T) >> $WORKDIR/jobs_log_at.txt
We should redirect the output on our own because at would mail it with sendmail otherwise.
Next, we need a launch_at.sh script to set the jobs:
#!/bin/bash
WORKDIR=/home/joe/prj/random
n_jobs=$1 # number of jobs
time_span=$2 #launching period in minutes
for number in $(seq $n_jobs)
do
minutes=$((RANDOM*time_span/32768)) # dividing by the maximal value of RANDOM plus one
at -f $WORKDIR/rscript_at.sh now + $minutes minutes
done
Let’s test our construction by starting the launch_at.sh script by hand and then listing the queued jobs with atq:
$ ./launch_at.sh 5 60
warning: commands will be executed using /bin/sh
job 30 at Thu Jul 13 12:47:00 2023
# ...
$ atq
30 Thu Jul 13 12:47:00 2023 a joe
32 Thu Jul 13 13:21:00 2023 a joe
34 Thu Jul 13 13:01:00 2023 a joe
31 Thu Jul 13 13:21:00 2023 a joe
33 Thu Jul 13 13:09:00 2023 a joe
We see five jobs spread randomly over an hourly interval, starting from 12:47.
6.2. Adding the Script to crontab
To automate job scheduling, let’s add the launch_at.sh script to the crontab. We’re going to start 20 jobs within eight hours at 9:00 AM with the entry:
0 0 9 * * * $WORKDIR/launch_at.sh 20 480
7. Conclusion
In this article, we looked through different ways to start tasks at randomly chosen time points, within some time period. First, we employed the cron scheduler and covered the differences between Ubuntu’s vixie and Fedora’s cronie implementations. The latter offered built-in support for randomization.
Next, we paid attention to the systemd timer service. We focused on time-scheduling expressions and learned about using random delays. Finally, we applied the at command to start jobs in a random manner.