istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/kube/controllers/queue.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package controllers 16 17 import ( 18 "fmt" 19 "time" 20 21 "go.uber.org/atomic" 22 "k8s.io/apimachinery/pkg/types" 23 "k8s.io/client-go/util/workqueue" 24 25 "istio.io/istio/pkg/config" 26 istiolog "istio.io/istio/pkg/log" 27 ) 28 29 type ReconcilerFn func(key types.NamespacedName) error 30 31 // Queue defines an abstraction around Kubernetes' workqueue. 32 // Items enqueued are deduplicated; this generally means relying on ordering of events in the queue is not feasible. 33 type Queue struct { 34 queue workqueue.RateLimitingInterface 35 initialSync *atomic.Bool 36 name string 37 maxAttempts int 38 workFn func(key any) error 39 closed chan struct{} 40 log *istiolog.Scope 41 } 42 43 // WithName sets a name for the queue. This is used for logging 44 func WithName(name string) func(q *Queue) { 45 return func(q *Queue) { 46 q.name = name 47 } 48 } 49 50 // WithRateLimiter allows defining a custom rate limiter for the queue 51 func WithRateLimiter(r workqueue.RateLimiter) func(q *Queue) { 52 return func(q *Queue) { 53 q.queue = workqueue.NewRateLimitingQueue(r) 54 } 55 } 56 57 // WithMaxAttempts allows defining a custom max attempts for the queue. If not set, items will not be retried 58 func WithMaxAttempts(n int) func(q *Queue) { 59 return func(q *Queue) { 60 q.maxAttempts = n 61 } 62 } 63 64 // WithReconciler defines the handler function to handle items in the queue. 65 func WithReconciler(f ReconcilerFn) func(q *Queue) { 66 return func(q *Queue) { 67 q.workFn = func(key any) error { 68 return f(key.(types.NamespacedName)) 69 } 70 } 71 } 72 73 // WithGenericReconciler defines the handler function to handle items in the queue that can handle any type 74 func WithGenericReconciler(f func(key any) error) func(q *Queue) { 75 return func(q *Queue) { 76 q.workFn = func(key any) error { 77 return f(key) 78 } 79 } 80 } 81 82 // NewQueue creates a new queue 83 func NewQueue(name string, options ...func(*Queue)) Queue { 84 q := Queue{ 85 name: name, 86 closed: make(chan struct{}), 87 initialSync: atomic.NewBool(false), 88 } 89 for _, o := range options { 90 o(&q) 91 } 92 if q.queue == nil { 93 q.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) 94 } 95 q.log = log.WithLabels("controller", q.name) 96 return q 97 } 98 99 // Add an item to the queue. 100 func (q Queue) Add(item any) { 101 q.queue.Add(item) 102 } 103 104 // AddObject takes an Object and adds the types.NamespacedName associated. 105 func (q Queue) AddObject(obj Object) { 106 q.queue.Add(config.NamespacedName(obj)) 107 } 108 109 // Run the queue. This is synchronous, so should typically be called in a goroutine. 110 func (q Queue) Run(stop <-chan struct{}) { 111 defer q.queue.ShutDown() 112 q.log.Infof("starting") 113 q.queue.Add(defaultSyncSignal) 114 go func() { 115 // Process updates until we return false, which indicates the queue is terminated 116 for q.processNextItem() { 117 } 118 close(q.closed) 119 }() 120 select { 121 case <-stop: 122 case <-q.closed: 123 } 124 q.log.Infof("stopped") 125 } 126 127 // syncSignal defines a dummy signal that is enqueued when .Run() is called. This allows us to detect 128 // when we have processed all items added to the queue prior to Run(). 129 type syncSignal struct{} 130 131 // defaultSyncSignal is a singleton instanceof syncSignal. 132 var defaultSyncSignal = syncSignal{} 133 134 // HasSynced returns true if the queue has 'synced'. A synced queue has started running and has 135 // processed all events that were added prior to Run() being called Warning: these items will be 136 // processed at least once, but may have failed. 137 func (q Queue) HasSynced() bool { 138 return q.initialSync.Load() 139 } 140 141 // Closed returns a chan that will be signaled when the Instance has stopped processing tasks. 142 func (q Queue) Closed() <-chan struct{} { 143 return q.closed 144 } 145 146 // processNextItem is the main workFn loop for the queue 147 func (q Queue) processNextItem() bool { 148 // Wait until there is a new item in the working queue 149 key, quit := q.queue.Get() 150 if quit { 151 // We are done, signal to exit the queue 152 return false 153 } 154 155 // We got the sync signal. This is not a real event, so we exit early after signaling we are synced 156 if key == defaultSyncSignal { 157 q.log.Debugf("synced") 158 q.initialSync.Store(true) 159 return true 160 } 161 162 q.log.Debugf("handling update: %v", formatKey(key)) 163 164 // 'Done marks item as done processing' - should be called at the end of all processing 165 defer q.queue.Done(key) 166 167 err := q.workFn(key) 168 if err != nil { 169 retryCount := q.queue.NumRequeues(key) + 1 170 if retryCount < q.maxAttempts { 171 q.log.Errorf("error handling %v, retrying (retry count: %d): %v", formatKey(key), retryCount, err) 172 q.queue.AddRateLimited(key) 173 // Return early, so we do not call Forget(), allowing the rate limiting to backoff 174 return true 175 } 176 q.log.Errorf("error handling %v, and retry budget exceeded: %v", formatKey(key), err) 177 } 178 // 'Forget indicates that an item is finished being retried.' - should be called whenever we do not want to backoff on this key. 179 q.queue.Forget(key) 180 return true 181 } 182 183 // WaitForClose blocks until the Instance has stopped processing tasks or the timeout expires. 184 // If the timeout is zero, it will wait until the queue is done processing. 185 // WaitForClose an error if the timeout expires. 186 func (q Queue) WaitForClose(timeout time.Duration) error { 187 closed := q.Closed() 188 if timeout == 0 { 189 <-closed 190 return nil 191 } 192 timer := time.NewTimer(timeout) 193 defer timer.Stop() 194 select { 195 case <-closed: 196 return nil 197 case <-timer.C: 198 return fmt.Errorf("timeout waiting for queue to close after %v", timeout) 199 } 200 } 201 202 func formatKey(key any) string { 203 if t, ok := key.(types.NamespacedName); ok { 204 if len(t.Namespace) > 0 { 205 return t.String() 206 } 207 // because we use namespacedName for non namespace scope resource as well 208 return t.Name 209 } 210 if t, ok := key.(Event); ok { 211 key = t.Latest() 212 } 213 if t, ok := key.(Object); ok { 214 if len(t.GetNamespace()) > 0 { 215 return t.GetNamespace() + "/" + t.GetName() 216 } 217 return t.GetName() 218 } 219 res := fmt.Sprint(key) 220 if len(res) >= 50 { 221 return res[:50] 222 } 223 return res 224 }