github.com/ngicks/gokugen@v0.0.5/scheduler/worker.go (about) 1 package scheduler 2 3 import ( 4 "context" 5 "sync" 6 ) 7 8 // Worker represents a single task executor. 9 // It will work on a single task at a time. 10 // It may be in stopped-state where loop is stopped, 11 // working-state where looping in goroutine, 12 // or ended-state where no way is given to step into working-state again. 13 type Worker[T any] struct { 14 workingState 15 endState 16 mu sync.Mutex 17 id T 18 stopCh chan struct{} 19 killCh chan struct{} 20 taskCh <-chan *Task 21 onTaskReceived func() 22 onTaskDone func() 23 cancel func() 24 } 25 26 func NewWorker[T any](id T, taskCh <-chan *Task, taskReceived, taskDone func()) (*Worker[T], error) { 27 if taskCh == nil { 28 return nil, ErrInvalidArg 29 } 30 31 if taskReceived == nil { 32 taskReceived = func() {} 33 } 34 if taskDone == nil { 35 taskDone = func() {} 36 } 37 38 worker := &Worker[T]{ 39 id: id, 40 stopCh: make(chan struct{}, 1), 41 killCh: make(chan struct{}), 42 taskCh: taskCh, 43 onTaskReceived: taskReceived, 44 onTaskDone: taskDone, 45 cancel: func() {}, 46 } 47 return worker, nil 48 } 49 50 // Start starts worker loop. So it would block long. 51 // 52 // If worker is already ended, it returns `ErrAlreadyEnded`. 53 // If worker is already started, it returns `ErrAlreadyStarted`. 54 // If taskCh is closed, Start returns nil, becoming ended-state. 55 func (w *Worker[T]) Start() (err error) { 56 if w.IsEnded() { 57 return ErrAlreadyEnded 58 } 59 if !w.setWorking() { 60 return ErrAlreadyStarted 61 } 62 defer w.setWorking(false) 63 64 defer func() { 65 select { 66 case <-w.stopCh: 67 default: 68 } 69 }() 70 71 var normalReturn bool 72 defer func() { 73 if !normalReturn { 74 w.setEnded() 75 } 76 }() 77 78 LOOP: 79 for { 80 select { 81 case <-w.killCh: 82 // in case of racy kill 83 break LOOP 84 case <-w.stopCh: 85 break LOOP 86 default: 87 select { 88 case <-w.stopCh: 89 break LOOP 90 case task, ok := <-w.taskCh: 91 if !ok { 92 w.setEnded() 93 break LOOP 94 } 95 func() { 96 ctx, cancel := context.WithCancel(context.Background()) 97 defer cancel() 98 99 w.mu.Lock() 100 w.cancel = cancel 101 w.mu.Unlock() 102 103 select { 104 // in case of racy kill 105 case <-w.killCh: 106 return 107 default: 108 } 109 110 w.onTaskReceived() 111 defer w.onTaskDone() 112 task.Do(ctx) 113 }() 114 } 115 } 116 } 117 // If task exits abnormally, called runtime.Goexit or panicking, it would not reach this line. 118 normalReturn = true 119 return 120 } 121 122 // Stop stops an active Start loop. 123 // If Start is not in use when Stop is called, 124 // it will stops next Start immediately. 125 func (w *Worker[T]) Stop() { 126 select { 127 case <-w.stopCh: 128 default: 129 } 130 w.stopCh <- struct{}{} 131 return 132 } 133 134 // Kill kills this worker. 135 // If a task is being worked at the time of invocation, 136 // a contex passed to the task will be cancelled immediately. 137 // Kill makes this worker to step into ended state, making it impossible to Start-ed again. 138 func (w *Worker[T]) Kill() { 139 if w.setEnded() { 140 close(w.killCh) 141 } 142 143 w.mu.Lock() 144 w.cancel() 145 w.mu.Unlock() 146 147 w.Stop() 148 } 149 150 func (w *Worker[T]) Id() T { 151 return w.id 152 }