civodul pushed a commit to branch main in repository shepherd. commit b36e97a730596dbf3c376f940150d5fd1f08ecf1 Author: Ludovic Courtès <l...@gnu.org> AuthorDate: Mon Mar 31 22:33:06 2025 +0200
timer: Correctly compute ‘seconds-to-wait’ on summer DST change. Fixes <https://issues.guix.gnu.org/77401>. Fixes a bug whereby, on the CET -> CEST change (UTC+1 to UTC+2), ‘seconds-to-wait’ would return 0 for timers with an event between 02:00 CEST and 03:00 CEST (the hour that is skipped), thereby firing the timer endlessly. * modules/shepherd/service/timer.scm (seconds-to-wait): Check whether DIFF is zero or negative and add the timezone offset difference when it is. * tests/services/timer-events.scm ("seconds-to-wait, CET -> CEST, hourly"): Fix test so start on 29 March 2025 CET. ("seconds-to-wait, CEST -> CET, every 15mn") ("seconds-to-wait, CET -> CEST, every 15mn"): New tests. * NEWS: Update. Reported-by: Timo Wilken <g...@twilken.net> --- NEWS | 11 +++++++++++ modules/shepherd/service/timer.scm | 15 +++++++++++---- tests/services/timer-events.scm | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index c55ddce..e6a7cfe 100644 --- a/NEWS +++ b/NEWS @@ -50,6 +50,17 @@ was logging has just stopped. This is now fixed. Programs such as nginx can compress log files as they write them. The ‘log-rotation’ service no longer re-compresses such log files. +** Timers correctly handle winter-to-summer DST change + (<https://issues.guix.gnu.org/77401>) + +This is a followup to an incomplete fix in +<https://issues.guix.gnu.org/75622>: during the summer-to-winter daylight +saving time (DST) change, for example from CET (UTC+1) to CEST (UTC+2) on 30 +March 2025 in Western Europe, the interval between consecutive calendar events +would be incorrectly calculated when the event would fall between 02:00am and +03:00am, leading the timer to trigger many times in a row, unless it had +#:wait-for-termination? #true. This is now fixed; next year will be better! + * Changes in 1.0.3 ** ‘spawn-command’ now honors #:log-file diff --git a/modules/shepherd/service/timer.scm b/modules/shepherd/service/timer.scm index 9b29112..092dff5 100644 --- a/modules/shepherd/service/timer.scm +++ b/modules/shepherd/service/timer.scm @@ -377,10 +377,17 @@ represent the local time on that date, taking DST into account." (define* (seconds-to-wait event #:optional (now (current-time time-utc))) "Return the number of seconds to wait from @var{now} until the next occurrence of @var{event} (the result is an inexact number, always greater than zero)." - (let* ((then (next-calendar-event event (time-utc->date now))) - (diff (time-difference (date->time-utc then) now))) - (+ (time-second diff) - (/ (time-nanosecond diff) 1e9)))) + (let* ((now* (time-utc->date now)) + (then (next-calendar-event event now*)) + (diff (time-difference (date->time-utc then) now)) + (result (+ (time-second diff) + (/ (time-nanosecond diff) 1e9)))) + ;; If RESULT is zero or negative, that's presumably because of the + ;; timezone offset difference between THEN and NOW--e.g., when switching + ;; from CET (= UTC+1) to CEST (= UTC+2). Compensate. + (if (<= result 0) + (+ result (- (date-zone-offset then) (date-zone-offset now*))) + result))) (define (cron-string->calendar-event str) "Convert @var{str}, which contains a Vixie cron date line, into the diff --git a/tests/services/timer-events.scm b/tests/services/timer-events.scm index b902fa2..cda86c3 100644 --- a/tests/services/timer-events.scm +++ b/tests/services/timer-events.scm @@ -288,11 +288,28 @@ (cons seconds result))) (reverse result))))) +(test-equal "seconds-to-wait, CEST -> CET, every 15mn" + (append (make-list 11 (* 15 60)) ;Oct. 26th, from 00:00 CEST to 02:45 CEST + (list (+ 3600 (* 15 60))) ;Oct. 26th, from 02:45 CEST to 03:00 CET + (make-list 4 (* 15 60))) + (let ((event (calendar-event #:minutes '(0 15 30 45)))) + (let loop ((time (date->time-utc + (make-date 0 0 00 00 26 10 2025 7200))) ;CEST + (n 0) + (result '())) + (if (< n 16) + (let ((seconds (inexact->exact (seconds-to-wait event time)))) + (pk (time-utc->date time)) + (loop (make-time time-utc 0 (+ seconds (time-second time))) + (+ 1 n) + (cons seconds result))) + (reverse result))))) + (test-equal "seconds-to-wait, CET -> CEST, hourly" (make-list 24 3600) (let ((event (calendar-event #:minutes '(30)))) (let loop ((time (date->time-utc - (make-date 0 0 30 23 29 10 2025 7200))) ;CEST + (make-date 0 0 30 23 29 03 2025 3600))) ;CET (n 0) (result '())) (if (< n 24) @@ -302,6 +319,20 @@ (cons seconds result))) (reverse result))))) +(test-equal "seconds-to-wait, CET -> CEST, every 15mn" + (make-list 16 900) + (let ((event (calendar-event #:minutes '(0 15 30 45)))) + (let loop ((time (date->time-utc + (make-date 0 0 00 00 30 03 2025 3600))) ;CET + (n 0) + (result '())) + (if (< n 16) + (let ((seconds (inexact->exact (seconds-to-wait event time)))) + (loop (make-time time-utc 0 (+ seconds (time-second time))) + (+ 1 n) + (cons seconds result))) + (reverse result))))) + (let-syntax ((test-cron (syntax-rules () ((_ str calendar) (test-equal (string-append