github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/scheduler.go (about) 1 // Copyright 2020 Kentaro Hibino. All rights reserved. 2 // Use of this source code is governed by a MIT license 3 // that can be found in the LICENSE file. 4 5 package asynq 6 7 import ( 8 "fmt" 9 "os" 10 "sync" 11 "time" 12 13 "github.com/google/uuid" 14 "github.com/redis/go-redis/v9" 15 "github.com/robfig/cron/v3" 16 "github.com/wfusion/gofusion/common/infra/asynq/pkg/base" 17 "github.com/wfusion/gofusion/common/infra/asynq/pkg/log" 18 "github.com/wfusion/gofusion/common/infra/asynq/pkg/rdb" 19 ) 20 21 // A Scheduler kicks off tasks at regular intervals based on the user defined schedule. 22 // 23 // Schedulers are safe for concurrent use by multiple goroutines. 24 type Scheduler struct { 25 id string 26 27 state *serverState 28 29 logger *log.Logger 30 client *Client 31 rdb *rdb.RDB 32 cron *cron.Cron 33 location *time.Location 34 done chan struct{} 35 wg sync.WaitGroup 36 disableRedisConnClose bool 37 preEnqueueFunc func(task *Task, opts []Option) (err error) 38 postEnqueueFunc func(info *TaskInfo, err error) 39 errHandler func(task *Task, opts []Option, err error) 40 41 // guards idmap 42 mu sync.Mutex 43 // idmap maps Scheduler's entry ID to cron.EntryID 44 // to avoid using cron.EntryID as the public API of 45 // the Scheduler. 46 idmap map[string]cron.EntryID 47 } 48 49 // NewScheduler returns a new Scheduler instance given the redis connection option. 50 // The parameter opts is optional, defaults will be used if opts is set to nil 51 func NewScheduler(r RedisConnOpt, opts *SchedulerOpts) *Scheduler { 52 c, ok := r.MakeRedisClient().(redis.UniversalClient) 53 if !ok { 54 panic(fmt.Sprintf("asynq: unsupported RedisConnOpt type %T", r)) 55 } 56 if opts == nil { 57 opts = &SchedulerOpts{} 58 } 59 60 logger := log.NewLogger(opts.Logger) 61 loglevel := opts.LogLevel 62 if loglevel == level_unspecified { 63 loglevel = InfoLevel 64 } 65 logger.SetLevel(toInternalLogLevel(loglevel)) 66 67 loc := opts.Location 68 if loc == nil { 69 loc = time.UTC 70 } 71 72 return &Scheduler{ 73 id: generateSchedulerID(), 74 state: &serverState{value: srvStateNew}, 75 logger: logger, 76 client: NewClient(r), 77 rdb: rdb.NewRDB(c), 78 disableRedisConnClose: opts.DisableRedisConnClose, 79 cron: cron.New(cron.WithLocation(loc)), 80 location: loc, 81 done: make(chan struct{}), 82 preEnqueueFunc: opts.PreEnqueueFunc, 83 postEnqueueFunc: opts.PostEnqueueFunc, 84 errHandler: opts.EnqueueErrorHandler, 85 idmap: make(map[string]cron.EntryID), 86 } 87 } 88 89 func generateSchedulerID() string { 90 host, err := os.Hostname() 91 if err != nil { 92 host = "unknown-host" 93 } 94 return fmt.Sprintf("%s:%d:%v", host, os.Getpid(), uuid.New()) 95 } 96 97 // SchedulerOpts specifies scheduler options. 98 type SchedulerOpts struct { 99 // Logger specifies the logger used by the scheduler instance. 100 // 101 // If unset, the default logger is used. 102 Logger Logger 103 104 // LogLevel specifies the minimum log level to enable. 105 // 106 // If unset, InfoLevel is used by default. 107 LogLevel LogLevel 108 109 // Location specifies the time zone location. 110 // 111 // If unset, the UTC time zone (time.UTC) is used. 112 Location *time.Location 113 114 // PreEnqueueFunc, if provided, is called before a task gets enqueued by Scheduler. 115 // The callback function should return quickly to not block the current thread. 116 PreEnqueueFunc func(task *Task, opts []Option) (err error) 117 118 // PostEnqueueFunc, if provided, is called after a task gets enqueued by Scheduler. 119 // The callback function should return quickly to not block the current thread. 120 PostEnqueueFunc func(info *TaskInfo, err error) 121 122 // Deprecated: Use PostEnqueueFunc instead 123 // EnqueueErrorHandler gets called when scheduler cannot enqueue a registered task 124 // due to an error. 125 EnqueueErrorHandler func(task *Task, opts []Option, err error) 126 127 DisableRedisConnClose bool 128 } 129 130 // enqueueJob encapsulates the job of enqueuing a task and recording the event. 131 type enqueueJob struct { 132 id uuid.UUID 133 cronspec string 134 task *Task 135 opts []Option 136 location *time.Location 137 logger *log.Logger 138 client *Client 139 rdb *rdb.RDB 140 preEnqueueFunc func(task *Task, opts []Option) (err error) 141 postEnqueueFunc func(info *TaskInfo, err error) 142 errHandler func(task *Task, opts []Option, err error) 143 } 144 145 func (j *enqueueJob) Run() { 146 var ( 147 info *TaskInfo 148 err error 149 ) 150 if j.preEnqueueFunc != nil { 151 err = j.preEnqueueFunc(j.task, j.opts) 152 } 153 if err == nil { 154 info, err = j.client.Enqueue(j.task, j.opts...) 155 } 156 if j.postEnqueueFunc != nil { 157 j.postEnqueueFunc(info, err) 158 } 159 if err != nil { 160 if j.errHandler != nil { 161 j.errHandler(j.task, j.opts, err) 162 } 163 return 164 } 165 j.logger.Debugf("scheduler enqueued a task: %+v", info) 166 event := &base.SchedulerEnqueueEvent{ 167 TaskID: info.ID, 168 EnqueuedAt: time.Now().In(j.location), 169 } 170 err = j.rdb.RecordSchedulerEnqueueEvent(j.id.String(), event) 171 if err != nil { 172 j.logger.Warnf("scheduler could not record enqueue event of enqueued task %s: %v", info.ID, err) 173 } 174 } 175 176 // Register registers a task to be enqueued on the given schedule specified by the cronspec. 177 // It returns an ID of the newly registered entry. 178 func (s *Scheduler) Register(cronspec string, task *Task, opts ...Option) (entryID string, err error) { 179 job := &enqueueJob{ 180 id: uuid.New(), 181 cronspec: cronspec, 182 task: task, 183 opts: opts, 184 location: s.location, 185 client: s.client, 186 rdb: s.rdb, 187 logger: s.logger, 188 preEnqueueFunc: s.preEnqueueFunc, 189 postEnqueueFunc: s.postEnqueueFunc, 190 errHandler: s.errHandler, 191 } 192 cronID, err := s.cron.AddJob(cronspec, job) 193 if err != nil { 194 return "", err 195 } 196 s.mu.Lock() 197 s.idmap[job.id.String()] = cronID 198 s.mu.Unlock() 199 return job.id.String(), nil 200 } 201 202 // Unregister removes a registered entry by entry ID. 203 // Unregister returns a non-nil error if no entries were found for the given entryID. 204 func (s *Scheduler) Unregister(entryID string) error { 205 s.mu.Lock() 206 defer s.mu.Unlock() 207 cronID, ok := s.idmap[entryID] 208 if !ok { 209 return fmt.Errorf("asynq: no scheduler entry found") 210 } 211 delete(s.idmap, entryID) 212 s.cron.Remove(cronID) 213 return nil 214 } 215 216 // Run starts the scheduler until an os signal to exit the program is received. 217 // It returns an error if scheduler is already running or has been shutdown. 218 func (s *Scheduler) Run() error { 219 if err := s.Start(); err != nil { 220 return err 221 } 222 s.waitForSignals() 223 s.Shutdown() 224 return nil 225 } 226 227 // Start starts the scheduler. 228 // It returns an error if the scheduler is already running or has been shutdown. 229 func (s *Scheduler) Start() error { 230 if err := s.start(); err != nil { 231 return err 232 } 233 s.logger.Info("[Common] asynq scheduler starting") 234 s.logger.Infof("[Common] asynq scheduler timezone is set to %v", s.location) 235 s.cron.Start() 236 s.wg.Add(1) 237 go s.runHeartbeater() 238 return nil 239 } 240 241 // Checks server state and returns an error if pre-condition is not met. 242 // Otherwise it sets the server state to active. 243 func (s *Scheduler) start() error { 244 s.state.mu.Lock() 245 defer s.state.mu.Unlock() 246 switch s.state.value { 247 case srvStateActive: 248 return fmt.Errorf("asynq: the scheduler is already running") 249 case srvStateClosed: 250 return fmt.Errorf("asynq: the scheduler has already been stopped") 251 } 252 s.state.value = srvStateActive 253 return nil 254 } 255 256 // Shutdown stops and shuts down the scheduler. 257 func (s *Scheduler) Shutdown() { 258 s.state.mu.Lock() 259 if s.state.value == srvStateNew || s.state.value == srvStateClosed { 260 // scheduler is not running, do nothing and return. 261 s.state.mu.Unlock() 262 return 263 } 264 s.state.value = srvStateClosed 265 s.state.mu.Unlock() 266 267 s.logger.Info("[Common] asynq scheduler shutting down") 268 close(s.done) // signal heartbeater to stop 269 ctx := s.cron.Stop() 270 <-ctx.Done() 271 s.wg.Wait() 272 273 s.clearHistory() 274 275 if !s.disableRedisConnClose { 276 _ = s.client.Close() 277 _ = s.rdb.Close() 278 } 279 280 s.logger.Info("[Common] asynq scheduler stopped") 281 } 282 283 func (s *Scheduler) runHeartbeater() { 284 defer s.wg.Done() 285 ticker := time.NewTicker(5 * time.Second) 286 for { 287 select { 288 case <-s.done: 289 s.logger.Debugf("[Common] asynq scheduler heatbeater shutting down") 290 _ = s.rdb.ClearSchedulerEntries(s.id) 291 ticker.Stop() 292 return 293 case <-ticker.C: 294 s.beat() 295 } 296 } 297 } 298 299 // beat writes a snapshot of entries to redis. 300 func (s *Scheduler) beat() { 301 var entries []*base.SchedulerEntry 302 for _, entry := range s.cron.Entries() { 303 job := entry.Job.(*enqueueJob) 304 e := &base.SchedulerEntry{ 305 ID: job.id.String(), 306 Spec: job.cronspec, 307 Type: job.task.Type(), 308 Payload: job.task.Payload(), 309 Opts: stringifyOptions(job.opts), 310 Next: entry.Next, 311 Prev: entry.Prev, 312 } 313 entries = append(entries, e) 314 } 315 s.logger.Debugf("[Common] asynq writing entries %v", entries) 316 if err := s.rdb.WriteSchedulerEntries(s.id, entries, 5*time.Second); err != nil { 317 s.logger.Warnf("[Common] asynq scheduler could not write heartbeat data: %v", err) 318 } 319 } 320 321 func stringifyOptions(opts []Option) []string { 322 var res []string 323 for _, opt := range opts { 324 res = append(res, opt.String()) 325 } 326 return res 327 } 328 329 func (s *Scheduler) clearHistory() { 330 for _, entry := range s.cron.Entries() { 331 job := entry.Job.(*enqueueJob) 332 if err := s.rdb.ClearSchedulerHistory(job.id.String()); err != nil { 333 s.logger.Warnf("[Common] asynq could not clear scheduler history for entry %q: %v", 334 job.id.String(), err) 335 } 336 } 337 }