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  }