From eda09dd8f1374cdbf3404f54e0b9d7fb846a9704 Mon Sep 17 00:00:00 2001 From: millert Date: Sat, 6 May 2023 23:06:27 +0000 Subject: [PATCH] Support random offsets when using ranges with a step value in cron. 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 | 19 +++++++++++++++---- usr.sbin/cron/entry.c | 21 ++++++++++++++++++--- usr.sbin/cron/macros.h | 4 +++- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/usr.sbin/cron/crontab.5 b/usr.sbin/cron/crontab.5 index 88fe5e72c72..b54cc5d9caf 100644 --- a/usr.sbin/cron/crontab.5 +++ b/usr.sbin/cron/crontab.5 @@ -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 * diff --git a/usr.sbin/cron/entry.c b/usr.sbin/cron/entry.c index ab683b8476a..0fc853edbd9 100644 --- a/usr.sbin/cron/entry.c +++ b/usr.sbin/cron/entry.c @@ -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. */ diff --git a/usr.sbin/cron/macros.h b/usr.sbin/cron/macros.h index c1cc8f84a6f..efd2f5c92f0 100644 --- a/usr.sbin/cron/macros.h +++ b/usr.sbin/cron/macros.h @@ -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); -- 2.20.1