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 }