github.com/hasnat/dolt/go@v0.0.0-20210628190320-9eb5d843fbb7/libraries/utils/async/action_executor.go (about) 1 // Copyright 2020 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package async 16 17 import ( 18 "container/list" 19 "context" 20 "fmt" 21 "sync" 22 ) 23 24 // Action is the function called by an ActionExecutor on each given value. 25 type Action func(ctx context.Context, val interface{}) error 26 27 // ActionExecutor is designed for asynchronous workloads that should run when a new task is available. The closest analog 28 // would be to have a long-running goroutine that receives from a channel, however ActionExecutor provides three major 29 // points of differentiation. The first is that there is no need to close the queue, as goroutines automatically exit 30 // when the queue is empty. The second is that a concurrency parameter may be set, that will spin up goroutines as 31 // needed until the maximum number is attained. The third is that you don't have to declare the buffer size beforehand 32 // as with channels, allowing the queue to respond to demand. You may declare a max buffer though, for RAM-limited 33 // situations, which then blocks appends until the buffer is below the max given. 34 type ActionExecutor struct { 35 action Action 36 ctx context.Context 37 concurrency uint32 38 err error 39 finished *sync.WaitGroup 40 linkedList *list.List 41 running uint32 42 maxBuffer uint64 43 syncCond *sync.Cond 44 } 45 46 // NewActionExecutor returns an ActionExecutor that will run the given action on each appended value, and run up to the max 47 // number of goroutines as defined by concurrency. If concurrency is 0, then it is set to 1. If maxBuffer is 0, then it 48 // is unlimited. Panics on a nil action. 49 func NewActionExecutor(ctx context.Context, action Action, concurrency uint32, maxBuffer uint64) *ActionExecutor { 50 if action == nil { 51 panic("action cannot be nil") 52 } 53 if concurrency == 0 { 54 concurrency = 1 55 } 56 return &ActionExecutor{ 57 action: action, 58 concurrency: concurrency, 59 ctx: ctx, 60 finished: &sync.WaitGroup{}, 61 linkedList: list.New(), 62 running: 0, 63 maxBuffer: maxBuffer, 64 syncCond: sync.NewCond(&sync.Mutex{}), 65 } 66 } 67 68 type work struct { 69 val interface{} 70 wg *sync.WaitGroup 71 } 72 73 // Execute adds the value to the end of the queue to be executed. If any action encountered an error before this call, 74 // then the value is not added and this returns immediately. 75 func (aq *ActionExecutor) Execute(val interface{}) { 76 aq.syncCond.L.Lock() 77 defer aq.syncCond.L.Unlock() 78 79 if aq.err != nil { // If we've errored before, then no point in running anything again until we return the error. 80 return 81 } 82 83 for aq.maxBuffer != 0 && uint64(aq.linkedList.Len()) >= aq.maxBuffer { 84 aq.syncCond.Wait() 85 } 86 aq.finished.Add(1) 87 aq.linkedList.PushBack(work{val, aq.finished}) 88 89 if aq.running < aq.concurrency { 90 aq.running++ 91 go aq.work() 92 } 93 } 94 95 // WaitForEmpty waits until all the work that has been submitted before 96 // the call to |WaitForEmpty| has completed. It returns any errors that 97 // any actions may have encountered. 98 func (aq *ActionExecutor) WaitForEmpty() error { 99 aq.syncCond.L.Lock() 100 wg := aq.finished 101 aq.finished = &sync.WaitGroup{} 102 aq.syncCond.L.Unlock() 103 wg.Wait() 104 aq.syncCond.L.Lock() 105 defer aq.syncCond.L.Unlock() 106 err := aq.err 107 aq.err = nil 108 return err 109 } 110 111 // work runs until the list is empty. If any error occurs from any action, then we do not call any further actions, 112 // although we still iterate over the list and clear it. 113 func (aq *ActionExecutor) work() { 114 for { 115 aq.syncCond.L.Lock() // check element list and valid state, so we lock 116 117 element := aq.linkedList.Front() 118 if element == nil { 119 aq.running-- 120 aq.syncCond.L.Unlock() // early exit, so we unlock 121 return // we don't signal here since the buffer is empty, hence the return in the first place 122 } 123 _ = aq.linkedList.Remove(element) 124 encounteredError := aq.err != nil 125 126 aq.syncCond.Signal() // if an append is waiting because of a full buffer, we signal for it to continue 127 aq.syncCond.L.Unlock() // done checking list and state, so we unlock 128 129 if !encounteredError { 130 var err error 131 func() { // this func is to capture a potential panic from the action, and present it as an error instead 132 defer func() { 133 if r := recover(); r != nil { 134 err = fmt.Errorf("panic in ActionExecutor:\n%v", r) 135 } 136 }() 137 err = aq.action(aq.ctx, element.Value.(work).val) 138 }() 139 // Technically, two actions could error at the same time and only one would persist their error. For async 140 // tasks, we don't care as much about which action errored, just that an action error. 141 if err != nil { 142 aq.syncCond.L.Lock() 143 aq.err = err 144 aq.syncCond.L.Unlock() 145 } 146 } 147 148 element.Value.(work).wg.Done() 149 } 150 }