github.com/ngicks/gokugen@v0.0.5/cron/cron_like_rescheduler.go (about) 1 package cron 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "sync/atomic" 9 "time" 10 11 "github.com/ngicks/gokugen" 12 ) 13 14 var ( 15 ErrOnceTask = errors.New("task returned same schedule time") 16 ErrNonexistentWorkId = errors.New("nonexistent work id") 17 ErrStillWorking = errors.New("task is still working") 18 ErrAlreadyScheduled = errors.New("task is already scheduled") 19 ) 20 21 type WorkFnWParam = gokugen.WorkFnWParam 22 type WorkRegistry interface { 23 Load(key string) (value WorkFnWParam, ok bool) 24 } 25 26 type Scheduler interface { 27 Schedule(ctx gokugen.SchedulerContext) (gokugen.Task, error) 28 } 29 30 type RowLike interface { 31 NextScheduler 32 GetCommand() []string 33 } 34 35 // CronLikeRescheduler schedules command of RowLike according to its configuration. 36 type CronLikeRescheduler struct { 37 mu sync.Mutex 38 err error 39 scheduler Scheduler 40 row RowLike 41 state *ScheduleState 42 scheduled bool 43 isWorking int64 44 ongoingTask gokugen.Task 45 shouldReschedule func(workErr error, callCount int) bool 46 workRegistry WorkRegistry 47 } 48 49 func NewCronLikeRescheduler( 50 rowLike RowLike, 51 whence time.Time, 52 shouldReschedule func(workErr error, callCount int) bool, 53 scheduler Scheduler, 54 workRegistry WorkRegistry, 55 ) *CronLikeRescheduler { 56 return &CronLikeRescheduler{ 57 row: rowLike, 58 state: NewScheduleState(rowLike, whence), 59 scheduler: scheduler, 60 shouldReschedule: shouldReschedule, 61 workRegistry: workRegistry, 62 } 63 } 64 65 // Schedule starts scheduling. 66 // If shouldReschedule is non nil and if it returns true, 67 // Rowlike would be rescheduled to its next time. 68 // 69 // - ErrStillWorking is returned if task c created is still being worked on. 70 // - Schedule right after Cancel may cause this state. No overlapping schedule is not allowed. 71 // - ErrAlreadyScheduled is returned if second or more call is without preceding Cancel. 72 // - ErrOnceTask is returned if RowLike is once task. 73 // - c is returned if command of RowLike is invalid. 74 // - ErrNonexistentWorkId is returned when command does not exist in WorkRegistry. 75 // - Scheduler's error is returned when Schedule returns error. 76 // 77 // ErrOnceTask, ErrOnceTask and Scheduler's error are sticky. 78 // Once Schedule returned it, Schedule always return that error. 79 // 80 // All error may or may not be wrapped. User should use errors.Is(). 81 func (c *CronLikeRescheduler) Schedule() error { 82 c.mu.Lock() 83 defer c.mu.Unlock() 84 85 if c.err != nil { 86 return c.err 87 } 88 89 if atomic.LoadInt64(&c.isWorking) == 1 { 90 // Schedule right after Cancel may lead to this state. 91 // Overlapping scheduling is not allowed. 92 return ErrStillWorking 93 } 94 95 if c.ongoingTask != nil || c.scheduled { 96 return ErrAlreadyScheduled 97 } 98 c.scheduled = true 99 100 c.mu.Unlock() 101 // defer-ing in case of runtime panic. 102 defer c.mu.Lock() 103 // schedule takes lock by itself. 104 err := c.schedule() 105 return err 106 } 107 108 func (c *CronLikeRescheduler) schedule() error { 109 c.mu.Lock() 110 defer c.mu.Unlock() 111 112 if errors.Is(c.err, ErrOnceTask) { 113 return nil 114 } else if c.err != nil { 115 return c.err 116 } 117 118 same, callCount, next := c.state.Next() 119 if same { 120 c.err = ErrOnceTask 121 } 122 123 command := c.row.GetCommand() 124 if command == nil || len(command) == 0 { 125 c.err = ErrMalformed 126 return c.err 127 } 128 129 workRaw, ok := c.workRegistry.Load(command[0]) 130 if !ok { 131 c.err = fmt.Errorf("%w: workId = %s", ErrNonexistentWorkId, command[0]) 132 return c.err 133 } 134 135 task, err := c.scheduler.Schedule( 136 gokugen.BuildContext( 137 next, 138 nil, 139 nil, 140 gokugen.WithWorkId(command[0]), 141 gokugen.WithParam(command[1:]), 142 gokugen.WithWorkFn( 143 func(taskCtx context.Context, scheduled time.Time) (any, error) { 144 atomic.StoreInt64(&c.isWorking, 1) 145 defer atomic.StoreInt64(&c.isWorking, 0) 146 147 ret, err := workRaw(taskCtx, scheduled, command[1:]) 148 if c.shouldReschedule != nil && c.shouldReschedule(err, callCount) { 149 c.schedule() 150 } 151 return ret, err 152 }, 153 ), 154 ), 155 ) 156 if err != nil { 157 c.err = err 158 return c.err 159 } 160 if task != nil { 161 c.ongoingTask = task 162 } 163 return nil 164 } 165 166 // Cancel stops scheduling of c. 167 // If a task is being worked on at the time Cancel is called, 168 // this also cancels task itself. 169 func (c *CronLikeRescheduler) Cancel() (cancelled bool) { 170 c.mu.Lock() 171 defer c.mu.Unlock() 172 173 c.scheduled = false 174 if c.ongoingTask != nil { 175 cancelled := c.ongoingTask.Cancel() 176 c.ongoingTask = nil 177 return cancelled 178 } 179 return 180 }