"encoding/json"
"errors"
"fmt"
- "log"
+ "log/slog"
"os"
"slices"
"strconv"
// openSchedules reads the schedules of the file at path. A missing file holds
// none. The clock gives the time and the time zone of the schedules, and logger
// gets one line for each run.
-func openSchedules(path string, clock func() time.Time, logger *log.Logger) (*Schedules, error) {
+func openSchedules(path string, clock func() time.Time, logger *slog.Logger) (*Schedules, error) {
s := &Schedules{
path: path,
clock: clock,
type Schedules struct {
path string
clock func() time.Time
- logger *log.Logger
+ logger *slog.Logger
check time.Duration
changed chan struct{}
return err
}
- var timer *time.Timer
- var timeout <-chan time.Time
+ var (
+ timer *time.Timer
+ timeout <-chan time.Time
+ )
+
wait, found := s.Wait(s.clock())
if found {
timer = time.NewTimer(min(wait, s.check))
// text of the schedule.
func (s *Schedules) deliver(ctx context.Context, l *TurnLoop) error {
now := s.clock()
+
var turns []Message
+
for _, schedule := range s.Due(now) {
message, err := l.Work(scheduleSource, schedule.Message(now), "")
if err != nil {
return err
}
+ attrs := []any{"id", schedule.ID}
if schedule.late(now) {
- s.logger.Printf("schedule %d ran %s late", schedule.ID, durationText(now.Sub(schedule.Due)))
- } else {
- s.logger.Printf("schedule %d ran", schedule.ID)
+ attrs = append(attrs, "late", now.Sub(schedule.Due))
}
+ s.logger.Info("schedule ran", attrs...)
+
turns = append(turns, message)
}
// Due returns the schedules whose time is not after now, by due time.
func (s *Schedules) Due(now time.Time) []Schedule {
var due []Schedule
+
for _, schedule := range s.sorted() {
if !schedule.Due.After(now) {
due = append(due, schedule)
// sorted returns a copy of the schedules by due time, then by ID.
func (s *Schedules) sorted() []Schedule {
s.mutex.Lock()
+
schedules := slices.Clone(s.file.Schedules)
s.mutex.Unlock()