github.com/decred/dcrlnd@v0.7.6/kvdb/etcd/commit_queue.go (about) 1 //go:build kvdb_etcd 2 // +build kvdb_etcd 3 4 package etcd 5 6 import ( 7 "container/list" 8 "context" 9 "sync" 10 ) 11 12 // commitQueue is a simple execution queue to manage conflicts for transactions 13 // and thereby reduce the number of times conflicting transactions need to be 14 // retried. When a new transaction is added to the queue, we first upgrade the 15 // read/write counts in the queue's own accounting to decide whether the new 16 // transaction has any conflicting dependencies. If the transaction does not 17 // conflict with any other, then it is comitted immediately, otherwise it'll be 18 // queued up for later exection. 19 // The algorithm is described in: http://www.cs.umd.edu/~abadi/papers/vll-vldb13.pdf 20 type commitQueue struct { 21 ctx context.Context 22 mx sync.Mutex 23 readerMap map[string]int 24 writerMap map[string]int 25 26 queue *list.List 27 queueMx sync.Mutex 28 queueCond *sync.Cond 29 30 shutdown chan struct{} 31 } 32 33 type commitQueueTxn struct { 34 commitLoop func() 35 blocked bool 36 rset []string 37 wset []string 38 } 39 40 // NewCommitQueue creates a new commit queue, with the passed abort context. 41 func NewCommitQueue(ctx context.Context) *commitQueue { 42 q := &commitQueue{ 43 ctx: ctx, 44 readerMap: make(map[string]int), 45 writerMap: make(map[string]int), 46 queue: list.New(), 47 shutdown: make(chan struct{}), 48 } 49 q.queueCond = sync.NewCond(&q.queueMx) 50 51 // Start the queue consumer loop. 52 go q.mainLoop() 53 54 return q 55 } 56 57 // Stop signals the queue to stop after the queue context has been canceled and 58 // waits until the has stopped. 59 func (c *commitQueue) Stop() { 60 // Signal the queue's condition variable to ensure the mainLoop reliably 61 // unblocks to check for the exit condition. 62 c.queueCond.Signal() 63 <-c.shutdown 64 } 65 66 // Add increases lock counts and queues up tx commit closure for execution. 67 // Transactions that don't have any conflicts are executed immediately by 68 // "downgrading" the count mutex to allow concurrency. 69 func (c *commitQueue) Add(commitLoop func(), rset []string, wset []string) { 70 c.mx.Lock() 71 blocked := false 72 73 // Mark as blocked if there's any writer changing any of the keys in 74 // the read set. Do not increment the reader counts yet as we'll need to 75 // use the original reader counts when scanning through the write set. 76 for _, key := range rset { 77 if c.writerMap[key] > 0 { 78 blocked = true 79 break 80 } 81 } 82 83 // Mark as blocked if there's any writer or reader for any of the keys 84 // in the write set. 85 for _, key := range wset { 86 blocked = blocked || c.readerMap[key] > 0 || c.writerMap[key] > 0 87 88 // Increment the writer count. 89 c.writerMap[key] += 1 90 } 91 92 // Finally we can increment the reader counts for keys in the read set. 93 for _, key := range rset { 94 c.readerMap[key] += 1 95 } 96 97 c.queueCond.L.Lock() 98 c.queue.PushBack(&commitQueueTxn{ 99 commitLoop: commitLoop, 100 blocked: blocked, 101 rset: rset, 102 wset: wset, 103 }) 104 c.queueCond.L.Unlock() 105 106 c.mx.Unlock() 107 108 c.queueCond.Signal() 109 } 110 111 // done decreases lock counts of the keys in the read/write sets. 112 func (c *commitQueue) done(rset []string, wset []string) { 113 c.mx.Lock() 114 defer c.mx.Unlock() 115 116 for _, key := range rset { 117 c.readerMap[key] -= 1 118 if c.readerMap[key] == 0 { 119 delete(c.readerMap, key) 120 } 121 } 122 123 for _, key := range wset { 124 c.writerMap[key] -= 1 125 if c.writerMap[key] == 0 { 126 delete(c.writerMap, key) 127 } 128 } 129 } 130 131 // mainLoop executes queued transaction commits for transactions that have 132 // dependencies. The queue ensures that the top element doesn't conflict with 133 // any other transactions and therefore can be executed freely. 134 func (c *commitQueue) mainLoop() { 135 defer close(c.shutdown) 136 137 for { 138 // Wait until there are no unblocked transactions being 139 // executed, and for there to be at least one blocked 140 // transaction in our queue. 141 c.queueCond.L.Lock() 142 for c.queue.Front() == nil { 143 c.queueCond.Wait() 144 145 // Check the exit condition before looping again. 146 select { 147 case <-c.ctx.Done(): 148 c.queueCond.L.Unlock() 149 return 150 default: 151 } 152 } 153 154 // Now collect all txns until we find the next blocking one. 155 // These shouldn't conflict (if the precollected read/write 156 // keys sets don't grow), meaning we can safely commit them 157 // in parallel. 158 work := make([]*commitQueueTxn, 1) 159 e := c.queue.Front() 160 work[0] = c.queue.Remove(e).(*commitQueueTxn) 161 162 for { 163 e := c.queue.Front() 164 if e == nil { 165 break 166 } 167 168 next := e.Value.(*commitQueueTxn) 169 if !next.blocked { 170 work = append(work, next) 171 c.queue.Remove(e) 172 } else { 173 // We found the next blocking txn which means 174 // the block of work needs to be cut here. 175 break 176 } 177 } 178 179 c.queueCond.L.Unlock() 180 181 // Check if we need to exit before continuing. 182 select { 183 case <-c.ctx.Done(): 184 return 185 default: 186 } 187 188 var wg sync.WaitGroup 189 wg.Add(len(work)) 190 191 // Fire up N goroutines where each will run its commit loop 192 // and then clean up the reader/writer maps. 193 for _, txn := range work { 194 go func(txn *commitQueueTxn) { 195 defer wg.Done() 196 txn.commitLoop() 197 198 // We can safely cleanup here as done only 199 // holds the main mutex. 200 c.done(txn.rset, txn.wset) 201 }(txn) 202 } 203 204 wg.Wait() 205 206 // Check if we need to exit before continuing. 207 select { 208 case <-c.ctx.Done(): 209 return 210 default: 211 } 212 } 213 }