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 }