Bug 24485

Summary: [patch] to make cron(8) handle clock jumps
Product: Base System Reporter: Gerhard Sittig <Gerhard.Sittig>
Component: binAssignee: freebsd-bugs (Nobody) <bugs>
Status: Closed FIXED    
Severity: Affects Only Me CC: asomers
Priority: Normal    
Version: 5.0-CURRENT   
Hardware: Any   
OS: Any   

Description Gerhard Sittig 2001-01-20 14:50:01 UTC
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.

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:

-----------------------------------------------------------------


+# 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.--yld0RuMpLwda17Wna588xARGdRfMZo46hDo2hQThjH7QjxMZ
Content-Type: text/plain; name="file.diff"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="file.diff"

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       -
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).
Comment 1 Eitan Adler freebsd_committer freebsd_triage 2017-12-31 07:59:51 UTC
For bugs matching the following criteria:

Status: In Progress Changed: (is less than) 2014-06-01

Reset to default assignee and clear in-progress tags.

Mail being skipped
Comment 2 Alan Somers freebsd_committer freebsd_triage 2022-01-25 21:54:24 UTC
Sorry for the two-decade delay in answering your bug report, but I believe the problem was fixed by SVN r74010.