github.com/mailru/activerecord@v1.12.2/pkg/iproto/syncutil/taskgroup.go (about) 1 package syncutil 2 3 import ( 4 "sync" 5 6 "golang.org/x/net/context" 7 ) 8 9 // TaskGroup helps to control execution flow of repeatable tasks. 10 // It is intended to execute at most N tasks at one time. 11 type TaskGroup struct { 12 // N is a maximum number of tasks TaskGroup can allow to execute. 13 // If N is zero then TaskGroup with value 1 is used by default. 14 N int 15 16 // Goer starts a goroutine which executes a given task. 17 // It is useful when client using some pool of goroutines. 18 // 19 // If nil, then default `go` is used and context is ignored. 20 // 21 // Non-nil error from Goer means that some resources are temporarily 22 // unavailable and given task will not be executed. 23 // 24 // Note that for goroutine pool implementations it is required for pool to 25 // have at least capacity of N goroutines. In other way deadlock may occur. 26 Goer GoerFn 27 28 mu sync.Mutex 29 once sync.Once 30 n int 31 pending []chan error 32 cancel func() 33 } 34 35 func (t *TaskGroup) init() { 36 t.once.Do(func() { 37 if t.N == 0 { 38 t.N = 1 39 } 40 41 t.pending = make([]chan error, t.N) 42 }) 43 } 44 45 // Do executes given function task in separate goroutine n minus <currently 46 // running tasks number> times. It returns slice of n channels which 47 // fulfillment means the end of appropriate task execution. 48 // 49 // That is, for m already running tasks Do(n, n < m) will return n channels 50 // referring to a previously spawned task goroutines. 51 // 52 // All currenlty executing tasks can be signaled to cancel by calling 53 // TaskGroup's Cancel() method. 54 // 55 // nolint:gocognit 56 func (t *TaskGroup) Do(ctx context.Context, n int, task func(context.Context, int) error) []<-chan error { 57 t.init() 58 59 if n > t.N { 60 n = t.N 61 } 62 63 ret := make([]<-chan error, 0, n) 64 65 t.mu.Lock() 66 defer t.mu.Unlock() 67 68 if exec := n - t.n; exec > 0 { 69 // Start remaining tasks. 70 subctx, cancel := context.WithCancel(ctx) 71 // Append current call context to previous. 72 prev := t.cancel 73 t.cancel = func() { 74 if prev != nil { 75 prev() 76 } 77 78 cancel() 79 } 80 81 for i := 0; i < exec; i++ { 82 var j int 83 84 for ; j < len(t.pending); j++ { 85 if t.pending[j] != nil { 86 // Filter out already active "promises". 87 continue 88 } 89 90 break 91 } 92 93 done := make(chan error, 1) 94 err := goer(ctx, t.Goer, func() { 95 done <- task(subctx, j) 96 97 t.mu.Lock() 98 defer t.mu.Unlock() 99 100 exec-- 101 if exec == 0 { 102 // Cancel current sub context. 103 cancel() 104 } 105 if t.pending[j] == done { 106 // Current activity was not canceled. 107 t.pending[j] = nil 108 t.n-- 109 if t.n == 0 { 110 t.cancel = nil 111 } 112 } 113 }) 114 115 if err != nil { 116 // Spawn goroutine error. Fulfill channel immediately. 117 done <- err 118 } else { 119 t.pending[j] = done 120 t.n++ 121 } 122 } 123 } 124 125 for i := 0; i < len(t.pending) && len(ret) < n; i++ { 126 if t.pending[i] == nil { 127 continue 128 } 129 130 ret = append(ret, t.pending[i]) 131 } 132 133 return ret 134 } 135 136 // Cancel cancels context of all currently running tasks. Further Do() calls 137 // will not be blocked on waiting for exit of previous tasks. 138 func (t *TaskGroup) Cancel() { 139 t.init() 140 141 t.mu.Lock() 142 defer t.mu.Unlock() 143 144 if t.cancel != nil { 145 t.cancel() 146 t.cancel = nil 147 } 148 149 for i := range t.pending { 150 // NOTE: Do not close the pending channel. 151 // It will be closed by a task runner. 152 // 153 // Set to nil to prevent memory leaks. 154 t.pending[i] = nil 155 } 156 157 t.n = 0 158 }