k8s.io/client-go@v0.31.1/util/workqueue/queue.go (about)

     1  /*
     2  Copyright 2015 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package workqueue
    18  
    19  import (
    20  	"sync"
    21  	"time"
    22  
    23  	"k8s.io/utils/clock"
    24  )
    25  
    26  // Deprecated: Interface is deprecated, use TypedInterface instead.
    27  type Interface TypedInterface[any]
    28  
    29  type TypedInterface[T comparable] interface {
    30  	Add(item T)
    31  	Len() int
    32  	Get() (item T, shutdown bool)
    33  	Done(item T)
    34  	ShutDown()
    35  	ShutDownWithDrain()
    36  	ShuttingDown() bool
    37  }
    38  
    39  // Queue is the underlying storage for items. The functions below are always
    40  // called from the same goroutine.
    41  type Queue[T comparable] interface {
    42  	// Touch can be hooked when an existing item is added again. This may be
    43  	// useful if the implementation allows priority change for the given item.
    44  	Touch(item T)
    45  	// Push adds a new item.
    46  	Push(item T)
    47  	// Len tells the total number of items.
    48  	Len() int
    49  	// Pop retrieves an item.
    50  	Pop() (item T)
    51  }
    52  
    53  // DefaultQueue is a slice based FIFO queue.
    54  func DefaultQueue[T comparable]() Queue[T] {
    55  	return new(queue[T])
    56  }
    57  
    58  // queue is a slice which implements Queue.
    59  type queue[T comparable] []T
    60  
    61  func (q *queue[T]) Touch(item T) {}
    62  
    63  func (q *queue[T]) Push(item T) {
    64  	*q = append(*q, item)
    65  }
    66  
    67  func (q *queue[T]) Len() int {
    68  	return len(*q)
    69  }
    70  
    71  func (q *queue[T]) Pop() (item T) {
    72  	item = (*q)[0]
    73  
    74  	// The underlying array still exists and reference this object, so the object will not be garbage collected.
    75  	(*q)[0] = *new(T)
    76  	*q = (*q)[1:]
    77  
    78  	return item
    79  }
    80  
    81  // QueueConfig specifies optional configurations to customize an Interface.
    82  // Deprecated: use TypedQueueConfig instead.
    83  type QueueConfig = TypedQueueConfig[any]
    84  
    85  type TypedQueueConfig[T comparable] struct {
    86  	// Name for the queue. If unnamed, the metrics will not be registered.
    87  	Name string
    88  
    89  	// MetricsProvider optionally allows specifying a metrics provider to use for the queue
    90  	// instead of the global provider.
    91  	MetricsProvider MetricsProvider
    92  
    93  	// Clock ability to inject real or fake clock for testing purposes.
    94  	Clock clock.WithTicker
    95  
    96  	// Queue provides the underlying queue to use. It is optional and defaults to slice based FIFO queue.
    97  	Queue Queue[T]
    98  }
    99  
   100  // New constructs a new work queue (see the package comment).
   101  //
   102  // Deprecated: use NewTyped instead.
   103  func New() *Type {
   104  	return NewWithConfig(QueueConfig{
   105  		Name: "",
   106  	})
   107  }
   108  
   109  // NewTyped constructs a new work queue (see the package comment).
   110  func NewTyped[T comparable]() *Typed[T] {
   111  	return NewTypedWithConfig(TypedQueueConfig[T]{
   112  		Name: "",
   113  	})
   114  }
   115  
   116  // NewWithConfig constructs a new workqueue with ability to
   117  // customize different properties.
   118  //
   119  // Deprecated: use NewTypedWithConfig instead.
   120  func NewWithConfig(config QueueConfig) *Type {
   121  	return NewTypedWithConfig(config)
   122  }
   123  
   124  // NewTypedWithConfig constructs a new workqueue with ability to
   125  // customize different properties.
   126  func NewTypedWithConfig[T comparable](config TypedQueueConfig[T]) *Typed[T] {
   127  	return newQueueWithConfig(config, defaultUnfinishedWorkUpdatePeriod)
   128  }
   129  
   130  // NewNamed creates a new named queue.
   131  // Deprecated: Use NewWithConfig instead.
   132  func NewNamed(name string) *Type {
   133  	return NewWithConfig(QueueConfig{
   134  		Name: name,
   135  	})
   136  }
   137  
   138  // newQueueWithConfig constructs a new named workqueue
   139  // with the ability to customize different properties for testing purposes
   140  func newQueueWithConfig[T comparable](config TypedQueueConfig[T], updatePeriod time.Duration) *Typed[T] {
   141  	var metricsFactory *queueMetricsFactory
   142  	if config.MetricsProvider != nil {
   143  		metricsFactory = &queueMetricsFactory{
   144  			metricsProvider: config.MetricsProvider,
   145  		}
   146  	} else {
   147  		metricsFactory = &globalMetricsFactory
   148  	}
   149  
   150  	if config.Clock == nil {
   151  		config.Clock = clock.RealClock{}
   152  	}
   153  
   154  	if config.Queue == nil {
   155  		config.Queue = DefaultQueue[T]()
   156  	}
   157  
   158  	return newQueue(
   159  		config.Clock,
   160  		config.Queue,
   161  		metricsFactory.newQueueMetrics(config.Name, config.Clock),
   162  		updatePeriod,
   163  	)
   164  }
   165  
   166  func newQueue[T comparable](c clock.WithTicker, queue Queue[T], metrics queueMetrics, updatePeriod time.Duration) *Typed[T] {
   167  	t := &Typed[T]{
   168  		clock:                      c,
   169  		queue:                      queue,
   170  		dirty:                      set[T]{},
   171  		processing:                 set[T]{},
   172  		cond:                       sync.NewCond(&sync.Mutex{}),
   173  		metrics:                    metrics,
   174  		unfinishedWorkUpdatePeriod: updatePeriod,
   175  	}
   176  
   177  	// Don't start the goroutine for a type of noMetrics so we don't consume
   178  	// resources unnecessarily
   179  	if _, ok := metrics.(noMetrics); !ok {
   180  		go t.updateUnfinishedWorkLoop()
   181  	}
   182  
   183  	return t
   184  }
   185  
   186  const defaultUnfinishedWorkUpdatePeriod = 500 * time.Millisecond
   187  
   188  // Type is a work queue (see the package comment).
   189  // Deprecated: Use Typed instead.
   190  type Type = Typed[any]
   191  
   192  type Typed[t comparable] struct {
   193  	// queue defines the order in which we will work on items. Every
   194  	// element of queue should be in the dirty set and not in the
   195  	// processing set.
   196  	queue Queue[t]
   197  
   198  	// dirty defines all of the items that need to be processed.
   199  	dirty set[t]
   200  
   201  	// Things that are currently being processed are in the processing set.
   202  	// These things may be simultaneously in the dirty set. When we finish
   203  	// processing something and remove it from this set, we'll check if
   204  	// it's in the dirty set, and if so, add it to the queue.
   205  	processing set[t]
   206  
   207  	cond *sync.Cond
   208  
   209  	shuttingDown bool
   210  	drain        bool
   211  
   212  	metrics queueMetrics
   213  
   214  	unfinishedWorkUpdatePeriod time.Duration
   215  	clock                      clock.WithTicker
   216  }
   217  
   218  type empty struct{}
   219  type t interface{}
   220  type set[t comparable] map[t]empty
   221  
   222  func (s set[t]) has(item t) bool {
   223  	_, exists := s[item]
   224  	return exists
   225  }
   226  
   227  func (s set[t]) insert(item t) {
   228  	s[item] = empty{}
   229  }
   230  
   231  func (s set[t]) delete(item t) {
   232  	delete(s, item)
   233  }
   234  
   235  func (s set[t]) len() int {
   236  	return len(s)
   237  }
   238  
   239  // Add marks item as needing processing.
   240  func (q *Typed[T]) Add(item T) {
   241  	q.cond.L.Lock()
   242  	defer q.cond.L.Unlock()
   243  	if q.shuttingDown {
   244  		return
   245  	}
   246  	if q.dirty.has(item) {
   247  		// the same item is added again before it is processed, call the Touch
   248  		// function if the queue cares about it (for e.g, reset its priority)
   249  		if !q.processing.has(item) {
   250  			q.queue.Touch(item)
   251  		}
   252  		return
   253  	}
   254  
   255  	q.metrics.add(item)
   256  
   257  	q.dirty.insert(item)
   258  	if q.processing.has(item) {
   259  		return
   260  	}
   261  
   262  	q.queue.Push(item)
   263  	q.cond.Signal()
   264  }
   265  
   266  // Len returns the current queue length, for informational purposes only. You
   267  // shouldn't e.g. gate a call to Add() or Get() on Len() being a particular
   268  // value, that can't be synchronized properly.
   269  func (q *Typed[T]) Len() int {
   270  	q.cond.L.Lock()
   271  	defer q.cond.L.Unlock()
   272  	return q.queue.Len()
   273  }
   274  
   275  // Get blocks until it can return an item to be processed. If shutdown = true,
   276  // the caller should end their goroutine. You must call Done with item when you
   277  // have finished processing it.
   278  func (q *Typed[T]) Get() (item T, shutdown bool) {
   279  	q.cond.L.Lock()
   280  	defer q.cond.L.Unlock()
   281  	for q.queue.Len() == 0 && !q.shuttingDown {
   282  		q.cond.Wait()
   283  	}
   284  	if q.queue.Len() == 0 {
   285  		// We must be shutting down.
   286  		return *new(T), true
   287  	}
   288  
   289  	item = q.queue.Pop()
   290  
   291  	q.metrics.get(item)
   292  
   293  	q.processing.insert(item)
   294  	q.dirty.delete(item)
   295  
   296  	return item, false
   297  }
   298  
   299  // Done marks item as done processing, and if it has been marked as dirty again
   300  // while it was being processed, it will be re-added to the queue for
   301  // re-processing.
   302  func (q *Typed[T]) Done(item T) {
   303  	q.cond.L.Lock()
   304  	defer q.cond.L.Unlock()
   305  
   306  	q.metrics.done(item)
   307  
   308  	q.processing.delete(item)
   309  	if q.dirty.has(item) {
   310  		q.queue.Push(item)
   311  		q.cond.Signal()
   312  	} else if q.processing.len() == 0 {
   313  		q.cond.Signal()
   314  	}
   315  }
   316  
   317  // ShutDown will cause q to ignore all new items added to it and
   318  // immediately instruct the worker goroutines to exit.
   319  func (q *Typed[T]) ShutDown() {
   320  	q.cond.L.Lock()
   321  	defer q.cond.L.Unlock()
   322  
   323  	q.drain = false
   324  	q.shuttingDown = true
   325  	q.cond.Broadcast()
   326  }
   327  
   328  // ShutDownWithDrain will cause q to ignore all new items added to it. As soon
   329  // as the worker goroutines have "drained", i.e: finished processing and called
   330  // Done on all existing items in the queue; they will be instructed to exit and
   331  // ShutDownWithDrain will return. Hence: a strict requirement for using this is;
   332  // your workers must ensure that Done is called on all items in the queue once
   333  // the shut down has been initiated, if that is not the case: this will block
   334  // indefinitely. It is, however, safe to call ShutDown after having called
   335  // ShutDownWithDrain, as to force the queue shut down to terminate immediately
   336  // without waiting for the drainage.
   337  func (q *Typed[T]) ShutDownWithDrain() {
   338  	q.cond.L.Lock()
   339  	defer q.cond.L.Unlock()
   340  
   341  	q.drain = true
   342  	q.shuttingDown = true
   343  	q.cond.Broadcast()
   344  
   345  	for q.processing.len() != 0 && q.drain {
   346  		q.cond.Wait()
   347  	}
   348  }
   349  
   350  func (q *Typed[T]) ShuttingDown() bool {
   351  	q.cond.L.Lock()
   352  	defer q.cond.L.Unlock()
   353  
   354  	return q.shuttingDown
   355  }
   356  
   357  func (q *Typed[T]) updateUnfinishedWorkLoop() {
   358  	t := q.clock.NewTicker(q.unfinishedWorkUpdatePeriod)
   359  	defer t.Stop()
   360  	for range t.C() {
   361  		if !func() bool {
   362  			q.cond.L.Lock()
   363  			defer q.cond.L.Unlock()
   364  			if !q.shuttingDown {
   365  				q.metrics.updateUnfinishedWork()
   366  				return true
   367  			}
   368  			return false
   369  
   370  		}() {
   371  			return
   372  		}
   373  	}
   374  }