civodul pushed a commit to branch devel
in repository shepherd.
commit 0a444bd1685d6613086a020a40428b8bc1130632
Author: Ludovic Courtès <[email protected]>
AuthorDate: Wed Oct 9 15:56:30 2024 +0200
timer: Add #:occurrences to ‘make-timer-constructor’.
* modules/shepherd/service/timer.scm (serialize-timer): Add
‘remaining-occurrences’.
(timer-remaining-occurrences): New procedure.
(make-timer-constructor): Add #:occurrences.
[stop-self]: New procedure.
[run-timer]: Add ‘occurrences’ loop variable. Call ‘stop-self’ when
OCCURRENCES drops to zero. Handle 'remaining-occurrences messages.
Add ‘assert’ statement for OCCURRENCES.
* modules/shepherd/scripts/herd.scm (display-service-status): Check the
‘remaining-occurrences’ value.
* tests/services/timer.sh: Test it, with ‘timer-with-two-occurrences’.
* doc/shepherd.texi (Timers): Update.
---
doc/shepherd.texi | 11 ++++---
modules/shepherd/scripts/herd.scm | 22 +++++++++----
modules/shepherd/service/timer.scm | 63 ++++++++++++++++++++++++++++++--------
tests/services/timer.sh | 10 ++++++
4 files changed, 82 insertions(+), 24 deletions(-)
diff --git a/doc/shepherd.texi b/doc/shepherd.texi
index 52bd526..8857627 100644
--- a/doc/shepherd.texi
+++ b/doc/shepherd.texi
@@ -1524,13 +1524,14 @@ below as its @code{start} and @code{stop} methods
(@pxref{Defining
Services}).
@deffn {Procedure} make-timer-constructor @var{event} @var{action} @
- [#:log-file #f] [#:max-duration #f] @
+ [#:occurrences +inf.0] [#:log-file #f] [#:max-duration #f] @
[#:wait-for-termination? #f]
Return a procedure for use as the @code{start} method of a service. The
-procedure will perform @var{action} at every occurrence of @code{event}, a
-calendar event as returned by @code{calendar-event}. @var{action} may be
-either a command (returned by @code{command}) or a thunk; in the latter case,
-the thunk must be suspendable or it could block the whole shepherd process.
+procedure will perform @var{action} for @var{occurrences} iterations of
+@code{event}, a calendar event as returned by @code{calendar-event}.
+@var{action} may be either a command (returned by @code{command}) or a thunk;
+in the latter case, the thunk must be suspendable or it could block the whole
+shepherd process.
When @var{log-file} is true, log the output of @var{action} to that file
rather than in the global shepherd log.
diff --git a/modules/shepherd/scripts/herd.scm
b/modules/shepherd/scripts/herd.scm
index b1cd43f..15ae02b 100644
--- a/modules/shepherd/scripts/herd.scm
+++ b/modules/shepherd/scripts/herd.scm
@@ -609,12 +609,22 @@ to upgrade).~%"))))
(reverse (at-most log-history-size messages)))))
(match (live-service-running-value service)
- (('timer ('version 0) ('event sexp) _ ...)
- (let ((event (and=> sexp sexp->calendar-event)))
- (when event
- (newline)
- (format #t (highlight (l10n "Upcoming timer alarms:~%")))
- (display-timer-events event))))
+ (('timer ('version 0) properties ...)
+ (alist-let* properties (event remaining-occurrences)
+ (let ((event (and=> event sexp->calendar-event)))
+ (when event
+ (newline)
+ (if (zero? remaining-occurrences)
+ (format #t
+ (highlight/warn
+ (l10n "No upcoming timer alarm: about to stop.~%")))
+ (let ((count (min 5 (if (exact? remaining-occurrences)
+ remaining-occurrences
+ 100))))
+ (format #t (highlight (l10n "Upcoming timer alarm:~%"
+ "Upcoming timer alarms:~%"
+ count)))
+ (display-timer-events event count)))))))
(_ #t))))
(define (display-event-log services)
diff --git a/modules/shepherd/service/timer.scm
b/modules/shepherd/service/timer.scm
index 2b48e8a..1c3ad85 100644
--- a/modules/shepherd/service/timer.scm
+++ b/modules/shepherd/service/timer.scm
@@ -559,6 +559,7 @@ list, to be executed as @var{user} and @var{group}, with
the given
((? command? command) (command->sexp command))
(_ 'procedure)))
(processes ,(timer-processes timer))
+ (remaining-occurrences ,(timer-remaining-occurrences timer))
(past-runs ,(ring-buffer->list (timer-past-runs timer)))))
(define (timer-request message)
@@ -581,6 +582,11 @@ list, to be executed as @var{user} and @var{group}, with
the given
;; respectively, as integers (seconds since the Epoch).
(timer-request 'past-runs))
+(define timer-remaining-occurrences
+ ;; Return the number of remaining occurrences for this timer, possibly
+ ;; +inf.0 (infinity).
+ (timer-request 'remaining-occurrences))
+
(define sleep (@ (fibers) sleep))
(define %past-run-log-size
@@ -588,15 +594,18 @@ list, to be executed as @var{user} and @var{group}, with
the given
50)
(define* (make-timer-constructor event action
- #:key log-file
+ #:key
+ (occurrences +inf.0)
+ log-file
max-duration
(termination-signal SIGTERM)
wait-for-termination?)
"Return a procedure for use as the @code{start} method of a service. The
-procedure will perform @var{action} at every occurrence of @code{event}, a
-calendar event as returned by @code{calendar-event}. @var{action} may be
-either a command (returned by @code{command}) or a thunk; in the latter case,
-the thunk must be suspendable or it could block the whole shepherd process.
+procedure will perform @var{action} for @var{occurrences} iterations of
+@code{event}, a calendar event as returned by @code{calendar-event}.
+@var{action} may be either a command (returned by @code{command}) or a thunk;
+in the latter case, the thunk must be suspendable or it could block the whole
+shepherd process.
When @var{log-file} is true, log the output of @var{action} to that file
rather than in the global shepherd log.
@@ -610,15 +619,28 @@ When @var{max-duration} is true, it is the maximum
duration in seconds that a
run may last, provided @var{action} is a command. Past @var{max-duration}
seconds, the timer's process is forcefully terminated with signal
@var{termination-signal}."
+ (define (stop-self)
+ ;; When the last occurrence has completed, the only way to let the service
+ ;; controller know that we're done is by asking it to stop the service.
+ (spawn-fiber
+ (lambda ()
+ (let ((self (current-service)))
+ (local-output (l10n "Finished last occurrence of timer '~a'.")
+ (service-canonical-name self))
+ (stop-service self)))))
+
(define (run-timer)
(let ((channel (make-channel))
(name (service-canonical-name (current-service))))
(spawn-fiber
(lambda ()
- (let-loop loop ((processes '()) ;PID/start time
+ (let-loop loop ((processes '()) ;PID/start time
(past-runs (ring-buffer %past-run-log-size))
+ (occurrences occurrences)
(termination #f))
+
(match (if (or termination
+ (zero? occurrences)
(and (pair? processes) wait-for-termination?))
(get-message channel)
(get-message* channel (seconds-to-wait event)
@@ -651,17 +673,23 @@ after ~a seconds.")
(- end-time start-time))
(if (and termination (null? remaining))
(put-message termination #t) ;done
- (loop (processes remaining)
- (past-runs
- (ring-buffer-insert
- (list status end-time start-time)
- past-runs))))))
+ (begin
+ (when (zero? occurrences)
+ (stop-self))
+ (loop (processes remaining)
+ (past-runs
+ (ring-buffer-insert
+ (list status end-time start-time)
+ past-runs)))))))
(('processes reply)
(put-message reply processes)
(loop))
(('past-runs reply)
(put-message reply past-runs)
(loop))
+ (('remaining-occurrences reply)
+ (put-message reply occurrences)
+ (loop))
('timeout
;; Time to perform ACTION.
(if (command? action)
@@ -695,7 +723,8 @@ process ~a of timer '~a' after maximum duration of ~a
seconds.")
(local-output (l10n "Timer '~a' spawned process ~a.")
name pid)
- (loop (processes
+ (loop (occurrences (- occurrences 1))
+ (processes
(alist-cons pid ((@ (guile) current-time))
processes))))
(let ((start-time ((@ (guile) current-time))))
@@ -711,7 +740,10 @@ timer '~a': ~s")
name (cons key args))
`(exception ,key ,@args))))
- (loop (past-runs
+ (when (= 1 occurrences)
+ (stop-self))
+ (loop (occurrences (- occurrences 1))
+ (past-runs
(ring-buffer-insert
(list result ((@ (guile) current-time)) start-time)
past-runs))))))
@@ -729,6 +761,11 @@ from sleep state?).")
(timer channel event action)))
(lambda ()
+ (assert (or (eqv? occurrences +inf.0)
+ (and (integer? occurrences)
+ (exact? occurrences)
+ (positive? occurrences))))
+
;; First check that EVENT has matching events and return #f otherwise.
(let ((now (time-utc->date (current-time time-utc))))
(if (false-if-exception (next-calendar-event event now))
diff --git a/tests/services/timer.sh b/tests/services/timer.sh
index 7bcc022..ca1e7fb 100644
--- a/tests/services/timer.sh
+++ b/tests/services/timer.sh
@@ -72,6 +72,12 @@ cat > "$conf" <<EOF
#:max-duration 1)
#:stop (make-timer-destructor)
#:actions (list timer-trigger-action))
+ (service '(timer-with-two-occurrences)
+ #:start (make-timer-constructor
+ (calendar-event #:seconds (iota 60))
+ (lambda () (display "limited number of
occurrences\n"))
+ #:occurrences 2)
+ #:stop (make-timer-destructor))
(service '(timer-without-matching-events)
#:start (make-timer-constructor
(calendar-event #:seconds '()) ;broken event!
@@ -138,6 +144,10 @@ $herd status timer-that-takes-too-long | \
grep "terminated with signal 15" # recent runs
grep "Terminating.*after maximum duration" "$log"
+$herd start timer-with-two-occurrences
+until $herd status timer-with-two-occurrences | grep stopped; do sleep 0.3;
done
+test $(grep "limited number of occurrences" "$log" | wc -l) -eq 2
+
$herd start timer-without-matching-events && false
$herd status timer-without-matching-events | grep "stopped.*failing"