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{}