civodul pushed a commit to branch main
in repository shepherd.
commit b36e97a730596dbf3c376f940150d5fd1f08ecf1
Author: Ludovic Courtès <[email protected]>
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 <[email protected]>
---
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