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

Reply via email to