Crontab supports things like "*/20" in the minutes column to run every 20 minutes. For example, given:
*/20 * * * * echo I am right on time The job above would run at 0, 20, and 40 minutes of every hours. job@ asked whether we could support a random offset so that jobs would not always start at the same time, but still use the same period (in this example every 20 minutes). This turns out to be fairly simple. Below is a small diff to support step intervals with a random offset. The syntax adds a '~' after the step value. For example: */~20 * * * * echo mix it up a bit will still run every 20 minutes but the initial run will be some time between 0-19 minutes after the hour (inclusive). Like the existing random support, the starting offset for an entry is chosen when the crontab file is first loaded and remains the same unless the crontab file is modified (and reloaded). The man page bits are from job@ Opinions? Does the proposed syntax seem OK? - todd Index: usr.sbin/cron/crontab.5 =================================================================== RCS file: /cvs/src/usr.sbin/cron/crontab.5,v retrieving revision 1.41 diff -u -p -u -r1.41 crontab.5 --- usr.sbin/cron/crontab.5 18 Apr 2020 17:11:40 -0000 1.41 +++ usr.sbin/cron/crontab.5 3 May 2023 20:17:31 -0000 @@ -174,6 +174,15 @@ Steps are also permitted after an asteri just use .Dq */2 . .Pp +A step value may be preceded with a +.Ql ~ +character to specify a one-off random wait period before the step cycle begins. +For example, to avoid a thundering herd at the top and bottom of the hour, +.Dq */~30 +can be used in the +.Ar minute +field to specify command execution happen twice an hour at consistent intervals. +.Pp An asterisk .Pq Ql * is short form for a range of all allowed values. Index: usr.sbin/cron/entry.c =================================================================== RCS file: /cvs/src/usr.sbin/cron/entry.c,v retrieving revision 1.53 diff -u -p -u -r1.53 entry.c --- usr.sbin/cron/entry.c 21 May 2022 01:21:29 -0000 1.53 +++ usr.sbin/cron/entry.c 3 May 2023 17:24:47 -0000 @@ -524,12 +524,22 @@ get_range(bitstr_t *bits, int low, int h /* check for step size */ if (ch == '/') { + int rndstep = 0; + /* eat the slash */ ch = get_char(file); if (ch == EOF) return (EOF); + /* check for random step size offset. */ + if (ch == '~') { + rndstep = 1; + ch = get_char(file); + if (ch == EOF) + return (EOF); + } + /* get the step size -- note: we don't pass the * names here, because the number is not an * element id, it's a step size. 'low' is @@ -538,6 +548,11 @@ get_range(bitstr_t *bits, int low, int h ch = get_number(&num3, 0, NULL, ch, file, ", \t\n"); if (ch == EOF || num3 == 0) return (EOF); + + if (rndstep) { + /* add a random offset less than the step size */ + num1 += arc4random_uniform(num3); + } } else { /* no step. default==1. */