]> git.giacomolli.uk - onechat/blobdiff - schedule.go
Separate multiline Go statements
[onechat] / schedule.go
index 5da024d39b0e4442235b3329695416cb9ac132eb..5fa35da63cb00d6cbbb13aaf6aba6ae055120a23 100644 (file)
@@ -33,10 +33,10 @@ const (
        // scheduleSource is the source of the work events of the runs.
        scheduleSource = "schedule"
 
        // 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
 
        // scheduleLate is how long after its time a run is late.
        scheduleLate = time.Minute
@@ -48,15 +48,50 @@ const (
        scheduleLateLine = "This run is late: the daemon was stopped at that time."
 )
 
        scheduleLateLine = "This run is late: the daemon was stopped at that time."
 )
 
+// Schedules holds the schedules of the agent: tasks that start a turn at one
+// time or at the times of a cron expression. Each change writes the file whole
+// before it returns, so a schedule survives a restart. The mutex protects the
+// schedules, because the tools change them from a turn while Run reads them.
+// 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
+       changed chan struct{}
+
+       mutex sync.Mutex
+       file  ScheduleFile
+}
+
+// ScheduleFile is the content of the file of the schedules: the ID of the last
+// schedule, so that an ID is never used again, and the schedules.
+type ScheduleFile struct {
+       Next      int        `json:"next"`
+       Schedules []Schedule `json:"schedules"`
+}
+
+// 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 {
+       Owner string    `json:"owner"`
+       ID    int       `json:"id"`
+       Text  string    `json:"text"`
+       Cron  string    `json:"cron"`
+       Due   time.Time `json:"due"`
+}
+
 // openSchedules reads the schedules of the file at path. A missing file holds
 // 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,
        s := &Schedules{
                path:    path,
+               owner:   owner,
                clock:   clock,
                logger:  logger,
                clock:   clock,
                logger:  logger,
-               check:   scheduleCheck,
                changed: make(chan struct{}, 1),
        }
 
                changed: make(chan struct{}, 1),
        }
 
@@ -74,7 +109,18 @@ func openSchedules(path string, clock func() time.Time, logger *slog.Logger) (*S
                return nil, fmt.Errorf("%s: %w", path, err)
        }
 
                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
                }
                if schedule.Cron == "" {
                        continue
                }
@@ -85,29 +131,21 @@ func openSchedules(path string, clock func() time.Time, logger *slog.Logger) (*S
                }
        }
 
                }
        }
 
-       return s, nil
-}
-
-// Schedules holds the schedules of the agent: tasks that start a turn at one
-// time or at the times of a cron expression. Each change writes the file whole
-// before it returns, so a schedule survives a restart. The mutex protects the
-// schedules, because the tools change them from a turn while Run reads them.
-// changed tells Run that a set or a remove changed the times.
-type Schedules struct {
-       path    string
-       clock   func() time.Time
-       logger  *slog.Logger
-       check   time.Duration
-       changed chan struct{}
+       if changed {
+               err = s.save()
+               if err != nil {
+                       return nil, fmt.Errorf("record schedule owner: %w", err)
+               }
+       }
 
 
-       mutex sync.Mutex
-       file  ScheduleFile
+       return s, nil
 }
 
 // Run starts the runs of the due schedules in l until ctx ends. It waits until
 }
 
 // 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)
 func (s *Schedules) Run(ctx context.Context, l *TurnLoop) error {
        for {
                err := s.deliver(ctx, l)
@@ -122,7 +160,7 @@ func (s *Schedules) Run(ctx context.Context, l *TurnLoop) error {
 
                wait, found := s.Wait(s.clock())
                if found {
 
                wait, found := s.Wait(s.clock())
                if found {
-                       timer = time.NewTimer(min(wait, s.check))
+                       timer = time.NewTimer(min(wait, scheduleClockWaitTimeout))
                        timeout = timer.C
                }
 
                        timeout = timer.C
                }
 
@@ -151,7 +189,8 @@ func (s *Schedules) Set(text string, at, cron *string) (string, error) {
 
        now := s.clock()
        schedule := Schedule{
 
        now := s.clock()
        schedule := Schedule{
-               Text: text,
+               Text:  text,
+               Owner: s.owner,
        }
 
        switch {
        }
 
        switch {
@@ -255,6 +294,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 {
 // 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
        now := s.clock()
 
        var turns []Message
@@ -395,22 +438,6 @@ func (s *Schedules) save() error {
        return nil
 }
 
        return nil
 }
 
-// ScheduleFile is the content of the file of the schedules: the ID of the last
-// schedule, so that an ID is never used again, and the schedules.
-type ScheduleFile struct {
-       Next      int        `json:"next"`
-       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.
-type Schedule struct {
-       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
 // location of now. The first line starts with [Schedule ID] and gives the due
 // time and once or the cron expression. Then come the text, the line about a
 // Message returns the text of a run at now for the log, with the times in the
 // location of now. The first line starts with [Schedule ID] and gives the due
 // time and once or the cron expression. Then come the text, the line about a