golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/schedule.go (about)

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package relui
     6  
     7  import (
     8  	"context"
     9  	"database/sql"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"log"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/jackc/pgx/v4"
    18  	"github.com/robfig/cron/v3"
    19  	"golang.org/x/build/internal/relui/db"
    20  	"golang.org/x/exp/slices"
    21  )
    22  
    23  // ScheduleType determines whether a workflow runs immediately or on
    24  // some future date or cadence.
    25  type ScheduleType string
    26  
    27  // ElementID returns a string suitable for a HTML element ID.
    28  func (s ScheduleType) ElementID() string {
    29  	return strings.ReplaceAll(string(s), " ", "")
    30  }
    31  
    32  // FormField returns a string representing which datatype to present
    33  // the user on the creation form.
    34  func (s ScheduleType) FormField() string {
    35  	switch s {
    36  	case ScheduleCron:
    37  		return "cron"
    38  	case ScheduleOnce:
    39  		return "datetime-local"
    40  	}
    41  	return ""
    42  }
    43  
    44  const (
    45  	ScheduleImmediate ScheduleType = "Immediate"
    46  	ScheduleOnce      ScheduleType = "Future Date"
    47  	ScheduleCron      ScheduleType = "Cron"
    48  )
    49  
    50  var (
    51  	ScheduleTypes = []ScheduleType{ScheduleImmediate, ScheduleOnce, ScheduleCron}
    52  )
    53  
    54  // Schedule represents the interval on which a job should be run. Only
    55  // Type and one other field should be set.
    56  type Schedule struct {
    57  	Once time.Time
    58  	Cron string
    59  	Type ScheduleType
    60  }
    61  
    62  func (s Schedule) Parse() (cron.Schedule, error) {
    63  	if err := s.Valid(); err != nil {
    64  		return nil, err
    65  	}
    66  	switch s.Type {
    67  	case ScheduleOnce:
    68  		return &RunOnce{next: s.Once}, nil
    69  	case ScheduleCron:
    70  		return cron.ParseStandard(s.Cron)
    71  	}
    72  	return nil, fmt.Errorf("unschedulable Schedule.Type %q", s.Type)
    73  }
    74  
    75  func (s Schedule) Valid() error {
    76  	switch s.Type {
    77  	case ScheduleOnce:
    78  		if s.Once.IsZero() {
    79  			return fmt.Errorf("time not set for %q", ScheduleOnce)
    80  		}
    81  		return nil
    82  	case ScheduleCron:
    83  		_, err := cron.ParseStandard(s.Cron)
    84  		return err
    85  	case ScheduleImmediate:
    86  		return nil
    87  	}
    88  	return fmt.Errorf("invalid ScheduleType %q", s.Type)
    89  }
    90  
    91  func (s *Schedule) setType() {
    92  	switch {
    93  	case !s.Once.IsZero():
    94  		s.Type = ScheduleOnce
    95  	case s.Cron != "":
    96  		s.Type = ScheduleCron
    97  	default:
    98  		s.Type = ScheduleImmediate
    99  	}
   100  }
   101  
   102  // NewScheduler returns a Scheduler ready to run jobs.
   103  func NewScheduler(db db.PGDBTX, w *Worker) *Scheduler {
   104  	c := cron.New()
   105  	c.Start()
   106  	return &Scheduler{
   107  		w:    w,
   108  		cron: c,
   109  		db:   db,
   110  	}
   111  }
   112  
   113  type Scheduler struct {
   114  	w    *Worker
   115  	cron *cron.Cron
   116  	db   db.PGDBTX
   117  }
   118  
   119  // Create schedules a job and records it in the database.
   120  func (s *Scheduler) Create(ctx context.Context, sched Schedule, workflowName string, params map[string]any) (row db.Schedule, err error) {
   121  	def := s.w.dh.Definition(workflowName)
   122  	if def == nil {
   123  		return row, fmt.Errorf("no workflow named %q", workflowName)
   124  	}
   125  	m, err := json.Marshal(params)
   126  	if err != nil {
   127  		return row, err
   128  	}
   129  	// Validate parameters against workflow definition before enqueuing.
   130  	params, err = UnmarshalWorkflow(string(m), def)
   131  	if err != nil {
   132  		return row, err
   133  	}
   134  	cronSched, err := sched.Parse()
   135  	if err != nil {
   136  		return row, err
   137  	}
   138  	err = s.db.BeginFunc(ctx, func(tx pgx.Tx) error {
   139  		now := time.Now()
   140  		q := db.New(tx)
   141  		row, err = q.CreateSchedule(ctx, db.CreateScheduleParams{
   142  			WorkflowName:   workflowName,
   143  			WorkflowParams: sql.NullString{String: string(m), Valid: len(m) > 0},
   144  			Once:           sched.Once,
   145  			Spec:           sched.Cron,
   146  			CreatedAt:      now,
   147  			UpdatedAt:      now,
   148  		})
   149  		if err != nil {
   150  			return err
   151  		}
   152  		return nil
   153  	})
   154  	s.cron.Schedule(cronSched, &WorkflowSchedule{Schedule: row, worker: s.w, Params: params})
   155  	return row, err
   156  }
   157  
   158  // Resume fetches schedules from the database and schedules them.
   159  func (s *Scheduler) Resume(ctx context.Context) error {
   160  	q := db.New(s.db)
   161  	rows, err := q.Schedules(ctx)
   162  	if err != nil {
   163  		return err
   164  	}
   165  	for _, row := range rows {
   166  		def := s.w.dh.Definition(row.WorkflowName)
   167  		if def == nil {
   168  			log.Printf("Unable to schedule %q (schedule.id: %d): no definition found", row.WorkflowName, row.ID)
   169  			continue
   170  		}
   171  		params, err := UnmarshalWorkflow(row.WorkflowParams.String, def)
   172  		if err != nil {
   173  			log.Printf("Error in UnmarshalWorkflow(%q, %q) for schedule %d: %q", row.WorkflowParams.String, row.WorkflowName, row.ID, err)
   174  			continue
   175  		}
   176  		sched := Schedule{Once: row.Once, Cron: row.Spec}
   177  		sched.setType()
   178  		if sched.Type == ScheduleOnce && row.Once.Before(time.Now()) {
   179  			log.Printf("Skipping %q Schedule (schedule.id: %d): %q is in the past", sched.Type, row.ID, sched.Once.String())
   180  			continue
   181  		}
   182  
   183  		cronSched, err := sched.Parse()
   184  		if err != nil {
   185  			log.Printf("Unable to schedule %q (schedule.id %d): invalid Schedule: %q", row.WorkflowName, row.ID, err)
   186  			continue
   187  		}
   188  
   189  		s.cron.Schedule(cronSched, &WorkflowSchedule{
   190  			Schedule: row,
   191  			Params:   params,
   192  			worker:   s.w,
   193  		})
   194  	}
   195  	return nil
   196  }
   197  
   198  // Entries returns a slice of active jobs.
   199  //
   200  // Entries are filtered by workflowNames. An empty slice returns
   201  // all entries.
   202  func (s *Scheduler) Entries(workflowNames ...string) []ScheduleEntry {
   203  	q := db.New(s.db)
   204  	rows, err := q.SchedulesLastRun(context.Background())
   205  	if err != nil {
   206  		log.Printf("q.SchedulesLastRun() = _, %q, wanted no error", err)
   207  	}
   208  	rowMap := make(map[int32]db.SchedulesLastRunRow)
   209  	for _, row := range rows {
   210  		rowMap[row.ID] = row
   211  	}
   212  	entries := s.cron.Entries()
   213  	ret := make([]ScheduleEntry, 0, len(entries))
   214  	for _, e := range s.cron.Entries() {
   215  		entry := ScheduleEntry{Entry: e}
   216  		if len(workflowNames) != 0 && !slices.Contains(workflowNames, entry.WorkflowJob().Schedule.WorkflowName) {
   217  			continue
   218  		}
   219  		if row, ok := rowMap[entry.WorkflowJob().Schedule.ID]; ok {
   220  			entry.LastRun = row
   221  		}
   222  		ret = append(ret, entry)
   223  	}
   224  	return ret
   225  }
   226  
   227  var ErrScheduleNotFound = errors.New("schedule not found")
   228  
   229  // Delete removes a schedule from the scheduler, preventing subsequent
   230  // runs, and deletes the schedule from the database.
   231  //
   232  // Jobs in progress are not interrupted, but will be prevented from
   233  // starting again.
   234  func (s *Scheduler) Delete(ctx context.Context, id int) error {
   235  	entries := s.Entries()
   236  	i := slices.IndexFunc(entries, func(e ScheduleEntry) bool { return int(e.WorkflowJob().Schedule.ID) == id })
   237  	if i == -1 {
   238  		return ErrScheduleNotFound
   239  	}
   240  	entry := entries[i]
   241  	s.cron.Remove(entry.ID)
   242  	return s.db.BeginFunc(ctx, func(tx pgx.Tx) error {
   243  		q := db.New(tx)
   244  		if _, err := q.ClearWorkflowSchedule(ctx, int32(id)); err != nil {
   245  			return err
   246  		}
   247  		if _, err := q.DeleteSchedule(ctx, int32(id)); err != nil {
   248  			return err
   249  		}
   250  		return nil
   251  	})
   252  }
   253  
   254  type ScheduleEntry struct {
   255  	cron.Entry
   256  	LastRun db.SchedulesLastRunRow
   257  }
   258  
   259  // type ScheduleEntry cron.Entry
   260  
   261  // WorkflowJob returns a *WorkflowSchedule for the ScheduleEntry.
   262  func (s *ScheduleEntry) WorkflowJob() *WorkflowSchedule {
   263  	return s.Job.(*WorkflowSchedule)
   264  }
   265  
   266  // WorkflowSchedule represents the data needed to create a Workflow.
   267  type WorkflowSchedule struct {
   268  	Schedule db.Schedule
   269  	Params   map[string]any
   270  	worker   *Worker
   271  }
   272  
   273  // Run starts a Workflow.
   274  func (w *WorkflowSchedule) Run() {
   275  	id, err := w.worker.StartWorkflow(context.Background(), w.Schedule.WorkflowName, w.Params, int(w.Schedule.ID))
   276  	log.Printf("StartWorkflow(_, %q, %v, %d) = %q, %q", w.Schedule.WorkflowName, w.Params, w.Schedule.ID, id, err)
   277  }
   278  
   279  // RunOnce is a cron.Schedule for running a job at a specific time.
   280  type RunOnce struct {
   281  	next time.Time
   282  }
   283  
   284  // Next returns the next time a job should run.
   285  func (r *RunOnce) Next(t time.Time) time.Time {
   286  	if t.After(r.next) {
   287  		return time.Time{}
   288  	}
   289  	return r.next
   290  }
   291  
   292  var _ cron.Schedule = &RunOnce{}