X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/790279faf0fda789d93a75f6f5996e65d5de4a18..195c3fbb075383ac47bba310eb701b02774e8d10:/schedule.go?ds=sidebyside diff --git a/schedule.go b/schedule.go index 5da024d..28a5a76 100644 --- a/schedule.go +++ b/schedule.go @@ -33,10 +33,10 @@ const ( // scheduleSource is the source of the work events of the runs. scheduleSource = "schedule" - // scheduleCheck is the longest wait of Run before it looks at the times - // again, so that it sees a suspend of the machine or a change of the - // clock. - scheduleCheck = time.Minute + // scheduleClockWaitTimeout is the longest wait of Run before it looks + // at the times again, so that it sees a suspend of the machine or a + // change of the clock. + scheduleClockWaitTimeout = time.Minute // scheduleLate is how long after its time a run is late. scheduleLate = time.Minute @@ -49,14 +49,15 @@ const ( ) // 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 *slog.Logger) (*Schedules, error) { +// none. The owner names its agent. Schedules without an owner take this name. A +// different stored owner is an error. The clock gives the time and the time +// zone of the schedules, and logger gets one line for each run. +func openSchedules(path, owner string, clock func() time.Time, logger *slog.Logger) (*Schedules, error) { s := &Schedules{ path: path, + owner: owner, clock: clock, logger: logger, - check: scheduleCheck, changed: make(chan struct{}, 1), } @@ -74,7 +75,18 @@ func openSchedules(path string, clock func() time.Time, logger *slog.Logger) (*S return nil, fmt.Errorf("%s: %w", path, err) } - for _, schedule := range s.file.Schedules { + changed := false + for i := range s.file.Schedules { + schedule := &s.file.Schedules[i] + if schedule.Owner == "" { + schedule.Owner = owner + changed = true + } + + if schedule.Owner != owner { + return nil, fmt.Errorf("schedule %d belongs to %q, not %q", schedule.ID, schedule.Owner, owner) + } + if schedule.Cron == "" { continue } @@ -85,6 +97,13 @@ func openSchedules(path string, clock func() time.Time, logger *slog.Logger) (*S } } + if changed { + err = s.save() + if err != nil { + return nil, fmt.Errorf("record schedule owner: %w", err) + } + } + return s, nil } @@ -95,9 +114,9 @@ func openSchedules(path string, clock func() time.Time, logger *slog.Logger) (*S // changed tells Run that a set or a remove changed the times. type Schedules struct { path string + owner string clock func() time.Time logger *slog.Logger - check time.Duration changed chan struct{} mutex sync.Mutex @@ -105,9 +124,10 @@ type Schedules struct { } // Run starts the runs of the due schedules in l until ctx ends. It waits until -// the first due time, at most check at a time, and also wakes at a set or a -// remove. So a schedule that came due while the daemon was stopped runs at the -// start. It returns the error of a write of the log or of the file. +// the first due time, at most scheduleClockWaitTimeout at a time, and also +// wakes at a set or a remove. So a schedule that came due while the daemon was +// stopped runs at the start. It returns the error of a write of the log or of +// the file. func (s *Schedules) Run(ctx context.Context, l *TurnLoop) error { for { err := s.deliver(ctx, l) @@ -122,7 +142,7 @@ func (s *Schedules) Run(ctx context.Context, l *TurnLoop) error { wait, found := s.Wait(s.clock()) if found { - timer = time.NewTimer(min(wait, s.check)) + timer = time.NewTimer(min(wait, scheduleClockWaitTimeout)) timeout = timer.C } @@ -151,7 +171,8 @@ func (s *Schedules) Set(text string, at, cron *string) (string, error) { now := s.clock() schedule := Schedule{ - Text: text, + Text: text, + Owner: s.owner, } switch { @@ -255,6 +276,10 @@ func (s *Schedules) Remove(id int) (string, error) { // the ID of the schedule and, for a late run, how late it is. The line holds no // text of the schedule. func (s *Schedules) deliver(ctx context.Context, l *TurnLoop) error { + if l.agent.config.Name != s.owner { + return fmt.Errorf("schedule owner %q does not match agent %q", s.owner, l.agent.config.Name) + } + now := s.clock() var turns []Message @@ -402,13 +427,15 @@ type ScheduleFile struct { Schedules []Schedule `json:"schedules"` } -// Schedule is one schedule: its ID, the task for the agent, the cron expression -// of a repeating schedule, empty for a one-time schedule, and its next time. +// Schedule is one schedule: its owner, its ID, the task for the agent, the cron +// expression of a repeating schedule, empty for a one-time schedule, and its +// next time. type Schedule struct { - ID int `json:"id"` - Text string `json:"text"` - Cron string `json:"cron"` - Due time.Time `json:"due"` + Owner string `json:"owner"` + ID int `json:"id"` + Text string `json:"text"` + Cron string `json:"cron"` + Due time.Time `json:"due"` } // Message returns the text of a run at now for the log, with the times in the