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  }