go.uber.org/yarpc@v1.72.1/peer/pendingheap/heap.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package pendingheap
    22  
    23  import (
    24  	"container/heap"
    25  	"sync"
    26  	"time"
    27  
    28  	"go.uber.org/yarpc/api/peer"
    29  	"go.uber.org/yarpc/api/transport"
    30  	"go.uber.org/yarpc/peer/abstractlist"
    31  )
    32  
    33  type pendingHeap struct {
    34  	sync.Mutex
    35  
    36  	peers []*peerScore
    37  
    38  	// next is an incrementing counter for every push, which is compared when
    39  	// scores are equal. This ends up implementing round-robin when scores are
    40  	// equal.
    41  	next int
    42  
    43  	// nextRand is used for random insertions among equally scored peers when new
    44  	// peers are added.
    45  	//
    46  	// nextRand MUST return a number in [0, numPeers)
    47  	nextRand func(numPeers int) int
    48  }
    49  
    50  // Option configures the peer list implementation constructor.
    51  type Option interface {
    52  	apply(*options)
    53  }
    54  
    55  type options struct{}
    56  
    57  // NewImplementation creates a new fewest pending heap
    58  // abstractlist.Implementation.
    59  //
    60  // Use this constructor instead of NewList, when wanting to do custom peer
    61  // connection management.
    62  func NewImplementation(opts ...Option) abstractlist.Implementation {
    63  	return newHeap(nextRand(time.Now().UnixNano()))
    64  }
    65  
    66  func newHeap(nextRand func(numPeers int) int) *pendingHeap {
    67  	return &pendingHeap{
    68  		nextRand: nextRand,
    69  	}
    70  }
    71  
    72  func (ph *pendingHeap) Choose(req *transport.Request) peer.StatusPeer {
    73  	ph.Lock()
    74  	ps, ok := ph.popPeer()
    75  	if !ok {
    76  		ph.Unlock()
    77  		return nil
    78  	}
    79  
    80  	// Note: We push the peer back to reset the "next" counter.
    81  	// This gives us round-robin behavior.
    82  	ph.pushPeer(ps)
    83  
    84  	ph.Unlock()
    85  	return ps.peer
    86  }
    87  
    88  func (ph *pendingHeap) Add(p peer.StatusPeer, _ peer.Identifier) abstractlist.Subscriber {
    89  	if p == nil {
    90  		return nil
    91  	}
    92  
    93  	ps := &peerScore{peer: p, heap: ph}
    94  
    95  	ph.Lock()
    96  	ph.pushPeerRandom(ps)
    97  	ph.Unlock()
    98  	return ps
    99  }
   100  
   101  func (ph *pendingHeap) Remove(p peer.StatusPeer, _ peer.Identifier, sub abstractlist.Subscriber) {
   102  	ps, ok := sub.(*peerScore)
   103  	if !ok {
   104  		return
   105  	}
   106  
   107  	ph.Lock()
   108  	ph.delete(ps)
   109  	ph.Unlock()
   110  }
   111  
   112  func (ph *pendingHeap) updatePendingRequestCount(ps *peerScore, pendingRequestCount int) {
   113  	ph.Lock()
   114  	// If the index is negative, the subscriber has already been deleted from the
   115  	// heap. This may occur when calling a peer and simultaneously removing it
   116  	// from the heap.
   117  	if ps.index < 0 {
   118  		ph.Unlock()
   119  		return
   120  	}
   121  
   122  	ps.pending = pendingRequestCount
   123  	ph.update(ps.index)
   124  	ph.Unlock()
   125  }
   126  
   127  // Len must be called in the context of a lock, as it is indirectly called
   128  // through heap.Push and heap.Pop.
   129  func (ph *pendingHeap) Len() int {
   130  	return len(ph.peers)
   131  }
   132  
   133  // Less returns whether the left peer has a lower score. If the scores are
   134  // equal, it returns the older peer (where "last" is lower.)
   135  // Less must be called in the context of a lock, as it is indirectly called
   136  // through heap.Push and heap.Pop.
   137  func (ph *pendingHeap) Less(i, j int) bool {
   138  	p1 := ph.peers[i]
   139  	p2 := ph.peers[j]
   140  	if p1.pending == p2.pending {
   141  		return p1.last < p2.last
   142  	}
   143  	return p1.pending < p2.pending
   144  }
   145  
   146  // Swap implements the heap.Interface. Do NOT use this method directly.
   147  // Swap must be called in the context of a lock, as it is indirectly called
   148  // through heap.Push and heap.Pop.
   149  func (ph *pendingHeap) Swap(i, j int) {
   150  	p1 := ph.peers[i]
   151  	p2 := ph.peers[j]
   152  
   153  	ph.peers[i], ph.peers[j] = ph.peers[j], ph.peers[i]
   154  	p1.index = j
   155  	p2.index = i
   156  }
   157  
   158  // Push implements the heap.Interface. Do NOT use this method directly.
   159  // Use pushPeer instead.
   160  // Push must be called in the context of a lock, as it is indirectly called
   161  // through heap.Push.
   162  func (ph *pendingHeap) Push(x interface{}) {
   163  	ps := x.(*peerScore)
   164  	ps.index = len(ph.peers)
   165  	ph.peers = append(ph.peers, ps)
   166  }
   167  
   168  // Pop implements the heap.Interface. Do NOT use this method directly.
   169  // Use popPeer instead.
   170  // Pop must be called in the context of a lock, as it is indirectly called
   171  // through heap.Pop.
   172  func (ph *pendingHeap) Pop() interface{} {
   173  	lastIndex := len(ph.peers) - 1
   174  	last := ph.peers[lastIndex]
   175  	ph.peers = ph.peers[:lastIndex]
   176  	return last
   177  }
   178  
   179  // delete removes the score at the given index.
   180  // delete must be called in a lock.
   181  func (ph *pendingHeap) delete(ps *peerScore) {
   182  	if ps.index < 0 {
   183  		return
   184  	}
   185  	index := ps.index
   186  
   187  	// Swap the element we want to delete with the last element, then pop it off.
   188  	ph.Swap(index, ph.Len()-1)
   189  	ph.Pop()
   190  
   191  	// If the original index still exists in the list, it contains a different
   192  	// element so update the heap.
   193  	if index < ph.Len() {
   194  		ph.update(index)
   195  	}
   196  
   197  	// Set the index to negative so we do not try to delete it again.
   198  	ps.index = -1
   199  }
   200  
   201  // pushPeer must be called in the context of a lock.
   202  func (ph *pendingHeap) pushPeer(ps *peerScore) {
   203  	ph.next++
   204  	ps.last = ph.next
   205  	heap.Push(ph, ps)
   206  }
   207  
   208  // pushPeerRandom inserts a peer randomly into the heap among equally scored
   209  // peers. This is expected to be called only by Add.
   210  //
   211  // This ensures that batches of peer updates are inserted randomly throughout
   212  // the heap, preventing hearding behavior that may be observed during batch
   213  // deployments.
   214  //
   215  // This must be called in the context of a lock.
   216  func (ph *pendingHeap) pushPeerRandom(ps *peerScore) {
   217  	ph.next++
   218  	ps.last = ph.next
   219  
   220  	random := ph.nextRand(len(ph.peers) + 1)
   221  	if random < len(ph.peers) {
   222  		randPeer := ph.peers[random]
   223  		ps.last, randPeer.last = randPeer.last, ps.last
   224  		heap.Fix(ph, random)
   225  	}
   226  
   227  	heap.Push(ph, ps)
   228  }
   229  
   230  // popPeer must be called in the context of a lock.
   231  func (ph *pendingHeap) popPeer() (*peerScore, bool) {
   232  	if ph.Len() == 0 {
   233  		return nil, false
   234  	}
   235  
   236  	peer := heap.Pop(ph).(*peerScore)
   237  	return peer, true
   238  }
   239  
   240  // update must be called in the context of a lock.
   241  func (ph *pendingHeap) update(i int) {
   242  	heap.Fix(ph, i)
   243  }