Support random offsets when using ranges with a step value in cron.
authormillert <millert@openbsd.org>
Sat, 6 May 2023 23:06:27 +0000 (23:06 +0000)
committermillert <millert@openbsd.org>
Sat, 6 May 2023 23:06:27 +0000 (23:06 +0000)
This extends the random range syntax to support step values.  Instead
of choosing a random number between the high and low values, the
field is treated as a range with a random offset less than the step
value.  This can be used to avoid thundering herd problems where
multiple machines contact a server all at the same time via cron jobs.

The syntax is similar to the existing range/step syntax but uses a
random range.  For example, instead of "0-59/10" in the minutes
field, "0~59/10" can be used to run a command every 10 minutes where
the first command starts at a random offset in the range [0,9].
The high and low numbers are optional, "~/10" can be used instead.

Requested by job@, OK phessler@

usr.sbin/cron/crontab.5
usr.sbin/cron/entry.c
usr.sbin/cron/macros.h

index 88fe5e7..b54cc5d 100644 (file)
@@ -17,9 +17,9 @@
 .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
 .\" OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 .\"
-.\" $OpenBSD: crontab.5,v 1.41 2020/04/18 17:11:40 jmc Exp $
+.\" $OpenBSD: crontab.5,v 1.42 2023/05/06 23:06:27 millert Exp $
 .\"
-.Dd $Mdocdate: April 18 2020 $
+.Dd $Mdocdate: May 6 2023 $
 .Dt CRONTAB 5
 .Os
 .Sh NAME
@@ -157,8 +157,7 @@ If either (or both) of the numbers on either side of the
 .Ql ~
 are omitted, the appropriate limit (low or high) for the field will be used.
 .Pp
-Step values can be used in conjunction with ranges (but not random ranges
-which represent a single number).
+Step values can be used in conjunction with ranges.
 Following a range with
 .No / Ns Ar number
 specifies skips of
@@ -173,6 +172,18 @@ Steps are also permitted after an asterisk, so to say
 .Dq every two hours ,
 just use
 .Dq */2 .
+A step value after a random range will execute the command at a random
+offset less than the step size.
+For example, to avoid a thundering herd at the top and bottom of the hour,
+.Dq 0~59/30
+.Po
+or simply
+.Dq ~/30
+.Pc
+can be used in the
+.Ar minute
+field to specify that command execution happen twice an hour at
+consistent intervals.
 .Pp
 An asterisk
 .Pq Ql *
index ab683b8..0fc853e 100644 (file)
@@ -1,4 +1,4 @@
-/*     $OpenBSD: entry.c,v 1.53 2022/05/21 01:21:29 deraadt Exp $      */
+/*     $OpenBSD: entry.c,v 1.54 2023/05/06 23:06:27 millert Exp $      */
 
 /*
  * Copyright 1988,1990,1993,1994 by Paul Vixie
@@ -456,10 +456,11 @@ get_range(bitstr_t *bits, int low, int high, const char *names[],
        /* range = number | number* "~" number* | number "-" number ["/" number]
         */
 
-       int i, num1, num2, num3;
+       int i, num1, num2, num3, rndstep;
 
        num1 = low;
        num2 = high;
+       rndstep = 0;
 
        if (ch == '*') {
                /* '*' means [low, high] but can still be modified by /step
@@ -497,7 +498,7 @@ get_range(bitstr_t *bits, int low, int high, const char *names[],
 
                        /* get the (optional) number following the tilde
                         */
-                       ch = get_number(&num2, low, names, ch, file, ", \t\n");
+                       ch = get_number(&num2, low, names, ch, file, "/, \t\n");
                        if (ch == EOF)
                                ch = get_char(file);
                        if (ch == EOF || num1 > num2) {
@@ -505,6 +506,13 @@ get_range(bitstr_t *bits, int low, int high, const char *names[],
                                return (EOF);
                        }
 
+                       if (ch == '/') {
+                               /* randomize the step value instead of num1
+                                */
+                               rndstep = 1;
+                               break;
+                       }
+
                        /* get a random number in the interval [num1, num2]
                         */
                        num3 = num1;
@@ -538,6 +546,13 @@ get_range(bitstr_t *bits, int low, int high, const char *names[],
                ch = get_number(&num3, 0, NULL, ch, file, ", \t\n");
                if (ch == EOF || num3 == 0)
                        return (EOF);
+               if (rndstep) {
+                       /*
+                        * use a random offset smaller than the step size
+                        * and the difference between high and low values.
+                        */
+                       num1 += arc4random_uniform(MINIMUM(num3, num2 - num1));
+               }
        } else {
                /* no step.  default==1.
                 */
index c1cc8f8..efd2f5c 100644 (file)
@@ -1,4 +1,4 @@
-/*     $OpenBSD: macros.h,v 1.15 2015/11/12 21:12:05 millert Exp $     */
+/*     $OpenBSD: macros.h,v 1.16 2023/05/06 23:06:27 millert Exp $     */
 
 /*
  * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
@@ -29,6 +29,8 @@
 #define        MAX_TEMPSTR     100     /* obvious */
 #define        MAX_UNAME       (_PW_NAME_LEN+1)        /* max length of username, should be overkill */
 
+#define        MINIMUM(a, b)   (((a) < (b)) ? (a) : (b))
+
 #define        Skip_Blanks(c, f) \
                        while (c == '\t' || c == ' ') \
                                c = get_char(f);