github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/periodic_task_manager.go (about)

     1  // Copyright 2022 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  	"crypto/sha256"
     9  	"fmt"
    10  	"io"
    11  	"sort"
    12  	"sync"
    13  	"time"
    14  )
    15  
    16  // PeriodicTaskManager manages scheduling of periodic tasks.
    17  // It syncs scheduler's entries by calling the config provider periodically.
    18  type PeriodicTaskManager struct {
    19  	s            *Scheduler
    20  	p            PeriodicTaskConfigProvider
    21  	syncInterval time.Duration
    22  	done         chan struct{}
    23  	wg           sync.WaitGroup
    24  	m            map[string]string // map[hash]entryID
    25  }
    26  
    27  type PeriodicTaskManagerOpts struct {
    28  	// Required: must be non nil
    29  	PeriodicTaskConfigProvider PeriodicTaskConfigProvider
    30  
    31  	// Required: must be non nil
    32  	RedisConnOpt RedisConnOpt
    33  
    34  	// Optional: scheduler options
    35  	*SchedulerOpts
    36  
    37  	// Optional: default is 3m
    38  	SyncInterval time.Duration
    39  }
    40  
    41  const defaultSyncInterval = 3 * time.Minute
    42  
    43  // NewPeriodicTaskManager returns a new PeriodicTaskManager instance.
    44  // The given opts should specify the RedisConnOp and PeriodicTaskConfigProvider at minimum.
    45  func NewPeriodicTaskManager(opts PeriodicTaskManagerOpts) (*PeriodicTaskManager, error) {
    46  	if opts.PeriodicTaskConfigProvider == nil {
    47  		return nil, fmt.Errorf("PeriodicTaskConfigProvider cannot be nil")
    48  	}
    49  	if opts.RedisConnOpt == nil {
    50  		return nil, fmt.Errorf("RedisConnOpt cannot be nil")
    51  	}
    52  	scheduler := NewScheduler(opts.RedisConnOpt, opts.SchedulerOpts)
    53  	syncInterval := opts.SyncInterval
    54  	if syncInterval == 0 {
    55  		syncInterval = defaultSyncInterval
    56  	}
    57  	return &PeriodicTaskManager{
    58  		s:            scheduler,
    59  		p:            opts.PeriodicTaskConfigProvider,
    60  		syncInterval: syncInterval,
    61  		done:         make(chan struct{}),
    62  		m:            make(map[string]string),
    63  	}, nil
    64  }
    65  
    66  // PeriodicTaskConfigProvider provides configs for periodic tasks.
    67  // GetConfigs will be called by a PeriodicTaskManager periodically to
    68  // sync the scheduler's entries with the configs returned by the provider.
    69  type PeriodicTaskConfigProvider interface {
    70  	GetConfigs() ([]*PeriodicTaskConfig, error)
    71  }
    72  
    73  // PeriodicTaskConfig specifies the details of a periodic task.
    74  type PeriodicTaskConfig struct {
    75  	Cronspec string   // required: must be non empty string
    76  	Task     *Task    // required: must be non nil
    77  	Opts     []Option // optional: can be nil
    78  }
    79  
    80  func (c *PeriodicTaskConfig) hash() string {
    81  	h := sha256.New()
    82  	io.WriteString(h, c.Cronspec)
    83  	io.WriteString(h, c.Task.Type())
    84  	h.Write(c.Task.Payload())
    85  	opts := stringifyOptions(c.Opts)
    86  	sort.Strings(opts)
    87  	for _, opt := range opts {
    88  		io.WriteString(h, opt)
    89  	}
    90  	return fmt.Sprintf("%x", h.Sum(nil))
    91  }
    92  
    93  func validatePeriodicTaskConfig(c *PeriodicTaskConfig) error {
    94  	if c == nil {
    95  		return fmt.Errorf("PeriodicTaskConfig cannot be nil")
    96  	}
    97  	if c.Task == nil {
    98  		return fmt.Errorf("PeriodicTaskConfig.Task cannot be nil")
    99  	}
   100  	if c.Cronspec == "" {
   101  		return fmt.Errorf("PeriodicTaskConfig.Cronspec cannot be empty")
   102  	}
   103  	return nil
   104  }
   105  
   106  func (mgr *PeriodicTaskManager) ID() (id string) {
   107  	if mgr == nil {
   108  		return
   109  	}
   110  	return mgr.s.id
   111  }
   112  
   113  // Start starts a scheduler and background goroutine to sync the scheduler with the configs
   114  // returned by the provider.
   115  //
   116  // Start returns any error encountered at start up time.
   117  func (mgr *PeriodicTaskManager) Start() error {
   118  	if mgr.s == nil || mgr.p == nil {
   119  		panic("asynq: cannot start uninitialized PeriodicTaskManager; use NewPeriodicTaskManager to initialize")
   120  	}
   121  	if err := mgr.initialSync(); err != nil {
   122  		return fmt.Errorf("asynq: %v", err)
   123  	}
   124  	if err := mgr.s.Start(); err != nil {
   125  		return fmt.Errorf("asynq: %v", err)
   126  	}
   127  	mgr.wg.Add(1)
   128  	go func() {
   129  		defer mgr.wg.Done()
   130  		ticker := time.NewTicker(mgr.syncInterval)
   131  		for {
   132  			select {
   133  			case <-mgr.done:
   134  				mgr.s.logger.Debugf("[Common] asynq stopping syncer goroutine")
   135  				ticker.Stop()
   136  				return
   137  			case <-ticker.C:
   138  				mgr.sync()
   139  			}
   140  		}
   141  	}()
   142  	return nil
   143  }
   144  
   145  // Shutdown gracefully shuts down the manager.
   146  // It notifies a background syncer goroutine to stop and stops scheduler.
   147  func (mgr *PeriodicTaskManager) Shutdown() {
   148  	close(mgr.done)
   149  	mgr.wg.Wait()
   150  	mgr.s.Shutdown()
   151  }
   152  
   153  // Run starts the manager and blocks until an os signal to exit the program is received.
   154  // Once it receives a signal, it gracefully shuts down the manager.
   155  func (mgr *PeriodicTaskManager) Run() error {
   156  	if err := mgr.Start(); err != nil {
   157  		return err
   158  	}
   159  	mgr.s.waitForSignals()
   160  	mgr.Shutdown()
   161  	mgr.s.logger.Debugf("[Common] asynq periodic task manager exiting")
   162  	return nil
   163  }
   164  
   165  func (mgr *PeriodicTaskManager) initialSync() error {
   166  	configs, err := mgr.p.GetConfigs()
   167  	if err != nil {
   168  		return fmt.Errorf("initial call to GetConfigs failed: %v", err)
   169  	}
   170  	for _, c := range configs {
   171  		if err := validatePeriodicTaskConfig(c); err != nil {
   172  			return fmt.Errorf("initial call to GetConfigs contained an invalid config: %v", err)
   173  		}
   174  	}
   175  	mgr.add(configs)
   176  	return nil
   177  }
   178  
   179  func (mgr *PeriodicTaskManager) add(configs []*PeriodicTaskConfig) {
   180  	for _, c := range configs {
   181  		entryID, err := mgr.s.Register(c.Cronspec, c.Task, c.Opts...)
   182  		if err != nil {
   183  			mgr.s.logger.Errorf("[Common] asynq failed to register periodic task: cronspec=%q task=%q",
   184  				c.Cronspec, c.Task.Type())
   185  			continue
   186  		}
   187  		mgr.m[c.hash()] = entryID
   188  		mgr.s.logger.Infof("[Common] asynq registered periodic task successfully: cronspec=%q task=%q, entryID=%s",
   189  			c.Cronspec, c.Task.Type(), entryID)
   190  	}
   191  }
   192  
   193  func (mgr *PeriodicTaskManager) remove(removed map[string]string) {
   194  	for hash, entryID := range removed {
   195  		if err := mgr.s.Unregister(entryID); err != nil {
   196  			mgr.s.logger.Errorf("[Common] asynq failed to unregister periodic task: %v", err)
   197  			continue
   198  		}
   199  		delete(mgr.m, hash)
   200  		mgr.s.logger.Infof("[Common] asynq unregistered periodic task successfully: entryID=%s", entryID)
   201  	}
   202  }
   203  
   204  func (mgr *PeriodicTaskManager) sync() {
   205  	configs, err := mgr.p.GetConfigs()
   206  	if err != nil {
   207  		mgr.s.logger.Errorf("[Common] asynq failed to get periodic task configs: %v", err)
   208  		return
   209  	}
   210  	for _, c := range configs {
   211  		if err := validatePeriodicTaskConfig(c); err != nil {
   212  			mgr.s.logger.Errorf("[Common] asynq failed to sync: GetConfigs returned an invalid config: %v", err)
   213  			return
   214  		}
   215  	}
   216  	// Diff and only register/unregister the newly added/removed entries.
   217  	removed := mgr.diffRemoved(configs)
   218  	added := mgr.diffAdded(configs)
   219  	mgr.remove(removed)
   220  	mgr.add(added)
   221  }
   222  
   223  // diffRemoved diffs the incoming configs with the registered config and returns
   224  // a map containing hash and entryID of each config that was removed.
   225  func (mgr *PeriodicTaskManager) diffRemoved(configs []*PeriodicTaskConfig) map[string]string {
   226  	newConfigs := make(map[string]string)
   227  	for _, c := range configs {
   228  		newConfigs[c.hash()] = "" // empty value since we don't have entryID yet
   229  	}
   230  	removed := make(map[string]string)
   231  	for k, v := range mgr.m {
   232  		// test whether existing config is present in the incoming configs
   233  		if _, found := newConfigs[k]; !found {
   234  			removed[k] = v
   235  		}
   236  	}
   237  	return removed
   238  }
   239  
   240  // diffAdded diffs the incoming configs with the registered configs and returns
   241  // a list of configs that were added.
   242  func (mgr *PeriodicTaskManager) diffAdded(configs []*PeriodicTaskConfig) []*PeriodicTaskConfig {
   243  	var added []*PeriodicTaskConfig
   244  	for _, c := range configs {
   245  		if _, found := mgr.m[c.hash()]; !found {
   246  			added = append(added, c)
   247  		}
   248  	}
   249  	return added
   250  }