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  }