Date: Sat, 20 Jan 2001 15:39:41 +0100 From: Gerhard Sittig <Gerhard.Sittig@gmx.net> To: FreeBSD-gnats-submit@freebsd.org Subject: bin/24485: [PATCH] to make cron(8) handle clock jumps Message-ID: <20010120153941.L253@speedy.gsinet>
next in thread | raw e-mail | index | archive | help
>Number: 24485 >Category: bin >Synopsis: [PATCH] to make cron(8) handle clock jumps >Confidential: no >Severity: non-critical >Priority: low >Responsible: freebsd-bugs >State: open >Quarter: >Keywords: >Date-Required: >Class: change-request >Submitter-Id: current-users >Arrival-Date: Sat Jan 20 06:50:01 PST 2001 >Closed-Date: >Last-Modified: >Originator: Gerhard Sittig >Release: FreeBSD 5.0-CURRENT i386 >Organization: in private >Environment: typical: FreeBSD installation with neither NTP setup nor absent time correction -- i.e. administrator's (manual?) intervention by means of date(8), netdate(8), ntpdate(8), or any other method of _changing_ or _setting_ the system clock instead of _adjusting_ it much unexpected: FreeBSD installation with such high a load that cron(8) wakes up minutes after its last tick *not* applicable for DST changes (in its original form), but followups expected to maybe turn this approach into a solution for DST issues; see the audit trail for this aspect >Description: cron(8) by design only looks at the current time at wakeup and runs (schedules) jobs for the current time only. This could make it skip jobs in the rare case where load prevents cron(8) from waking up fast enough / often enough to catch up with the current minute (quite inprobable). But all of the above mentioned manual clock correction (wall clock triggered date(8) invocation, manual or cron scheduled netdate(8) invocation, etc) will make the clock jump, too, and cron could skip jobs scheduled for the time jumped over forward as well as cron could run jobs scheduled to run once multiple times in case the clock is corrected backwards. One suggestion in related public discussion has been that a clean setup would make use of NTP, but this is not always an option when there's no permanent connection to suport the ntp client's communication needs. And after all it may not be important to run jobs at exact times but much more to run them with the frequency specified. DST changes have a similar "visible" effect of turning the "once a day" specification provided by the admin into "maybe not at all" or "at least once" behaviour -- which makes discussions bubble up twice a year in public lists. But the patch attached below does not solve this situation. Although the doc suggests it does, the time(3) result the "jump detection" is based on is not affected by differently interpreting local time presentation and thus the mechanism doesn't jump into action should the local clock be switched due to a DST change. Although one could think of extending the way "wakeupKind" is determined by additionally taking some localtime(3) result into consideration. There has been and will be strong reservation against touching cron(8) and leaving it in its current state was demanded to not introduce new bugs as well as to stick with POLA while leaving "intelligence" out of such a simple mechanism as cron(8) is designed to be. So this PR is accompanied by PR conf/24358 ("[PATCH] etc/rc variables for cron(8)") and - in case the patch gets accepted - the suggestion to make the modified cron tree a copy of the currently established src/usr.sbin/cron tree or some port. The minimum modification making the change acceptable to those who demand cron(8) to stick with current behaviour would be to wrap the new code into an option defaulting to "off". The rc.conf knobs allow for passing a newly introduced command line switch should the admin want the new behaviour. >How-To-Repeat: For the manual correction: Schedule a job to execute at a given daytime (i.e. maximum frequency "daily"). Wait for a short moment before its expected execution and make the clock jump forward by means of 'date -v +1H' or something. The job will be skipped. Issue some 'date -v -1H' command after the job's execution and see how the job gets executed righ away -- for a second time at the current date. When the patch got applied, the job will run exactly once in any of the above situations. For comparison and to make sure the old behaviour still applies to jobs with wildcard specs: Schedule a job with a higher execution frequency of, say, 5 minutes. See this job getting executed as scheduled without as well as with the attached patch. For the DST change: Schedule a cronjob for a daytime which falls into the timeframe skipped over or passed repeatedly by the DST change (see /usr/src/share/zoneinfo/ or zdump(8) -v output for details on your region / timezone) and watch its execution happen one times, two times or not at all when running without DST changes and when the DST change takes place (from ST to DST as well as from DST to ST). >Fix: [ intro: The origin of my effort was the DST discussion bubbling up twice a year. I believed the OpenBSD project to have a solution (the manpage read this way) and tried to port it to FreeBSD. While in tests the patch turned out to not work for DST changes, yet I feel it to be of benefit where the clock is not run freely or under graceful correction methods like NTP daemons. And the code could serve as a skeleton to be extended for DST handling (which cannot be solved by means of NTP). ] The OpenBSD team has taken action and modified their vixie cron version in December 1997. I extracted the diff between the OpenBSD and FreeBSD versions of cron and stripped it down to the DST relevant parts only, so this functionality is "obtained from OpenBSD". The patch is cited here in verbatim form "for the record" although it could benefit from some further mangling. Due to the nature of the diff (heavy modification with still some single lines "being the same" -- mostly comment closing brackets) this seems to be one of the rare cases where context diff format is more appropriate than unified diff format. So I will enclose both for the readers' comfort. There's no doubt about what further modifications could look like: - the manpage shouldn't promise to handle DST but should mention one of the situations it really does handle, like date(8) - everything not covering the "most common case" of just one minute passed by should be made optional, defaulting to being turned off (the wakeupKind determination and its switch contruct) - the trigger level for "way too far a jump to be a valid correction, so it is some kind of new setup" could be made a knob tweakable by compile time options or command line parameters - maybe some localtime(3) call could be considered when determining wakeupKind, with another command line option to turn this new behaviour on -- this could support DST changes to be handled in a way most humans expect it to be There's been a rather lengthy thread on freebsd-hackers about this proposal, started by <20001205225656.Z27042@speedy.gsinet> as of December 5th, 2000, which is archived at http://www.freebsd.org/cgi/getmsg.cgi?fetch=211030+217815+/usr/local/www/db/text/2000/freebsd-hackers/20001210.freebsd-hackers. In its course I came to the conclusion that more appropriate ways to solve the DST issue (I don't want to call it a "problem" any longer) would be to - make admins more aware of the consequences to schedule jobs (and have them check the installed crontab they didn't provide themselves!) which is an educational issue and turns out to be just as ever lasting as the current debate is - provide some translater to / from a unified coordinate system which doesn't change in a day's run, proposals in the thread included - interpreting the daytime in the job specs read "passed since midnight no matter what the wallclock says" in combination with a comment that some days have 23 or 25 hours - specifying a TZ variable in the crontab file to determine how the job specs are to be interpreted - passing another command line option to read all crontab entries to be UTC plus maybe - wrapping 'crontab -e' into some converter presenting to / taking from the user local time representation and storing it in UTC which is what the cron daemon expects them to be # This is a shell archive. Save it in a file, remove anything before # this line, and then unpack it by entering "sh file". Note, it may # create directories; files and directories will be owned by you and # have default permissions. # # This archive contains: # # cron-diff.context # cron-diff.unified # echo x - cron-diff.context sed 's/^X//' >cron-diff.context << 'END-of-cron-diff.context' XIndex: cron/cron.8 X=================================================================== XRCS file: /CVSREPO/fbsd/src/usr.sbin/cron/cron/cron.8,v Xretrieving revision 1.7 Xdiff -u -c -r1.7 cron.8 Xcvs diff: conflicting specifications of output style X*** cron/cron.8 1999/08/28 01:15:49 1.7 X--- cron/cron.8 2000/11/28 21:45:13 X*************** X*** 68,73 **** X--- 68,92 ---- X .Xr crontab 1 X command updates the modtime of the spool directory whenever it changes a X crontab. X+ .Pp X+ Special considerations exist when the clock is changed by less than 3 X+ hours; for example, at the beginning and end of Daylight Saving X+ Time. X+ If the time has moved forward, those jobs which would have X+ run in the time that was skipped will be run soon after the change. X+ Conversely, if the time has moved backward by less than 3 hours, X+ those jobs that fall into the repeated time will not be run. X+ .Pp X+ Only jobs that run at a particular time (not specified as @hourly, nor with X+ .Ql * X+ in the hour or minute specifier) X+ are X+ affected. X+ Jobs which are specified with wildcards are run based on the X+ new time immediately. X+ .Pp X+ Clock changes of more than 3 hours are considered to be corrections to X+ the clock, and the new time is used immediately. X .Sh SEE ALSO X .Xr crontab 1 , X .Xr crontab 5 XIndex: cron/cron.c X=================================================================== XRCS file: /CVSREPO/fbsd/src/usr.sbin/cron/cron/cron.c,v Xretrieving revision 1.9 Xdiff -u -c -r1.9 cron.c Xcvs diff: conflicting specifications of output style X*** cron/cron.c 1999/08/28 01:15:49 1.9 X--- cron/cron.c 2000/11/28 21:58:22 X*************** X*** 34,42 **** X X static void usage __P((void)), X run_reboot_jobs __P((cron_db *)), X! cron_tick __P((cron_db *)), X! cron_sync __P((void)), X! cron_sleep __P((void)), X #ifdef USE_SIGCHLD X sigchld_handler __P((int)), X #endif X--- 34,42 ---- X X static void usage __P((void)), X run_reboot_jobs __P((cron_db *)), X! find_jobs __P((time_min, cron_db *, int, int)), X! set_time __P((void)), X! cron_sleep __P((time_min)), X #ifdef USE_SIGCHLD X sigchld_handler __P((int)), X #endif X*************** X*** 121,143 **** X database.tail = NULL; X database.mtime = (time_t) 0; X load_database(&database); X run_reboot_jobs(&database); X! cron_sync(); X while (TRUE) { X # if DEBUGGING X /* if (!(DebugFlags & DTEST)) */ X # endif /*DEBUGGING*/ X! cron_sleep(); X X load_database(&database); X X! /* do this iteration X */ X! cron_tick(&database); X X! /* sleep 1 minute X */ X! TargetTime += 60; X } X } X X--- 121,246 ---- X database.tail = NULL; X database.mtime = (time_t) 0; X load_database(&database); X+ X+ set_time(); X run_reboot_jobs(&database); X! timeRunning = virtualTime = clockTime; X! X! /* X! * too many clocks, not enough time (Al. Einstein) X! * These clocks are in minutes since the epoch (time()/60). X! * virtualTime: is the time it *would* be if we woke up X! * promptly and nobody ever changed the clock. It is X! * monotonically increasing... unless a timejump happens. X! * At the top of the loop, all jobs for 'virtualTime' have run. X! * timeRunning: is the time we last awakened. X! * clockTime: is the time when set_time was last called. X! */ X while (TRUE) { X # if DEBUGGING X /* if (!(DebugFlags & DTEST)) */ X # endif /*DEBUGGING*/ X! time_min timeDiff; X! int wakeupKind; X X load_database(&database); X X! /* ... wait for the time (in minutes) to change ... */ X! do { X! cron_sleep(timeRunning + 1); X! set_time(); X! } while (clockTime == timeRunning); X! timeRunning = clockTime; X! X! /* X! * ... calculate how the current time differs from X! * our virtual clock. Classify the change into one X! * of 4 cases X */ X! timeDiff = timeRunning - virtualTime; X X! /* shortcut for the most common case */ X! if (timeDiff == 1) { X! virtualTime = timeRunning; X! find_jobs(virtualTime, &database, TRUE, TRUE); X! } else { X! wakeupKind = -1; X! if (timeDiff > -(3*MINUTE_COUNT)) X! wakeupKind = 0; X! if (timeDiff > 0) X! wakeupKind = 1; X! if (timeDiff > 5) X! wakeupKind = 2; X! if (timeDiff > (3*MINUTE_COUNT)) X! wakeupKind = 3; X! X! switch (wakeupKind) { X! case 1: X! /* X! * case 1: timeDiff is a small positive number X! * (wokeup late) run jobs for each virtual minute X! * until caught up. X! */ X! Debug(DSCH, ("[%d], normal case %d minutes to go\n", X! getpid(), timeRunning - virtualTime)) X! do { X! if (job_runqueue()) X! sleep(10); X! virtualTime++; X! find_jobs(virtualTime, &database, TRUE, TRUE); X! } while (virtualTime< timeRunning); X! break; X! X! case 2: X! /* X! * case 2: timeDiff is a medium-sized positive number, X! * for example because we went to DST run wildcard X! * jobs once, then run any fixed-time jobs that would X! * otherwise be skipped if we use up our minute X! * (possible, if there are a lot of jobs to run) go X! * around the loop again so that wildcard jobs have X! * a chance to run, and we do our housekeeping X */ X! Debug(DSCH, ("[%d], DST begins %d minutes to go\n", X! getpid(), timeRunning - virtualTime)) X! /* run wildcard jobs for current minute */ X! find_jobs(timeRunning, &database, TRUE, FALSE); X! X! /* run fixed-time jobs for each minute missed */ X! do { X! if (job_runqueue()) X! sleep(10); X! virtualTime++; X! find_jobs(virtualTime, &database, FALSE, TRUE); X! set_time(); X! } while (virtualTime< timeRunning && X! clockTime == timeRunning); X! break; X! X! case 0: X! /* X! * case 3: timeDiff is a small or medium-sized X! * negative num, eg. because of DST ending just run X! * the wildcard jobs. The fixed-time jobs probably X! * have already run, and should not be repeated X! * virtual time does not change until we are caught up X! */ X! Debug(DSCH, ("[%d], DST ends %d minutes to go\n", X! getpid(), virtualTime - timeRunning)) X! find_jobs(timeRunning, &database, TRUE, FALSE); X! break; X! default: X! /* X! * other: time has changed a *lot*, X! * jump virtual time, and run everything X! */ X! Debug(DSCH, ("[%d], clock jumped\n", getpid())) X! virtualTime = timeRunning; X! find_jobs(timeRunning, &database, TRUE, TRUE); X! } X! } X! /* jobs to be run (if any) are loaded. clear the queue */ X! job_runqueue(); X } X } X X*************** X*** 161,170 **** X X X static void X! cron_tick(db) X cron_db *db; X { X! register struct tm *tm = localtime(&TargetTime); X register int minute, hour, dom, month, dow; X register user *u; X register entry *e; X--- 264,277 ---- X X X static void X! find_jobs(vtime, db, doWild, doNonWild) X! time_min vtime; X cron_db *db; X+ int doWild; X+ int doNonWild; X { X! time_t virtualSecond = vtime * SECONDS_PER_MINUTE; X! register struct tm *tm = localtime(&virtualSecond); X register int minute, hour, dom, month, dow; X register user *u; X register entry *e; X*************** X*** 197,204 **** X && ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR)) X ? (bit_test(e->dow,dow) && bit_test(e->dom,dom)) X : (bit_test(e->dow,dow) || bit_test(e->dom,dom)) X! ) X! ) { X job_add(e, u); X } X } X--- 304,314 ---- X && ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR)) X ? (bit_test(e->dow,dow) && bit_test(e->dom,dom)) X : (bit_test(e->dow,dow) || bit_test(e->dom,dom)) X! ) X! ) { X! if ((doNonWild && !(e->flags & (MIN_STAR|HR_STAR))) X! || (doWild && (e->flags & (MIN_STAR|HR_STAR))) X! ) X job_add(e, u); X } X } X*************** X*** 206,267 **** X } X X X! /* the task here is to figure out how long it's going to be until :00 of the X! * following minute and initialize TargetTime to this value. TargetTime X! * will subsequently slide 60 seconds at a time, with correction applied X! * implicitly in cron_sleep(). it would be nice to let cron execute in X! * the "current minute" before going to sleep, but by restarting cron you X! * could then get it to execute a given minute's jobs more than once. X! * instead we have the chance of missing a minute's jobs completely, but X! * that's something sysadmin's know to expect what with crashing computers.. X */ X static void X! cron_sync() { X! register struct tm *tm; X! X! TargetTime = time((time_t*)0); X! tm = localtime(&TargetTime); X! TargetTime += (60 - tm->tm_sec); X } X X- X- static void X- cron_sleep() { X- int seconds_to_wait = 0; X- X- /* X- * Loop until we reach the top of the next minute, sleep when possible. X- */ X- X- for (;;) { X- seconds_to_wait = (int) (TargetTime - time((time_t*)0)); X- X /* X! * If the seconds_to_wait value is insane, jump the cron X */ X! X! if (seconds_to_wait < -600 || seconds_to_wait > 600) { X! cron_sync(); X! continue; X! } X X Debug(DSCH, ("[%d] TargetTime=%ld, sec-to-wait=%d\n", X! getpid(), (long)TargetTime, seconds_to_wait)) X X! /* X! * If we've run out of wait time or there are no jobs left X! * to run, break X! */ X! X! if (seconds_to_wait <= 0) X! break; X! if (job_runqueue() == 0) { X! Debug(DSCH, ("[%d] sleeping for %d seconds\n", X! getpid(), seconds_to_wait)) X! X! sleep(seconds_to_wait); X! } X! } X } X X X--- 316,348 ---- X } X X X! /* X! * set StartTime and clockTime to the current time. X! * these are used for computing what time it really is right now. X! * note that clockTime is a unix wallclock time converted to minutes X */ X static void X! set_time() X! { X! StartTime = time((time_t *)0); X! clockTime = StartTime / (unsigned long)SECONDS_PER_MINUTE; X } X X /* X! * try to just hit the next minute X */ X! static void X! cron_sleep(target) X! time_min target; X! { X! register int seconds_to_wait; X X+ seconds_to_wait = (int)(target*SECONDS_PER_MINUTE - time((time_t*)0)) + 1; X Debug(DSCH, ("[%d] TargetTime=%ld, sec-to-wait=%d\n", X! getpid(), (long)target*SECONDS_PER_MINUTE, seconds_to_wait)) X X! if (seconds_to_wait > 0 && seconds_to_wait< 65) X! sleep((unsigned int) seconds_to_wait); X } X X XIndex: cron/cron.h X=================================================================== XRCS file: /CVSREPO/fbsd/src/usr.sbin/cron/cron/cron.h,v Xretrieving revision 1.10 Xdiff -u -c -r1.10 cron.h Xcvs diff: conflicting specifications of output style X*** cron/cron.h 2000/07/01 22:58:16 1.10 X--- cron/cron.h 2000/11/28 21:45:13 X*************** X*** 122,127 **** X--- 122,131 ---- X LineNumber = ln; \ X } X X+ typedef int time_min; X+ X+ #define SECONDS_PER_MINUTE 60 X+ X #define FIRST_MINUTE 0 X #define LAST_MINUTE 59 X #define MINUTE_COUNT (LAST_MINUTE - FIRST_MINUTE + 1) X*************** X*** 172,177 **** X--- 176,183 ---- X #define DOM_STAR 0x01 X #define DOW_STAR 0x02 X #define WHEN_REBOOT 0x04 X+ #define MIN_STAR 0x08 X+ #define HR_STAR 0x10 X } entry; X X /* the crontab database will be a list of the X*************** X*** 266,272 **** X X char *ProgramName; X int LineNumber; X! time_t TargetTime; X X # if DEBUGGING X int DebugFlags; X--- 272,281 ---- X X char *ProgramName; X int LineNumber; X! time_t StartTime; X! time_min timeRunning; X! time_min virtualTime; X! time_min clockTime; X X # if DEBUGGING X int DebugFlags; X*************** X*** 281,287 **** X *DowNames[], X *ProgramName; X extern int LineNumber; X! extern time_t TargetTime; X # if DEBUGGING X extern int DebugFlags; X extern char *DebugFlagNames[]; X--- 290,299 ---- X *DowNames[], X *ProgramName; X extern int LineNumber; X! extern time_t StartTime; X! extern time_min timeRunning; X! extern time_min virtualTime; X! extern time_min clockTime; X # if DEBUGGING X extern int DebugFlags; X extern char *DebugFlagNames[]; END-of-cron-diff.context echo x - cron-diff.unified sed 's/^X//' >cron-diff.unified << 'END-of-cron-diff.unified' XIndex: cron/cron.8 X=================================================================== XRCS file: /CVSREPO/fbsd/src/usr.sbin/cron/cron/cron.8,v Xretrieving revision 1.7 Xdiff -u -u -r1.7 cron.8 X--- cron/cron.8 1999/08/28 01:15:49 1.7 X+++ cron/cron.8 2000/11/28 21:45:13 X@@ -68,6 +68,25 @@ X .Xr crontab 1 X command updates the modtime of the spool directory whenever it changes a X crontab. X+.Pp X+Special considerations exist when the clock is changed by less than 3 X+hours; for example, at the beginning and end of Daylight Saving X+Time. X+If the time has moved forward, those jobs which would have X+run in the time that was skipped will be run soon after the change. X+Conversely, if the time has moved backward by less than 3 hours, X+those jobs that fall into the repeated time will not be run. X+.Pp X+Only jobs that run at a particular time (not specified as @hourly, nor with X+.Ql * X+in the hour or minute specifier) X+are X+affected. X+Jobs which are specified with wildcards are run based on the X+new time immediately. X+.Pp X+Clock changes of more than 3 hours are considered to be corrections to X+the clock, and the new time is used immediately. X .Sh SEE ALSO X .Xr crontab 1 , X .Xr crontab 5 XIndex: cron/cron.c X=================================================================== XRCS file: /CVSREPO/fbsd/src/usr.sbin/cron/cron/cron.c,v Xretrieving revision 1.9 Xdiff -u -u -r1.9 cron.c X--- cron/cron.c 1999/08/28 01:15:49 1.9 X+++ cron/cron.c 2000/11/28 21:58:22 X@@ -34,9 +34,9 @@ X X static void usage __P((void)), X run_reboot_jobs __P((cron_db *)), X- cron_tick __P((cron_db *)), X- cron_sync __P((void)), X- cron_sleep __P((void)), X+ find_jobs __P((time_min, cron_db *, int, int)), X+ set_time __P((void)), X+ cron_sleep __P((time_min)), X #ifdef USE_SIGCHLD X sigchld_handler __P((int)), X #endif X@@ -121,23 +121,126 @@ X database.tail = NULL; X database.mtime = (time_t) 0; X load_database(&database); X+ X+ set_time(); X run_reboot_jobs(&database); X- cron_sync(); X+ timeRunning = virtualTime = clockTime; X+ X+ /* X+ * too many clocks, not enough time (Al. Einstein) X+ * These clocks are in minutes since the epoch (time()/60). X+ * virtualTime: is the time it *would* be if we woke up X+ * promptly and nobody ever changed the clock. It is X+ * monotonically increasing... unless a timejump happens. X+ * At the top of the loop, all jobs for 'virtualTime' have run. X+ * timeRunning: is the time we last awakened. X+ * clockTime: is the time when set_time was last called. X+ */ X while (TRUE) { X # if DEBUGGING X /* if (!(DebugFlags & DTEST)) */ X # endif /*DEBUGGING*/ X- cron_sleep(); X+ time_min timeDiff; X+ int wakeupKind; X X load_database(&database); X X- /* do this iteration X+ /* ... wait for the time (in minutes) to change ... */ X+ do { X+ cron_sleep(timeRunning + 1); X+ set_time(); X+ } while (clockTime == timeRunning); X+ timeRunning = clockTime; X+ X+ /* X+ * ... calculate how the current time differs from X+ * our virtual clock. Classify the change into one X+ * of 4 cases X */ X- cron_tick(&database); X+ timeDiff = timeRunning - virtualTime; X X- /* sleep 1 minute X+ /* shortcut for the most common case */ X+ if (timeDiff == 1) { X+ virtualTime = timeRunning; X+ find_jobs(virtualTime, &database, TRUE, TRUE); X+ } else { X+ wakeupKind = -1; X+ if (timeDiff > -(3*MINUTE_COUNT)) X+ wakeupKind = 0; X+ if (timeDiff > 0) X+ wakeupKind = 1; X+ if (timeDiff > 5) X+ wakeupKind = 2; X+ if (timeDiff > (3*MINUTE_COUNT)) X+ wakeupKind = 3; X+ X+ switch (wakeupKind) { X+ case 1: X+ /* X+ * case 1: timeDiff is a small positive number X+ * (wokeup late) run jobs for each virtual minute X+ * until caught up. X+ */ X+ Debug(DSCH, ("[%d], normal case %d minutes to go\n", X+ getpid(), timeRunning - virtualTime)) X+ do { X+ if (job_runqueue()) X+ sleep(10); X+ virtualTime++; X+ find_jobs(virtualTime, &database, TRUE, TRUE); X+ } while (virtualTime< timeRunning); X+ break; X+ X+ case 2: X+ /* X+ * case 2: timeDiff is a medium-sized positive number, X+ * for example because we went to DST run wildcard X+ * jobs once, then run any fixed-time jobs that would X+ * otherwise be skipped if we use up our minute X+ * (possible, if there are a lot of jobs to run) go X+ * around the loop again so that wildcard jobs have X+ * a chance to run, and we do our housekeeping X */ X- TargetTime += 60; X+ Debug(DSCH, ("[%d], DST begins %d minutes to go\n", X+ getpid(), timeRunning - virtualTime)) X+ /* run wildcard jobs for current minute */ X+ find_jobs(timeRunning, &database, TRUE, FALSE); X+ X+ /* run fixed-time jobs for each minute missed */ X+ do { X+ if (job_runqueue()) X+ sleep(10); X+ virtualTime++; X+ find_jobs(virtualTime, &database, FALSE, TRUE); X+ set_time(); X+ } while (virtualTime< timeRunning && X+ clockTime == timeRunning); X+ break; X+ X+ case 0: X+ /* X+ * case 3: timeDiff is a small or medium-sized X+ * negative num, eg. because of DST ending just run X+ * the wildcard jobs. The fixed-time jobs probably X+ * have already run, and should not be repeated X+ * virtual time does not change until we are caught up X+ */ X+ Debug(DSCH, ("[%d], DST ends %d minutes to go\n", X+ getpid(), virtualTime - timeRunning)) X+ find_jobs(timeRunning, &database, TRUE, FALSE); X+ break; X+ default: X+ /* X+ * other: time has changed a *lot*, X+ * jump virtual time, and run everything X+ */ X+ Debug(DSCH, ("[%d], clock jumped\n", getpid())) X+ virtualTime = timeRunning; X+ find_jobs(timeRunning, &database, TRUE, TRUE); X+ } X+ } X+ /* jobs to be run (if any) are loaded. clear the queue */ X+ job_runqueue(); X } X } X X@@ -161,10 +264,14 @@ X X X static void X-cron_tick(db) X+find_jobs(vtime, db, doWild, doNonWild) X+ time_min vtime; X cron_db *db; X+ int doWild; X+ int doNonWild; X { X- register struct tm *tm = localtime(&TargetTime); X+ time_t virtualSecond = vtime * SECONDS_PER_MINUTE; X+ register struct tm *tm = localtime(&virtualSecond); X register int minute, hour, dom, month, dow; X register user *u; X register entry *e; X@@ -197,8 +304,11 @@ X && ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR)) X ? (bit_test(e->dow,dow) && bit_test(e->dom,dom)) X : (bit_test(e->dow,dow) || bit_test(e->dom,dom)) X- ) X- ) { X+ ) X+ ) { X+ if ((doNonWild && !(e->flags & (MIN_STAR|HR_STAR))) X+ || (doWild && (e->flags & (MIN_STAR|HR_STAR))) X+ ) X job_add(e, u); X } X } X@@ -206,62 +316,33 @@ X } X X X-/* the task here is to figure out how long it's going to be until :00 of the X- * following minute and initialize TargetTime to this value. TargetTime X- * will subsequently slide 60 seconds at a time, with correction applied X- * implicitly in cron_sleep(). it would be nice to let cron execute in X- * the "current minute" before going to sleep, but by restarting cron you X- * could then get it to execute a given minute's jobs more than once. X- * instead we have the chance of missing a minute's jobs completely, but X- * that's something sysadmin's know to expect what with crashing computers.. X+/* X+ * set StartTime and clockTime to the current time. X+ * these are used for computing what time it really is right now. X+ * note that clockTime is a unix wallclock time converted to minutes X */ X static void X-cron_sync() { X- register struct tm *tm; X- X- TargetTime = time((time_t*)0); X- tm = localtime(&TargetTime); X- TargetTime += (60 - tm->tm_sec); X+set_time() X+{ X+ StartTime = time((time_t *)0); X+ clockTime = StartTime / (unsigned long)SECONDS_PER_MINUTE; X } X X- X-static void X-cron_sleep() { X- int seconds_to_wait = 0; X- X- /* X- * Loop until we reach the top of the next minute, sleep when possible. X- */ X- X- for (;;) { X- seconds_to_wait = (int) (TargetTime - time((time_t*)0)); X- X /* X- * If the seconds_to_wait value is insane, jump the cron X+ * try to just hit the next minute X */ X- X- if (seconds_to_wait < -600 || seconds_to_wait > 600) { X- cron_sync(); X- continue; X- } X+static void X+cron_sleep(target) X+ time_min target; X+{ X+ register int seconds_to_wait; X X+ seconds_to_wait = (int)(target*SECONDS_PER_MINUTE - time((time_t*)0)) + 1; X Debug(DSCH, ("[%d] TargetTime=%ld, sec-to-wait=%d\n", X- getpid(), (long)TargetTime, seconds_to_wait)) X+ getpid(), (long)target*SECONDS_PER_MINUTE, seconds_to_wait)) X X- /* X- * If we've run out of wait time or there are no jobs left X- * to run, break X- */ X- X- if (seconds_to_wait <= 0) X- break; X- if (job_runqueue() == 0) { X- Debug(DSCH, ("[%d] sleeping for %d seconds\n", X- getpid(), seconds_to_wait)) X- X- sleep(seconds_to_wait); X- } X- } X+ if (seconds_to_wait > 0 && seconds_to_wait< 65) X+ sleep((unsigned int) seconds_to_wait); X } X X XIndex: cron/cron.h X=================================================================== XRCS file: /CVSREPO/fbsd/src/usr.sbin/cron/cron/cron.h,v Xretrieving revision 1.10 Xdiff -u -u -r1.10 cron.h X--- cron/cron.h 2000/07/01 22:58:16 1.10 X+++ cron/cron.h 2000/11/28 21:45:13 X@@ -122,6 +122,10 @@ X LineNumber = ln; \ X } X X+typedef int time_min; X+ X+#define SECONDS_PER_MINUTE 60 X+ X #define FIRST_MINUTE 0 X #define LAST_MINUTE 59 X #define MINUTE_COUNT (LAST_MINUTE - FIRST_MINUTE + 1) X@@ -172,6 +176,8 @@ X #define DOM_STAR 0x01 X #define DOW_STAR 0x02 X #define WHEN_REBOOT 0x04 X+#define MIN_STAR 0x08 X+#define HR_STAR 0x10 X } entry; X X /* the crontab database will be a list of the X@@ -266,7 +272,10 @@ X X char *ProgramName; X int LineNumber; X-time_t TargetTime; X+time_t StartTime; X+time_min timeRunning; X+time_min virtualTime; X+time_min clockTime; X X # if DEBUGGING X int DebugFlags; X@@ -281,7 +290,10 @@ X *DowNames[], X *ProgramName; X extern int LineNumber; X-extern time_t TargetTime; X+extern time_t StartTime; X+extern time_min timeRunning; X+extern time_min virtualTime; X+extern time_min clockTime; X # if DEBUGGING X extern int DebugFlags; X extern char *DebugFlagNames[]; END-of-cron-diff.unified exit This patch was tested in the following environment: FreeBSD 4.2-STABLE as well as FreeBSD-4.1-RELEASE got installed and the patch was applied to the source tree. Timezone data was modified to make the next DST changes come sooner without artificially jumping the clock by means of date(1) and thus falsifying the test result. Several cronjobs were sheduled to see how they're dispatched. In parallel a stock OpenBSD 2.7 installation was fed with a modified zoneinfo data as well as a list of test case cronjobs. Emulating DST changes with 'echo date 0300 | at 0200' is what failed, as I stated above (a few times:). That's what the following modification is for: ----------------------------------------------------------------- Index: /usr/src/share/zoneinfo/europe =================================================================== RCS file: /usr/fcvs/src/share/zoneinfo/europe,v retrieving revision 1.18.2.2 diff -u -r1.18.2.2 europe --- /usr/src/share/zoneinfo/europe 2000/10/25 19:44:08 1.18.2.2 +++ /usr/src/share/zoneinfo/europe 2000/12/03 12:36:54 @@ -406,6 +406,10 @@ Rule EU 1981 max - Mar lastSun 1:00u 1:00 S Rule EU 1996 max - Oct lastSun 1:00u 0 - +# this is my private modification to test out DST handling code in cron(8). +Rule EU 2000 only - Dec 4 1:00u 1:00 S +Rule EU 2000 only - Dec 5 1:00u 0 - + # W-Eur differs from EU only in that W-Eur uses standard time. Rule W-Eur 1977 1980 - Apr Sun>=1 1:00s 1:00 S Rule W-Eur 1977 only - Sep lastSun 1:00s 0 - ----------------------------------------------------------------- This TZ manipulation was done on several machines for several dates and needs customization for those people to reproduce the setup, of course. But the idea should be clear: I want to arrange for my "personal" DST period to happen when it fits best in my test scenario and as many times a year as I want it to. :) There's no need to wait a whole year, as well as it would be dangerous and contrary to the test goal to manipulate the system clock. This manipulation can be installed together with a world or by means of zic(8) and cp(1): zic -d . edited_zonefile cp Europe/Berlin /etc/localtime Successful installation can be tested by some command like zdump -v /etc/localtime | grep `date '+%Y'` The test case cronjob and crontab look like this: ----------------------------------------------------------------- #!/bin/sh # test case cronjob, script file ~/bin/crontest.sh echo `date '+%d.%m.%Y %H:%M:%S'` "$# args: [$@]" | \ /usr/bin/logger -t crontest ----------------------------------------------------------------- ----------------------------------------------------------------- --- /etc/crontab 2001/01/06 21:45:44 1.1 +++ /etc/crontab 2001/01/06 22:02:32 @@ -22,3 +22,23 @@ # does nothing, if you have UTC cmos clock. # See adjkerntz(8) for details. 1,31 0-5 * * * root adjkerntz -a + +# ----- test scenario for cron DST extension -------------------- + +0 0-1 * * * root $HOME/bin/crontest.sh daily 0:00, 1:00 +0 1 * * * root $HOME/bin/crontest.sh daily 1:00 +0 1-2 * * * root $HOME/bin/crontest.sh daily 1:00, 2:00 +0 2 * * * root $HOME/bin/crontest.sh daily 2:00 +0 2-3 * * * root $HOME/bin/crontest.sh daily 2:00, 3:00 +0 3 * * * root $HOME/bin/crontest.sh daily 3:00 +0 3-4 * * * root $HOME/bin/crontest.sh daily 3:00, 4:00 +0 4 * * * root $HOME/bin/crontest.sh daily 4:00 + +2 1 * * * root $HOME/bin/crontest.sh daily 1:02 +2 2 * * * root $HOME/bin/crontest.sh daily 2:02 +2 3 * * * root $HOME/bin/crontest.sh daily 3:02 +2 4 * * * root $HOME/bin/crontest.sh daily 4:02 + +*/5 1-4 * * * root $HOME/bin/crontest.sh every 5 min @ 1:00 - 4:00 + +# ----- end of cron DST test ------------------------------------ ----------------------------------------------------------------- The modified cron is run like this: # tail -f /var/log/messages & # kill `cat /var/run/cron.pid` # cron -x sch & This will produce a whole lot of debugging output with cron's decision about scheduling. One might want to run this in a script(1) environment for later reference. What we (don't) see: When DST jumps happen to set the system clock (better: its localtime representation), cron won't notice. Jobs will miss or run twice. What we see: Manual correction like 'date 1315' at 12:15 and 'date 1615' at 17:15 will be recognized as "DST begins" and "DST ends". Special action is taken to catch up with the skipped over timeframe's jobs as well as to not again execute the repeatedly passed timeframe's jobs. This is what most users seem to expect when doing manual corrections. virtually yours 82D1 9B9C 01DC 4FB4 D7B4 61BE 3F49 4F77 72DE DA76 Gerhard Sittig true | mail -s "get gpg key" Gerhard.Sittig@gmx.net -- If you don't understand or are scared by any of the above ask your parents or an adult to help you. >Release-Note: >Audit-Trail: >Unformatted: To Unsubscribe: send mail to majordomo@FreeBSD.org with "unsubscribe freebsd-bugs" in the body of the message
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?20010120153941.L253>