github.com/cilium/statedb@v0.3.2/reconciler/retries.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package reconciler
     5  
     6  import (
     7  	"container/heap"
     8  	"math"
     9  	"time"
    10  
    11  	"github.com/cilium/statedb"
    12  	"github.com/cilium/statedb/index"
    13  )
    14  
    15  type exponentialBackoff struct {
    16  	min time.Duration
    17  	max time.Duration
    18  }
    19  
    20  func (e *exponentialBackoff) Duration(attempt int) time.Duration {
    21  	dur := float64(e.min) * math.Pow(2, float64(attempt))
    22  	if dur > float64(e.max) {
    23  		return e.max
    24  	}
    25  	return time.Duration(dur)
    26  }
    27  
    28  func newRetries(minDuration, maxDuration time.Duration, objectToKey func(any) index.Key) *retries {
    29  	return &retries{
    30  		backoff: exponentialBackoff{
    31  			min: minDuration,
    32  			max: maxDuration,
    33  		},
    34  		queue:       nil,
    35  		items:       make(map[string]*retryItem),
    36  		objectToKey: objectToKey,
    37  		waitTimer:   nil,
    38  		waitChan:    make(chan struct{}),
    39  	}
    40  }
    41  
    42  // retries holds the items that failed to be reconciled in
    43  // a priority queue ordered by retry time. Methods of 'retries'
    44  // are not safe to access concurrently.
    45  type retries struct {
    46  	backoff     exponentialBackoff
    47  	queue       retryPrioQueue
    48  	items       map[string]*retryItem
    49  	objectToKey func(any) index.Key
    50  	waitTimer   *time.Timer
    51  	waitChan    chan struct{}
    52  }
    53  
    54  func (rq *retries) errors() []error {
    55  	errs := make([]error, 0, len(rq.items))
    56  	for _, item := range rq.items {
    57  		errs = append(errs, item.lastError)
    58  	}
    59  	return errs
    60  }
    61  
    62  type retryItem struct {
    63  	object any // the object that is being retried. 'any' to avoid specializing this internal code.
    64  	rev    statedb.Revision
    65  	delete bool
    66  
    67  	index      int       // item's index in the priority queue
    68  	retryAt    time.Time // time at which to retry
    69  	numRetries int       // number of retries attempted (for calculating backoff)
    70  	lastError  error
    71  }
    72  
    73  // Wait returns a channel that is closed when there is an item to retry.
    74  // Returns nil channel if no items are queued.
    75  func (rq *retries) Wait() <-chan struct{} {
    76  	return rq.waitChan
    77  }
    78  
    79  func (rq *retries) Top() (*retryItem, bool) {
    80  	if rq.queue.Len() == 0 {
    81  		return nil, false
    82  	}
    83  	return rq.queue[0], true
    84  }
    85  
    86  func (rq *retries) Pop() {
    87  	// Pop the object from the queue, but leave it into the map until
    88  	// the object is cleared or re-added.
    89  	rq.queue[0].index = -1
    90  	heap.Pop(&rq.queue)
    91  
    92  	rq.resetTimer()
    93  }
    94  
    95  func (rq *retries) resetTimer() {
    96  	if rq.waitTimer == nil || !rq.waitTimer.Stop() {
    97  		// Already fired so the channel was closed. Create a new one
    98  		// channel and timer.
    99  		waitChan := make(chan struct{})
   100  		rq.waitChan = waitChan
   101  		if rq.queue.Len() == 0 {
   102  			rq.waitTimer = nil
   103  		} else {
   104  			d := time.Until(rq.queue[0].retryAt)
   105  			rq.waitTimer = time.AfterFunc(d, func() { close(waitChan) })
   106  		}
   107  	} else if rq.queue.Len() > 0 {
   108  		d := time.Until(rq.queue[0].retryAt)
   109  		// Did not fire yet so we can just reset the timer.
   110  		rq.waitTimer.Reset(d)
   111  	}
   112  }
   113  
   114  func (rq *retries) Add(obj any, rev statedb.Revision, delete bool, lastError error) {
   115  	var (
   116  		item *retryItem
   117  		ok   bool
   118  	)
   119  	key := rq.objectToKey(obj)
   120  	if item, ok = rq.items[string(key)]; !ok {
   121  		item = &retryItem{
   122  			numRetries: 0,
   123  			index:      -1,
   124  		}
   125  		rq.items[string(key)] = item
   126  	}
   127  	item.object = obj
   128  	item.rev = rev
   129  	item.delete = delete
   130  	item.numRetries += 1
   131  	item.lastError = lastError
   132  	duration := rq.backoff.Duration(item.numRetries)
   133  	item.retryAt = time.Now().Add(duration)
   134  
   135  	if item.index >= 0 {
   136  		// The item was already in the queue, fix up its position.
   137  		heap.Fix(&rq.queue, item.index)
   138  	} else {
   139  		heap.Push(&rq.queue, item)
   140  	}
   141  
   142  	// Item is at the head of the queue, reset the timer.
   143  	if item.index == 0 {
   144  		rq.resetTimer()
   145  	}
   146  }
   147  
   148  func (rq *retries) Clear(obj any) {
   149  	key := rq.objectToKey(obj)
   150  	if item, ok := rq.items[string(key)]; ok {
   151  		// Remove the object from the queue if it is still there.
   152  		index := item.index // hold onto the index as heap.Remove messes with it
   153  		if item.index >= 0 && item.index < len(rq.queue) &&
   154  			key.Equal(rq.objectToKey(rq.queue[item.index].object)) {
   155  			heap.Remove(&rq.queue, item.index)
   156  
   157  			// Reset the timer in case we removed the top item.
   158  			if index == 0 {
   159  				rq.resetTimer()
   160  			}
   161  		}
   162  		// Completely forget the object and its retry count.
   163  		delete(rq.items, string(key))
   164  	}
   165  }
   166  
   167  // retryPrioQueue is a slice-backed priority heap with the next
   168  // expiring 'retryItem' at top. Implementation is adapted from the
   169  // 'container/heap' PriorityQueue example.
   170  type retryPrioQueue []*retryItem
   171  
   172  func (pq retryPrioQueue) Len() int { return len(pq) }
   173  
   174  func (pq retryPrioQueue) Less(i, j int) bool {
   175  	return pq[i].retryAt.Before(pq[j].retryAt)
   176  }
   177  
   178  func (pq retryPrioQueue) Swap(i, j int) {
   179  	pq[i], pq[j] = pq[j], pq[i]
   180  	pq[i].index = i
   181  	pq[j].index = j
   182  }
   183  
   184  func (pq *retryPrioQueue) Push(x any) {
   185  	retryObj := x.(*retryItem)
   186  	retryObj.index = len(*pq)
   187  	*pq = append(*pq, retryObj)
   188  }
   189  
   190  func (pq *retryPrioQueue) Pop() any {
   191  	old := *pq
   192  	n := len(old)
   193  	item := old[n-1]
   194  	old[n-1] = nil  // avoid memory leak
   195  	item.index = -1 // for safety
   196  	*pq = old[0 : n-1]
   197  	return item
   198  }